From 83e945c7ff177ae3bf0bb4aaecce4d9c881a28bd Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Fri, 3 Dec 2021 19:44:12 +0100 Subject: [PATCH] refactor: split MapPage into components --- frontend/src/components/Map/index.tsx | 98 ++++++++++++ frontend/src/components/index.js | 1 + frontend/src/pages/HomePage.tsx | 5 +- .../{MapPage.tsx => MapPage/RoadInfo.tsx} | 147 +----------------- frontend/src/pages/MapPage/index.tsx | 50 ++++++ .../styles.module.less} | 0 frontend/src/pages/TrackPage/TrackMap.tsx | 6 +- 7 files changed, 160 insertions(+), 147 deletions(-) create mode 100644 frontend/src/components/Map/index.tsx rename frontend/src/pages/{MapPage.tsx => MapPage/RoadInfo.tsx} (54%) create mode 100644 frontend/src/pages/MapPage/index.tsx rename frontend/src/pages/{MapPage.module.less => MapPage/styles.module.less} (100%) diff --git a/frontend/src/components/Map/index.tsx b/frontend/src/components/Map/index.tsx new file mode 100644 index 0000000..7d9f25c --- /dev/null +++ b/frontend/src/components/Map/index.tsx @@ -0,0 +1,98 @@ +import React, {useState, useCallback, useMemo, useEffect} from 'react' +import _ from 'lodash' +import ReactMapGl, {WebMercatorViewport, AttributionControl, NavigationControl} from 'react-map-gl' +import turfBbox from '@turf/bbox' +import {useHistory, useLocation} from 'react-router-dom' + +import {useConfig} from 'config' + +import {basemap} from '../../mapstyles' + +const EMPTY_VIEWPORT = {longitude: 0, latitude: 0, zoom: 0} + +function parseHash(v) { + if (!v) return null + const m = v.match(/^#([0-9\.]+)\/([0-9\.]+)\/([0-9\.]+)$/) + if (!m) return null + return { + zoom: Number.parseFloat(m[1]), + latitude: Number.parseFloat(m[2]), + longitude: Number.parseFloat(m[3]), + } +} + +function buildHash(v) { + return `${v.zoom.toFixed(2)}/${v.latitude}/${v.longitude}` +} + +function useViewportFromUrl() { + const history = useHistory() + const location = useLocation() + const value = useMemo(() => parseHash(location.hash), [location.hash]) + const setter = useCallback( + (v) => { + history.replace({ + hash: buildHash(v), + }) + }, + [history] + ) + return [value || EMPTY_VIEWPORT, setter] +} + + +export default function Map({ + viewportFromUrl, + children, + boundsFromJson, + ...props +}: { + viewportFromUrl?: boolean + children: React.ReactNode + boundsFromJson: GeoJSON.Geometry +}) { + const [viewportState, setViewportState] = useState(EMPTY_VIEWPORT) + const [viewportUrl, setViewportUrl] = useViewportFromUrl() + + const [viewport, setViewport] = viewportFromUrl ? [viewportUrl, setViewportUrl] : [viewportState, setViewportState] + + const config = useConfig() + useEffect(() => { + if (config?.mapHome && viewport.latitude === 0 && viewport.longitude === 0 && !boundsFromJson) { + setViewport(config.mapHome) + } + }, [config, boundsFromJson]) + + useEffect(() => { + if (boundsFromJson) { + const [minX, minY, maxX, maxY] = turfBbox(boundsFromJson) + const vp = new WebMercatorViewport({width: 1000, height: 800}).fitBounds( + [ + [minX, minY], + [maxX, maxY], + ], + { + padding: 20, + offset: [0, -100], + } + ) + setViewport(_.pick(vp, ['zoom', 'latitude', 'longitude'])) + } + }, [boundsFromJson]) + + return ( + + © OpenStreetMap contributors', + '© OpenMapTiles', + '© OpenBikeSensor', + ]} + /> + + + {children} + + ) +} diff --git a/frontend/src/components/index.js b/frontend/src/components/index.js index 16bdb66..15443b9 100644 --- a/frontend/src/components/index.js +++ b/frontend/src/components/index.js @@ -3,6 +3,7 @@ export {default as FileDrop} from './FileDrop' export {default as FileUploadField} from './FileUploadField' export {default as FormattedDate} from './FormattedDate' export {default as LoginButton} from './LoginButton' +export {default as Map} from './Map' export {default as Page} from './Page' export {default as Stats} from './Stats' export {default as StripMarkdown} from './StripMarkdown' diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 79e1f53..edd6049 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -6,10 +6,9 @@ import {of, from} from 'rxjs' import {map, switchMap} from 'rxjs/operators' import api from 'api' -import {Stats, Page} from 'components' +import {Stats, Page, Map} from 'components' import {TrackListItem} from './TracksPage' -import {CustomMap} from './MapPage' import styles from './HomePage.module.less' function MostRecentTrack() { @@ -47,7 +46,7 @@ export default function HomePage() {
- +
diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage/RoadInfo.tsx similarity index 54% rename from frontend/src/pages/MapPage.tsx rename to frontend/src/pages/MapPage/RoadInfo.tsx index 9341f3d..183bd4a 100644 --- a/frontend/src/pages/MapPage.tsx +++ b/frontend/src/pages/MapPage/RoadInfo.tsx @@ -1,109 +1,13 @@ -import React, {useState, useCallback, useMemo, useEffect} from 'react' +import React, {useState, useCallback} from 'react' import _ from 'lodash' import {Segment, Menu, Header, Label, Icon, Table} from 'semantic-ui-react' -import ReactMapGl, {WebMercatorViewport, AttributionControl, NavigationControl, Layer, Source} from 'react-map-gl' -import turfBbox from '@turf/bbox' -import {useHistory, useLocation} from 'react-router-dom' +import {Layer, Source} from 'react-map-gl' import {of, from, concat} from 'rxjs' import {useObservable} from 'rxjs-hooks' import {switchMap, distinctUntilChanged} from 'rxjs/operators' -import {Page} from 'components' -import {useConfig} from 'config' import api from 'api' -import {roadsLayer, basemap} from '../mapstyles' - -import styles from './MapPage.module.less' - -const EMPTY_VIEWPORT = {longitude: 0, latitude: 0, zoom: 0} - -function parseHash(v) { - if (!v) return null - const m = v.match(/^#([0-9\.]+)\/([0-9\.]+)\/([0-9\.]+)$/) - if (!m) return null - return { - zoom: Number.parseFloat(m[1]), - latitude: Number.parseFloat(m[2]), - longitude: Number.parseFloat(m[3]), - } -} - -function buildHash(v) { - return `${v.zoom.toFixed(2)}/${v.latitude}/${v.longitude}` -} - -function useViewportFromUrl() { - const history = useHistory() - const location = useLocation() - const value = useMemo(() => parseHash(location.hash), [location.hash]) - const setter = useCallback( - (v) => { - history.replace({ - hash: buildHash(v), - }) - }, - [history] - ) - return [value || EMPTY_VIEWPORT, setter] -} - -export function CustomMap({ - viewportFromUrl, - children, - boundsFromJson, - ...props -}: { - viewportFromUrl?: boolean - children: React.ReactNode - boundsFromJson: GeoJSON.Geometry -}) { - const [viewportState, setViewportState] = useState(EMPTY_VIEWPORT) - const [viewportUrl, setViewportUrl] = useViewportFromUrl() - - const [viewport, setViewport] = viewportFromUrl ? [viewportUrl, setViewportUrl] : [viewportState, setViewportState] - - const config = useConfig() - useEffect(() => { - if (config?.mapHome && viewport.latitude === 0 && viewport.longitude === 0 && !boundsFromJson) { - setViewport(config.mapHome) - } - }, [config, boundsFromJson]) - - useEffect(() => { - if (boundsFromJson) { - const [minX, minY, maxX, maxY] = turfBbox(boundsFromJson) - const vp = new WebMercatorViewport({width: 1000, height: 800}).fitBounds( - [ - [minX, minY], - [maxX, maxY], - ], - { - padding: 20, - offset: [0, -100], - } - ) - setViewport(_.pick(vp, ['zoom', 'latitude', 'longitude'])) - } - }, [boundsFromJson]) - - return ( - - © OpenStreetMap contributors', - '© OpenMapTiles', - '© OpenBikeSensor', - ]} - /> - - - {children} - - ) -} - const UNITS = {distanceOvertaker: 'm', distanceStationary: 'm', speed: 'm/s'} const LABELS = {distanceOvertaker: 'Overtaker', distanceStationary: 'Stationary', speed: 'Speed'} const ZONE_COLORS = {urban: 'olive', rural: 'brown', motorway: 'purple'} @@ -144,7 +48,7 @@ function RoadStatsTable({data}) { ) } -function CurrentRoadInfo({clickLocation}) { +export default function RoadInfo({clickLocation}) { const [direction, setDirection] = useState('forwards') const onClickDirection = useCallback( @@ -186,7 +90,7 @@ function CurrentRoadInfo({clickLocation}) { const loading = info == null - const offsetDirection = info?.road.oneway ? 0 : direction === 'forwards' ? 1 : -1; // TODO: change based on left-hand/right-hand traffic + const offsetDirection = info?.road.oneway ? 0 : direction === 'forwards' ? 1 : -1 // TODO: change based on left-hand/right-hand traffic const content = !loading && !info.road ? ( @@ -234,7 +138,7 @@ function CurrentRoadInfo({clickLocation}) { 'line-width': ['interpolate', ['linear'], ['zoom'], 14, 6, 17, 12], 'line-color': '#18FFFF', 'line-opacity': 0.5, - ...({ + ...{ 'line-offset': [ 'interpolate', ['exponential', 1.5], @@ -244,8 +148,7 @@ function CurrentRoadInfo({clickLocation}) { 19, offsetDirection * 8, ], - }) - + }, }} /> @@ -259,41 +162,3 @@ function CurrentRoadInfo({clickLocation}) { ) } - -export default function MapPage() { - const {obsMapSource} = useConfig() || {} - const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null) - - const onClick = useCallback( - (e) => { - let node = e.target - while (node) { - if (node?.classList?.contains(styles.mapInfoBox)) { - return - } - node = node.parentNode - } - - setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]}) - }, - [setClickLocation] - ) - - if (!obsMapSource) { - return null - } - - return ( - -
- - - - - - - -
-
- ) -} diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx new file mode 100644 index 0000000..b8cef55 --- /dev/null +++ b/frontend/src/pages/MapPage/index.tsx @@ -0,0 +1,50 @@ +import React, {useState, useCallback} from 'react' +import _ from 'lodash' +import {Layer, Source} from 'react-map-gl' + +import {Page, Map} from 'components' +import {useConfig} from 'config' + +import {roadsLayer} from '../../mapstyles' + +import RoadInfo from './RoadInfo' +import styles from './styles.module.less' + + +export default function MapPage() { + const {obsMapSource} = useConfig() || {} + const [clickLocation, setClickLocation] = useState<{longitude: number; latitude: number} | null>(null) + + const onClick = useCallback( + (e) => { + let node = e.target + while (node) { + if (node?.classList?.contains(styles.mapInfoBox)) { + return + } + node = node.parentNode + } + + setClickLocation({longitude: e.lngLat[0], latitude: e.lngLat[1]}) + }, + [setClickLocation] + ) + + if (!obsMapSource) { + return null + } + + return ( + +
+ + + + + + + +
+
+ ) +} diff --git a/frontend/src/pages/MapPage.module.less b/frontend/src/pages/MapPage/styles.module.less similarity index 100% rename from frontend/src/pages/MapPage.module.less rename to frontend/src/pages/MapPage/styles.module.less diff --git a/frontend/src/pages/TrackPage/TrackMap.tsx b/frontend/src/pages/TrackPage/TrackMap.tsx index d9ccf02..34207ba 100644 --- a/frontend/src/pages/TrackPage/TrackMap.tsx +++ b/frontend/src/pages/TrackPage/TrackMap.tsx @@ -2,7 +2,7 @@ import React from 'react' import {Source, Layer} from 'react-map-gl' import type {TrackData} from 'types' -import {CustomMap} from '../MapPage' +import {Map} from 'components' import {colorByDistance, trackLayer} from '../../mapstyles' @@ -24,7 +24,7 @@ export default function TrackMap({ return (
- + {showTrack && ( @@ -73,7 +73,7 @@ export default function TrackMap({ ))} )} - +
) }