@deck.gl/core
Version:
deck.gl core library
143 lines • 6.34 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { clamp } from '@math.gl/core';
import Controller from "./controller.js";
import { MapState } from "./map-controller.js";
import { mod } from "../utils/math-utils.js";
import LinearInterpolator from "../transitions/linear-interpolator.js";
import { zoomAdjust, GLOBE_RADIUS } from "../viewports/globe-viewport.js";
import { MAX_LATITUDE } from '@math.gl/web-mercator';
const DEGREES_TO_RADIANS = Math.PI / 180;
const RADIANS_TO_DEGREES = 180 / Math.PI;
function degreesToPixels(angle, zoom = 0) {
const radians = Math.min(180, angle) * DEGREES_TO_RADIANS;
const size = GLOBE_RADIUS * 2 * Math.sin(radians / 2);
return size * Math.pow(2, zoom);
}
function pixelsToDegrees(pixels, zoom = 0) {
const size = pixels / Math.pow(2, zoom);
const radians = Math.asin(Math.min(1, size / GLOBE_RADIUS / 2)) * 2;
return radians * RADIANS_TO_DEGREES;
}
class GlobeState extends MapState {
constructor(options) {
const { startPanPos, ...mapStateOptions } = options;
mapStateOptions.normalize = false; // disable MapState default normalization
super(mapStateOptions);
if (startPanPos !== undefined) {
this._state.startPanPos = startPanPos;
}
}
panStart({ pos }) {
const { latitude, longitude, zoom } = this.getViewportProps();
return this._getUpdatedState({
startPanLngLat: [longitude, latitude],
startPanPos: pos,
startZoom: zoom
});
}
pan({ pos, startPos }) {
const state = this.getState();
const startPanLngLat = state.startPanLngLat || this._unproject(startPos);
if (!startPanLngLat)
return this;
const startZoom = state.startZoom ?? this.getViewportProps().zoom;
const startPanPos = state.startPanPos || startPos;
const coords = [startPanLngLat[0], startPanLngLat[1], startZoom];
const viewport = this.makeViewport(this.getViewportProps());
const newProps = viewport.panByPosition(coords, pos, startPanPos);
return this._getUpdatedState(newProps);
}
panEnd() {
return this._getUpdatedState({
startPanLngLat: null,
startPanPos: null,
startZoom: null
});
}
zoom({ scale }) {
// In Globe view zoom does not take into account the mouse position
const startZoom = this.getState().startZoom || this.getViewportProps().zoom;
const zoom = startZoom + Math.log2(scale);
return this._getUpdatedState({ zoom });
}
applyConstraints(props) {
// Ensure zoom is within specified range
const { longitude, latitude, maxBounds } = props;
props.zoom = this._constrainZoom(props.zoom, props);
if (longitude < -180 || longitude > 180) {
props.longitude = mod(longitude + 180, 360) - 180;
}
props.latitude = clamp(latitude, -MAX_LATITUDE, MAX_LATITUDE);
if (maxBounds) {
props.longitude = clamp(props.longitude, maxBounds[0][0], maxBounds[1][0]);
props.latitude = clamp(props.latitude, maxBounds[0][1], maxBounds[1][1]);
}
if (maxBounds) {
// calculate center and zoom ranges at pitch=0 and bearing=0
// to maintain visual stability when rotating
const effectiveZoom = props.zoom - zoomAdjust(latitude);
const lngSpan = maxBounds[1][0] - maxBounds[0][0];
const latSpan = maxBounds[1][1] - maxBounds[0][1];
if (latSpan > 0 && latSpan < MAX_LATITUDE * 2) {
const halfHeightDegrees = Math.min(pixelsToDegrees(props.height, effectiveZoom), latSpan) / 2;
props.latitude = clamp(props.latitude, maxBounds[0][1] + halfHeightDegrees, maxBounds[1][1] - halfHeightDegrees);
}
if (lngSpan > 0 && lngSpan < 360) {
const halfWidthDegrees = Math.min(pixelsToDegrees(props.width / Math.cos(props.latitude * DEGREES_TO_RADIANS), effectiveZoom), lngSpan) / 2;
props.longitude = clamp(props.longitude, maxBounds[0][0] + halfWidthDegrees, maxBounds[1][0] - halfWidthDegrees);
}
}
if (props.latitude !== latitude) {
props.zoom += zoomAdjust(props.latitude) - zoomAdjust(latitude);
}
return props;
}
_constrainZoom(zoom, props) {
props || (props = this.getViewportProps());
const { latitude, maxZoom, maxBounds } = props;
let { minZoom } = props;
const ZOOM0 = zoomAdjust(0);
const zoomAdjustment = zoomAdjust(latitude) - ZOOM0;
const shouldApplyMaxBounds = maxBounds !== null && props.width > 0 && props.height > 0;
if (shouldApplyMaxBounds) {
const minLatitude = maxBounds[0][1];
const maxLatitude = maxBounds[1][1];
// latitude at which the bounding box is the widest
const fitLatitude = Math.sign(minLatitude) === Math.sign(maxLatitude)
? Math.min(Math.abs(minLatitude), Math.abs(maxLatitude))
: 0;
const w = degreesToPixels(maxBounds[1][0] - maxBounds[0][0]) *
Math.cos(fitLatitude * DEGREES_TO_RADIANS);
const h = degreesToPixels(maxBounds[1][1] - maxBounds[0][1]);
if (w > 0) {
minZoom = Math.max(minZoom, Math.log2(props.width / w) + ZOOM0);
}
if (h > 0) {
minZoom = Math.max(minZoom, Math.log2(props.height / h) + ZOOM0);
}
if (minZoom > maxZoom)
minZoom = maxZoom;
}
return clamp(zoom, minZoom + zoomAdjustment, maxZoom + zoomAdjustment);
}
}
export default class GlobeController extends Controller {
constructor() {
super(...arguments);
this.ControllerState = GlobeState;
this.transition = {
transitionDuration: 300,
transitionInterpolator: new LinearInterpolator(['longitude', 'latitude', 'zoom'])
};
this.dragMode = 'pan';
}
setProps(props) {
super.setProps(props);
// TODO - support pitching?
this.dragRotate = false;
this.touchRotate = false;
}
}
//# sourceMappingURL=globe-controller.js.map