// Daoob Real Estate — Suhail-style interactive map
// Real OSM tiles + parcel polygons + district price-per-sqm heatmap + click loads rich data
const LeafletMap = ({
parcels,
zoning,
districts,
selectedId,
onSelect,
basemap = "street",
filterZones = null,
lang = "en",
heatmap = true,
onMapClick, // (latlng, nearestParcelId) => void — for "drop pin anywhere"
}) => {
const containerRef = React.useRef(null);
const mapRef = React.useRef(null);
const layerRef = React.useRef({ base: null, heat: null, parcels: null });
const selectedRef = React.useRef(selectedId);
// ===== Geometry helpers =====
const centerFor = (p) => {
const d = districts[p.district];
const seed = p.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0);
const sign1 = (seed % 2 ? 1 : -1);
const sign2 = ((seed >> 2) % 2 ? 1 : -1);
const ox = ((seed * 7) % 100) / 14000 * sign1;
const oy = ((seed * 11) % 100) / 14000 * sign2;
return [d.lat + oy, d.lng + ox];
};
const polygonFor = (p) => {
const [cy, cx] = centerFor(p);
const seed = p.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0);
const ext = Math.max(Math.sqrt(p.area) / 110000 * 8, 0.0024);
const j = (n) => ((seed * n) % 5 - 2) * 0.00018;
return [
[cy - ext + j(1), cx - ext + j(2)],
[cy - ext + j(3), cx + ext + j(4)],
[cy + ext + j(5), cx + ext + j(6)],
[cy + ext + j(7), cx - ext + j(8)],
];
};
// ===== Init map =====
React.useEffect(() => {
if (!containerRef.current || mapRef.current) return;
const map = L.map(containerRef.current, {
center: [24.7136, 46.6753],
zoom: 12,
zoomControl: false,
attributionControl: true,
minZoom: 10,
maxZoom: 18,
});
mapRef.current = map;
L.control.zoom({ position: "topright" }).addTo(map);
// Re-sync size after layout settles
const ro = new ResizeObserver(() => {
if (mapRef.current) mapRef.current.invalidateSize({ animate: false });
});
ro.observe(containerRef.current);
requestAnimationFrame(() => map.invalidateSize({ animate: false }));
const t = setTimeout(() => map.invalidateSize({ animate: false }), 250);
// Click ANYWHERE on the map → drop a visible pin → snap to nearest parcel
let dropPin = null;
const onClick = (e) => {
const { lat, lng } = e.latlng;
// Drop a visible gold pin where user clicked
if (dropPin) map.removeLayer(dropPin);
dropPin = L.marker([lat, lng], {
icon: L.divIcon({
className: "daoob-droppin",
html: `
`,
iconSize: [36, 48],
iconAnchor: [18, 44],
}),
});
dropPin.addTo(map);
// Find nearest parcel within ~1.5km
let best = null;
let bestDist = Infinity;
parcels.forEach(p => {
const seed = p.id.split("").reduce((a, c) => a + c.charCodeAt(0), 0);
const sign1 = (seed % 2 ? 1 : -1);
const sign2 = ((seed >> 2) % 2 ? 1 : -1);
const ox = ((seed * 7) % 100) / 14000 * sign1;
const oy = ((seed * 11) % 100) / 14000 * sign2;
const d = districts[p.district];
const py = d.lat + oy;
const px = d.lng + ox;
const dist = Math.hypot(py - lat, px - lng);
if (dist < bestDist) { bestDist = dist; best = p; }
});
if (best) onSelect(best.id);
if (onMapClick) onMapClick({ lat, lng }, best ? best.id : null);
};
map.on("click", onClick);
return () => {
map.off("click", onClick);
if (dropPin) map.removeLayer(dropPin);
ro.disconnect();
clearTimeout(t);
map.remove();
mapRef.current = null;
};
}, [parcels, districts, onMapClick, onSelect]);
// ===== Basemap =====
React.useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
if (layerRef.current.base) map.removeLayer(layerRef.current.base);
let tile;
if (basemap === "satellite") {
tile = L.tileLayer(
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
{ maxZoom: 19, attribution: "Tiles © Esri" }
);
} else if (basemap === "dark") {
tile = L.tileLayer(
"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
{ maxZoom: 19, attribution: "© OpenStreetMap © CARTO" }
);
} else {
// street (default) — light Voyager
tile = L.tileLayer(
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
{ maxZoom: 19, attribution: "© OpenStreetMap © CARTO" }
);
}
tile.addTo(map);
layerRef.current.base = tile;
}, [basemap]);
// ===== District price-per-sqm heatmap =====
// Compute avg price per district; render colored circle per district
const districtStats = React.useMemo(() => {
const stats = {};
Object.keys(districts).forEach(k => stats[k] = { count: 0, totalPrice: 0, totalArea: 0 });
parcels.forEach(p => {
if (!p.price) return;
const s = stats[p.district];
s.count++;
s.totalPrice += p.price;
s.totalArea += p.area;
});
Object.keys(stats).forEach(k => {
const s = stats[k];
s.avgPrice = s.count > 0 ? Math.round(s.totalPrice / s.count) : 0;
s.totalValue = s.totalPrice * 0; // unused
});
return stats;
}, [parcels, districts]);
// Color scale by price/sqm
const priceColor = (price) => {
if (!price) return "#5a5247";
if (price >= 140) return "#7C2D12"; // dark red
if (price >= 100) return "#C2410C"; // red
if (price >= 80) return "#D97757"; // orange
if (price >= 60) return "#C8A35A"; // gold
if (price >= 50) return "#5BA77A"; // green
return "#4A8A6B"; // dark green
};
React.useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
if (layerRef.current.heat) map.removeLayer(layerRef.current.heat);
if (!heatmap) return;
// Only render district labels (no overlapping translucent circles — they block clicks visually)
const group = L.featureGroup();
Object.entries(districts).forEach(([key, d]) => {
const s = districtStats[key];
if (!s || s.count === 0) return;
const color = priceColor(s.avgPrice);
const label = L.marker([d.lat, d.lng], {
icon: L.divIcon({
className: "daoob-district-label",
html: `${d[lang]}
${s.avgPrice.toLocaleString()} ${lang === "ar" ? "ر.س/م²" : "SAR/m²"}
`,
iconSize: [130, 38],
iconAnchor: [65, 60], // offset DOWN so labels don't cover parcel dots
}),
interactive: false,
keyboard: false,
});
label.addTo(group);
});
group.addTo(map);
layerRef.current.heat = group;
}, [districts, districtStats, heatmap, lang]);
// ===== Parcel polygons + markers =====
React.useEffect(() => {
const map = mapRef.current;
if (!map) return;
if (layerRef.current.parcels) map.removeLayer(layerRef.current.parcels);
const visible = filterZones && filterZones.length
? parcels.filter(p => filterZones.includes(p.zone))
: parcels;
const group = L.featureGroup();
visible.forEach(p => {
const isSelected = p.id === selectedId;
const z = zoning[p.zone];
const district = districts[p.district][lang];
const pColor = priceColor(p.price);
const tip = `
${p.id}
${district}
${z[lang]} · ${p.area.toLocaleString()} m²
${p.price ? `
${p.price} ${lang === "ar" ? "ر.س/م²" : "SAR/m²"}
` : ""}
${lang === "ar" ? "↳ انقر لعرض كل البيانات" : "↳ Click for full data"}
`;
// Parcel polygon
const poly = L.polygon(polygonFor(p), {
color: isSelected ? "#C8A35A" : pColor,
weight: isSelected ? 3 : 1.5,
fillColor: pColor,
fillOpacity: isSelected ? 0.6 : 0.35,
});
poly.bindTooltip(tip, { className: "daoob-tip", sticky: true, direction: "top", offset: [0, -8] });
poly.on("click", (e) => {
L.DomEvent.stopPropagation(e);
onSelect(p.id);
});
poly.addTo(group);
// Always-clickable circle marker on top — BIG, glowing, hard to miss
const ctr = centerFor(p);
const dot = L.circleMarker(ctr, {
radius: isSelected ? 16 : 13,
color: "#FFFFFF",
weight: 3,
fillColor: isSelected ? "#C8A35A" : pColor,
fillOpacity: 1,
pane: "markerPane",
className: "daoob-parcel-dot",
});
dot.bindTooltip(tip, { className: "daoob-tip", sticky: true, direction: "top", offset: [0, -14] });
dot.on("click", (e) => {
L.DomEvent.stopPropagation(e);
onSelect(p.id);
});
dot.addTo(group);
// Pulse ring on selected
if (isSelected) {
L.circleMarker(ctr, {
radius: 30, color: "#C8A35A", weight: 3, opacity: 0.85, fillOpacity: 0,
interactive: false, className: "daoob-pulse-ring",
}).addTo(group);
}
});
group.addTo(map);
layerRef.current.parcels = group;
// Pan to selected
if (selectedId && selectedId !== selectedRef.current) {
const sel = visible.find(p => p.id === selectedId);
if (sel) {
const ctr = centerFor(sel);
map.flyTo(ctr, Math.max(map.getZoom(), 14), { duration: 0.7 });
}
}
selectedRef.current = selectedId;
}, [parcels, zoning, districts, selectedId, filterZones, lang]);
return (
{/* Click-hint banner top-center */}
{lang === "ar" ? "انقر أي مكان على الخريطة لإسقاط دبوس وعرض البيانات" : "Click anywhere on the map to drop a pin and view data"}
{/* Heatmap legend bottom-left */}
{lang === "ar" ? "السعر ر.س/م²" : "Price SAR/m²"}
<50
80
120
150+
{/* Amanat link */}
أ
{lang === "ar" ? "بيانات أمانة الرياض" : "Amanat Riyadh data"}
);
};
Object.assign(window, { LeafletMap });