البحث عن العقارات مشكلة مكانية. يفكر المشترون والمستأجرون من حيث الأحياء وأوقات التنقل والقرب من المدارس، لا من حيث الرموز البريدية وقوائم الشوارع. الواجهة التي تبدأ بالخريطة تُحقق تحولاً أفضل من الواجهة التي تبدأ بقائمة، لأنها تتيح للمستخدمين تثبيت بحثهم مكانياً قبل التصفية حسب السعر أو عدد الغرف أو أي خاصية أخرى.
يبني هذا الدرس خريطة قوائم عقارية جاهزة للإنتاج: علامات مجمّعة عند مستويات تكبير منخفضة، تسميات أسعار على العلامات الفردية، نقر لفتح نافذة منبثقة بتفاصيل القائمة، بحث برمز بريدي يُعيد توسيط الخريطة، وشريط تمرير نطاق سعري يُصفّي القوائم المرئية في الوقت الفعلي. يأتي مكوّن React الكامل في أقل من 100 سطر. نفس المنطق يعمل بـ JavaScript عادي إذا كنت تفضل عدم استخدام إطار عمل.
ستجد أيضاً ملاحظة عن التزامات GDPR الخاصة بمنصات PropTech في الاتحاد الأوروبي، لأن بيانات الموقع الناتجة عن عمليات بحث العقارات هي بيانات شخصية وتحتاج للتعامل معها وفق ذلك.
إذا كنت جديداً على واجهات برمجة الخرائط، يغطي درس كيفية إضافة خرائط تفاعلية إلى موقعك الأساسيات قبل أن يستمر هذا.
هيكل بيانات العقارات
تحتاج كل قائمة إلى إحداثيات وسعر وبيانات وصفية كافية للنافذة المنبثقة. احتفظ بها كـ GeoJSON FeatureCollection، تنسجم مباشرة مع مصدر الخريطة دون تحويل.
const properties = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
properties: {
id: "prop-001",
price: 485000,
bedrooms: 3,
sqm: 112,
address: "Keizersgracht 142, Amsterdam",
type: "apartment",
status: "for-sale"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
properties: {
id: "prop-002",
price: 1250,
bedrooms: 2,
sqm: 78,
address: "Sarphatistraat 58, Amsterdam",
type: "apartment",
status: "for-rent"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
properties: {
id: "prop-003",
price: 720000,
bedrooms: 4,
sqm: 195,
address: "Herengracht 380, Amsterdam",
type: "house",
status: "for-sale"
}
}
]
};
في منصة حقيقية يأتي هذا المصفوفة من API القوائم، استدعاء fetch عند تحميل الخريطة أو تغيير معلمة الاستعلام.
إعداد الخريطة
هيّئ الخريطة بعرض مركزي فوق سوق عقاراتك. MapAtlas Maps API متوافق مع Mapbox GL JS، لذا استدعاء التهيئة مطابق لما ستكتبه لـ Mapbox، مع عنوان URL لبلاطات MapAtlas.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/bright/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
يعمل نمط Bright بشكل جيد بشكل خاص لخرائط العقارات، إذ تتيح القاعدة الأفتح لتسميات الأسعار والعلامات الملونة أن تبرز دون فوضى بصرية.
التجميع مع تسميات الأسعار
النمط الرئيسي لواجهة خريطة عقارات هو إظهار تسميات أسعار على العلامات الفردية وفقاعات عدد على دوائر المجموعات. خيار المصدر cluster: true يجمع النقاط القريبة تلقائياً. تضيف طبقات منفصلة للمجموعات والعلامات الفردية.
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: properties,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Cluster circles, size scales with listing count
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'properties',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#2563EB',
'circle-radius': [
'step', ['get', 'point_count'],
22, 5, 30, 20, 38
],
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff'
}
});
// Cluster count labels
map.addLayer({
id: 'cluster-label',
type: 'symbol',
source: 'properties',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 14
},
paint: { 'text-color': '#ffffff' }
});
// Individual property markers, white circle with price text
map.addLayer({
id: 'property-price',
type: 'symbol',
source: 'properties',
filter: ['!', ['has', 'point_count']],
layout: {
'text-field': [
'concat',
'€',
['to-string', ['round', ['/', ['get', 'price'], 1000]]],
'k'
],
'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'],
'text-size': 12
},
paint: {
'text-color': '#1e293b',
'text-halo-color': '#ffffff',
'text-halo-width': 2
}
});
});
تفاعلات النقر
النقر على مجموعة يكبّر الخريطة لإظهار القوائم الفردية. النقر على عقار فردي يفتح نافذة منبثقة بالتفاصيل.
// Zoom into clicked cluster
map.on('click', 'clusters', (e) => {
const [feature] = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
map.getSource('properties').getClusterExpansionZoom(
feature.properties.cluster_id,
(err, zoom) => {
if (!err) map.easeTo({ center: feature.geometry.coordinates, zoom });
}
);
});
// Detail popup for individual listing
map.on('click', 'property-price', (e) => {
const { address, price, bedrooms, sqm, status, id } = e.features[0].properties;
const coords = e.features[0].geometry.coordinates.slice();
const formattedPrice = status === 'for-rent'
? `€${price.toLocaleString()}/mo`
: `€${price.toLocaleString()}`;
new mapmetricsgl.Popup({ offset: 10 })
.setLngLat(coords)
.setHTML(`
<div style="min-width:200px">
<strong style="font-size:15px">${formattedPrice}</strong>
<p style="margin:4px 0">${address}</p>
<p style="margin:4px 0;color:#64748b">${bedrooms} bed · ${sqm} m²</p>
<a href="/listings/${id}" style="color:#2563EB;font-weight:600">View listing →</a>
</div>
`)
.addTo(map);
});
// Change cursor on hover
map.on('mouseenter', 'property-price', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'property-price', () => { map.getCanvas().style.cursor = ''; });
map.on('mouseenter', 'clusters', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'clusters', () => { map.getCanvas().style.cursor = ''; });
فلتر نطاق السعر
شريط تمرير يُصفّي القوائم المرئية هو أحد أعلى إضافات الواجهة قيمة لخريطة عقارات. نظام تعبيرات Mapbox GL JS يتيح تحديث فلتر المصدر من جانب العميل دون رحلة شبكة، يحدث التصفية في المتصفح على GeoJSON المُحمَّل بالفعل.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// Wire up range inputs
const minInput = document.getElementById('price-min');
const maxInput = document.getElementById('price-max');
function onRangeChange() {
applyPriceFilter(Number(minInput.value), Number(maxInput.value));
}
minInput.addEventListener('input', onRangeChange);
maxInput.addEventListener('input', onRangeChange);
لاحظ أن طبقة المجموعة تتحدث تلقائياً عند تغيير المصدر الأساسي، العقارات المُصفّاة من طبقة العلامات الفردية تختفي أيضاً من أعداد المجموعات.
البحث عن العنوان مع Geocoding API
دع المستخدمين يكتبون حياً أو رمزاً بريدياً لإعادة توسيط الخريطة. Geocoding API يُعيد ميزات GeoJSON، لذا تنتقل الإحداثيات مباشرة إلى map.flyTo.
async function searchArea(query) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', query);
url.searchParams.set('key', 'YOUR_API_KEY');
url.searchParams.set('size', '1');
const res = await fetch(url);
const data = await res.json();
if (!data.features.length) return;
const [lng, lat] = data.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 13 });
}
document.getElementById('area-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') searchArea(e.target.value.trim());
});
لبحث وقت السفر، "أرني عقارات ضمن 20 دقيقة من هذه المدرسة"، أضف تراكب isochrone باستخدام MapAtlas Travel Time API. تُظهر مقالة Isochrone Maps Explained كيفية جلب وعرض تلك المضلع.
مكوّن React الكامل
تغليف كل ما سبق في مكوّن React مع إدارة دورة حياة صحيحة:
import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
export function PropertyMap({ listings, apiKey }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const [priceRange, setPriceRange] = useState([0, 2000000]);
useEffect(() => {
const map = new mapmetricsgl.Map({
container: containerRef.current,
style: `https://tiles.mapatlas.eu/styles/bright/style.json?key=${apiKey}`,
center: [4.9041, 52.3676],
zoom: 13
});
mapRef.current = map;
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: { type: 'FeatureCollection', features: listings },
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Add cluster, cluster-label, property-price layers (see above)
// Add click handlers (see above)
});
return () => map.remove();
}, [apiKey]);
// Update filter when price range slider changes
useEffect(() => {
const map = mapRef.current;
if (!map || !map.getLayer('property-price')) return;
map.setFilter('property-price', [
'all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], priceRange[0]],
['<=', ['get', 'price'], priceRange[1]]
]);
}, [priceRange]);
return (
<div>
<div style={{ padding: '12px 0', display: 'flex', gap: 12 }}>
<label>
Min €
<input type="range" min={0} max={2000000} step={10000}
value={priceRange[0]}
onChange={e => setPriceRange([+e.target.value, priceRange[1]])} />
</label>
<label>
Max €
<input type="range" min={0} max={2000000} step={10000}
value={priceRange[1]}
onChange={e => setPriceRange([priceRange[0], +e.target.value])} />
</label>
</div>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
</div>
);
}
هذا هو المكوّن الكامل، في أقل من 100 سطر يشمل شريط تمرير نطاق السعر وتنظيف دورة الحياة.
ملاحظات GDPR لـ PropTech في الاتحاد الأوروبي
عندما يكتب باحث عقاري عنوان منزله أو ينقر على "استخدم موقعي الحالي" على خريطتك، يشارك بيانات شخصية بموجب المادة 4 من GDPR. تكشف بيانات الموقع هذه أين يعيشون، مما قد يستنتج خصائص حساسة أخرى عنهم.
خطوات عملية للامتثال في الاتحاد الأوروبي:
- وجّه طلبات Geocoding عبر خادمك الخلفي بدلاً من استدعاء Geocoding API مباشرة من المتصفح. يمكن لخادمك الخلفي إزالة الرؤوس المعرّفة قبل إعادة التوجيه.
- لا تُسجّل أو تحتفظ بإحداثيات البحث بعد انتهاء الجلسة إلا إذا كانت لديك أساس قانوني موثق وجمعت موافقة محددة.
- إذا نفّذت "أخطرني عندما يتطابق عقار بالقرب مني مع معاييري"، عامل منطقة البحث المحفوظة كبيانات شخصية بفترة احتفاظ محددة.
يغطي دليل المطور الأوروبي لـ GDPR وواجهات برمجة الخرائط المتوافقة هذا بالتفصيل، بما في ذلك اتفاقيات المعالج ومتطلبات إقامة البيانات. MapAtlas تعالج جميع البيانات داخل الاتحاد الأوروبي وتوفر اتفاقية معالجة البيانات المطلوبة بموجب المادة 28 من GDPR.
تحتوي صفحة حلول صناعة العقارات على سياق إضافي حول كيفية استخدام منصات PropTech لـ MapAtlas في الإنتاج.
الخطوات التالية
- سجّل للحصول على مفتاح MapAtlas API مجاناً وابدأ بالطبقة المجانية
- أضف تراكبات وقت السفر مع Travel Time API، أظهر للمشترين ما يمكن الوصول إليه في 20 دقيقة
- استكشف دليل تنسيق Maps API لمطابقة هوية منصة عقاراتك البصرية
الأسئلة الشائعة
هل يمكنني استخدام MapAtlas لموقع قوائم عقارات؟
نعم. تستخدم منصات PropTech في جميع أنحاء الاتحاد الأوروبي MapAtlas لخرائط بحث العقارات ولوحات الاستثمار ومواقع قوائم الإيجار. Maps API متوافق مع Mapbox GL JS، لذا تنتقل أكواد خرائط العقارات المبنية على Mapbox بتغييرات طفيفة.
كيف يعمل تجميع العلامات لخرائط العقارات؟
يجمّع التجميع علامات العقارات القريبة في دائرة واحدة تُظهر العدد عند تصغير الخريطة. عندما يكبّر المستخدم، تنقسم المجموعات إلى علامات فردية. يدعم MapAtlas تجميع مصدر GeoJSON بشكل أصلي، اضبط cluster: true على المصدر وتتولى SDK الباقي.
هل بيانات الموقع من باحثي العقارات تخضع لـ GDPR؟
نعم. عندما يبحث مستخدم بعنوان منزله أو موقعه الحالي على موقع عقارات، يُعدّ ذلك بيانات شخصية بموجب المادة 4 من GDPR. يجب على منصات PropTech في الاتحاد الأوروبي توجيه طلبات Geocoding عبر وكيل خلفي وتطبيق سياسات تقليل البيانات والاحتفاظ المناسبة بدلاً من إجراء استدعاءات API من جانب العميل تُسجّل مواقع المستخدمين لخوادم طرف ثالث.

