142 lines
No EOL
7.4 KiB
TypeScript
142 lines
No EOL
7.4 KiB
TypeScript
/**
|
|
* Shared Leaflet marker factories.
|
|
*
|
|
* Used by `SupplierMap` and `CollectionMap`. Kept in a sibling file
|
|
* (rather than co-located in `geo.ts`) so importing this pulls in the
|
|
* Leaflet runtime — only do that from a `"use client"` page.
|
|
*/
|
|
import L from "leaflet";
|
|
|
|
/**
|
|
* Small circular pin. Used for both supplier coordinates and assigned
|
|
* stops. `label` renders inside the circle (empty string for blank).
|
|
*/
|
|
export function pinMarker(color: string, label: string) {
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="display:grid;place-items:center;width:22px;height:22px;border-radius:999px;background:${color};color:white;border:2px solid white;box-shadow:0 6px 14px rgba(15,23,42,.24);font-size:9px;font-weight:800;">${label}</div>`,
|
|
iconSize: [22, 22],
|
|
iconAnchor: [11, 11],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Status-aware pin — same coordinate, four distinct shapes/colours so
|
|
* the operator can read the bucket state at a glance when the All filter
|
|
* is active.
|
|
*
|
|
* assigned → 22px filled circle in the driver's palette colour, white border.
|
|
* unassigned → 28px rotated-square "diamond", hollow centre with a dashed
|
|
* white inset ring, amber fill, amber outer shadow. The
|
|
* diamond is rotated via CSS transform so Leaflet's iconAnchor
|
|
* stays centered.
|
|
* collected → 24px filled emerald circle, white border (closed-state —
|
|
* the supplier handed goods over successfully).
|
|
* failed → 24px filled emerald circle, white border, plus a 3px red
|
|
* ring on the outside so the sub-state stays visible without
|
|
* overriding the green "collected" semantic.
|
|
*/
|
|
export function pinMarkerKind(
|
|
kind: "assigned" | "unassigned" | "collected" | "failed",
|
|
color: string,
|
|
label: string,
|
|
count?: number
|
|
) {
|
|
if (kind === "unassigned") {
|
|
const size = 28;
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="position:relative;width:${size}px;height:${size}px;transform:rotate(45deg);"><div style="position:absolute;inset:0;background:${color};border:2.5px solid white;border-radius:5px;box-shadow:0 8px 18px rgba(217,119,6,.42);"></div><div style="position:absolute;inset:4px;border:1.5px dashed rgba(255,255,255,.85);border-radius:2px;"></div><div style="position:absolute;inset:0;display:grid;place-items:center;transform:rotate(-45deg);color:#ffffff;font-size:10px;font-weight:900;letter-spacing:.3px;text-shadow:0 1px 2px rgba(0,0,0,.45);">${label}</div></div>`,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
});
|
|
}
|
|
if (kind === "collected" || kind === "failed") {
|
|
const size = 24;
|
|
const outerRing = kind === "failed"
|
|
? `<div style="position:absolute;inset:-3px;border:3px solid #DC2626;border-radius:999px;"></div>`
|
|
: "";
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="position:relative;width:${size}px;height:${size}px;">${outerRing}<div style="position:absolute;inset:0;background:${color};color:white;border:2.5px solid white;border-radius:999px;box-shadow:0 6px 14px rgba(15,23,42,.28);"></div><div style="position:absolute;inset:0;display:grid;place-items:center;color:#ffffff;font-size:9px;font-weight:800;text-shadow:0 1px 2px rgba(0,0,0,.45);">${label}</div></div>`,
|
|
iconSize: [size + (kind === "failed" ? 6 : 0), size + (kind === "failed" ? 6 : 0)],
|
|
iconAnchor: [(size + (kind === "failed" ? 6 : 0)) / 2, (size + (kind === "failed" ? 6 : 0)) / 2],
|
|
});
|
|
}
|
|
// assigned — preserve the original shape for back-compat
|
|
return pinMarker(color, label);
|
|
}
|
|
|
|
/**
|
|
* Square pin with the brand-yellow ring. Used exclusively for drivers
|
|
* to distinguish them from supplier/stop pins at a glance. The yellow
|
|
* ring is the PixelForge brand accent and reads on both light and dark
|
|
* tile layers.
|
|
*/
|
|
export function driverMarker(color: string, initials: string) {
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="display:grid;place-items:center;width:28px;height:28px;border-radius:6px;background:${color};color:#0a0e1a;border:2.5px solid #FFE600;box-shadow:0 6px 14px rgba(15,23,42,.34);font-size:10px;font-weight:800;letter-spacing:0.5px;">${initials}</div>`,
|
|
iconSize: [28, 28],
|
|
iconAnchor: [14, 14],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Round cluster bubble with a count. Scales up for larger clusters
|
|
* so 25+ doesn't render at the same size as 2.
|
|
*/
|
|
export function clusterBubble(color: string, count: number) {
|
|
const size = count >= 25 ? 52 : count >= 10 ? 46 : 38;
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="display:grid;place-items:center;width:${size}px;height:${size}px;border-radius:999px;background:${color};color:white;border:3px solid white;box-shadow:0 12px 28px rgba(15,23,42,.32);font-size:14px;font-weight:900;">${count}</div>`,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Blue circle with the driver letter (A-Z, then 1-9) inside. Used by
|
|
* the new collection-map "one pin per driver per supplier" model. The
|
|
* count renders as a small round superscript in the top-right corner.
|
|
*
|
|
* Default colour is `DRIVER_PALETTE[1]` (#1B76BB) — the existing deep
|
|
* blue from src/lib/geo.ts — so the helper stays consistent with the
|
|
* rest of the app's driver signal even though the page no longer uses
|
|
* the palette for differentiating drivers (the letter does that now).
|
|
*/
|
|
export function driverLetterPin(
|
|
letter: string,
|
|
count: number,
|
|
color: string = "#1B76BB"
|
|
) {
|
|
const size = 28;
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="position:relative;width:${size}px;height:${size}px;"><div style="position:absolute;inset:0;display:grid;place-items:center;background:${color};color:white;border:2.5px solid white;border-radius:999px;box-shadow:0 6px 14px rgba(15,23,42,.28);font-size:13px;font-weight:800;letter-spacing:0.3px;text-shadow:0 1px 2px rgba(0,0,0,.45);">${letter}</div><div style="position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;display:grid;place-items:center;background:white;color:#1F2937;border-radius:999px;border:1.5px solid ${color};font-size:10px;font-weight:800;box-shadow:0 2px 6px rgba(15,23,42,.22);">${count}</div></div>`,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Amber circle with ✓ or ✕ inside, for unassigned buckets on the
|
|
* collection map. Same size as the driver letter pin so they read as
|
|
* sibling shapes. The count renders as a white-bordered superscript
|
|
* in the top-right, mirroring the driver pin.
|
|
*/
|
|
export function unassignedPoolPin(
|
|
glyph: "ready" | "unready",
|
|
count: number
|
|
) {
|
|
const size = 28;
|
|
const amber = "#F59E0B";
|
|
const symbol = glyph === "ready" ? "✓" : "✕";
|
|
return L.divIcon({
|
|
className: "",
|
|
html: `<div style="position:relative;width:${size}px;height:${size}px;"><div style="position:absolute;inset:0;display:grid;place-items:center;background:${amber};color:white;border:2.5px solid white;border-radius:999px;box-shadow:0 6px 14px rgba(217,119,6,.32);font-size:14px;font-weight:900;line-height:1;text-shadow:0 1px 2px rgba(0,0,0,.45);">${symbol}</div><div style="position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;display:grid;place-items:center;background:white;color:#1F2937;border-radius:999px;border:1.5px solid ${amber};font-size:10px;font-weight:800;box-shadow:0 2px 6px rgba(15,23,42,.22);">${count}</div></div>`,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
});
|
|
} |