البحث عن العقارات مشكلة مكانية. يفكر المشترون والمستأجرون من حيث الأحياء وأوقات التنقل والقرب من المدارس، لا من حيث الرموز البريدية وقوائم الشوارع. الواجهة التي تبدأ بالخريطة تناسب طريقة بحث الناس، لأنها تتيح للمستخدمين تثبيت بحثهم مكانياً قبل التصفية حسب السعر أو عدد الغرف أو أي خاصية أخرى.
يبني هذا الدرس خريطة قوائم عقارية جاهزة للإنتاج: علامات مجمّعة عند مستويات تكبير منخفضة، تسميات أسعار على العلامات الفردية، نقر لفتح نافذة منبثقة بتفاصيل القائمة، بحث برمز بريدي يُعيد توسيط الخريطة، وشريط تمرير نطاق سعري يُصفّي القوائم المرئية في الوقت الفعلي. يبلغ مكوّن React نفسه نحو 65 سطراً، ويصل البناء الكامل مع الطبقات ومعالجات النقر إلى نحو 150 سطراً. نفس المنطق يعمل بـ 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://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
يناسب النمط الفاتح منخفض التباين خرائط العقارات بشكل خاص، إذ تتيح القاعدة الأفتح لتسميات الأسعار والعلامات الملونة أن تبرز دون فوضى بصرية.
التجميع مع تسميات الأسعار
النمط الرئيسي لواجهة خريطة عقارات هو إظهار تسميات أسعار على العلامات الفردية وفقاعات عدد على دوائر المجموعات. خيار المصدر 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);
لاحظ أن المجموعات تُحسب من بيانات المصدر وليس من فلاتر الطبقات، لذا تظل أعداد المجموعات تشمل القوائم التي أخفاها شريط التمرير. للحفاظ على التزامن بينهما، صفِّ القوائم في JavaScript ومرّر النتيجة إلى map.getSource('properties').setData().
البحث عن العنوان مع Geocoding API
دع المستخدمين يكتبون حياً أو رمزاً بريدياً لإعادة توسيط الخريطة. Geocoding API يُعيد ميزات GeoJSON، لذا تنتقل الإحداثيات مباشرة إلى map.flyTo.
async function searchArea(query) {
const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
url.searchParams.set('text', query);
url.searchParams.set('token', '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://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=${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>
);
}
هذا هو الهيكل الأساسي للمكوّن، نحو 65 سطراً تشمل شريط تمرير نطاق السعر وتنظيف دورة الحياة؛ أضف كود الطبقات ومعالجات النقر من الأقسام السابقة لإكماله.
ملاحظات GDPR لـ PropTech في الاتحاد الأوروبي
عندما يكتب باحث عقاري عنوان منزله أو ينقر على "استخدم موقعي الحالي" على خريطتك، يشارك بيانات شخصية بموجب المادة 4(1) من GDPR، التي تذكر بيانات الموقع صراحةً كمعرّف. تكشف بيانات الموقع هذه أين يعيشون، مما قد يستنتج خصائص حساسة أخرى عنهم.
خطوات عملية للامتثال في الاتحاد الأوروبي:
- وجّه طلبات Geocoding عبر خادمك الخلفي بدلاً من استدعاء Geocoding API مباشرة من المتصفح. يمكن لخادمك الخلفي إزالة الرؤوس المعرّفة قبل إعادة التوجيه.
- لا تُسجّل أو تحتفظ بإحداثيات البحث بعد انتهاء الجلسة إلا إذا كان لديك أساس قانوني موثق بموجب المادة 6 من GDPR، مثل الموافقة.
- إذا نفّذت "أخطرني عندما يتطابق عقار بالقرب مني مع معاييري"، عامل منطقة البحث المحفوظة كبيانات شخصية بفترة احتفاظ محددة.
يغطي دليل المطور الأوروبي لـ GDPR وواجهات برمجة الخرائط المتوافقة هذا بالتفصيل، بما في ذلك اتفاقيات المعالج ومتطلبات إقامة البيانات. تعالج MapAtlas البيانات داخل الاتحاد الأوروبي وتوفر اتفاقية معالجة البيانات التي تشترطها المادة 28(3) من GDPR بين المتحكم والمعالج.
تحتوي صفحة حلول صناعة العقارات على سياق إضافي حول كيفية استخدام منصات PropTech لـ MapAtlas في الإنتاج.
الخطوات التالية
- سجّل للحصول على مفتاح MapAtlas API مجاناً وابدأ بالطبقة المجانية
- أضف تراكبات وقت السفر مع Travel Time API، أظهر للمشترين ما يمكن الوصول إليه في 20 دقيقة
- استكشف دليل تنسيق Maps API لمطابقة هوية منصة عقاراتك البصرية
الأسئلة الشائعة
هل يمكنني استخدام MapAtlas لموقع قوائم عقارات؟
نعم. يغطي MapAtlas ما تحتاجه مواقع العقارات: خرائط بحث العقارات ولوحات الاستثمار وخرائط قوائم الإيجار. Maps API متوافق مع Mapbox GL JS، لذا تنتقل أكواد خرائط العقارات المبنية على Mapbox بتغييرات طفيفة.
كيف يعمل تجميع العلامات لخرائط العقارات؟
يجمّع التجميع علامات العقارات القريبة في دائرة واحدة تُظهر العدد عند تصغير الخريطة. عندما يكبّر المستخدم، تنقسم المجموعات إلى علامات فردية. يدعم MapAtlas تجميع مصدر GeoJSON بشكل أصلي، اضبط cluster: true على المصدر وتتولى SDK الباقي.
هل بيانات الموقع من باحثي العقارات تخضع لـ GDPR؟
نعم. عندما يبحث مستخدم بعنوان منزله أو موقعه الحالي على موقع عقارات، يُعدّ ذلك بيانات شخصية بموجب المادة 4 من GDPR. يجب على منصات PropTech في الاتحاد الأوروبي توجيه طلبات Geocoding عبر وكيل خلفي وتطبيق سياسات تقليل البيانات والاحتفاظ المناسبة بدلاً من إجراء استدعاءات API من جانب العميل تُسجّل مواقع المستخدمين لخوادم طرف ثالث.
الأسئلة الشائعة
هل يمكنني استخدام MapAtlas لموقع قوائم عقارات؟
نعم. يغطي MapAtlas ما تحتاجه مواقع العقارات: خرائط بحث العقارات ولوحات الاستثمار وخرائط قوائم الإيجار. Maps API متوافق مع Mapbox GL JS، لذا تنتقل أكواد خرائط العقارات المبنية على Mapbox بتغييرات طفيفة.
كيف يعمل تجميع العلامات لخرائط العقارات؟
يجمّع التجميع علامات العقارات القريبة في دائرة واحدة تُظهر العدد عند تصغير الخريطة. عندما يكبّر المستخدم، تنقسم المجموعات إلى علامات فردية. يدعم MapAtlas تجميع مصدر GeoJSON بشكل أصلي، اضبط `cluster: true` على المصدر وتتولى SDK الباقي.
هل بيانات الموقع من باحثي العقارات تخضع لـ GDPR؟
نعم. عندما يبحث مستخدم بعنوان منزله أو موقعه الحالي على موقع عقارات، يُعدّ ذلك بيانات شخصية بموجب المادة 4 من GDPR. يجب على منصات PropTech في الاتحاد الأوروبي توجيه طلبات Geocoding عبر وكيل خلفي وتطبيق سياسات تقليل البيانات والاحتفاظ المناسبة بدلاً من إجراء استدعاءات API من جانب العميل تُسجّل مواقع المستخدمين لدى خوادم طرف ثالث.

