// Real Umm al-Fahm map (CARTO/OSM tiles stitched, z=15, 1024×1024 ≈ 4km square). // Pin positions precomputed in pixel coords of the 1024×1024 source. const MAP_IMG = 'assets/umm-al-fahm-map.png'; const MAP_NATIVE = 1024; // Pin pixel positions on the 1024×1024 source (computed at z=15) const PIN_COORDS = { CENTER: { x: 611, y: 513 }, PICKUP: { x: 662, y: 504 }, DROPOFF: { x: 753, y: 712 }, DRIVER: { x: 541, y: 402 }, o1: { x: 662, y: 504 }, o2: { x: 559, y: 648 }, o3: { x: 513, y: 557 }, o4: { x: 765, y: 480 }, o5: { x: 681, y: 692 }, o6: { x: 632, y: 601 }, }; const VERTICAL_COLOR = { food: '#E11D48', parcel: '#2563EB', grocery: '#16A34A', }; // ───────────────────────────────────────────────────────────── // Pin variants // ───────────────────────────────────────────────────────────── // Pool pin — vertical-colored pin + payout badge attached. Glove-friendly. function PoolPin({ x, y, color, payout, bonus, kind, selected = false, onClick }) { return (
{/* Pin body */}
{/* Payout badge attached above pin */}
{kind && } ₪{payout + (bonus || 0)} {bonus > 0 && +}
{/* Pin teardrop */} {selected && }
); } // Driver "you are here" pin — pulsing teal circle function DriverPin({ x, y }) { return (
); } // Route flag (pickup or dropoff) — small flag pin function RouteFlag({ x, y, color, label, big = false }) { const s = big ? 36 : 28; return (
); } // ───────────────────────────────────────────────────────────── // MapView — real Umm al-Fahm static image + pins // `mode`: 'pool' | 'route' | 'driver-only' | 'empty' // ───────────────────────────────────────────────────────────── // Off-screen pin indicator — when a pool pin is outside the visible // map area, show a smaller marker on the edge with a chevron pointing // toward its true position and the payout. function OffScreenPin({ x, y, dir, color, payout, kind, onClick }) { // dir: 'top' | 'bottom' | 'left' | 'right' — which edge it's clamped to // We point the chevron in the direction of the true pin const chevRot = { top: 0, right: 90, bottom: 180, left: 270 }[dir] || 0; return (
{/* chevron rotated to point at off-screen direction */} {kind && } ₪{payout}
); } // Map z=15 ground resolution at lat ~32.5°: ~4.03 m/source-px. // At display scale 0.6 → ~6.72 m/display-px → 1 km ≈ 149 display-px. const M_PER_SOURCE_PX = 4.03; function kmToDisplayPx(km, scale) { return (km * 1000 / M_PER_SOURCE_PX) * scale; } // Image centre lat/lng — defaults match the bundled umm-al-fahm-map.png tile // (OSM tiles stitched around this point at z=15). Overridden at runtime by the // server config so the city centre is NOT hardcoded: admins set cityLat/cityLng // in Settings. If you change the city you also swap the map image to match. const DEFAULT_CENTRE_LAT = 32.5167; const DEFAULT_CENTRE_LNG = 35.1500; function mapCentre() { const c = window.RESDRI_CONFIG || {}; return { lat: typeof c.cityLat === 'number' ? c.cityLat : DEFAULT_CENTRE_LAT, lng: typeof c.cityLng === 'number' ? c.cityLng : DEFAULT_CENTRE_LNG, }; } // Project a real-world lat/lng to the 1024×1024 source-pixel space. // Small-area approximation — accurate enough across a 4 km tile. function latLngToSourcePx(lat, lng) { if (typeof lat !== 'number' || typeof lng !== 'number') return null; const { lat: cLat, lng: cLng } = mapCentre(); const cosLat = Math.cos(cLat * Math.PI / 180); const dxM = (lng - cLng) * 111320 * cosLat; const dyM = -(lat - cLat) * 111320; // y grows downward return { x: 512 + dxM / M_PER_SOURCE_PX, y: 512 + dyM / M_PER_SOURCE_PX }; } // Out-of-radius indicator — pinned to the visible-area edge along the direction // from the driver. Visually distinct from in-radius off-screen pins (dim/muted). function OutOfRadiusPin({ x, y, dir, color, payout, distanceKm, onClick }) { const chevRot = { top: 0, right: 90, bottom: 180, left: 270 }[dir] || 0; return (
₪{payout} · {distanceKm.toFixed(1)} كم
); } // Compute where a ray from `from` through `to` exits the rectangle `area`. // Returns { x, y, dir } — dir is which edge it hit. function rayToEdge(from, to, area, pad = 22) { const dx = to.x - from.x; const dy = to.y - from.y; if (dx === 0 && dy === 0) return { x: from.x, y: from.y, dir: 'top' }; const ts = []; if (dx > 0) ts.push({ t: (area.right - pad - from.x) / dx, dir: 'right' }); if (dx < 0) ts.push({ t: (area.left + pad - from.x) / dx, dir: 'left' }); if (dy > 0) ts.push({ t: (area.bottom - pad - from.y) / dy, dir: 'bottom' }); if (dy < 0) ts.push({ t: (area.top + pad - from.y) / dy, dir: 'top' }); const valid = ts.filter(s => s.t > 0).sort((a, b) => a.t - b.t); const hit = valid[0] || { t: 0, dir: 'top' }; return { x: from.x + hit.t * dx, y: from.y + hit.t * dy, dir: hit.dir }; } function MapView({ mode = 'pool', scale = 0.60, offsetX = -125, offsetY = -21, dim = false, selectedOrderId = null, onPickPin, height = '100%', driverPos = 'DRIVER', driverLatLng = null, // {lat, lng} — overrides driverPos when provided orders = null, visibleArea = null, radiusKm = null, outOfRadiusOrders = null, }) { // Driver position resolution priority: real lat/lng > named pose > city centre const drv = driverLatLng ? (latLngToSourcePx(driverLatLng.lat, driverLatLng.lng) || PIN_COORDS.DRIVER) : driverPos === 'route-pickup' ? { x: 610, y: 460 } : driverPos === 'route-dropoff' ? { x: 710, y: 610 } : PIN_COORDS.DRIVER; // Pool pins are always supplied by the caller from live orders. No demo pins. const poolPins = orders || []; const route = mode === 'route' ? { from: PIN_COORDS.PICKUP, to: PIN_COORDS.DROPOFF, } : null; // Project pixel-coord to screen const proj = (p) => ({ x: p.x * scale + offsetX, y: p.y * scale + offsetY }); return (
{/* Dim overlay for offline */} {dim &&
} {/* Route polyline */} {route && (() => { const a = proj(route.from); const b = proj(route.to); // simple bezier curve between return ( ); })()} {/* Route flags */} {route && ( <> )} {/* Working-radius circle around the driver */} {radiusKm && mode === 'pool' && !dim && (() => { const c = proj(drv); const r = kmToDisplayPx(radiusKm, scale); return ( {/* Filled translucent circle */} {/* Label on the radius edge — at the bottom of the circle */} {radiusKm.toFixed(1).replace(/\.0$/, '')} كم ); })()} {/* Pool pins (with off-screen indicators for pins outside the visible map area) */} {poolPins.map((p) => { const sp = proj(p.pos); // Determine if pin is within visible area (default to whole container if none provided) const va = visibleArea || { top: 0, left: 0, right: 9999, bottom: 9999 }; const PAD = 28; // clamp inset from the edge const inside = sp.x >= va.left + PAD && sp.x <= va.right - PAD && sp.y >= va.top + PAD && sp.y <= va.bottom - PAD; if (inside) { return ( onPickPin && onPickPin(p.id)} /> ); } // Out of visible area — clamp to edge, determine direction let dir = 'top'; let cx = Math.max(va.left + PAD, Math.min(va.right - PAD, sp.x)); let cy = Math.max(va.top + PAD, Math.min(va.bottom - PAD, sp.y)); // Pick the edge that's most relevant const dxR = sp.x - (va.right - PAD); const dxL = (va.left + PAD) - sp.x; const dyT = (va.top + PAD) - sp.y; const dyB = sp.y - (va.bottom - PAD); const max = Math.max(dxR, dxL, dyT, dyB); if (max === dyB) { dir = 'bottom'; cy = va.bottom - PAD; } else if (max === dyT) { dir = 'top'; cy = va.top + PAD; } else if (max === dxR) { dir = 'right'; cx = va.right - PAD; } else { dir = 'left'; cx = va.left + PAD; } return ( onPickPin && onPickPin(p.id)} /> ); })} {/* Driver */} {(mode !== 'empty' || dim) && } {/* OUT-OF-RADIUS edge indicators — for each order outside the working radius, show a muted indicator stuck to the visible-area edge along the ray from the driver toward the order's true location. */} {outOfRadiusOrders && outOfRadiusOrders.length > 0 && (() => { const drvScreen = proj(drv); const va = visibleArea || { top: 0, left: 0, right: 9999, bottom: 9999 }; return outOfRadiusOrders.map((p) => { const target = proj(p.pos); const edge = rayToEdge(drvScreen, target, va, 22); return ( onPickPin && onPickPin(p.id)} /> ); }); })()}
); } Object.assign(window, { MapView, PIN_COORDS, VERTICAL_COLOR, kmToDisplayPx, latLngToSourcePx, mapCentre, });