Skip to content

Commit 33fc188

Browse files
Shadab KhanShadab Khan
authored andcommitted
feat: Automatic zoom to fit data bounds
Fix #689
1 parent c13e952 commit 33fc188

File tree

3 files changed

+123
-49
lines changed

3 files changed

+123
-49
lines changed

react/src/demo/example-data/deckgl-map.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@
3939
437720,
4040
6481113
4141
],
42-
"zoom": -3,
4342
"layers": [
44-
4543
{
4644
"@@type": "ColormapLayer",
4745
"image": "@@#resources.propertyMap",

react/src/lib/components/DeckGLMap/components/Map.tsx

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { JSONConfiguration, JSONConverter } from "@deck.gl/json";
22
import DeckGL from "@deck.gl/react";
33
import { PickInfo } from "deck.gl";
44
import { Feature } from "geojson";
5-
import React, { useEffect, useState, useCallback } from "react";
5+
import React, { useEffect, useState, useCallback, useRef } from "react";
66
import Settings from "./settings/Settings";
77
import JSON_CONVERTER_CONFIG from "../utils/configuration";
88
import { MapState } from "../redux/store";
@@ -17,6 +17,7 @@ import { DeckGLView } from "./DeckGLView";
1717
import { Viewport } from "@deck.gl/core";
1818
import { colorTablesArray } from "@emerson-eps/color-tables/";
1919
import { LayerProps, LayerContext } from "@deck.gl/core/lib/layer";
20+
import { ViewStateProps } from "@deck.gl/core/lib/deck";
2021
import { ViewProps } from "@deck.gl/core/views/view";
2122
import { isEmpty } from "lodash";
2223
import ColorLegend from "./ColorLegend";
@@ -26,6 +27,7 @@ import {
2627
getLayersWithDefaultProps,
2728
} from "../layers/utils/layerTools";
2829
import ViewFooter from "./ViewFooter";
30+
import fitBounds from "../utils/fit-bounds";
2931

3032
// eslint-disable-next-line @typescript-eslint/no-var-requires
3133
const colorTables = require("@emerson-eps/color-tables/src/component/color-tables.json");
@@ -184,14 +186,6 @@ const Map: React.FC<MapProps> = ({
184186
setEditedData,
185187
children,
186188
}: MapProps) => {
187-
// state for initial views prop (target and zoom) of DeckGL component
188-
const [initialViewState, setInitialViewState] =
189-
useState<Record<string, unknown>>();
190-
useEffect(() => {
191-
if (bounds == undefined || zoom == undefined) return;
192-
setInitialViewState(getInitialViewState(bounds, zoom));
193-
}, [bounds, zoom]);
194-
195189
// state for views prop of DeckGL component
196190
const [viewsProps, setViewsProps] = useState<ViewProps[]>([]);
197191
useEffect(() => {
@@ -230,16 +224,6 @@ const Map: React.FC<MapProps> = ({
230224
setDeckGLLayers(jsonToObject(layers, enumerations) as Layer<unknown>[]);
231225
}, [st_layers, resources, editedData]);
232226

233-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
234-
const [viewState, setViewState] = useState<any>();
235-
236-
const refCb = useCallback((deckRef) => {
237-
if (deckRef && deckRef.deck) {
238-
// Needed to initialize the viewState on first load
239-
setViewState(deckRef.deck.viewState);
240-
}
241-
}, []);
242-
243227
// eslint-disable-next-line @typescript-eslint/no-explicit-any
244228
const [hoverInfo, setHoverInfo] = useState<any>([]);
245229
const onHover = useCallback(
@@ -259,8 +243,39 @@ const Map: React.FC<MapProps> = ({
259243
[coords]
260244
);
261245

262-
const [isLoaded, setIsLoaded] = useState<boolean>(false);
246+
const deckRef = useRef<DeckGL>(null);
247+
const [viewState, setViewState] = useState<ViewStateProps>();
248+
249+
const onLoad = useCallback(() => {
250+
if (deckRef == null) return;
251+
252+
const deck = deckRef.current?.deck;
253+
if (deck && bounds) {
254+
const width = deck.width;
255+
const height = deck.height;
256+
const [west, south] = [bounds[0], bounds[1]];
257+
const [east, north] = [bounds[2], bounds[3]];
258+
const fitted_bound = fitBounds({
259+
width,
260+
height,
261+
bounds: [
262+
[west, south],
263+
[east, north],
264+
],
265+
});
263266

267+
const zoom_defined_by_consumer = zoom != undefined;
268+
const view_state = {
269+
target: [fitted_bound.x, fitted_bound.y, 0],
270+
zoom: zoom_defined_by_consumer ? zoom : fitted_bound.zoom,
271+
rotationX: 90, // look down z -axis.
272+
rotationOrbit: 0,
273+
};
274+
setViewState(view_state);
275+
}
276+
}, [deckRef]);
277+
278+
const [isLoaded, setIsLoaded] = useState<boolean>(false);
264279
const onAfterRender = useCallback(() => {
265280
if (deckGLLayers) {
266281
const state = deckGLLayers.every((layer) => layer.isLoaded);
@@ -301,7 +316,6 @@ const Map: React.FC<MapProps> = ({
301316
<DeckGL
302317
id={id}
303318
viewState={viewState}
304-
initialViewState={initialViewState}
305319
views={deckGLViews}
306320
layerFilter={layerFilter}
307321
layers={deckGLLayers}
@@ -325,11 +339,13 @@ const Map: React.FC<MapProps> = ({
325339
return feat?.properties?.["name"];
326340
}
327341
}}
328-
ref={refCb}
329342
onHover={onHover}
330343
onViewStateChange={(viewport) =>
331344
setViewState(viewport.viewState)
332345
}
346+
ref={deckRef}
347+
onLoad={onLoad}
348+
onResize={onLoad}
333349
onAfterRender={onAfterRender}
334350
>
335351
{children}
@@ -372,11 +388,7 @@ const Map: React.FC<MapProps> = ({
372388

373389
{scale?.visible ? (
374390
<DistanceScale
375-
zoom={
376-
viewState?.zoom
377-
? viewState.zoom
378-
: initialViewState?.["zoom"]
379-
}
391+
zoom={viewState?.zoom}
380392
incrementValue={scale.incrementValue}
381393
widthPerUnit={scale.widthPerUnit}
382394
position={scale.position}
@@ -412,7 +424,6 @@ Map.defaultProps = {
412424
horizontal: false,
413425
},
414426
coordinateUnit: "m",
415-
zoom: -3,
416427
views: {
417428
layout: [1, 1],
418429
showLabel: false,
@@ -450,25 +461,6 @@ function jsonToObject(
450461
return jsonConverter.convert(filtered_data);
451462
}
452463

453-
// returns initial view state for DeckGL
454-
function getInitialViewState(
455-
bounds: [number, number, number, number],
456-
zoom: number
457-
): Record<string, unknown> {
458-
const width = bounds[2] - bounds[0]; // right - left
459-
const height = bounds[3] - bounds[1]; // top - bottom
460-
461-
const initial_view_state = {
462-
// target to center of the bound
463-
target: [bounds[0] + width / 2, bounds[1] + height / 2, 0],
464-
zoom: zoom,
465-
rotationX: 90, // look down z -axis.
466-
rotationOrbit: 0,
467-
};
468-
469-
return initial_view_state;
470-
}
471-
472464
// construct views object for DeckGL component
473465
function getViews(views: ViewsType | undefined): Record<string, unknown>[] {
474466
const deckgl_views = [];
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Returns map settings {latitude, longitude, zoom}
2+
// that will contain the provided corners within the provided width.
3+
// Only supports non-perspective mode.
4+
5+
export function clamp(x, min, max) {
6+
return x < min ? min : x > max ? max : x;
7+
}
8+
9+
function ieLog2(x) {
10+
return Math.log(x) * Math.LOG2E;
11+
}
12+
13+
// Handle missing log2 in IE 11
14+
export const log2 = Math.log2 || ieLog2;
15+
16+
export default function fitBounds({
17+
width,
18+
height,
19+
bounds,
20+
minExtent = 0, // 0.01 would be about 1000 meters (degree is ~110KM)
21+
maxZoom = 24, // ~x4,000,000 => About 10 meter extents
22+
// options
23+
padding = 0,
24+
offset = [0, 0],
25+
}) {
26+
if (Number.isFinite(padding)) {
27+
const p = padding;
28+
padding = {
29+
top: p,
30+
bottom: p,
31+
left: p,
32+
right: p,
33+
};
34+
} else {
35+
// Make sure all the required properties are set
36+
console.assert(
37+
Number.isFinite(padding.top) &&
38+
Number.isFinite(padding.bottom) &&
39+
Number.isFinite(padding.left) &&
40+
Number.isFinite(padding.right)
41+
);
42+
}
43+
44+
const [[west, south], [east, north]] = bounds;
45+
46+
const nw = [west, north];
47+
const se = [east, south];
48+
49+
// width/height on the Web Mercator plane
50+
const size = [
51+
Math.max(Math.abs(se[0] - nw[0]), minExtent),
52+
Math.max(Math.abs(se[1] - nw[1]), minExtent),
53+
];
54+
55+
const targetSize = [
56+
width - padding.left - padding.right - Math.abs(offset[0]) * 2,
57+
height - padding.top - padding.bottom - Math.abs(offset[1]) * 2,
58+
];
59+
console.assert(targetSize[0] > 0 && targetSize[1] > 0);
60+
61+
// scale = screen pixels per unit on the Web Mercator plane
62+
const scaleX = targetSize[0] / size[0];
63+
const scaleY = targetSize[1] / size[1];
64+
65+
// Find how much we need to shift the center
66+
const offsetX = (padding.right - padding.left) / 2 / scaleX;
67+
const offsetY = (padding.bottom - padding.top) / 2 / scaleY;
68+
69+
const center = [
70+
(se[0] + nw[0]) / 2 + offsetX,
71+
(se[1] + nw[1]) / 2 + offsetY,
72+
];
73+
74+
const centerLngLat = center;
75+
const zoom = Math.min(maxZoom, log2(Math.abs(Math.min(scaleX, scaleY))));
76+
77+
console.assert(Number.isFinite(zoom));
78+
79+
return {
80+
x: centerLngLat[0],
81+
y: centerLngLat[1],
82+
zoom,
83+
};
84+
}

0 commit comments

Comments
 (0)