geocrop-platform./apps/web/src/MapComponent.tsx

131 lines
3.8 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import XYZ from 'ol/source/XYZ';
import { fromLonLat, toLonLat } from 'ol/proj';
import 'ol/ol.css';
const TITILER_ENDPOINT = 'https://tiles.portfolio.techarvest.co.zw';
// Dynamic World class mapping for legend
const DW_CLASSES = [
{ id: 0, name: "No Data", color: "#000000" },
{ id: 1, name: "Water", color: "#419BDF" },
{ id: 2, name: "Trees", color: "#397D49" },
{ id: 3, name: "Grass", color: "#88B53E" },
{ id: 4, name: "Flooded Veg", color: "#FFAA5D" },
{ id: 5, name: "Crops", color: "#DA913D" },
{ id: 6, name: "Shrub/Scrub", color: "#919636" },
{ id: 7, name: "Built", color: "#B9B9B9" },
{ id: 8, name: "Bare", color: "#D6D6D6" },
{ id: 9, name: "Snow/Ice", color: "#FFFFFF" },
];
interface MapComponentProps {
onCoordsSelected: (lat: number, lon: number) => void;
resultUrl?: string;
roi?: { lat: number, lon: number, radius_m: number };
}
const MapComponent: React.FC<MapComponentProps> = ({ onCoordsSelected, resultUrl, roi }) => {
const mapRef = useRef<HTMLDivElement>(null);
const mapInstance = useRef<Map | null>(null);
const [activeResultLayer, setActiveResultLayer] = useState<TileLayer<XYZ> | null>(null);
useEffect(() => {
if (!mapRef.current) return;
mapInstance.current = new Map({
target: mapRef.current,
layers: [
new TileLayer({
source: new OSM(),
}),
],
view: new View({
center: fromLonLat([29.1549, -19.0154]),
zoom: 6,
}),
});
mapInstance.current.on('click', (event) => {
const coords = toLonLat(event.coordinate);
onCoordsSelected(coords[1], coords[0]);
});
return () => {
if (mapInstance.current) {
mapInstance.current.setTarget(undefined);
}
};
}, []);
// Handle Result Layer and Zoom
useEffect(() => {
if (!mapInstance.current || !resultUrl) return;
// Remove existing result layer if any
if (activeResultLayer) {
mapInstance.current.removeLayer(activeResultLayer);
}
// Add new result layer
// Format: TITILER/cog/tiles/{z}/{x}/{y}?url=S3_URL
const newLayer = new TileLayer({
source: new XYZ({
url: `${TITILER_ENDPOINT}/cog/tiles/{z}/{x}/{y}?url=${resultUrl}`,
}),
});
mapInstance.current.addLayer(newLayer);
setActiveResultLayer(newLayer);
// Zoom to ROI if provided
if (roi) {
mapInstance.current.getView().animate({
center: fromLonLat([roi.lon, roi.lat]),
zoom: 14,
duration: 1000
});
}
}, [resultUrl, roi]);
return (
<div style={{ position: 'relative', width: '100%', height: '100vh' }}>
<div ref={mapRef} style={{ width: '100%', height: '100%' }} />
{/* Map Legend */}
<div style={{
position: 'absolute',
bottom: '30px',
right: '20px',
background: 'rgba(255, 255, 255, 0.9)',
padding: '10px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
zIndex: 1000,
fontSize: '12px',
maxWidth: '150px'
}}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '13px', borderBottom: '1px solid #ddd', paddingBottom: '3px' }}>Class Legend</h4>
{DW_CLASSES.map(cls => (
<div key={cls.id} style={{ display: 'flex', alignItems: 'center', marginBottom: '4px' }}>
<div style={{
width: '12px',
height: '12px',
backgroundColor: cls.color,
marginRight: '8px',
border: '1px solid #999'
}} />
<span>{cls.name}</span>
</div>
))}
</div>
</div>
);
};
export default MapComponent;