feat: conversation from event

This commit is contained in:
Leon Morival 2026-05-12 14:48:25 +02:00
parent 8ff01f5f37
commit 88253c3ca6
2 changed files with 209 additions and 31 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 MiB

View File

@ -1,21 +1,85 @@
import { useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import L from "leaflet"; import L from "leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import "./App.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() { function App() {
const mapRef = useRef<HTMLDivElement | null>(null); const mapRef = useRef<HTMLDivElement | null>(null);
const map = useRef<any>(null); const map = useRef<LeafletMap | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null); const imageRef = useRef<HTMLImageElement | null>(null);
const drawRef = useRef<() => void>(() => {});
const centerMarker = useRef<any>(null); const mapLayer = useRef<LeafletTileLayer | null>(null);
const cornerMarkers = useRef<any[]>([]); const satelliteLayer = useRef<LeafletTileLayer | null>(null);
const imageUrl = "/V5.png"; const centerMarker = useRef<LeafletMarker | null>(null);
const cornerMarkers = useRef<LeafletMarker[]>([]);
const pointMarker = useRef<LeafletMarker | null>(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<BaseLayerMode>("map");
const [rotation, setRotation] = useState(0); const [rotation, setRotation] = useState(0);
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
@ -26,23 +90,74 @@ function App() {
const toRad = (d: number) => (d * Math.PI) / 180; const toRad = (d: number) => (d * Math.PI) / 180;
const getMetersPerPixel = (zoom: number) => const getMetersPerPixel = useCallback(
(156543.03392 * Math.cos((center.lat * Math.PI) / 180)) / Math.pow(2, zoom); (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); const metersPerPixel = getMetersPerPixel(zoom);
return { return {
widthPx: (imgWidth * scale) / metersPerPixel, widthPx: (imgWidth * scale) / metersPerPixel,
heightPx: (imgHeight * 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) // CORNERS (SIG CORRECT : project/unproject)
// -------------------------------------------------- // --------------------------------------------------
const computeCorners = () => { const computeCorners = useCallback((): Corner[] => {
const m = map.current; const m = map.current;
if (!m) return [];
const zoom = m.getZoom(); const zoom = m.getZoom();
const centerPoint = m.latLngToLayerPoint(center); const centerPoint = m.latLngToLayerPoint(center);
const { widthPx, heightPx } = getImageSizeInScreenPixels(zoom); const { widthPx, heightPx } = getImageSizeInScreenPixels(zoom);
@ -68,13 +183,14 @@ function App() {
return { name: p.name, latlng }; return { name: p.name, latlng };
}); });
}; }, [center, getImageSizeInScreenPixels, rotation]);
// -------------------------------------------------- // --------------------------------------------------
// MARKERS CORNERS // MARKERS CORNERS
// -------------------------------------------------- // --------------------------------------------------
const updateCorners = (corners: any[]) => { const updateCorners = useCallback((corners: Corner[]) => {
const m = map.current; const m = map.current;
if (!m) return;
corners.forEach((c, i) => { corners.forEach((c, i) => {
if (!cornerMarkers.current[i]) { if (!cornerMarkers.current[i]) {
@ -93,12 +209,12 @@ function App() {
cornerMarkers.current[i].setLatLng(c.latlng); cornerMarkers.current[i].setLatLng(c.latlng);
} }
}); });
}; }, []);
// -------------------------------------------------- // --------------------------------------------------
// DRAW IMAGE OVERLAY // DRAW IMAGE OVERLAY
// -------------------------------------------------- // --------------------------------------------------
const draw = () => { const draw = useCallback(() => {
if (!map.current || !canvasRef.current || !imageRef.current) return; if (!map.current || !canvasRef.current || !imageRef.current) return;
const m = map.current; const m = map.current;
@ -111,7 +227,9 @@ function App() {
canvas.style.width = `${size.x}px`; canvas.style.width = `${size.x}px`;
canvas.style.height = `${size.y}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)); L.DomUtil.setPosition(canvas, panePos.multiplyBy(-1));
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -138,13 +256,14 @@ function App() {
const corners = computeCorners(); const corners = computeCorners();
updateCorners(corners); updateCorners(corners);
}; }, [center, computeCorners, getImageSizeInScreenPixels, opacity, rotation, updateCorners]);
// -------------------------------------------------- // --------------------------------------------------
// EXPORT GDAL (RESTORED) // EXPORT GDAL (RESTORED)
// -------------------------------------------------- // --------------------------------------------------
const exportGCP = () => { const exportGCP = () => {
const gcp = computeCorners(); const gcp = computeCorners();
if (gcp.length < 4) return;
const text = ` const text = `
# GDAL GCP EXPORT # GDAL GCP EXPORT
@ -168,16 +287,28 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif
// INIT MAP // INIT MAP
// -------------------------------------------------- // --------------------------------------------------
useEffect(() => { useEffect(() => {
if (map.current) return; drawRef.current = draw;
}, [draw]);
map.current = L.map(mapRef.current!).setView( useEffect(() => {
[center.lat, center.lng], if (map.current || !mapRef.current) return;
const createdMap = L.map(mapRef.current).setView(
[INITIAL_CENTER.lat, INITIAL_CENTER.lng],
13 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", 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"); const canvas = document.createElement("canvas");
canvas.style.position = "absolute"; canvas.style.position = "absolute";
@ -186,29 +317,35 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif
canvas.style.pointerEvents = "none"; canvas.style.pointerEvents = "none";
canvas.style.zIndex = "400"; canvas.style.zIndex = "400";
map.current.getPanes().overlayPane.appendChild(canvas); createdMap.getPanes().overlayPane.appendChild(canvas);
canvasRef.current = canvas; canvasRef.current = canvas;
imageRef.current = new Image(); 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, draggable: true,
}).addTo(map.current); }).addTo(createdMap);
centerMarker.current.on("drag", (e: any) => { marker.on("drag", (e: MarkerDragEvent) => {
const p = e.target.getLatLng(); const p = e.target.getLatLng();
setCenter({ lat: p.lat, lng: p.lng }); 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(() => { useEffect(() => {
draw(); draw();
}, [center, rotation, scale, opacity, imgWidth, imgHeight]); }, [draw]);
// -------------------------------------------------- // --------------------------------------------------
// UI // UI
@ -219,6 +356,12 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif
<div style={{ width: 320, background: "#111", color: "#0f0", padding: 12 }}> <div style={{ width: 320, background: "#111", color: "#0f0", padding: 12 }}>
<h3>Image control</h3> <h3>Image control</h3>
<button onClick={toggleBaseLayer}>
{baseLayerMode === "map" ? "Vue satellite" : "Vue plan"}
</button>
<hr />
<label>Width</label> <label>Width</label>
<input <input
type="number" type="number"
@ -273,6 +416,41 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif
<button onClick={exportGCP}> <button onClick={exportGCP}>
📦 Export GDAL GCP 📦 Export GDAL GCP
</button> </button>
<hr />
<h3>Point</h3>
<p>Clique sur la carte pour poser le point.</p>
{selectedPoint ? (
<>
<div>
<label>Latitude</label>
<input
readOnly
value={formatCoordinate(selectedPoint.lat)}
/>
</div>
<div>
<label>Longitude</label>
<input
readOnly
value={formatCoordinate(selectedPoint.lng)}
/>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(
`${formatCoordinate(selectedPoint.lat)}, ${formatCoordinate(selectedPoint.lng)}`
);
}}
>
Copier les coordonnées
</button>
</>
) : (
<p>Aucun point posé.</p>
)}
</div> </div>
{/* MAP */} {/* MAP */}