131 lines
3.8 KiB
TypeScript
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;
|