diff --git a/src/components/FeaturePanel/Climbing/CragMap.tsx b/src/components/FeaturePanel/Climbing/CragMap.tsx index 057d54e2..8f3e9844 100644 --- a/src/components/FeaturePanel/Climbing/CragMap.tsx +++ b/src/components/FeaturePanel/Climbing/CragMap.tsx @@ -186,6 +186,8 @@ const useInitMap = () => { const containerRef = React.useRef(null); const mapRef = React.useRef(null); const [isMapLoaded, setIsMapLoaded] = useState(false); + const [isFirstMapLoad, setIsFirstMapLoad] = useState(true); + const { feature } = useFeatureContext(); const { photoPaths } = useClimbingContext(); @@ -230,7 +232,18 @@ const useInitMap = () => { mapRef.current.on('load', () => { setIsMapLoaded(true); - if (mapRef.current) { + }); + + return () => { + if (map) { + map.remove(); + } + }; + }, [containerRef]); + + useEffect(() => { + mapRef.current?.on('load', () => { + if (isFirstMapLoad) { mapRef.current.jumpTo({ center: feature.center as [number, number], zoom: 18.5, @@ -240,15 +253,15 @@ const useInitMap = () => { type: 'FeatureCollection' as const, features: transformMemberFeaturesToGeojson(feature.memberFeatures), }); + setIsFirstMapLoad(false); } }); - - return () => { - if (map) { - map.remove(); - } - }; - }, [containerRef, feature.center, feature.memberFeatures, getClimbingSource]); + }, [ + feature.center, + feature.memberFeatures, + getClimbingSource, + isFirstMapLoad, + ]); return { containerRef, isMapLoaded }; }; diff --git a/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx b/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx index 9718b155..15af396a 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx @@ -26,36 +26,38 @@ export const EditContent = () => { const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); return ( <> - -
e.preventDefault()}> - - - {items.length > 1 && ( - { - setCurrent(newShortId); - }} - sx={{ - borderRight: isSmallScreen ? 0 : 1, - borderBottom: isSmallScreen ? 1 : 0, - borderColor: 'divider', - '&& .MuiTab-root': { - alignItems: isSmallScreen ? undefined : 'baseline', - textAlign: isSmallScreen ? undefined : 'left', - }, - }} - > - {items.map(({ shortId, tags }, idx) => ( - - ))} - - )} + + {items.length > 1 && ( + { + setCurrent(newShortId); + }} + sx={{ + borderRight: isSmallScreen ? 0 : 1, + borderBottom: isSmallScreen ? 1 : 0, + borderColor: 'divider', + '&& .MuiTab-root': { + alignItems: isSmallScreen ? undefined : 'baseline', + textAlign: isSmallScreen ? undefined : 'left', + }, + }} + > + {items.map(({ shortId, tags }, idx) => ( + + ))} + + )} + + e.preventDefault()}> + +
@@ -63,9 +65,9 @@ export const EditContent = () => {
-
- -
+ + + ); diff --git a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap.tsx b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap.tsx new file mode 100644 index 00000000..5d0b225a --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/EditFeatureMap.tsx @@ -0,0 +1,199 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import maplibregl, { LngLat } from 'maplibre-gl'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + CircularProgress, + Typography, +} from '@mui/material'; +import styled from '@emotion/styled'; +import { outdoorStyle } from '../../../../Map/styles/outdoorStyle'; +import { COMPASS_TOOLTIP } from '../../../../Map/useAddTopRightControls'; +import { createMapEffectHook } from '../../../../helpers'; +import { LonLat } from '../../../../../services/types'; +import { useFeatureEditData } from './SingleFeatureEditContext'; +import { useEditContext } from '../../EditContext'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { t } from '../../../../../services/intl'; + +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%; +`; + +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/FeatureEditSection.tsx b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx index 4632d1de..bc4e4f0f 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/FeatureEditSection/FeatureEditSection.tsx @@ -8,6 +8,12 @@ import { } from './SingleFeatureEditContext'; import { MembersEditor } from '../MembersEditor'; import { ParentsEditor } from '../ParentsEditor'; +import dynamic from 'next/dynamic'; + +const EditFeatureMapDynamic = dynamic(() => import('./EditFeatureMap'), { + ssr: false, + loading: () =>
, +}); import { Stack, Typography } from '@mui/material'; import { useEditContext } from '../../EditContext'; @@ -45,6 +51,7 @@ export const FeatureEditSection = ({ shortId }: Props) => ( + diff --git a/src/components/FeaturePanel/EditDialog/EditContent/MembersEditor.tsx b/src/components/FeaturePanel/EditDialog/EditContent/MembersEditor.tsx index 7a951c3b..80b41e22 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/MembersEditor.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/MembersEditor.tsx @@ -4,6 +4,7 @@ import { Accordion, AccordionDetails, AccordionSummary, + Chip, List, Stack, Typography, @@ -30,9 +31,8 @@ export const MembersEditor = () => { id="panel1-header" > - - {t('editdialog.members')} ({members.length}) - + {t('editdialog.members')} + diff --git a/src/components/FeaturePanel/EditDialog/EditContent/ParentsEditor.tsx b/src/components/FeaturePanel/EditDialog/EditContent/ParentsEditor.tsx index ee51ed11..1b3a480b 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/ParentsEditor.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/ParentsEditor.tsx @@ -4,6 +4,7 @@ import { Accordion, AccordionDetails, AccordionSummary, + Chip, List, Stack, Typography, @@ -43,9 +44,8 @@ export const ParentsEditor = () => { id="panel1-header" > - - {t('editdialog.parents')} ({parents.length}) - + {t('editdialog.parents')} + diff --git a/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx b/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx index 47c706ea..86662896 100644 --- a/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx +++ b/src/components/FeaturePanel/MemberFeatures/MemberFeatures.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box } from '@mui/material'; +import { Box, Chip } from '@mui/material'; import { getOsmappLink } from '../../../services/helpers'; import { useFeatureContext } from '../../utils/FeatureContext'; import { PanelLabel } from '../Climbing/PanelLabel'; @@ -67,7 +67,8 @@ export const MemberFeatures = () => { return ( }> - {getHeading(feature)} ({memberFeatures.length}) + {getHeading(feature)}{' '} + {climbingRoutesFeatures.length > 0 && (
    diff --git a/src/locales/cs.js b/src/locales/cs.js index 84ac8de8..ee4184fe 100644 --- a/src/locales/cs.js +++ b/src/locales/cs.js @@ -175,6 +175,7 @@ export default { můžete přidat doplňující poznámku. Vhodné je podložit váš příspěvek odkazem na zdroj informace (web, foto atd.).`, 'editdialog.tags_editor': 'Všechny vlastnosti – tagy', + 'editdialog.location': 'Poloha', '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.', diff --git a/src/locales/vocabulary.js b/src/locales/vocabulary.js index c280c471..65b5f6d4 100644 --- a/src/locales/vocabulary.js +++ b/src/locales/vocabulary.js @@ -214,6 +214,7 @@ export default { 'editdialog.info_note': 'Your suggestion will be reviewed by OpenStreetMap volunteers. You can add additional information such as a link to a photo or a link to source material for them below!', 'editdialog.tags_editor': 'All properties – Tags', + 'editdialog.location': 'Location', 'editdialog.tags_editor_info': `Tags contain the data used to display objects on the map.
    You can find a reference for all tags on the OpenStreetMap Wiki.`, 'editdialog.login_in_progress': `Logging in...`, 'editdialog.save_refused': 'Unable to save your changes.',