diff --git a/public/v5x5.png b/public/v5x5.png deleted file mode 100644 index 6a9e37c..0000000 Binary files a/public/v5x5.png and /dev/null differ diff --git a/src/App.tsx b/src/App.tsx index 5c0dde3..186cb83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,85 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import "./App.css"; +const IMAGE_URL = "/V5.png"; +const INITIAL_CENTER = { lat: 43.5488, lng: 7.0176 }; + +type BaseLayerMode = "map" | "satellite"; + +type LatLng = { + lat: number; + lng: number; +}; + +type LatLngExpression = LatLng | [number, number]; + +type LeafletPoint = { + x: number; + y: number; + subtract: (point: LeafletPoint) => LeafletPoint; + multiplyBy: (factor: number) => LeafletPoint; +}; + +type LeafletMap = { + setView: (center: [number, number], zoom: number) => LeafletMap; + getZoom: () => number; + latLngToLayerPoint: (latlng: LatLng) => LeafletPoint; + layerPointToLatLng: (point: LeafletPoint) => LatLng; + getSize: () => { x: number; y: number }; + getPanes: () => { overlayPane: HTMLElement }; + on: (eventName: string, handler: (event: unknown) => void) => LeafletMap; +}; + +type LeafletTileLayer = { + addTo: (map: LeafletMap) => LeafletTileLayer; + remove: () => LeafletTileLayer; +}; + +type LeafletMarker = { + addTo: (map: LeafletMap) => LeafletMarker; + bindTooltip: ( + content: string, + options?: { permanent?: boolean; direction?: string } + ) => LeafletMarker; + on: (eventName: string, handler: (event: MarkerDragEvent) => void) => LeafletMarker; + setLatLng: (latlng: LatLngExpression) => LeafletMarker; +}; + +type MarkerDragEvent = { + target: { + getLatLng: () => LatLng; + }; +}; + +type MapClickEvent = { + latlng: LatLng; +}; + +type Corner = { + name: string; + latlng: LatLng; +}; + function App() { const mapRef = useRef(null); - const map = useRef(null); + const map = useRef(null); const canvasRef = useRef(null); const imageRef = useRef(null); + const drawRef = useRef<() => void>(() => {}); - const centerMarker = useRef(null); - const cornerMarkers = useRef([]); + const mapLayer = useRef(null); + const satelliteLayer = useRef(null); - const imageUrl = "/V5.png"; + const centerMarker = useRef(null); + const cornerMarkers = useRef([]); + const pointMarker = useRef(null); - const [center, setCenter] = useState({ lat: 43.5488, lng: 7.0176 }); + const [center, setCenter] = useState(INITIAL_CENTER); + const [selectedPoint, setSelectedPoint] = useState<{ lat: number; lng: number } | null>(null); + const [baseLayerMode, setBaseLayerMode] = useState("map"); const [rotation, setRotation] = useState(0); const [scale, setScale] = useState(1); @@ -26,23 +90,74 @@ function App() { const toRad = (d: number) => (d * Math.PI) / 180; - const getMetersPerPixel = (zoom: number) => - (156543.03392 * Math.cos((center.lat * Math.PI) / 180)) / Math.pow(2, zoom); + const getMetersPerPixel = useCallback( + (zoom: number) => + (156543.03392 * Math.cos((center.lat * Math.PI) / 180)) / Math.pow(2, zoom), + [center.lat] + ); - const getImageSizeInScreenPixels = (zoom: number) => { + const getImageSizeInScreenPixels = useCallback((zoom: number) => { const metersPerPixel = getMetersPerPixel(zoom); return { widthPx: (imgWidth * scale) / metersPerPixel, heightPx: (imgHeight * scale) / metersPerPixel, }; + }, [getMetersPerPixel, imgHeight, imgWidth, scale]); + + const formatCoordinate = (value: number) => value.toFixed(6); + + const toggleBaseLayer = () => { + const currentMap = map.current; + if (!currentMap) return; + + setBaseLayerMode((currentMode) => { + const nextMode = currentMode === "map" ? "satellite" : "map"; + const currentLayer = currentMode === "map" ? mapLayer.current : satelliteLayer.current; + const nextLayer = nextMode === "map" ? mapLayer.current : satelliteLayer.current; + + currentLayer?.remove(); + nextLayer?.addTo(currentMap); + + return nextMode; + }); }; + const placePoint = useCallback((latlng: LatLng) => { + const point = { lat: latlng.lat, lng: latlng.lng }; + setSelectedPoint(point); + + const currentMap = map.current; + if (!currentMap) return; + + if (!pointMarker.current) { + const marker: LeafletMarker = L.marker([point.lat, point.lng], { + draggable: true, + }) + .addTo(currentMap) + .bindTooltip("Point", { + permanent: true, + direction: "top", + }); + + marker.on("dragend", (e: MarkerDragEvent) => { + const p = e.target.getLatLng(); + setSelectedPoint({ lat: p.lat, lng: p.lng }); + }); + + pointMarker.current = marker; + } else { + pointMarker.current.setLatLng([point.lat, point.lng]); + } + }, []); + // -------------------------------------------------- // CORNERS (SIG CORRECT : project/unproject) // -------------------------------------------------- - const computeCorners = () => { + const computeCorners = useCallback((): Corner[] => { const m = map.current; + if (!m) return []; + const zoom = m.getZoom(); const centerPoint = m.latLngToLayerPoint(center); const { widthPx, heightPx } = getImageSizeInScreenPixels(zoom); @@ -68,13 +183,14 @@ function App() { return { name: p.name, latlng }; }); - }; + }, [center, getImageSizeInScreenPixels, rotation]); // -------------------------------------------------- // MARKERS CORNERS // -------------------------------------------------- - const updateCorners = (corners: any[]) => { + const updateCorners = useCallback((corners: Corner[]) => { const m = map.current; + if (!m) return; corners.forEach((c, i) => { if (!cornerMarkers.current[i]) { @@ -93,12 +209,12 @@ function App() { cornerMarkers.current[i].setLatLng(c.latlng); } }); - }; + }, []); // -------------------------------------------------- // DRAW IMAGE OVERLAY // -------------------------------------------------- - const draw = () => { + const draw = useCallback(() => { if (!map.current || !canvasRef.current || !imageRef.current) return; const m = map.current; @@ -111,7 +227,9 @@ function App() { canvas.style.width = `${size.x}px`; canvas.style.height = `${size.y}px`; - const panePos = L.DomUtil.getPosition(m.getPanes().overlayPane) ?? L.point(0, 0); + const panePos = ( + L.DomUtil.getPosition(m.getPanes().overlayPane) ?? L.point(0, 0) + ) as LeafletPoint; L.DomUtil.setPosition(canvas, panePos.multiplyBy(-1)); ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -138,13 +256,14 @@ function App() { const corners = computeCorners(); updateCorners(corners); - }; + }, [center, computeCorners, getImageSizeInScreenPixels, opacity, rotation, updateCorners]); // -------------------------------------------------- // EXPORT GDAL (RESTORED) // -------------------------------------------------- const exportGCP = () => { const gcp = computeCorners(); + if (gcp.length < 4) return; const text = ` # GDAL GCP EXPORT @@ -168,16 +287,28 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif // INIT MAP // -------------------------------------------------- useEffect(() => { - if (map.current) return; + drawRef.current = draw; + }, [draw]); - map.current = L.map(mapRef.current!).setView( - [center.lat, center.lng], + useEffect(() => { + if (map.current || !mapRef.current) return; + + const createdMap = L.map(mapRef.current).setView( + [INITIAL_CENTER.lat, INITIAL_CENTER.lng], 13 - ); + ) as LeafletMap; + map.current = createdMap; - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + mapLayer.current = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OSM", - }).addTo(map.current); + }).addTo(createdMap); + + satelliteLayer.current = L.tileLayer( + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + { + attribution: "Tiles © Esri", + } + ); const canvas = document.createElement("canvas"); canvas.style.position = "absolute"; @@ -186,29 +317,35 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif canvas.style.pointerEvents = "none"; canvas.style.zIndex = "400"; - map.current.getPanes().overlayPane.appendChild(canvas); + createdMap.getPanes().overlayPane.appendChild(canvas); canvasRef.current = canvas; imageRef.current = new Image(); - imageRef.current.src = imageUrl; + imageRef.current.src = IMAGE_URL; - imageRef.current.onload = () => draw(); + imageRef.current.onload = () => drawRef.current(); - centerMarker.current = L.marker([center.lat, center.lng], { + const marker: LeafletMarker = L.marker([INITIAL_CENTER.lat, INITIAL_CENTER.lng], { draggable: true, - }).addTo(map.current); + }).addTo(createdMap); - centerMarker.current.on("drag", (e: any) => { + marker.on("drag", (e: MarkerDragEvent) => { const p = e.target.getLatLng(); setCenter({ lat: p.lat, lng: p.lng }); }); - map.current.on("move zoom", () => draw()); - }, []); + centerMarker.current = marker; + + createdMap.on("click", (e: unknown) => { + placePoint((e as MapClickEvent).latlng); + }); + + createdMap.on("move zoom", () => drawRef.current()); + }, [placePoint]); useEffect(() => { draw(); - }, [center, rotation, scale, opacity, imgWidth, imgHeight]); + }, [draw]); // -------------------------------------------------- // UI @@ -219,6 +356,12 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif

Image control

+ + +
+ 📦 Export GDAL GCP + +
+ +

Point

+

Clique sur la carte pour poser le point.

+ + {selectedPoint ? ( + <> +
+ + +
+
+ + +
+ + + ) : ( +

Aucun point posé.

+ )}
{/* MAP */}