import { useEffect, useRef, useState } from 'react'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import MaplibreDraw from 'maplibre-gl-draw'; import 'maplibre-gl-draw/dist/mapbox-gl-draw.css'; import { useGeoAIWorker } from './hooks/useGeoAIWorker'; import './App.css'; const config = { provider: "esri", serviceUrl: "https://server.arcgisonline.com/ArcGIS/rest/services", serviceName: "World_Imagery", tileSize: 256, attribution: "ESRI World Imagery", }; function App() { const mapContainer = useRef(null); const map = useRef(null); const drawRef = useRef(null); const [detections, setDetections] = useState([]); const [currentPolygon, setCurrentPolygon] = useState(null); const [isDrawing, setIsDrawing] = useState(false); const [zoom, setZoom] = useState(null); const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light'); // when true we temporarily ignore any existing `result` from the worker (used after Clear) const [suppressResult, setSuppressResult] = useState(false); const { isReady, isProcessing, result, initialize, runInference } = useGeoAIWorker(); useEffect(() => { localStorage.setItem('theme', theme); }, [theme]); const toggleTheme = () => setTheme(t => (t === 'light' ? 'dark' : 'light')); useEffect(() => { initialize([{ task: "building-detection" }], config); }, []); useEffect(() => { if (!mapContainer.current) return; map.current = new maplibregl.Map({ container: mapContainer.current, style: { version: 8, sources: { satellite: { type: 'raster', tiles: [ `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}`, ], tileSize: 256, }, }, layers: [{ id: 'satellite', type: 'raster', source: 'satellite' }], }, center: [-117.59, 47.653], zoom: 18, }); // set initial zoom state setZoom(map.current.getZoom()); // update zoom on changes const onZoom = () => setZoom(Number(map.current.getZoom().toFixed(2))); map.current.on('zoom', onZoom); const draw = new MaplibreDraw({ displayControlsDefault: false, // don't render the default draw toolbar on the map; we'll control draw from the control panel }); drawRef.current = draw; // @ts-ignore map.current.addControl(draw); // cleanup map listeners on unmount const cleanupMap = () => { try { map.current?.off('zoom', onZoom); } catch (e) {} }; map.current.on('draw.create', (e) => { const polygon = e.features[0]; setCurrentPolygon(polygon); // exit draw mode when a polygon is created try { drawRef.current.changeMode('simple_select'); } catch (err) {} setIsDrawing(false); // DO NOT run inference automatically here. User must click Analyze. }); map.current.on('draw.update', (e) => { const polygon = e.features[0]; setCurrentPolygon(polygon); }); map.current.on('draw.delete', (e) => { setCurrentPolygon(null); setDetections([]); setIsDrawing(false); // remove detections layer/source if present if (map.current?.getSource('detections')) { try { map.current.removeLayer('detections'); map.current.removeSource('detections'); } catch (err) {} } }); return () => { cleanupMap(); try { // remove draw control if attached if (map.current && drawRef.current) { try { map.current.removeControl(drawRef.current); } catch (e) {} } map.current?.remove(); } catch (e) {} // clear refs map.current = null; drawRef.current = null; }; }, []); // initialize map once on mount; previously depended on isReady which caused the map to recreate when the worker became ready useEffect(() => { if (!result || suppressResult) return; const features = result.detections?.features || []; setDetections(features); if (map.current?.getSource('detections')) { map.current.removeLayer('detections'); map.current.removeSource('detections'); } // set detection fill color to yellow const fillColor = '#fdb306ff'; map.current?.addSource('detections', { type: 'geojson', data: result.detections }); map.current?.addLayer({ id: 'detections', type: 'fill', source: 'detections', paint: { 'fill-color': fillColor, 'fill-opacity': 0.7 }, }); }, [result, theme]); // manual analyze handler const handleAnalyze = () => { if (!isReady || !currentPolygon) return; // allow the latest result to be shown again when user triggers analyze setSuppressResult(false); runInference({ inputs: { polygon: currentPolygon }, mapSourceParams: { zoomLevel: Math.round(map.current?.getZoom() || 18) }, }); }; // clear / reset everything on the map const handleClear = () => { // remove drawn shapes if (drawRef.current) { try { drawRef.current.deleteAll(); } catch (e) {} } // clear state setCurrentPolygon(null); setDetections([]); // suppress previously computed result so it won't be re-applied to the map setSuppressResult(true); setIsDrawing(false); // remove detections layer/source if present try { if (map.current?.getLayer('detections')) map.current.removeLayer('detections'); } catch (e) {} try { if (map.current?.getSource('detections')) map.current.removeSource('detections'); } catch (e) {} // optionally fly back to initial view try { map.current?.flyTo({ center: [-117.59, 47.653], zoom: 18 }); } catch (e) {} }; const getDetectionsGeoJSON = () => result?.detections || null; const handleCopy = async () => { const geojson = getDetectionsGeoJSON(); if (!geojson) return; try { await navigator.clipboard.writeText(JSON.stringify(geojson, null, 2)); // optionally give feedback } catch (err) { console.error('Copy failed', err); } }; const handleDownload = () => { const geojson = getDetectionsGeoJSON(); if (!geojson) return; const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/geo+json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'detections.geojson'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }; return (
Inspect satellite imagery and detect buildings with GeoAI