summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-09-03 17:38:24 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-09-10 10:03:47 +0200
commita0318e4f107e91bfcece741ffa77b841e1292c95 (patch)
tree950b56d91027b29a1bc6bacf2b19c286d2cf432b /gui/src
parentde022a5eda44c0363c9c65ea698ff48c02c34ecb (diff)
downloadmullvadvpn-a0318e4f107e91bfcece741ffa77b841e1292c95.tar.xz
mullvadvpn-a0318e4f107e91bfcece741ffa77b841e1292c95.zip
Convert SvgMap to functional component
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/SvgMap.tsx297
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>
+ );
}