Skip to content

feat: Automatic zoom to fit data bounds #909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions react/src/demo/example-data/deckgl-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@
437720,
6481113
],
"zoom": -3,
"layers": [

{
"@@type": "ColormapLayer",
"image": "@@#resources.propertyMap",
Expand Down
64 changes: 42 additions & 22 deletions react/src/lib/components/DeckGLMap/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { JSONConfiguration, JSONConverter } from "@deck.gl/json";
import DeckGL from "@deck.gl/react";
import { PickInfo } from "deck.gl";
import { Feature } from "geojson";
import React, { useEffect, useState, useCallback } from "react";
import React, { useEffect, useState, useCallback, useRef } from "react";
import Settings from "./settings/Settings";
import JSON_CONVERTER_CONFIG from "../utils/configuration";
import { MapState } from "../redux/store";
Expand All @@ -17,6 +17,7 @@ import { DeckGLView } from "./DeckGLView";
import { Viewport } from "@deck.gl/core";
import { colorTablesArray } from "@emerson-eps/color-tables/";
import { LayerProps, LayerContext } from "@deck.gl/core/lib/layer";
import { ViewStateProps } from "@deck.gl/core/lib/deck";
import { ViewProps } from "@deck.gl/core/views/view";
import { isEmpty } from "lodash";
import ColorLegend from "./ColorLegend";
Expand All @@ -26,6 +27,7 @@ import {
getLayersWithDefaultProps,
} from "../layers/utils/layerTools";
import ViewFooter from "./ViewFooter";
import fitBounds from "../utils/fit-bounds";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const colorTables = require("@emerson-eps/color-tables/src/component/color-tables.json");
Expand Down Expand Up @@ -188,7 +190,7 @@ const Map: React.FC<MapProps> = ({
const [initialViewState, setInitialViewState] =
useState<Record<string, unknown>>();
useEffect(() => {
if (bounds == undefined || zoom == undefined) return;
if (bounds == undefined) return;
setInitialViewState(getInitialViewState(bounds, zoom));
}, [bounds, zoom]);

Expand Down Expand Up @@ -230,16 +232,6 @@ const Map: React.FC<MapProps> = ({
setDeckGLLayers(jsonToObject(layers, enumerations) as Layer<unknown>[]);
}, [st_layers, resources, editedData]);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [viewState, setViewState] = useState<any>();

const refCb = useCallback((deckRef) => {
if (deckRef && deckRef.deck) {
// Needed to initialize the viewState on first load
setViewState(deckRef.deck.viewState);
}
}, []);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [hoverInfo, setHoverInfo] = useState<any>([]);
const onHover = useCallback(
Expand All @@ -259,8 +251,39 @@ const Map: React.FC<MapProps> = ({
[coords]
);

const [isLoaded, setIsLoaded] = useState<boolean>(false);
const deckRef = useRef<DeckGL>(null);
const [viewState, setViewState] = useState<ViewStateProps>();

const onLoad = useCallback(() => {
if (deckRef == null) return;

const deck = deckRef.current?.deck;
if (deck && bounds) {
const width = deck.width;
const height = deck.height;
const [west, south] = [bounds[0], bounds[1]];
const [east, north] = [bounds[2], bounds[3]];
const fitted_bound = fitBounds({
width,
height,
bounds: [
[west, south],
[east, north],
],
});

const zoom_defined_by_consumer = zoom != undefined;
const view_state = {
target: [fitted_bound.x, fitted_bound.y, 0],
zoom: zoom_defined_by_consumer ? zoom : fitted_bound.zoom,
rotationX: 90, // look down z -axis.
rotationOrbit: 0,
};
setViewState(view_state);
}
}, [deckRef]);

const [isLoaded, setIsLoaded] = useState<boolean>(false);
const onAfterRender = useCallback(() => {
if (deckGLLayers) {
const state = deckGLLayers.every((layer) => layer.isLoaded);
Expand Down Expand Up @@ -325,11 +348,13 @@ const Map: React.FC<MapProps> = ({
return feat?.properties?.["name"];
}
}}
ref={refCb}
ref={deckRef}
onHover={onHover}
onViewStateChange={(viewport) =>
setViewState(viewport.viewState)
}
onLoad={onLoad}
onResize={onLoad}
onAfterRender={onAfterRender}
>
{children}
Expand Down Expand Up @@ -372,11 +397,7 @@ const Map: React.FC<MapProps> = ({

{scale?.visible ? (
<DistanceScale
zoom={
viewState?.zoom
? viewState.zoom
: initialViewState?.["zoom"]
}
zoom={viewState?.zoom}
incrementValue={scale.incrementValue}
widthPerUnit={scale.widthPerUnit}
position={scale.position}
Expand Down Expand Up @@ -412,7 +433,6 @@ Map.defaultProps = {
horizontal: false,
},
coordinateUnit: "m",
zoom: -3,
views: {
layout: [1, 1],
showLabel: false,
Expand Down Expand Up @@ -453,15 +473,15 @@ function jsonToObject(
// returns initial view state for DeckGL
function getInitialViewState(
bounds: [number, number, number, number],
zoom: number
zoom?: number
): Record<string, unknown> {
const width = bounds[2] - bounds[0]; // right - left
const height = bounds[3] - bounds[1]; // top - bottom

const initial_view_state = {
// target to center of the bound
target: [bounds[0] + width / 2, bounds[1] + height / 2, 0],
zoom: zoom,
zoom: zoom ?? 0,
rotationX: 90, // look down z -axis.
rotationOrbit: 0,
};
Expand Down
84 changes: 84 additions & 0 deletions react/src/lib/components/DeckGLMap/utils/fit-bounds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Returns map settings {latitude, longitude, zoom}
// that will contain the provided corners within the provided width.
// Only supports non-perspective mode.

export function clamp(x, min, max) {
return x < min ? min : x > max ? max : x;
}

function ieLog2(x) {
return Math.log(x) * Math.LOG2E;
}

// Handle missing log2 in IE 11
export const log2 = Math.log2 || ieLog2;

export default function fitBounds({
width,
height,
bounds,
minExtent = 0, // 0.01 would be about 1000 meters (degree is ~110KM)
maxZoom = 24, // ~x4,000,000 => About 10 meter extents
// options
padding = 0,
offset = [0, 0],
}) {
if (Number.isFinite(padding)) {
const p = padding;
padding = {
top: p,
bottom: p,
left: p,
right: p,
};
} else {
// Make sure all the required properties are set
console.assert(
Number.isFinite(padding.top) &&
Number.isFinite(padding.bottom) &&
Number.isFinite(padding.left) &&
Number.isFinite(padding.right)
);
}

const [[west, south], [east, north]] = bounds;

const nw = [west, north];
const se = [east, south];

// width/height on the Web Mercator plane
const size = [
Math.max(Math.abs(se[0] - nw[0]), minExtent),
Math.max(Math.abs(se[1] - nw[1]), minExtent),
];

const targetSize = [
width - padding.left - padding.right - Math.abs(offset[0]) * 2,
height - padding.top - padding.bottom - Math.abs(offset[1]) * 2,
];
console.assert(targetSize[0] > 0 && targetSize[1] > 0);

// scale = screen pixels per unit on the Web Mercator plane
const scaleX = targetSize[0] / size[0];
const scaleY = targetSize[1] / size[1];

// Find how much we need to shift the center
const offsetX = (padding.right - padding.left) / 2 / scaleX;
const offsetY = (padding.bottom - padding.top) / 2 / scaleY;

const center = [
(se[0] + nw[0]) / 2 + offsetX,
(se[1] + nw[1]) / 2 + offsetY,
];

const centerLngLat = center;
const zoom = Math.min(maxZoom, log2(Math.abs(Math.min(scaleX, scaleY))));

console.assert(Number.isFinite(zoom));

return {
x: centerLngLat[0],
y: centerLngLat[1],
zoom,
};
}