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 "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 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue