allow to place popups ontop of geojson elements

This commit is contained in:
Anton Tranelis 2024-05-09 11:03:49 +02:00
parent 6b8e0562f3
commit 8bfef3c072
4 changed files with 81 additions and 64 deletions

6
package-lock.json generated
View File

@ -906,9 +906,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001547",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz",
"integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==",
"version": "1.0.30001617",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz",
"integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==",
"dev": true,
"funding": [
{

View File

@ -1,13 +1,11 @@
.leaflet-container {
text-align: left;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-attachment: fixed;
background-position: 50% 80%;
}
input {
box-sizing: border-box;
}

View File

@ -1,7 +1,7 @@
import { TileLayer, MapContainer, useMapEvents, GeoJSON } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import * as React from "react";
import { Geometry, Item, LayerProps, UtopiaMapProps } from "../../types"
import { UtopiaMapProps } from "../../types"
import "./UtopiaMap.css"
import { LatLng } from "leaflet";
import MarkerClusterGroup from 'react-leaflet-cluster'
@ -14,21 +14,10 @@ import { QuestControl } from "./Subcomponents/Controls/QuestControl";
import { Control } from "./Subcomponents/Controls/Control";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { TagsControl } from "./Subcomponents/Controls/TagsControl";
import { useSelectPosition, useSetSelectPosition } from "./hooks/useSelectPosition";
import { useUpdateItem } from "./hooks/useItems";
import { toast } from "react-toastify";
import { useSelectPosition, useSetMapClicked,useSetSelectPosition } from "./hooks/useSelectPosition";
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
import { Feature, Geometry as GeoJSONGeometry } from 'geojson';
export interface MapEventListenerProps {
selectNewItemPosition: LayerProps | Item | null,
setSelectNewItemPosition: React.Dispatch<any>,
setItemFormPopup: React.Dispatch<React.SetStateAction<any>>
}
// for refreshing map on resize (needs to be implemented)
const mapDivRef = React.createRef();
@ -38,60 +27,38 @@ function UtopiaMap({
center = [50.6, 9.5],
zoom = 10,
children,
geo}
geo }
: UtopiaMapProps) {
function MapEventListener(props: MapEventListenerProps) {
function MapEventListener() {
useMapEvents({
click: (e) => {
let params = new URLSearchParams(window.location.search);
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`)
document.title = document.title.split("-")[0];
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title);
document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
resetMetaTags();
console.log(e.latlng.lat + ',' + e.latlng.lng);
if (selectNewItemPosition != null) {
if ('menuIcon' in selectNewItemPosition) {
props.setItemFormPopup({ layer: props.selectNewItemPosition, position: e.latlng })
props.setSelectNewItemPosition(null)
}
if ('text' in selectNewItemPosition) {
const position = new Geometry(e.latlng.lng,e.latlng.lat);
itemUpdate({...selectNewItemPosition as Item, position: position })
setSelectNewItemPosition(null);
}
}
selectNewItemPosition && setMapClicked({ position: e.latlng, setItemFormPopup: setItemFormPopup })
},
moveend: (e) => {
console.log(e);
},
}
})
return null
}
const itemUpdate = async (updatedItem: Item) => {
let success = false;
try {
await updatedItem?.layer?.api?.updateItem!({id: updatedItem.id, position: updatedItem.position })
success = true;
} catch (error) {
toast.error(error.toString());
}
if (success) {
updateItem(updatedItem)
toast.success("Item position updated");
navigate("/" + updatedItem.layer?.name + "/" + updatedItem.id)
}
const resetMetaTags = () => {
let params = new URLSearchParams(window.location.search);
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`)
document.title = document.title.split("-")[0];
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title);
document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
}
const selectNewItemPosition = useSelectPosition();
const setSelectNewItemPosition = useSetSelectPosition();
const location = useLocation();
const updateItem = useUpdateItem();
const navigate = useNavigate();
const setClusterRef = useSetClusterRef();
const clusterRef = useClusterRef();
const setMapClicked = useSetMapClicked();
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
@ -102,9 +69,9 @@ function UtopiaMap({
const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => {
if (feature.properties) {
layer.bindPopup(feature.properties.name);
console.log(feature);
layer.bindPopup(feature.properties.name);
console.log(feature);
}
}
@ -125,7 +92,7 @@ function UtopiaMap({
maxZoom={19}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
<MarkerClusterGroup ref={(r)=> setClusterRef(r)} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
<MarkerClusterGroup ref={(r) => setClusterRef(r)} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
{
React.Children.toArray(children).map((child) =>
React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null, clusterRef: React.MutableRefObject<undefined> }>(child) ?
@ -133,8 +100,13 @@ function UtopiaMap({
)
}
</MarkerClusterGroup>
{geo && <GeoJSON data={geo} onEachFeature={onEachFeature}/>}
<MapEventListener setSelectNewItemPosition={setSelectNewItemPosition} selectNewItemPosition={selectNewItemPosition} setItemFormPopup={setItemFormPopup} />
{geo && <GeoJSON data={geo} onEachFeature={onEachFeature} eventHandlers={{
click: (e) => {
e.layer!.closePopup();
selectNewItemPosition && setMapClicked({ position: e.latlng, setItemFormPopup: setItemFormPopup })
},
}} />}
<MapEventListener />
</MapContainer>
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
{selectNewItemPosition != null &&

View File

@ -1,8 +1,16 @@
import { createContext, useContext, useEffect, useState } from "react";
import { Item, LayerProps } from '../../../types';
import { Geometry, Item, LayerProps } from '../../../types';
import { useUpdateItem } from "./useItems";
import { toast } from "react-toastify";
import { useHasUserPermission } from "./usePermissions";
import { LatLng } from "leaflet";
import { ItemFormPopupProps } from "../Subcomponents/ItemFormPopup";
import { useNavigate } from "react-router-dom";
type PolygonClickedProps = {
position: LatLng
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}
type UseSelectPositionManagerResult = ReturnType<typeof useSelectPositionManager>;
@ -10,26 +18,46 @@ const SelectPositionContext = createContext<UseSelectPositionManagerResult>({
selectPosition: null,
setSelectPosition: () => { },
setMarkerClicked: () => { },
setMapClicked: () => {},
});
function useSelectPositionManager(): {
selectPosition: Item | LayerProps | null;
setSelectPosition: React.Dispatch<React.SetStateAction<Item | LayerProps | null>>;
setMarkerClicked: React.Dispatch<React.SetStateAction<Item>>;
setMapClicked: React.Dispatch<React.SetStateAction<PolygonClickedProps | undefined>>;
} {
const [selectPosition, setSelectPosition] = useState<LayerProps | null | Item>(null);
const [markerClicked, setMarkerClicked] = useState<Item | null>();
const [mapClicked, setMapClicked] = useState<PolygonClickedProps>();
const updateItem = useUpdateItem();
const hasUserPermission = useHasUserPermission();
useEffect(() => {
if (selectPosition && markerClicked && 'text' in selectPosition) {
itemUpdate({ ...selectPosition, parent: markerClicked.id })
itemUpdateParent({ ...selectPosition, parent: markerClicked.id })
}
}, [markerClicked])
const itemUpdate = async (updatedItem: Item) => {
useEffect(() => {
if (selectPosition != null) {
if ('menuIcon' in selectPosition) {
mapClicked && mapClicked.setItemFormPopup({ layer: selectPosition as LayerProps, position: mapClicked?.position })
setSelectPosition(null)
}
if ('text' in selectPosition) {
const position = mapClicked?.position.lng && new Geometry(mapClicked?.position.lng, mapClicked?.position.lat);
position && itemUpdatePosition({ ...selectPosition as Item, position: position })
setSelectPosition(null);
}
}
}, [mapClicked])
const itemUpdateParent = async (updatedItem: Item) => {
if (markerClicked?.layer?.api?.collectionName && hasUserPermission(markerClicked?.layer?.api?.collectionName, "update", markerClicked)) {
let success = false;
try {
@ -53,6 +81,20 @@ function useSelectPositionManager(): {
}
const itemUpdatePosition = async (updatedItem: Item) => {
let success = false;
try {
await updatedItem?.layer?.api?.updateItem!({ id: updatedItem.id, position: updatedItem.position })
success = true;
} catch (error) {
toast.error(error.toString());
}
if (success) {
updateItem(updatedItem)
toast.success("Item position updated");
}
}
const linkItem = async (id: string) => {
if (markerClicked) {
let new_relations = markerClicked.relations || [];
@ -75,7 +117,7 @@ function useSelectPositionManager(): {
}
}
}
return { selectPosition, setSelectPosition, setMarkerClicked };
return { selectPosition, setSelectPosition, setMarkerClicked, setMapClicked };
}
export const SelectPositionProvider: React.FunctionComponent<{
@ -99,4 +141,9 @@ export const useSetSelectPosition = (): UseSelectPositionManagerResult["setSelec
export const useSetMarkerClicked = (): UseSelectPositionManagerResult["setMarkerClicked"] => {
const { setMarkerClicked } = useContext(SelectPositionContext);
return setMarkerClicked;
}
export const useSetMapClicked = (): UseSelectPositionManagerResult["setMapClicked"] => {
const { setMapClicked } = useContext(SelectPositionContext);
return setMapClicked;
}