diff --git a/src/components/map.tsx b/src/components/map.tsx index 837b84a..0435798 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -9,6 +9,14 @@ import { useAppContext } from "~/appContext"; import { fetchCountriesData, fetchNodeDataFromBackend } from "~/backend"; import nominatimGeocoder from "~/components/nominatimGeocoder"; import { useLanguage } from "~/i18n"; +import { + addNodeIdToHash, + getMapLocation, + locationParameter, + parseParametersFromUrl, + removeNodeIdFromHash, + saveLocationToLocalStorage, +} from "~/location"; import ButtonsType from "~/model/buttonsType"; import { DefibrillatorData } from "~/model/defibrillatorData"; import { ModalType, initialModalState } from "~/model/modal"; @@ -62,21 +70,6 @@ function fillSidebarWithOsmDataAndShow( }); } -function parseHash(): Record { - const parameters: Record = {}; - for (const part of window.location.hash.slice(1).split("&")) { - const [key, value] = part.split("=", 2); - parameters[key] = value; - } - return parameters; -} - -function getNewHashString(parameters: Record) { - return Object.entries(parameters) - .map(([key, value]) => `${key}=${value}`) - .join("&"); -} - const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { const { authState: { auth }, @@ -92,41 +85,15 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { } = useAppContext(); const { t } = useTranslation(); const language = useLanguage(); - - const hash4MapName = "map"; - - const paramsFromHash = parseHash(); - - let initialLongitude = -8; - let initialLatitude = 47.74; - let initialZoom = 3; - - if (paramsFromHash[hash4MapName]) { - [initialZoom, initialLatitude, initialLongitude] = paramsFromHash[ - hash4MapName - ] - .split("/") - .map(Number); - } - + const { longitude, latitude, zoom } = getMapLocation(); const mapContainer = useRef(null); const mapRef = useRef(null); const maplibreGeocoderRef = useRef(null); const controlsLocation = "bottom-right"; - const [marker, setMarker] = useState(null); - const [sidebarLeftShown, setSidebarLeftShown] = useState(false); - const [footerButtonType, setFooterButtonType] = useState(ButtonsType.Basic); - const removeNodeIdFromHash = () => { - const hashParams = parseHash(); - // biome-ignore lint/performance/noDelete: using undefined assignment causes it to be part of url - delete hashParams.node_id; - window.location.hash = getNewHashString(hashParams); - }; - const deleteMarker = () => { if (marker !== null) { marker.remove(); @@ -225,11 +192,10 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { if (mapRef.current !== null) return; // stops map from initializing more than once const map = new maplibregl.Map({ container: mapContainer.current, - hash: hash4MapName, - // @ts-ignore + hash: locationParameter, style: mapStyle(language.toUpperCase(), countriesData), - center: [initialLongitude, initialLatitude], - zoom: initialZoom, + center: [longitude, latitude], + zoom: zoom, minZoom: 3, maxZoom: 19, maplibreLogo: false, @@ -286,6 +252,7 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { map.on("mouseleave", "unclustered-low-zoom", () => { map.getCanvas().style.cursor = ""; }); + map.on("moveend", saveLocationToLocalStorage); // biome-ignore lint/suspicious/noExplicitAny: unknown type type MapEventType = any; @@ -313,14 +280,8 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { }); }); function showObjectWithProperties(e: MapEventType) { - console.log( - "Clicked on object with properties: ", - e.features[0].properties, - ); if (e.features[0].properties !== undefined && mapRef.current !== null) { const osmNodeId = e.features[0].properties.node_id; - console.log("Clicked on object with osm_id: ", osmNodeId); - // show sidebar fillSidebarWithOsmDataAndShow( osmNodeId, mapRef.current, @@ -329,13 +290,7 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { setSidebarLeftShown, false, ); - // update hash - const params = { - ...parseHash(), - node_id: osmNodeId, - }; - console.log("new hash params", params); - window.location.hash = getNewHashString(params); + addNodeIdToHash(osmNodeId); } } @@ -344,7 +299,7 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { map.on("click", "unclustered-low-zoom", showObjectWithProperties); // if direct link to osm node then get its data and zoom in - const newParamsFromHash = parseHash(); + const newParamsFromHash = parseParametersFromUrl(); if (newParamsFromHash.node_id && mapRef.current !== null) { fillSidebarWithOsmDataAndShow( newParamsFromHash.node_id, @@ -356,9 +311,9 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { ); } }, [ - initialLatitude, - initialLongitude, - initialZoom, + latitude, + longitude, + zoom, setSidebarAction, setSidebarData, language, @@ -371,7 +326,6 @@ const MapView: FC = ({ openChangesetId, setOpenChangesetId }) => { const map = mapRef.current; addMaplibreGeocoder(map); if (countriesDataLanguage !== language) return; // wait for countries data to be loaded - // @ts-ignore map.setStyle(mapStyle(language.toUpperCase(), countriesData)); }, [countriesData, countriesDataLanguage, language]); diff --git a/src/components/map_style.ts b/src/components/map_style.ts index 4c7bfdc..fee3193 100644 --- a/src/components/map_style.ts +++ b/src/components/map_style.ts @@ -1,3 +1,4 @@ +import { StyleSpecification } from "maplibre-gl"; import { backendBaseUrl } from "~/backend"; import { Country } from "~/model/country"; @@ -8,7 +9,10 @@ const spriteUrl = new URL("img/sprite", baseUrl).href; const tilesUrl = `${backendBaseUrl}/api/v1/tile/{z}/{x}/{y}.mvt`; const TILE_COUNTRIES_MAX_ZOOM = 6; -const mapStyle = (lang: string, countriesData: Array) => { +const mapStyle = ( + lang: string, + countriesData: Array, +): StyleSpecification => { const countryCodeToName: Record = countriesData.reduce( (map: Record, country) => { if (country.names[lang.toUpperCase()] !== undefined) { @@ -217,7 +221,6 @@ const mapStyle = (lang: string, countriesData: Array) => { }, }, ], - id: "style", }; }; diff --git a/src/location.ts b/src/location.ts new file mode 100644 index 0000000..8f4cc36 --- /dev/null +++ b/src/location.ts @@ -0,0 +1,81 @@ +export const locationParameter = "map"; + +export interface MapLocation { + zoom: number; + latitude: number; + longitude: number; +} + +export function getMapLocation(): MapLocation { + const defaultLocation: MapLocation = { + zoom: 3, + latitude: 47.74, + longitude: -8, + }; + return ( + getLocationFromUrl() ?? getLocationFromLocalStorage() ?? defaultLocation + ); +} + +function getLocationStringFromUrl(): string | null { + const paramsFromHash = parseParametersFromUrl(); + return paramsFromHash[locationParameter] ?? null; +} + +function getLocationFromUrl(): MapLocation | null { + const locationString = getLocationStringFromUrl(); + return locationString !== null + ? parseLocationFromString(locationString) + : null; +} + +function getLocationFromLocalStorage(): MapLocation | null { + const locationString = localStorage.getItem(locationParameter); + return locationString !== null + ? parseLocationFromString(locationString) + : null; +} + +function parseLocationFromString(locationString: string): MapLocation | null { + try { + const [zoom, latitude, longitude] = locationString.split("/").map(Number); + return { zoom, latitude, longitude }; + } catch (e) { + return null; + } +} + +export function parseParametersFromUrl(): Record { + const parameters: Record = {}; + for (const part of window.location.hash.slice(1).split("&")) { + const [key, value] = part.split("=", 2); + parameters[key] = value; + } + return parameters; +} + +function serializeParametersToUrlTarget(parameters: Record) { + return Object.entries(parameters) + .map(([key, value]) => `${key}=${value}`) + .join("&"); +} + +export function removeNodeIdFromHash() { + const params = parseParametersFromUrl(); + // biome-ignore lint/performance/noDelete: using undefined assignment causes it to be part of url + delete params.node_id; + window.location.hash = serializeParametersToUrlTarget(params); +} + +export function addNodeIdToHash(nodeId: string) { + const params = parseParametersFromUrl(); + params.node_id = nodeId; + window.location.hash = serializeParametersToUrlTarget(params); +} + +export function saveLocationToLocalStorage() { + const location = getLocationStringFromUrl(); + if (location !== null) { + localStorage.setItem(locationParameter, location ?? ""); + } +} diff --git a/types/custom.d.ts b/types/custom.d.ts index 9fb567b..51b0457 100644 --- a/types/custom.d.ts +++ b/types/custom.d.ts @@ -2,10 +2,3 @@ declare module "*.svg" { const content: React.FunctionComponent>; export default content; } - -declare module "@mapbox/timespace" { - export function getFuzzyLocalTimeFromPoint( - timestamp: number, - point: number[], - ): { _d: Date }; -}