({ allFields, selectedFields, activeField, flyToField, onFlyToDone, onFieldClickOnMap, onAddField, editBoundaryFieldId, onUpdateField, onCancelEditBoundary }: MapViewProps)
| 52 | onUpdateField?: (field: Field) => void; |
| 53 | onCancelEditBoundary?: () => void; |
| 54 | } |
| 55 | |
| 56 | const MapView = ({ allFields, selectedFields, activeField, flyToField, onFlyToDone, onFieldClickOnMap, onAddField, editBoundaryFieldId, onUpdateField, onCancelEditBoundary }: MapViewProps) => { |
| 57 | const isMobile = useIsMobile(); |
| 58 | const mapContainer = useRef<HTMLDivElement>(null); |
| 59 | const mapRef = useRef<mapboxgl.Map | null>(null); |
| 60 | const [mapToken, setMapToken] = useState(""); |
| 61 | const [mapLoaded, setMapLoaded] = useState(false); |
| 62 | const [drawMode, setDrawMode] = useState(false); |
| 63 | const [drawVertices, setDrawVertices] = useState<[number, number][]>([]); |
| 64 | const [showNewFieldDialog, setShowNewFieldDialog] = useState(false); |
| 65 | const [showFields, setShowFields] = useState(true); |
| 66 | const [showNdvi, setShowNdvi] = useState(false); |
| 67 | const [geeNdviTileUrl, setGeeNdviTileUrl] = useState<string | null>(null); |
| 68 | const editMarkersRef = useRef<mapboxgl.Marker[]>([]); |
| 69 | const drawModeRef = useRef(false); |
| 70 | const editBoundaryFieldIdRef = useRef(editBoundaryFieldId); |
| 71 | const allFieldsRef = useRef(allFields); |
| 72 | const onFieldClickRef = useRef(onFieldClickOnMap); |
| 73 | const onUpdateFieldRef = useRef(onUpdateField); |
| 74 | |
| 75 | useEffect(() => { drawModeRef.current = drawMode; }, [drawMode]); |
| 76 | useEffect(() => { editBoundaryFieldIdRef.current = editBoundaryFieldId; }, [editBoundaryFieldId]); |
| 77 | useEffect(() => { allFieldsRef.current = allFields; }, [allFields]); |
| 78 | useEffect(() => { onFieldClickRef.current = onFieldClickOnMap; }, [onFieldClickOnMap]); |
| 79 | useEffect(() => { onUpdateFieldRef.current = onUpdateField; }, [onUpdateField]); |
| 80 | |
| 81 | useEffect(() => { |
| 82 | const fetchToken = async () => { |
| 83 | try { |
| 84 | const { data } = await callBackend("get-mapbox-token"); |
| 85 | if (data?.token) setMapToken(data.token); |
| 86 | } catch (e) { console.error("Failed to fetch mapbox token", e); } |
| 87 | }; |
| 88 | fetchToken(); |
| 89 | }, []); |
| 90 | |
| 91 | useEffect(() => { |
| 92 | if (!flyToField || !mapRef.current) return; |
| 93 | const coords = flyToField.coordinates[0]; |
| 94 | const lat = coords.reduce((sum, c) => sum + c[1], 0) / coords.length; |
| 95 | const lng = coords.reduce((sum, c) => sum + c[0], 0) / coords.length; |
| 96 | mapRef.current.flyTo({ center: [lng, lat], zoom: 16, duration: 1500 }); |
| 97 | onFlyToDone?.(); |
| 98 | }, [flyToField, onFlyToDone]); |
| 99 | |
| 100 | const refreshFieldLayers = useCallback((map: mapboxgl.Map, fields: Field[], selected: Field[]) => { |
| 101 | if (!map.isStyleLoaded()) return; |
| 102 | const style = map.getStyle(); |
| 103 | if (style?.layers) { |
| 104 | style.layers.filter((l) => l.id.startsWith("field-fill-") || l.id.startsWith("field-line-")).forEach((l) => { try { map.removeLayer(l.id); } catch {} }); |
| 105 | } |
| 106 | if (style?.sources) { |
| 107 | Object.keys(style.sources).filter((s) => s.startsWith("field-")).forEach((s) => { try { map.removeSource(s); } catch {} }); |
| 108 | } |
| 109 | fields.forEach((field) => { |
| 110 | const sourceId = `field-${field.id}`; |
| 111 | const isSelected = selected.some((f) => f.id === field.id); |
nothing calls this directly
no test coverage detected