From fac127a92f808bac38858ca2ecfe7fef97df04c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Sun, 12 Jan 2025 10:12:25 +0100 Subject: [PATCH] EditDialog: Show markers for all edited items (#888) --- .../EditDialog/EditContent/EditContent.tsx | 1 + .../FeatureEditSection/EditFeatureMap.tsx | 199 ------------------ .../EditFeatureMap/EditFeatureMap.tsx | 68 ++++++ .../EditFeatureMap/useDraggableMarker.ts | 59 ++++++ .../EditFeatureMap/useInitEditFeatureMap.ts | 85 ++++++++ .../EditFeatureMap/useStaticMarkers.tsx | 80 +++++++ .../FeatureEditSection/FeatureEditSection.tsx | 11 +- src/locales/cs.js | 1 + src/locales/vocabulary.js | 1 + 9 files changed, 302 insertions(+), 203 deletions(-) delete mode 100644 src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap.tsx create mode 100644 src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/EditFeatureMap.tsx create mode 100644 src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useDraggableMarker.ts create mode 100644 src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useInitEditFeatureMap.ts create mode 100644 src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useStaticMarkers.tsx diff --git a/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx b/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx index 15af396a..7a71f7f3 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx @@ -30,6 +30,7 @@ export const EditContent = () => { direction={isSmallScreen ? 'column' : 'row'} gap={2} overflow="hidden" + flex={1} > {items.length > 1 && ( ` - visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')}; - height: 100%; - width: 100%; -`; - -const useUpdateFeatureMarker = createMapEffectHook< - [ - { - onMarkerChange: (lngLat: LngLat) => void; - nodeLonLat: LonLat; - markerRef: React.MutableRefObject; - }, - ] ->((map, props) => { - const onDragEnd = () => { - const lngLat = markerRef.current?.getLngLat(); - if (lngLat) { - props.onMarkerChange(lngLat); - } - }; - - const { markerRef, nodeLonLat } = props; - - markerRef.current?.remove(); - markerRef.current = undefined; - - if (nodeLonLat) { - const [lng, lat] = nodeLonLat; - markerRef.current = new maplibregl.Marker({ - color: '#556cd6', - draggable: true, - }) - .setLngLat({ - lng: parseFloat(lng.toFixed(6)), - lat: parseFloat(lat.toFixed(6)), - }) - .addTo(map); - - markerRef.current?.on('dragend', onDragEnd); - } -}); - -const useInitMap = () => { - const containerRef = React.useRef(null); - const mapRef = React.useRef(null); - const [isMapLoaded, setIsMapLoaded] = useState(false); - const [isFirstMapLoad, setIsFirstMapLoad] = useState(true); - - const { current, items } = useEditContext(); - const currentItem = items.find((item) => item.shortId === current); - const markerRef = useRef(); - - const onMarkerChange = ({ lng, lat }: LngLat) => { - const newLonLat = [lng, lat]; - - currentItem.setNodeLonLat(newLonLat); - }; - - useUpdateFeatureMarker(mapRef.current, { - onMarkerChange, - nodeLonLat: currentItem.nodeLonLat, - markerRef, - }); - - React.useEffect(() => { - const geolocation = new maplibregl.GeolocateControl({ - positionOptions: { - enableHighAccuracy: true, - }, - fitBoundsOptions: { - duration: 4000, - }, - trackUserLocation: true, - }); - - setIsMapLoaded(false); - if (!containerRef.current) return undefined; - const map = new maplibregl.Map({ - container: containerRef.current, - style: outdoorStyle, - attributionControl: false, - refreshExpiredTiles: false, - locale: { - 'NavigationControl.ResetBearing': COMPASS_TOOLTIP, - }, - }); - - map.scrollZoom.setWheelZoomRate(1 / 200); // 1/450 is default, bigger value = faster - map.addControl(geolocation); - mapRef.current = map; - - mapRef.current?.on('load', () => { - setIsMapLoaded(true); - }); - - return () => { - if (map) { - map.remove(); - } - }; - }, [containerRef, current]); - - const updateCenter = useCallback(() => { - if (isFirstMapLoad) { - mapRef.current?.jumpTo({ - center: currentItem.nodeLonLat as [number, number], - zoom: 18.5, - }); - setIsFirstMapLoad(false); - } - }, [currentItem.nodeLonLat, isFirstMapLoad]); - - useEffect(() => { - mapRef.current?.on('load', () => { - updateCenter(); - }); - }, [currentItem.nodeLonLat, isFirstMapLoad, updateCenter]); - - useEffect(() => { - updateCenter(); - }, [current, updateCenter]); - - // edit data item switched - useEffect(() => { - setIsFirstMapLoad(true); - }, [current]); - - return { containerRef, isMapLoaded }; -}; - -const EditFeatureMap = () => { - const { containerRef, isMapLoaded } = useInitMap(); - const [expanded, setExpanded] = useState(false); - - const { shortId } = useFeatureEditData(); - const isNode = shortId[0] === 'n'; - - if (!isNode) return null; - - return ( - setExpanded(!expanded)} - > - }> - {t('editdialog.location')} - - - - {!isMapLoaded && ( - - - - )} - - - - - ); -}; -export default EditFeatureMap; // dynamic import diff --git a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/EditFeatureMap.tsx b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/EditFeatureMap.tsx new file mode 100644 index 00000000..b8a8d459 --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/EditFeatureMap.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + CircularProgress, + Typography, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + +import styled from '@emotion/styled'; +import { t } from '../../../../../../services/intl'; +import { useFeatureEditData } from '../SingleFeatureEditContext'; +import { useInitEditFeatureMap } from './useInitEditFeatureMap'; + +const Container = styled.div` + width: 100%; + height: 500px; + position: relative; +`; + +const LoadingContainer = styled.div` + height: 100%; + width: 100%; + position: absolute; + display: flex; + align-items: center; + justify-content: center; +`; + +const Map = styled.div<{ $isVisible: boolean }>` + visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')}; + height: 100%; + width: 100%; +`; + +export default function EditFeatureMap() { + const { containerRef, isMapLoaded } = useInitEditFeatureMap(); + const [expanded, setExpanded] = useState(false); + + const { shortId } = useFeatureEditData(); + const isNode = shortId[0] === 'n'; + if (!isNode) return null; + + return ( + setExpanded(!expanded)} + > + }> + {t('editdialog.location')} + + + + {!isMapLoaded && ( + + + + )} + + + + + ); +} diff --git a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useDraggableMarker.ts b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useDraggableMarker.ts new file mode 100644 index 00000000..90ef42d2 --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useDraggableMarker.ts @@ -0,0 +1,59 @@ +import React, { useRef } from 'react'; +import maplibregl, { LngLat } from 'maplibre-gl'; +import { createMapEffectHook } from '../../../../../helpers'; +import { LonLat } from '../../../../../../services/types'; + +const useUpdateDraggableFeatureMarker = createMapEffectHook< + [ + { + onMarkerChange: (lngLat: LngLat) => void; + nodeLonLat: LonLat; + markerRef: React.MutableRefObject; + }, + ] +>((map, props) => { + const { markerRef, nodeLonLat, onMarkerChange } = props; + + const onDragEnd = () => { + const lngLat = markerRef.current?.getLngLat(); + if (lngLat) { + onMarkerChange(lngLat); + } + }; + + markerRef.current?.remove(); + markerRef.current = undefined; + + if (nodeLonLat) { + const [lng, lat] = nodeLonLat; + markerRef.current = new maplibregl.Marker({ + color: 'salmon', + draggable: true, + }) + .setLngLat({ + lng: parseFloat(lng.toFixed(6)), + lat: parseFloat(lat.toFixed(6)), + }) + .addTo(map); + + markerRef.current?.on('dragend', onDragEnd); + } +}); + +export function useDraggableFeatureMarker( + mapRef: React.MutableRefObject, + currentItem: any, +) { + const markerRef = useRef(); + + const onMarkerChange = ({ lng, lat }: LngLat) => { + const newLonLat = [lng, lat]; + currentItem.setNodeLonLat(newLonLat); + }; + + useUpdateDraggableFeatureMarker(mapRef.current, { + onMarkerChange, + nodeLonLat: currentItem?.nodeLonLat, + markerRef, + }); +} diff --git a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useInitEditFeatureMap.ts b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useInitEditFeatureMap.ts new file mode 100644 index 00000000..5c00e15a --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useInitEditFeatureMap.ts @@ -0,0 +1,85 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import maplibregl from 'maplibre-gl'; +import { outdoorStyle } from '../../../../../Map/styles/outdoorStyle'; +import { COMPASS_TOOLTIP } from '../../../../../Map/useAddTopRightControls'; + +import { useEditContext } from '../../../EditContext'; +import { useFeatureMarkers } from './useStaticMarkers'; +import { useDraggableFeatureMarker } from './useDraggableMarker'; + +export function useInitEditFeatureMap() { + const containerRef = React.useRef(null); + const mapRef = React.useRef(null); + + const [isMapLoaded, setIsMapLoaded] = useState(false); + const [isFirstMapLoad, setIsFirstMapLoad] = useState(true); + + const { current, items, setCurrent } = useEditContext(); + const currentItem = items.find((item) => item.shortId === current); + + useFeatureMarkers(mapRef, items, setCurrent, current); + + useDraggableFeatureMarker(mapRef, currentItem); + + useEffect(() => { + setIsMapLoaded(false); + if (!containerRef.current) return undefined; + + const map = new maplibregl.Map({ + container: containerRef.current, + style: outdoorStyle, + attributionControl: false, + refreshExpiredTiles: false, + locale: { + 'NavigationControl.ResetBearing': COMPASS_TOOLTIP, + }, + }); + + map.scrollZoom.setWheelZoomRate(1 / 200); + + const geolocation = new maplibregl.GeolocateControl({ + positionOptions: { enableHighAccuracy: true }, + fitBoundsOptions: { duration: 4000 }, + trackUserLocation: true, + }); + map.addControl(geolocation); + + mapRef.current = map; + + mapRef.current.on('load', () => { + setIsMapLoaded(true); + }); + + return () => { + if (map) { + map.remove(); + } + }; + }, [containerRef, current]); + + const updateCenter = useCallback(() => { + if (isFirstMapLoad && currentItem?.nodeLonLat) { + mapRef.current?.jumpTo({ + center: currentItem.nodeLonLat as [number, number], + zoom: 18.5, + }); + setIsFirstMapLoad(false); + } + }, [currentItem?.nodeLonLat, isFirstMapLoad]); + + useEffect(() => { + mapRef.current?.on('load', () => { + updateCenter(); + }); + }, [updateCenter]); + + useEffect(() => { + updateCenter(); + }, [current, updateCenter]); + + useEffect(() => { + setIsFirstMapLoad(true); + }, [current]); + + return { containerRef, isMapLoaded }; +} diff --git a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useStaticMarkers.tsx b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useStaticMarkers.tsx new file mode 100644 index 00000000..0254223c --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap/useStaticMarkers.tsx @@ -0,0 +1,80 @@ +import React, { useRef } from 'react'; +import ReactDOM from 'react-dom'; +import maplibregl from 'maplibre-gl'; +import { Button, Stack, Typography } from '@mui/material'; +import { createMapEffectHook } from '../../../../../helpers'; +import { t } from '../../../../../../services/intl'; + +const useUpdateFeatureMarkers = createMapEffectHook< + [ + { + markerRefs: React.MutableRefObject; + items: any[]; + setCurrent: (shortId: string) => void; + current: string; + }, + ] +>((map, { items, markerRefs, setCurrent, current }) => { + markerRefs.current.forEach((m) => m.remove()); + markerRefs.current = []; + + items.forEach((item) => { + if (!item.nodeLonLat || item.shortId === current) return; + const [lng, lat] = item.nodeLonLat; + + const marker = new maplibregl.Marker({ + color: '#555', + opacity: '0.4', + }) + .setLngLat({ + lng: parseFloat(lng.toFixed(6)), + lat: parseFloat(lat.toFixed(6)), + }) + .addTo(map); + + const popupContainer = document.createElement('div'); + + const MyPopupContent = () => { + return ( + + + {item.tags.name} + + + + ); + }; + + // eslint-disable-next-line react/no-deprecated + ReactDOM.render(, popupContainer); + + const popup = new maplibregl.Popup({ offset: 25 }).setDOMContent( + popupContainer, + ); + marker.setPopup(popup); + + markerRefs.current.push(marker); + }); +}); + +export function useFeatureMarkers( + mapRef: React.MutableRefObject, + items: any[], + setCurrent: (shortId: string) => void, + current: string, +) { + const markerRefs = useRef([]); + + useUpdateFeatureMarkers(mapRef.current, { + markerRefs, + items, + setCurrent, + current, + }); +} diff --git a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx index bc4e4f0f..e451a965 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx @@ -10,10 +10,13 @@ import { MembersEditor } from '../MembersEditor'; import { ParentsEditor } from '../ParentsEditor'; import dynamic from 'next/dynamic'; -const EditFeatureMapDynamic = dynamic(() => import('./EditFeatureMap'), { - ssr: false, - loading: () =>
, -}); +const EditFeatureMapDynamic = dynamic( + () => import('./EditFeatureMap/EditFeatureMap'), + { + ssr: false, + loading: () =>
, + }, +); import { Stack, Typography } from '@mui/material'; import { useEditContext } from '../../EditContext'; diff --git a/src/locales/cs.js b/src/locales/cs.js index ee4184fe..37eeb5a4 100644 --- a/src/locales/cs.js +++ b/src/locales/cs.js @@ -179,6 +179,7 @@ export default { 'editdialog.tags_editor_info': `Tagy popisují vlastnosti mapového prvku v dohodnutém formátu. Zde naleznete úplný přehled všech tagů v OpenStreetMap.`, 'editdialog.save_refused': 'Změny se nepodařilo uložit.', + 'editdialog.location_change_current_item': 'Upravit', 'editsuccess.close_button': 'Zavřít', 'editsuccess.note.heading': 'Děkujeme za Váš návrh!', diff --git a/src/locales/vocabulary.js b/src/locales/vocabulary.js index 65b5f6d4..1d032c0d 100644 --- a/src/locales/vocabulary.js +++ b/src/locales/vocabulary.js @@ -220,6 +220,7 @@ export default { 'editdialog.save_refused': 'Unable to save your changes.', 'editdialog.parents': 'Parents', 'editdialog.members': 'Members', + 'editdialog.location_change_current_item': 'Edit', 'editsuccess.close_button': 'Done', 'editsuccess.note.heading': 'Thank you for your suggestion!',