UNPKG

@deck.gl/core

Version:

deck.gl core library

143 lines 6.34 kB
// 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