diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-18 15:07:37 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-08-15 17:39:38 +0200 |
| commit | 71592249b2dd669b6f24f37bfb7b0f4e43b74998 (patch) | |
| tree | a6097dc7e5d94d06e99c65fdfe160e824395f50c /app/components/SvgMap.js | |
| parent | e84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff) | |
| download | mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip | |
Add workspaces
Diffstat (limited to 'app/components/SvgMap.js')
| -rw-r--r-- | app/components/SvgMap.js | 336 |
1 files changed, 0 insertions, 336 deletions
diff --git a/app/components/SvgMap.js b/app/components/SvgMap.js deleted file mode 100644 index 43f1e2ef71..0000000000 --- a/app/components/SvgMap.js +++ /dev/null @@ -1,336 +0,0 @@ -// @flow - -import * as React from 'react'; -import { - ComposableMap, - ZoomableGroup, - Geographies, - Geography, - Markers, - Marker, -} from 'react-simple-maps'; - -import { geoTimes } from 'd3-geo-projection'; -import rbush from 'rbush'; - -import geographyData from '../assets/geo/geometry.json'; -import statesProvincesLinesData from '../assets/geo/states-provinces-lines.json'; - -import countryTreeData from '../assets/geo/countries.rbush.json'; -import cityTreeData from '../assets/geo/cities.rbush.json'; -import geometryTreeData from '../assets/geo/geometry.rbush.json'; -import statesProvincesLinesTreeData from '../assets/geo/states-provinces-lines.rbush.json'; - -const countryTree = rbush().fromJSON(countryTreeData); -const cityTree = rbush().fromJSON(cityTreeData); -const geometryTree = rbush().fromJSON(geometryTreeData); -const provincesStatesLinesTree = rbush().fromJSON(statesProvincesLinesTreeData); - -type BBox = [number, number, number, number]; - -export type SvgMapProps = { - width: number, - height: number, - center: [number, number], // longitude, latitude - offset: [number, number], // [x, y] in points - zoomLevel: number, - showMarker: boolean, - markerImagePath: string, -}; - -type SvgMapState = { - zoomCenter: [number, number], - zoomLevel: number, - visibleCities: Array<Object>, - visibleCountries: Array<Object>, - visibleGeometry: Array<Object>, - visibleStatesProvincesLines: Array<Object>, - viewportBbox: BBox, -}; - -const MOVE_SPEED = 2000; - -// @TODO: Calculate zoom level based on (center + span) (aka MKCoordinateSpan) -export default class SvgMap extends React.Component<SvgMapProps, SvgMapState> { - state = { - zoomCenter: [0, 0], - zoomLevel: 1, - visibleCities: [], - visibleCountries: [], - visibleGeometry: [], - visibleStatesProvincesLines: [], - viewportBbox: [0, 0, 0, 0], - }; - - _projectionConfig = { - scale: 160, - }; - - constructor(props: SvgMapProps) { - super(props); - - this.state = this._getNextState(null, props); - } - - componentWillReceiveProps(nextProps: SvgMapProps) { - if (this._shouldInvalidateState(nextProps)) { - this.setState((prevState) => this._getNextState(prevState, nextProps)); - } - } - - shouldComponentUpdate(nextProps: SvgMapProps, nextState: SvgMapState) { - 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 - ); - } - - render() { - const mapStyle = { - width: '100%', - height: '100%', - backgroundColor: '#192e45', - }; - - const zoomableGroupStyle = { - transition: `transform ${MOVE_SPEED}ms ease-in-out`, - }; - - const geographyStyle = this._mergeRsmStyle({ - default: { - fill: '#294d73', - stroke: '#192e45', - strokeWidth: `${1 / this.state.zoomLevel}`, - }, - }); - - const stateProvinceLineStyle = this._mergeRsmStyle({ - default: { - fill: 'transparent', - stroke: '#192e45', - strokeWidth: `${1 / this.state.zoomLevel}`, - }, - }); - - const markerStyle = this._mergeRsmStyle({ - default: { - transition: `transform ${MOVE_SPEED}ms ease-in-out`, - }, - }); - - // 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('-')}`} - marker={{ coordinates: this.props.center }} - style={markerStyle}> - <image x="-30" y="-30" href={this.props.markerImagePath} /> - </Marker> - ); - - const countryMarkers = this.state.visibleCountries.map((item) => ( - <Marker - key={`country-${item.id}`} - marker={{ coordinates: item.geometry.coordinates }} - style={markerStyle}> - <text fill="rgba(255,255,255,.6)" fontSize="22" textAnchor="middle"> - {item.properties.name} - </text> - </Marker> - )); - - const cityMarkers = this.state.visibleCities.map((item) => ( - <Marker - key={`city-${item.id}`} - marker={{ coordinates: item.geometry.coordinates }} - style={markerStyle}> - <circle r="2" fill="rgba(255,255,255,.6)" /> - <text x="0" y="-10" fill="rgba(255,255,255,.6)" fontSize="16" textAnchor="middle"> - {item.properties.name} - </text> - </Marker> - )); - - return ( - <ComposableMap - width={this.props.width} - height={this.props.height} - style={mapStyle} - projection={this._getProjection} - projectionConfig={this._projectionConfig}> - <ZoomableGroup - center={this.state.zoomCenter} - zoom={this.state.zoomLevel} - disablePanning={false} - style={zoomableGroupStyle}> - <Geographies geography={geographyData} disableOptimization={true}> - {(geographies, projection) => { - return this.state.visibleGeometry.map(({ id }) => ( - <Geography - key={id} - geography={geographies[id]} - projection={projection} - style={geographyStyle} - /> - )); - }} - </Geographies> - <Geographies geography={statesProvincesLinesData} disableOptimization={true}> - {(geographies, projection) => { - return this.state.visibleStatesProvincesLines.map(({ id }) => ( - <Geography - key={id} - geography={geographies[id]} - projection={projection} - style={stateProvinceLineStyle} - /> - )); - }} - </Geographies> - <Markers>{[...countryMarkers, ...cityMarkers, userMarker]}</Markers> - </ZoomableGroup> - </ComposableMap> - ); - } - - _mergeRsmStyle(style: Object) { - const defaultStyle = style.default || {}; - return { - default: defaultStyle, - hover: style.hover || defaultStyle, - pressed: style.pressed || defaultStyle, - }; - } - - _getProjection( - width: number, - height: number, - config: { - scale?: number, - xOffset?: number, - yOffset?: number, - rotation?: [number, number, number], - precision?: number, - }, - ) { - const scale = config.scale || 160; - const xOffset = config.xOffset || 0; - const yOffset = config.yOffset || 0; - const rotation = config.rotation || [0, 0, 0]; - const precision = config.precision || 0.1; - - return geoTimes() - .scale(scale) - .translate([xOffset + width / 2, yOffset + height / 2]) - .rotate(rotation) - .precision(precision); - } - - _getZoomCenter( - center: [number, number], - offset: [number, number], - projection: Function, - zoom: number, - ) { - const pos = projection(center); - return projection.invert([pos[0] + offset[0] / zoom, pos[1] + offset[1] / zoom]); - } - - _getViewportGeoBoundingBox( - centerCoordinate: [number, number], - width: number, - height: number, - projection: Function, - zoom: number, - ) { - const center = projection(centerCoordinate); - const halfWidth = (width * 0.5) / zoom; - const halfHeight = (height * 0.5) / zoom; - - const northWest = projection.invert([center[0] - halfWidth, center[1] - halfHeight]); - const southEast = projection.invert([center[0] + halfWidth, center[1] + halfHeight]); - - // normalize to [minX, minY, maxX, maxY] - return [ - Math.min(northWest[0], southEast[0]), - Math.min(northWest[1], southEast[1]), - Math.max(northWest[0], southEast[0]), - Math.max(northWest[1], southEast[1]), - ]; - } - - _shouldInvalidateState(nextProps: SvgMapProps) { - const oldProps = this.props; - 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 - ); - } - - _getNextState(prevState: ?SvgMapState, nextProps: SvgMapProps): SvgMapState { - const { width, height, center, offset, zoomLevel } = nextProps; - - const projection = this._getProjection(width, height, this._projectionConfig); - const zoomCenter = this._getZoomCenter(center, offset, projection, zoomLevel); - const viewportBbox = this._getViewportGeoBoundingBox( - zoomCenter, - width, - height, - projection, - zoomLevel, - ); - - const viewportBboxMatch = { - minX: viewportBbox[0], - minY: viewportBbox[1], - maxX: viewportBbox[2], - maxY: viewportBbox[3], - }; - - // combine previous and current viewports to get the rough area of transition - const combinedViewportBboxMatch = prevState - ? { - minX: Math.min(viewportBbox[0], prevState.viewportBbox[0]), - minY: Math.min(viewportBbox[1], prevState.viewportBbox[1]), - maxX: Math.max(viewportBbox[2], prevState.viewportBbox[2]), - maxY: Math.max(viewportBbox[3], prevState.viewportBbox[3]), - } - : { - minX: viewportBbox[0], - minY: viewportBbox[1], - maxX: viewportBbox[2], - maxY: viewportBbox[3], - }; - - const visibleCountries = - zoomLevel < 5 || zoomLevel > 20 ? [] : countryTree.search(viewportBboxMatch); - const visibleCities = zoomLevel >= 40 ? cityTree.search(viewportBboxMatch) : []; - const visibleGeometry = geometryTree.search(combinedViewportBboxMatch); - const visibleStatesProvincesLines = provincesStatesLinesTree.search(combinedViewportBboxMatch); - - return { - zoomCenter, - zoomLevel, - visibleCities, - visibleCountries, - visibleGeometry, - visibleStatesProvincesLines, - viewportBbox, - }; - } -} |
