summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-01-29 14:47:00 +0100
committerAndrej Mihajlov <and@mullvad.net>2018-01-29 14:47:00 +0100
commitb818240e9fead04e8f5981a44a4237a6a5cd0619 (patch)
tree57c5b67adc3831546871a9c05563cb3b420ab953
parentb90a42c33cd3f7c732d0f5d9233facc90bebcac0 (diff)
downloadmullvadvpn-b818240e9fead04e8f5981a44a4237a6a5cd0619.tar.xz
mullvadvpn-b818240e9fead04e8f5981a44a4237a6a5cd0619.zip
Decouple Map and SvgMap
-rw-r--r--app/components/Map.js358
-rw-r--r--app/components/SvgMap.js330
2 files changed, 393 insertions, 295 deletions
diff --git a/app/components/Map.js b/app/components/Map.js
index 256cdbf621..d416b77961 100644
--- a/app/components/Map.js
+++ b/app/components/Map.js
@@ -1,331 +1,99 @@
// @flow
-import React, { Component } from 'react';
-import { ComposableMap, ZoomableGroup, Geographies, Geography, Markers, Marker } from 'react-simple-maps';
+import React from 'react';
+import { Component, View } from 'reactxp';
-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);
-
-import type { Coordinate2d } from '../types';
-
-type BBox = [number, number, number, number];
+import SvgMap from './SvgMap';
export type MapProps = {
- width: number,
- height: number,
- center: Coordinate2d, // longitude, latitude
- offset: [number, number], // [x, y] in points
- zoomLevel: number,
+ center: [number, number], // longitude, latitude
+ offset: [number, number], // offset [x, y] from the center of the map
+ zoomLevel: 'high' | 'medium' | 'low',
showMarker: boolean,
- markerImagePath: string,
+ markerStyle: 'secure' | 'unsecure',
+ style: Object,
};
type MapState = {
- zoomCenter: [number, number],
- zoomLevel: number,
- visibleCities: Array<Object>,
- visibleCountries: Array<Object>,
- visibleGeometry: Array<Object>,
- visibleStatesProvincesLines: Array<Object>,
- viewportBbox: BBox,
+ bounds: {
+ width: number,
+ height: number
+ }
};
-const MOVE_SPEED = 2000;
-
export default class Map extends Component {
+
props: MapProps;
- state: MapState = {
- zoomCenter: [0, 0],
- zoomLevel: 1,
- visibleCities: [],
- visibleCountries: [],
- visibleGeometry: [],
- visibleStatesProvincesLines: [],
- viewportBbox: [0, 0, 0, 0],
- };
- _projectionConfig = {
- scale: 160
+ state: MapState = {
+ bounds: {
+ width: 0,
+ height: 0,
+ },
};
- constructor(props: MapProps) {
- super(props);
-
- this.state = this._getNextState(null, props);
- }
-
- componentWillReceiveProps(nextProps: MapProps) {
- if(this._shouldInvalidateState(nextProps)) {
- this.setState(prevState => this._getNextState(prevState, nextProps));
- }
- }
-
- shouldComponentUpdate(nextProps: MapProps, nextState: MapState) {
- 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>
- ));
-
+ const { width, height } = this.state.bounds;
+ const readyToRenderTheMap = (width > 0 && height > 0);
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) => {
- // see https://github.com/zcreativelabs/react-simple-maps/issues/51
- if(geographies.length === 0) {
- return [];
- }
- return this.state.visibleGeometry.map(({ id }) => (
- <Geography
- key={ id }
- geography={ geographies[id] }
- projection={ projection }
- style={ geographyStyle } />
- ));
- }}
- </Geographies>
- <Geographies geography={ statesProvincesLinesData } disableOptimization={ true }>
- {(geographies, projection) => {
- // see https://github.com/zcreativelabs/react-simple-maps/issues/51
- if(geographies.length === 0) {
- return [];
- }
- return this.state.visibleStatesProvincesLines.map(({ id }) => (
- <Geography
- key={ id }
- geography={ geographies[id] }
- projection={ projection }
- style={ stateProvinceLineStyle } />
- ));
- }}
- </Geographies>
- <Markers>
- { [...countryMarkers, ...cityMarkers, userMarker] }
- </Markers>
- </ZoomableGroup>
- </ComposableMap>
+ <View style={ this.props.style } onLayout={ this._onLayout }>
+ { readyToRenderTheMap && (
+ <SvgMap width={ width }
+ height={ height }
+ center={ this.props.center }
+ offset={ this.props.offset }
+ zoomLevel={ this._zoomLevel(this.props.zoomLevel) }
+ showMarker={ this.props.showMarker }
+ markerImagePath={ this._markerImage(this.props.markerStyle) } />
+ ) }
+ </View>
);
}
- _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: MapProps) {
+ shouldComponentUpdate(nextProps: MapProps, nextState: MapState) {
const oldProps = this.props;
+ const oldState = this.state;
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
+ oldProps.zoomLevel !== nextProps.zoomLevel ||
+ oldProps.showMarker !== nextProps.showMarker ||
+ oldProps.markerStyle !== nextProps.markerStyle ||
+
+ oldState.bounds.width !== nextState.bounds.width ||
+ oldState.bounds.height !== nextState.bounds.height
);
}
- _getNextState(prevState: ?MapState, nextProps: MapProps): MapState {
- 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],
- };
+ _onLayout = (layoutInfo) => {
+ this.setState({
+ bounds: {
+ width: layoutInfo.width,
+ height: layoutInfo.height,
+ }
+ });
+ }
- 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);
+ // TODO: Remove zoom level in favor of center + coordinate span
+ _zoomLevel(variant: $PropertyType<MapProps, 'zoomLevel'>) {
+ switch(variant) {
+ case 'high': return 1;
+ case 'medium': return 20;
+ case 'low': return 40;
+ }
+ }
- return {
- zoomCenter,
- zoomLevel,
- visibleCities,
- visibleCountries,
- visibleGeometry,
- visibleStatesProvincesLines,
- viewportBbox,
- };
+ _markerImage(style: $PropertyType<MapProps, 'markerStyle'>) {
+ switch(style) {
+ case 'secure':
+ return './assets/images/location-marker-secure.svg';
+ case 'unsecure':
+ return './assets/images/location-marker-unsecure.svg';
+ }
}
+
}
diff --git a/app/components/SvgMap.js b/app/components/SvgMap.js
new file mode 100644
index 0000000000..96bf6a40ba
--- /dev/null
+++ b/app/components/SvgMap.js
@@ -0,0 +1,330 @@
+// @flow
+
+import React, { Component } 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 Component {
+ props: SvgMapProps;
+ state: SvgMapState = {
+ 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) => {
+ // see https://github.com/zcreativelabs/react-simple-maps/issues/51
+ if(geographies.length === 0) {
+ return [];
+ }
+ return this.state.visibleGeometry.map(({ id }) => (
+ <Geography
+ key={ id }
+ geography={ geographies[id] }
+ projection={ projection }
+ style={ geographyStyle } />
+ ));
+ }}
+ </Geographies>
+ <Geographies geography={ statesProvincesLinesData } disableOptimization={ true }>
+ {(geographies, projection) => {
+ // see https://github.com/zcreativelabs/react-simple-maps/issues/51
+ if(geographies.length === 0) {
+ return [];
+ }
+ 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,
+ };
+ }
+}