|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useEffect, useMemo } from "react"; |
| 4 | +import { MapContainer, Marker, Polygon, TileLayer, Tooltip, useMap } from "react-leaflet"; |
| 5 | +import L from "leaflet"; |
| 6 | + |
| 7 | +const TILE_URL = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"; |
| 8 | + |
| 9 | +const SPECIALTY_COLORS = { |
| 10 | + AUS: "#f87171", CRP: "#fb923c", LCH: "#a3e635", |
| 11 | + LFK: "#a78bfa", NEW: "#fbbf24", OCN: "#60a5fa", RSG: "#e879f9", |
| 12 | +}; |
| 13 | +const DIRECTION_COLORS = { |
| 14 | + N: "#38bdf8", S: "#fb923c", E: "#34d399", W: "#c084fc", |
| 15 | +}; |
| 16 | +const EW_COLORS = { |
| 17 | + E: "#38bdf8", W: "#fb923c", |
| 18 | +}; |
| 19 | +const STANDARD_LABELS = { |
| 20 | + specialty: [ |
| 21 | + { label: "83", position: [30.555, -96.972] }, |
| 22 | + { label: "87", position: [27.782, -97.491] }, |
| 23 | + { label: "43", position: [28.935, -93.794] }, |
| 24 | + { label: "38", position: [31.182, -92.864] }, |
| 25 | + { label: "24", position: [29.984, -89.889] }, |
| 26 | + { label: "53", position: [26.289, -90.800] }, |
| 27 | + { label: "50", position: [30.396, -100.248] }, |
| 28 | + ], |
| 29 | + direction: [ |
| 30 | + { label: "50", position: [30.696, -99.248] }, |
| 31 | + { label: "38", position: [31.182, -92.864] }, |
| 32 | + { label: "87", position: [28.638, -97.187] }, |
| 33 | + { label: "24", position: [28.212, -89.634] }, |
| 34 | + ], |
| 35 | + ew: [ |
| 36 | + { label: "50", position: [29.696, -98.648] }, |
| 37 | + { label: "46", position: [29.696, -91.634] }, |
| 38 | + ], |
| 39 | +}; |
| 40 | + |
| 41 | +function geoJsonRingToLatLngs(ring) { |
| 42 | + return ring.map(([lng, lat]) => [lat, lng]); |
| 43 | +} |
| 44 | + |
| 45 | +function MapInvalidator() { |
| 46 | + const map = useMap(); |
| 47 | + useEffect(() => { |
| 48 | + map.invalidateSize(); |
| 49 | + }, [map]); |
| 50 | + return null; |
| 51 | +} |
| 52 | + |
| 53 | + |
| 54 | +function getStandardFill(feature, standardView) { |
| 55 | + const p = feature.properties; |
| 56 | + if (standardView === "specialty") return SPECIALTY_COLORS[p.category] ?? "#94a3b8"; |
| 57 | + if (standardView === "direction") return DIRECTION_COLORS[p.direction] ?? "#94a3b8"; |
| 58 | + if (standardView === "ew") return p.direction === "E" ? EW_COLORS.E : EW_COLORS.W; |
| 59 | + return "#94a3b8"; |
| 60 | +} |
| 61 | + |
| 62 | +export default function SplitMapExportView({ features, strata, mode, standardView, customColors, customLabels, mapView }) { |
| 63 | + const enrouteFeatures = useMemo( |
| 64 | + () => features.filter((f) => f.properties.strata === strata), |
| 65 | + [features, strata], |
| 66 | + ); |
| 67 | + |
| 68 | + const strataLabels = useMemo( |
| 69 | + () => Array.from(customLabels.entries()).filter(([, lbl]) => lbl.strata === strata), |
| 70 | + [customLabels, strata], |
| 71 | + ); |
| 72 | + |
| 73 | + return ( |
| 74 | + <MapContainer |
| 75 | + center={mapView?.center ? [mapView.center.lat, mapView.center.lng] : [30.5, -97.5]} |
| 76 | + zoom={mapView?.zoom ?? 6} |
| 77 | + zoomControl={false} |
| 78 | + attributionControl={false} |
| 79 | + style={{ height: "100%", width: "100%", background: "#0b1220" }} |
| 80 | + > |
| 81 | + <TileLayer url={TILE_URL} crossOrigin={true} /> |
| 82 | + <MapInvalidator /> |
| 83 | + {enrouteFeatures.map((f) => { |
| 84 | + const name = f.properties.name; |
| 85 | + let pathOptions; |
| 86 | + if (mode === "standard") { |
| 87 | + pathOptions = { color: "#0f172a", weight: 2, opacity: 1, fillColor: getStandardFill(f, standardView), fillOpacity: 1 }; |
| 88 | + } else { |
| 89 | + const colorHex = customColors.get(`${strata}-${name}`); |
| 90 | + pathOptions = colorHex |
| 91 | + ? { color: "#0f172a", weight: 2, opacity: 1, fillColor: colorHex, fillOpacity: 1 } |
| 92 | + : { color: "#334155", weight: 2, opacity: 1, fillColor: "#151c2c", fillOpacity: 1 }; |
| 93 | + } |
| 94 | + return ( |
| 95 | + <Polygon |
| 96 | + key={name} |
| 97 | + positions={geoJsonRingToLatLngs(f.geometry.coordinates[0])} |
| 98 | + pathOptions={pathOptions} |
| 99 | + /> |
| 100 | + ); |
| 101 | + })} |
| 102 | + {mode === "standard" && STANDARD_LABELS[standardView].map(({ label, position }) => ( |
| 103 | + <Marker |
| 104 | + key={`stdlabel-${label}-${position[0]}`} |
| 105 | + position={position} |
| 106 | + opacity={0} |
| 107 | + icon={L.divIcon({ className: "", iconSize: [0, 0] })} |
| 108 | + interactive={false} |
| 109 | + > |
| 110 | + <Tooltip permanent direction="center" opacity={1} className="split-map-label" > |
| 111 | + <span style={{ fontFamily: "ui-monospace, monospace", fontSize: "1.25rem", fontWeight: 700, color: "#ffffff" }}>{label}</span> |
| 112 | + </Tooltip> |
| 113 | + </Marker> |
| 114 | + ))} |
| 115 | + {mode === "custom" && strataLabels.map(([id, lbl]) => ( |
| 116 | + <Marker |
| 117 | + key={id} |
| 118 | + position={lbl.position} |
| 119 | + opacity={0} |
| 120 | + icon={L.divIcon({ className: "", iconSize: [0, 0] })} |
| 121 | + interactive={false} |
| 122 | + > |
| 123 | + <Tooltip permanent direction="center" opacity={1} className="split-map-label"> |
| 124 | + <span style={{ fontFamily: "ui-monospace, monospace", fontSize: "1.25rem", fontWeight: 700, color: "#ffffff" }}>{lbl.text}</span> |
| 125 | + </Tooltip> |
| 126 | + </Marker> |
| 127 | + ))} |
| 128 | + </MapContainer> |
| 129 | + ); |
| 130 | +} |
0 commit comments