// 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 });