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