diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-09-03 17:38:24 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-09-10 10:03:47 +0200 |
| commit | a0318e4f107e91bfcece741ffa77b841e1292c95 (patch) | |
| tree | 950b56d91027b29a1bc6bacf2b19c286d2cf432b /gui/src | |
| parent | de022a5eda44c0363c9c65ea698ff48c02c34ecb (diff) | |
| download | mullvadvpn-a0318e4f107e91bfcece741ffa77b841e1292c95.tar.xz mullvadvpn-a0318e4f107e91bfcece741ffa77b841e1292c95.zip | |
Convert SvgMap to functional component
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/components/SvgMap.tsx | 297 |
1 files changed, 132 insertions, 165 deletions
diff --git a/gui/src/renderer/components/SvgMap.tsx b/gui/src/renderer/components/SvgMap.tsx index ae65f6fb25..ea5dbf4978 100644 --- a/gui/src/renderer/components/SvgMap.tsx +++ b/gui/src/renderer/components/SvgMap.tsx @@ -1,6 +1,6 @@ import { geoMercator, GeoProjection } from 'd3-geo'; import rbush from 'rbush'; -import * as React from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps'; import geographyData from '../../../assets/geo/geometry.json'; @@ -26,6 +26,22 @@ type BBox = [number, number, number, number]; const MOVE_SPEED = 2000; +const mapStyle = { + width: '100%', + height: '100%', + backgroundColor: '#192e45', +}; + +const zoomableGroupStyle = { + transition: `transform ${MOVE_SPEED}ms ease-in-out`, +}; + +const markerStyle = mergeRsmStyle({ + default: { + transition: `transform ${MOVE_SPEED}ms ease-in-out`, + }, +}); + const projectionConfig = { scale: 160, }; @@ -43,16 +59,10 @@ function mergeRsmStyle(style: { }; } -function getProjection( - width: number, - height: number, - offsetX: number, - offsetY: number, - scale: number, -) { +function getProjection(width: number, height: number, offset: [number, number], scale: number) { return geoMercator() .scale(scale) - .translate([offsetX + width / 2, offsetY + height / 2]) + .translate([offset[0] + width / 2, offset[1] + height / 2]) .precision(0.1); } @@ -89,45 +99,13 @@ function getViewportGeoBoundingBox( ]; } -function shouldInvalidateState(oldProps: IProps, nextProps: IProps) { - return ( - oldProps.width !== nextProps.width || - oldProps.height !== nextProps.height || - oldProps.center[0] !== nextProps.center[0] || - oldProps.center[1] !== nextProps.center[1] || - oldProps.offset[0] !== nextProps.offset[0] || - oldProps.offset[1] !== nextProps.offset[1] || - oldProps.zoomLevel !== nextProps.zoomLevel - ); -} - -function getNextState(prevState: IState | null, nextProps: IProps): IState { - const { width, height, center, offset, zoomLevel } = nextProps; - const viewportBboxes = prevState === null ? [] : prevState.viewportBboxes; - - const projection = getProjection(width, height, offset[0], offset[1], projectionConfig.scale); - const zoomCenter = getZoomCenter(center, offset, projection, zoomLevel); - - const viewportBbox = getViewportGeoBoundingBox(zoomCenter, width, height, projection, zoomLevel); - viewportBboxes.push(viewportBbox); - - const combinedViewportBboxMatch = { +function getCombindedViewportBboxMatch(viewportBboxes: BBox[]) { + return { minX: Math.min(...viewportBboxes.map((viewportBbox) => viewportBbox[0])), minY: Math.min(...viewportBboxes.map((viewportBbox) => viewportBbox[1])), maxX: Math.max(...viewportBboxes.map((viewportBbox) => viewportBbox[2])), maxY: Math.max(...viewportBboxes.map((viewportBbox) => viewportBbox[3])), }; - - const visibleGeometry = geometryTree.search(combinedViewportBboxMatch); - const visibleStatesProvincesLines = provincesStatesLinesTree.search(combinedViewportBboxMatch); - - return { - zoomCenter, - zoomLevel, - visibleGeometry, - visibleStatesProvincesLines, - viewportBboxes, - }; } export interface IProps { @@ -140,136 +118,125 @@ export interface IProps { markerImagePath: string; } -interface IState { - zoomCenter: [number, number]; - zoomLevel: number; - visibleGeometry: IGeometryLeaf[]; - visibleStatesProvincesLines: IProvinceAndStateLineLeaf[]; - // combine previous and current viewports to get the rough area of transition. - viewportBboxes: BBox[]; -} - // @TODO: Calculate zoom level based on (center + span) (aka MKCoordinateSpan) -export default class SvgMap extends React.Component<IProps, IState> { - public state: IState = getNextState(null, this.props); - - public UNSAFE_componentWillReceiveProps(nextProps: IProps) { - if (shouldInvalidateState(this.props, nextProps)) { - this.setState((prevState) => getNextState(prevState, nextProps)); - } - } +export default function SvgMap(props: IProps) { + const { width, height, zoomLevel } = props; + const center = useMemo(() => props.center, [...props.center]); + const offset = useMemo(() => props.offset, [...props.offset]); + const [viewportBboxes, setViewportBboxes] = useState<BBox[]>([]); - public shouldComponentUpdate(nextProps: IProps, nextState: IState) { - return ( - this.props.width !== nextProps.width || - this.props.height !== nextProps.height || - this.props.center[0] !== nextProps.center[0] || - this.props.center[1] !== nextProps.center[1] || - this.props.offset[0] !== nextProps.offset[0] || - this.props.offset[1] !== nextProps.offset[1] || - this.props.zoomLevel !== nextProps.zoomLevel || - this.props.showMarker !== nextProps.showMarker || - this.props.markerImagePath !== nextProps.markerImagePath || - this.state.zoomCenter !== nextState.zoomCenter || - this.state.zoomLevel !== nextState.zoomLevel - ); - } + const projection = useMemo(() => getProjection(width, height, offset, projectionConfig.scale), [ + width, + height, + ...offset, + projectionConfig.scale, + ]); + const zoomCenter = useMemo(() => getZoomCenter(center, offset, projection, zoomLevel), [ + ...center, + ...offset, + projection, + zoomLevel, + ]); - public render() { - const mapStyle = { - width: '100%', - height: '100%', - backgroundColor: '#192e45', - }; + const viewportBbox = useMemo( + () => getViewportGeoBoundingBox(zoomCenter, width, height, projection, zoomLevel), + [...zoomCenter, width, height, projection, zoomLevel], + ); - const zoomableGroupStyle = { - transition: `transform ${MOVE_SPEED}ms ease-in-out`, - }; + const combinedViewportBboxMatch = useMemo(() => getCombindedViewportBboxMatch(viewportBboxes), [ + viewportBboxes, + ]); + const visibleGeometry = useMemo(() => geometryTree.search(combinedViewportBboxMatch), [ + combinedViewportBboxMatch, + ]); + const visibleStatesProvincesLines = useMemo( + () => provincesStatesLinesTree.search(combinedViewportBboxMatch), + [combinedViewportBboxMatch], + ); - const geographyStyle = mergeRsmStyle({ - default: { - fill: '#294d73', - stroke: '#192e45', - strokeWidth: `${1 / this.state.zoomLevel}`, - }, - }); + const geographyStyle = useMemo( + () => + mergeRsmStyle({ + default: { + fill: '#294d73', + stroke: '#192e45', + strokeWidth: `${1 / zoomLevel}`, + }, + }), + [zoomLevel], + ); - const stateProvinceLineStyle = mergeRsmStyle({ - default: { - fill: 'transparent', - stroke: '#192e45', - strokeWidth: `${1 / this.state.zoomLevel}`, - }, - }); + const stateProvinceLineStyle = useMemo( + () => + mergeRsmStyle({ + default: { + fill: 'transparent', + stroke: '#192e45', + strokeWidth: `${1 / zoomLevel}`, + }, + }), + [zoomLevel], + ); - const markerStyle = mergeRsmStyle({ - default: { - transition: `transform ${MOVE_SPEED}ms ease-in-out`, - }, - }); + const removeOldViewportBboxes = useCallback(() => { + setViewportBboxes((viewportBboxes) => viewportBboxes.slice(-1)); + }, []); - // disable CSS transition when moving between locations - // by using the different "key" - const userMarker = this.props.showMarker && ( - <Marker - key={`user-location-${this.props.center.join('-')}`} - coordinates={this.props.center} - style={markerStyle}> - <image x="-6" y="-6" width="12" xlinkHref={this.props.markerImagePath} /> - </Marker> - ); + useEffect(() => { + setViewportBboxes((viewportBboxes) => [...viewportBboxes, viewportBbox]); + }, [viewportBbox]); - return ( - <ComposableMap - width={this.props.width} - height={this.props.height} - style={mapStyle} - projection={ - // Workaround for incorrect type definition in @types/react-simple-maps. - /* @ts-ignore */ - getProjection( - this.props.height, - this.props.width, - this.props.offset[0], - this.props.offset[1], - projectionConfig.scale, - ) as () => GeoProjection + return ( + <ComposableMap + width={width} + height={height} + style={mapStyle} + projection={ + // Workaround for incorrect type definition in @types/react-simple-maps. + /* @ts-ignore */ + projection as () => GeoProjection + } + projectionConfig={projectionConfig}> + <ZoomableGroup + center={zoomCenter} + zoom={zoomLevel} + onTransitionEnd={removeOldViewportBboxes} + style={zoomableGroupStyle}> + <Geographies geography={geographyData}> + {({ geographies }) => { + return visibleGeometry.map(({ id }) => ( + <Geography + key={id} + geography={geographies[parseInt(id, 10)]} + style={geographyStyle} + /> + )); + }} + </Geographies> + <Geographies geography={statesProvincesLinesData}> + {({ geographies }) => { + return visibleStatesProvincesLines.map(({ id }) => ( + <Geography + key={id} + geography={geographies[parseInt(id, 10)]} + style={stateProvinceLineStyle} + /> + )); + }} + </Geographies> + { + // disable CSS transition when moving between locations + // by using the different "key" + props.showMarker && ( + <Marker + key={`user-location-${center.join('-')}`} + coordinates={center} + style={markerStyle}> + <image x="-6" y="-6" width="12" xlinkHref={props.markerImagePath} /> + </Marker> + ) } - projectionConfig={projectionConfig}> - <ZoomableGroup - center={this.state.zoomCenter} - zoom={this.state.zoomLevel} - onTransitionEnd={this.removeOldViewportBboxes} - style={zoomableGroupStyle}> - <Geographies geography={geographyData}> - {({ geographies }) => { - return this.state.visibleGeometry.map(({ id }) => ( - <Geography - key={id} - geography={geographies[parseInt(id, 10)]} - style={geographyStyle} - /> - )); - }} - </Geographies> - <Geographies geography={statesProvincesLinesData}> - {({ geographies }) => { - return this.state.visibleStatesProvincesLines.map(({ id }) => ( - <Geography - key={id} - geography={geographies[parseInt(id, 10)]} - style={stateProvinceLineStyle} - /> - )); - }} - </Geographies> - {[userMarker]} - </ZoomableGroup> - </ComposableMap> - ); - } - - private removeOldViewportBboxes = () => { - this.setState((state) => ({ viewportBboxes: state.viewportBboxes.slice(-1) })); - }; + </ZoomableGroup> + </ComposableMap> + ); } |
