From 538b4b3bf6f070f38d11cba20e2af3b93c8daf1c Mon Sep 17 00:00:00 2001 From: Quentin Roussel Date: Thu, 6 Jun 2024 11:04:55 +0200 Subject: [PATCH] implemented a map zone selector based on squares --- traque-front/components/admin/mapPicker.jsx | 45 +++++----- .../components/admin/mapZoneSelector.jsx | 29 +++++++ .../components/admin/zoneSelector.jsx | 48 +---------- traque-front/components/util/map.jsx | 20 +++++ traque-front/hook/mapDrawing.jsx | 82 ++++++++++++++++++- 5 files changed, 149 insertions(+), 75 deletions(-) create mode 100644 traque-front/components/admin/mapZoneSelector.jsx create mode 100644 traque-front/components/util/map.jsx diff --git a/traque-front/components/admin/mapPicker.jsx b/traque-front/components/admin/mapPicker.jsx index e511c3b..bcb9bb4 100644 --- a/traque-front/components/admin/mapPicker.jsx +++ b/traque-front/components/admin/mapPicker.jsx @@ -2,9 +2,12 @@ import { useLocation } from "@/hook/useLocation"; import { useEffect, useState } from "react"; import "leaflet/dist/leaflet.css"; -import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet"; +import L from "leaflet"; +import { Circle, LayersControl, MapContainer, Marker, Popup, TileLayer, useMap} from "react-leaflet"; import { useMapCircleDraw } from "@/hook/mapDrawing"; import useAdmin from "@/hook/useAdmin"; +import { MapGridZoneSelector } from "./mapZoneSelector.jsx"; + function MapPan(props) { const map = useMap(); @@ -36,7 +39,7 @@ function MapEventListener({ onClick, onMouseMove }) { return null; } -const DEFAULT_ZOOM = 17; +const DEFAULT_ZOOM = 13; export function CircularAreaPicker({ area, setArea, markerPosition, ...props }) { const location = useLocation(Infinity); const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea); @@ -58,38 +61,28 @@ export function CircularAreaPicker({ area, setArea, markerPosition, ...props }) ) } -export const EditMode = { - MIN: 0, - MAX: 1 -} -export function ZonePicker({ minZone, setMinZone, maxZone, setMaxZone, editMode, ...props }) { + +//https://stackoverflow.com/questions/71231865/show-fixed-100-m-x-100-m-grid-on-lowest-zoom-level + +export function ZonePicker({ ...props }) { const location = useLocation(Infinity); - const { handleClick: maxClick, handleMouseMove: maxHover, center: maxCenter, radius: maxRadius } = useMapCircleDraw(minZone, setMinZone); - const { handleClick: minClick, handleMouseMove: minHover, center: minCenter, radius: minRadius } = useMapCircleDraw(maxZone, setMaxZone); - function handleClick(e) { - if (editMode == EditMode.MAX) { - maxClick(e); - } else { - minClick(e); - } - } - function handleMouseMove(e) { - if (editMode == EditMode.MAX) { - maxHover(e); - } else { - minHover(e); - } - } + const [coloredTiles, setColoredTiles] = useState([]); + + useEffect(() => { + console.log(coloredTiles) + }, [coloredTiles]); return ( - {minCenter && minRadius && } - {maxCenter && maxRadius && } - + + + + + ) } diff --git a/traque-front/components/admin/mapZoneSelector.jsx b/traque-front/components/admin/mapZoneSelector.jsx new file mode 100644 index 0000000..ea3f57a --- /dev/null +++ b/traque-front/components/admin/mapZoneSelector.jsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; +import { useLeafletContext } from '@react-leaflet/core'; +import { useMapGrid } from '@/hook/mapDrawing'; +import { latLngToTileNumber } from '../util/map'; + +export function MapGridZoneSelector({ onSelectedTile, tileSize }) { + const [coloredTiles, setColoredTiles] = useState([]); + const { map } = useLeafletContext(); + + useEffect(() => { + map.on('click', (e) => { + const fractionalTileNumber = latLngToTileNumber(e.latlng, tileSize); + const tileNumber = new TileNumber(Math.floor(fractionalTileNumber.x), Math.floor(fractionalTileNumber.y)); + if (coloredTiles.some(t => t.equals(tileNumber))) { + setColoredTiles(coloredTiles.filter(t => !t.equals(tileNumber))); + } else { + setColoredTiles([...coloredTiles, tileNumber]); + } + }); + }); + + useEffect(() => { + onSelectedTile(coloredTiles); + }, [coloredTiles]); + + useMapGrid(coloredTiles, tileSize); + + return null; +} \ No newline at end of file diff --git a/traque-front/components/admin/zoneSelector.jsx b/traque-front/components/admin/zoneSelector.jsx index 2ef1781..1624892 100644 --- a/traque-front/components/admin/zoneSelector.jsx +++ b/traque-front/components/admin/zoneSelector.jsx @@ -5,57 +5,13 @@ import TextInput from "../util/textInput"; import useAdmin from "@/hook/useAdmin"; export function ZoneSelector() { - const [editMode, setEditMode] = useState(EditMode.MIN); - const [minZone, setMinZone] = useState(null); - const [maxZone, setMaxZone] = useState(null); - const [reductionCount, setReductionCount] = useState(""); - const [reductionDuration, setReductionDuration] = useState(""); - const [reductionInterval, setReductionInterval] = useState(""); - const {zoneSettings, changeZoneSettings} = useAdmin(); - - useEffect(() => { - if (zoneSettings) { - setMinZone(zoneSettings.min); - setMaxZone(zoneSettings.max); - setReductionCount(zoneSettings.reductionCount.toString()); - setReductionDuration(zoneSettings.reductionDuration.toString()); - setReductionInterval(zoneSettings.reductionInterval.toString()); - } - }, [zoneSettings]); - - function handleSettingsSubmit() { - changeZoneSettings({min:minZone, max:maxZone, reductionCount: Number(reductionCount), reductionDuration: Number(reductionDuration), reductionInterval: Number(reductionInterval)}); - } //When the user set one zone, switch to the other - useEffect(() => { - if(editMode == EditMode.MIN) { - setEditMode(EditMode.MAX); - }else { - setEditMode(EditMode.MIN); - } - - }, [minZone, maxZone]); - return

Edit zones

- {editMode == EditMode.MIN && setEditMode(EditMode.MAX)}>Edit end zone} - {editMode == EditMode.MAX && setEditMode(EditMode.MIN)}>Edit start zone}
- +
-
-

Number of reductions

- setReductionCount(e.target.value)}> -
-
-

Duration of each reduction

- setReductionDuration(e.target.value)}> -
-
-

Interval between reductions

- setReductionInterval(e.target.value)}> -
- Apply + Apply
} \ No newline at end of file diff --git a/traque-front/components/util/map.jsx b/traque-front/components/util/map.jsx new file mode 100644 index 0000000..39ca395 --- /dev/null +++ b/traque-front/components/util/map.jsx @@ -0,0 +1,20 @@ +export class TileNumber { + constructor(x, y) { + this.x = x; + this.y = y; + } + equals(other) { + return this.x === other.x && this.y === other.y; + } +} + +export function latLngToTileNumber(latLng, tileSize) { + const numTilesX = 2 ** tileSize; + const numTilesY = 2 ** tileSize; + const lngDegrees = latLng.lng; + const latRadians = latLng.lat * (Math.PI / 180); + return new L.Point( + numTilesX * ((lngDegrees + 180) / 360), + numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2 + ); +} \ No newline at end of file diff --git a/traque-front/hook/mapDrawing.jsx b/traque-front/hook/mapDrawing.jsx index 804a99a..0794849 100644 --- a/traque-front/hook/mapDrawing.jsx +++ b/traque-front/hook/mapDrawing.jsx @@ -1,4 +1,7 @@ +import { TileNumber, latLngToTileNumber } from "@/components/util/map"; import { useEffect, useState } from "react"; +import { useLeafletContext } from '@react-leaflet/core'; +import L from 'leaflet'; export function useMapCircleDraw(area, setArea) { const [drawing, setDrawing] = useState(false); @@ -12,18 +15,18 @@ export function useMapCircleDraw(area, setArea) { }, [area]) function handleClick(e) { - if(!drawing) { + if (!drawing) { setCenter(e.latlng); setRadius(null); setDrawing(true); } else { setDrawing(false); - setArea({center, radius}); + setArea({ center, radius }); } } function handleMouseMove(e) { - if(drawing) { + if (drawing) { setRadius(e.latlng.distanceTo(center)); } } @@ -33,4 +36,77 @@ export function useMapCircleDraw(area, setArea) { center, radius, } +} + +export function useMapGrid(coloredTiles, tileSize) { + const { layerContainer, map } = useLeafletContext(); + const [grid, setGrid] = useState(null); + + const Grid = L.GridLayer.extend({ + createTile: function (coords) { + const tile = L.DomUtil.create('canvas', 'leaflet-tile'); + const ctx = tile.getContext('2d'); + const size = this.getTileSize(); + tile.width = size.x + tile.height = size.y + + // calculate projection coordinates of top left tile pixel + const nwPoint = coords.scaleBy(size); + // calculate geographic coordinates of top left tile pixel + const nw = map.unproject(nwPoint, coords.z); + // calculate fraction tile number at top left point + const nwTile = latLngToTileNumber(nw, tileSize) + + // calculate projection coordinates of bottom right tile pixel + const sePoint = new L.Point(nwPoint.x + size.x - 1, nwPoint.y + size.y - 1) + // calculate geographic coordinates of bottom right tile pixel + const se = map.unproject(sePoint, coords.z); + // calculate fractional tile number at bottom right point + const seTile = latLngToTileNumber(se, tileSize) + + const minTileX = nwTile.x + const maxTileX = seTile.x + const minTileY = nwTile.y + const maxTileY = seTile.y + + for (let x = Math.ceil(minTileX) - 1; x <= Math.floor(maxTileX) + 1; x++) { + for (let y = Math.ceil(minTileY) - 1; y <= Math.floor(maxTileY) + 1; y++) { + + let tile = new TileNumber(x, y) + + const xMinPixel = Math.round(size.x * (x - minTileX) / (maxTileX - minTileX)); + const xMaxPixel = Math.round(size.x * (x + 1 - minTileX) / (maxTileX - minTileX)); + const yMinPixel = Math.round(size.y * (y - minTileY) / (maxTileY - minTileY)); + const yMaxPixel = Math.round(size.y * (y + 1 - minTileY) / (maxTileY - minTileY)); + + // fill the rectangle with a color + // if(coloredTiles.some(t => t.equals(tile))) { + // console.log(coloredTiles, "filling tile " + tile.x + " " + tile.y) + // } else { + // console.log(coloredTiles, "not filling tile " + tile.x + " " + tile.y) + // } + ctx.fillStyle = this.coloredTiles && this.coloredTiles.some(t => t.equals(tile)) + ? 'rgba(0, 0, 255, 0.3)' + : 'rgba(255, 255, 255, 0)'; + ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel); + } + } + return tile; + }, + }); + + useEffect(() => { + let g = new Grid({ + minZoom: 12, + }); + setGrid(g); + layerContainer.addLayer(g); + }, []) + + useEffect(() => { + if (grid) { + grid.coloredTiles = coloredTiles; + grid.redraw(); + } + }, [coloredTiles]); } \ No newline at end of file