trails/apps/planner/app/components/NoGoAreaLayer.tsx
Ullrich Schäfer 985ec54023
chore(ts): replace 7 \as unknown as\ shims with proper types
Most of the existing coercions are legitimate (Drizzle raw-SQL row
shapes, linkedom DOMParser interop, fit-file-parser's loose \`any\`
callback API) — leaving those is honest. The ones cleaned up here
were shims around APIs that have real types we just hadn't wired:

- **\`window.__leafletMap\`** (4 sites: MapHelpers.tsx, SessionView.tsx ×3)
  Added \`Window\` declaration in \`apps/planner/app/types/global.d.ts\`.
  Callers now use plain \`window.__leafletMap\` typed as
  \`L.Map | undefined\`.

- **\`L.markerClusterGroup\`** (PoiPanel.tsx) — leaflet.markercluster
  augments the global \`L\` namespace at runtime but ships no types.
  Added a \`declare module \"leaflet\"\` block in the same global.d.ts
  with a minimal signature for what we actually use.

- **\`L.DomEvent.preventDefault / .stop\`** taking a Leaflet event
  rather than a native one (PlannerMap.tsx, RouteInteraction.tsx,
  NoGoAreaLayer.tsx). Leaflet events carry the native event under
  \`.originalEvent\`; using that drops the cast entirely.

- **\`_creds as unknown as OAuthCredentials\`** orphan in
  wahoo/webhook.ts — \`_creds\` was unused inside the callback (the
  void-cast was a no-op preserving the type for documentation).
  Replaced with \`async ()\`, dropped the unused import.

Net: 26 \`as unknown as\` / \`as any\` sites → 19, all remaining ones
gated by external lib interop or Drizzle raw-SQL.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:02:25 +02:00

119 lines
3.2 KiB
TypeScript

import { useEffect, useRef, useCallback } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import * as Y from "yjs";
import "@geoman-io/leaflet-geoman-free";
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
interface NoGoAreaLayerProps {
noGoAreas: Y.Array<Y.Map<unknown>>;
doc: Y.Doc;
enabled: boolean;
onToggle: () => void;
}
const NO_GO_STYLE: L.PathOptions = {
color: "#ef4444",
fillColor: "#ef4444",
fillOpacity: 0.2,
weight: 2,
};
function yMapToLatLngs(yMap: Y.Map<unknown>): L.LatLng[] {
const points = yMap.get("points") as Array<{ lat: number; lon: number }> | undefined;
if (!points) return [];
return points.map((p) => L.latLng(p.lat, p.lon));
}
export function NoGoAreaLayer({ noGoAreas, doc, enabled, onToggle }: NoGoAreaLayerProps) {
const map = useMap();
const layerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
const polygonMapRef = useRef<Map<number, L.Polygon>>(new Map());
const suppressObserverRef = useRef(false);
// Sync Yjs → Leaflet layers
const syncLayers = useCallback(() => {
const lg = layerGroupRef.current;
// Clear all existing polygons
lg.clearLayers();
polygonMapRef.current.clear();
for (let i = 0; i < noGoAreas.length; i++) {
const yMap = noGoAreas.get(i);
const latLngs = yMapToLatLngs(yMap);
if (latLngs.length < 3) continue;
const polygon = L.polygon(latLngs, NO_GO_STYLE);
polygon.on("contextmenu", (e) => {
L.DomEvent.preventDefault(e.originalEvent);
suppressObserverRef.current = true;
doc.transact(() => noGoAreas.delete(i, 1), "local");
suppressObserverRef.current = false;
syncLayers();
});
lg.addLayer(polygon);
polygonMapRef.current.set(i, polygon);
}
}, [noGoAreas]);
// Add/remove layer group from map
useEffect(() => {
const lg = layerGroupRef.current;
lg.addTo(map);
return () => {
lg.removeFrom(map);
};
}, [map]);
// Observe Yjs changes
useEffect(() => {
const observer = () => {
if (!suppressObserverRef.current) {
syncLayers();
}
};
noGoAreas.observeDeep(observer);
syncLayers();
return () => noGoAreas.unobserveDeep(observer);
}, [noGoAreas, syncLayers]);
// Geoman drawing controls
useEffect(() => {
if (enabled) {
map.pm.enableDraw("Polygon", {
pathOptions: NO_GO_STYLE,
snappable: false,
});
} else {
map.pm.disableDraw();
}
const handleCreate = (e: { layer: L.Layer }) => {
const layer = e.layer as L.Polygon;
const latLngs = (layer.getLatLngs()[0] as L.LatLng[]);
const points = latLngs.map((ll) => ({ lat: ll.lat, lon: ll.lng }));
// Remove the geoman-created layer (we'll render from Yjs)
map.removeLayer(layer);
// Add to Yjs
const yMap = new Y.Map();
yMap.set("points", points);
doc.transact(() => {
noGoAreas.push([yMap]);
}, "local");
// Exit draw mode
onToggle();
};
map.on("pm:create", handleCreate);
return () => {
map.pm.disableDraw();
map.off("pm:create", handleCreate);
};
}, [map, enabled, noGoAreas, doc, onToggle]);
return null;
}