From d2ec9458a6e218d115a7b0eda824c02d1408e63a Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Mon, 11 Sep 2023 15:00:11 +0200 Subject: [PATCH] Added coords and a generic hook to render a custom control --- .../components/common/map/controls/coords.tsx | 77 +++++++++++++++++++ .../map/controls/hooks/use-themed-control.tsx | 50 ++++++++++++ .../common/map/hooks/use-map-compare.ts | 30 ++++---- .../components/common/map/hooks/use-maps.ts | 2 +- app/scripts/components/common/map/maps.tsx | 2 +- app/scripts/components/exploration/index.tsx | 4 +- 6 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 app/scripts/components/common/map/controls/coords.tsx create mode 100644 app/scripts/components/common/map/controls/hooks/use-themed-control.tsx diff --git a/app/scripts/components/common/map/controls/coords.tsx b/app/scripts/components/common/map/controls/coords.tsx new file mode 100644 index 000000000..0e5bc2f39 --- /dev/null +++ b/app/scripts/components/common/map/controls/coords.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from 'react'; +import { MapRef } from 'react-map-gl'; +import styled from 'styled-components'; +import { Button } from '@devseed-ui/button'; +import { themeVal } from '@devseed-ui/theme-provider'; +import useMaps from '../hooks/use-maps'; +import useThemedControl from './hooks/use-themed-control'; +import { round } from '$utils/format'; +import { CopyField } from '$components/common/copy-field'; + +const MapCoordsWrapper = styled.div` + /* Large width so parent will wrap */ + width: 100vw; + + ${Button} { + background: ${themeVal('color.base-400a')}; + font-weight: ${themeVal('type.base.regular')}; + font-size: 0.75rem; + } + + && ${Button /* sc-selector */}:hover { + background: ${themeVal('color.base-500')}; + } +`; + +const getCoords = (mapInstance?: MapRef) => { + if (!mapInstance) return { lng: 0, lat: 0 }; + const mapCenter = mapInstance.getCenter(); + return { + lng: round(mapCenter.lng, 4), + lat: round(mapCenter.lat, 4) + }; +}; + +export default function MapCoords() { + const { main } = useMaps(); + + const [position, setPosition] = useState(getCoords(main)); + + useEffect(() => { + const posListener = (e) => { + setPosition(getCoords(e.target)); + }; + + if (main) main.on('moveend', posListener); + + return () => { + if (main) main.off('moveend', posListener); + }; + }, [main]); + + const { lng, lat } = position; + const value = `W ${lng}, N ${lat}`; + + useThemedControl( + () => ( + + + {({ ref, showCopiedMsg }) => ( + + )} + + + ), + { position: 'bottom-left' } + ); + + return null; +} diff --git a/app/scripts/components/common/map/controls/hooks/use-themed-control.tsx b/app/scripts/components/common/map/controls/hooks/use-themed-control.tsx new file mode 100644 index 000000000..f2bf5a90c --- /dev/null +++ b/app/scripts/components/common/map/controls/hooks/use-themed-control.tsx @@ -0,0 +1,50 @@ +import { IControl } from 'mapbox-gl'; +import React, { ReactNode, useEffect, useRef } from 'react'; +import { createRoot } from 'react-dom/client'; +import { useControl } from 'react-map-gl'; +import { useTheme, ThemeProvider } from 'styled-components'; + +export default function useThemedControl( + renderFn: () => ReactNode, + opts?: any +) { + const theme = useTheme(); + const elementRef = useRef(null); + const rootRef = useRef | null>(null); + + // Define the control methods and its lifecycle + class ThemedControl implements IControl { + onAdd() { + const el = document.createElement('div'); + el.className = 'mapboxgl-ctrl'; + elementRef.current = el; + + // Create a root and render the component + rootRef.current = createRoot(el); + + rootRef.current.render( + {renderFn() as any} + ); + + return el; + } + + onRemove() { + // Cleanup if necessary + if (elementRef.current) { + rootRef.current?.unmount(); + } + } + } + + // Listen for changes in dependencies and re-render if necessary + useEffect(() => { + if (rootRef.current) { + rootRef.current.render( + {renderFn() as any} + ); + } + }, [renderFn, theme]); + useControl(() => new ThemedControl(), opts); + return null; +} diff --git a/app/scripts/components/common/map/hooks/use-map-compare.ts b/app/scripts/components/common/map/hooks/use-map-compare.ts index 7cb4d238f..b5d17b732 100644 --- a/app/scripts/components/common/map/hooks/use-map-compare.ts +++ b/app/scripts/components/common/map/hooks/use-map-compare.ts @@ -1,29 +1,25 @@ -import { useContext, useEffect } from "react"; +import { useContext, useEffect } from 'react'; import MapboxCompare from 'mapbox-gl-compare'; -import { MapsContext } from "../maps"; -import { useMaps } from "./use-maps"; +import { MapsContext } from '../maps'; +import useMaps from './use-maps'; export default function useMapCompare() { - const maps = useMaps(); + const { main, compared } = useMaps(); const { containerId } = useContext(MapsContext); - const hasMapCompare = !!maps.compared; + const hasMapCompare = !!compared; useEffect(() => { - if (!maps.main) return; + if (!main) return; - if (maps.compared) { - const compare = new MapboxCompare( - maps.main, - maps.compared, - `#${containerId}`, - { - mousemove: false, - orientation: 'vertical' - } - ); + if (compared) { + const compare = new MapboxCompare(main, compared, `#${containerId}`, { + mousemove: false, + orientation: 'vertical' + }); return () => { compare.remove(); }; } + // main should be stable, while we are only interested here in the absence or presence of compared }, [containerId, hasMapCompare]); -} \ No newline at end of file +} diff --git a/app/scripts/components/common/map/hooks/use-maps.ts b/app/scripts/components/common/map/hooks/use-maps.ts index add233002..56f877e2b 100644 --- a/app/scripts/components/common/map/hooks/use-maps.ts +++ b/app/scripts/components/common/map/hooks/use-maps.ts @@ -2,7 +2,7 @@ import { useContext } from 'react'; import { useMap } from 'react-map-gl'; import { MapsContext } from '../maps'; -export function useMaps() { +export default function useMaps() { const { mainId, comparedId } = useContext(MapsContext); const maps = useMap(); diff --git a/app/scripts/components/common/map/maps.tsx b/app/scripts/components/common/map/maps.tsx index 32070ea57..6872c3214 100644 --- a/app/scripts/components/common/map/maps.tsx +++ b/app/scripts/components/common/map/maps.tsx @@ -22,7 +22,7 @@ import MapboxStyleOverride from './mapbox-style-override'; import { Styles } from './styles'; import useMapCompare from './hooks/use-map-compare'; import MapComponent from './map-component'; -import { useMaps } from './hooks/use-maps'; +import useMaps from './hooks/use-maps'; const chevronRightURI = () => iconDataURI(CollecticonChevronRightSmall, { diff --git a/app/scripts/components/exploration/index.tsx b/app/scripts/components/exploration/index.tsx index 0ae490aa9..dd990d943 100644 --- a/app/scripts/components/exploration/index.tsx +++ b/app/scripts/components/exploration/index.tsx @@ -14,6 +14,7 @@ import Map, { Compare } from '$components/common/map'; import { Basemap } from '$components/common/map/style-generators/basemap'; import GeocoderControl from '$components/common/map/controls/geocoder'; import { NavigationControl, ScaleControl } from '$components/common/map/controls'; +import Coords from '$components/common/map/controls/coords'; const Container = styled.div` display: flex; @@ -58,7 +59,7 @@ const Container = styled.div` `; function Exploration() { - const [compare, setCompare] = useState(false); + const [compare, setCompare] = useState(true); const [datasetModalRevealed, setDatasetModalRevealed] = useState(true); const openModal = useCallback(() => setDatasetModalRevealed(true), []); @@ -82,6 +83,7 @@ function Exploration() { + {compare && (