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 "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<HTMLDivElement | null>(null);
const map = useRef<any>(null);
const map = useRef<LeafletMap | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null);
const drawRef = useRef<() => void>(() => {});
const centerMarker = useRef<any>(null);
const cornerMarkers = useRef<any[]>([]);
const mapLayer = useRef<LeafletTileLayer | null>(null);
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 [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
<div style={{ width: 320, background: "#111", color: "#0f0", padding: 12 }}>
<h3>Image control</h3>
<button onClick={toggleBaseLayer}>
{baseLayerMode === "map" ? "Vue satellite" : "Vue plan"}
</button>
<hr />
<label>Width</label>
<input
type="number"
@ -273,6 +416,41 @@ gdalwarp -r bilinear -tps new.tif newfinal.tif
<button onClick={exportGCP}>
📦 Export GDAL GCP
</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>
{/* MAP */}