feat: conversation from event
This commit is contained in:
parent
8ff01f5f37
commit
88253c3ca6
BIN
public/v5x5.png
BIN
public/v5x5.png
Binary file not shown.
|
Before Width: | Height: | Size: 202 MiB |
240
src/App.tsx
240
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 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 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue