UNPKG

@deck.gl/core

Version:

deck.gl core library

377 lines 13.3 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { clamp } from '@math.gl/core'; import Controller from "./controller.js"; import ViewState from "./view-state.js"; import { normalizeViewportProps } from '@math.gl/web-mercator'; import assert from "../utils/assert.js"; import LinearInterpolator from "../transitions/linear-interpolator.js"; const PITCH_MOUSE_THRESHOLD = 5; const PITCH_ACCEL = 1.2; /* Utils */ export class MapState extends ViewState { constructor(options) { const { /** Mapbox viewport properties */ /** The width of the viewport */ width, /** The height of the viewport */ height, /** The latitude at the center of the viewport */ latitude, /** The longitude at the center of the viewport */ longitude, /** The tile zoom level of the map. */ zoom, /** The bearing of the viewport in degrees */ bearing = 0, /** The pitch of the viewport in degrees */ pitch = 0, /** * Specify the altitude of the viewport camera * Unit: map heights, default 1.5 * Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137 */ altitude = 1.5, /** Viewport position */ position = [0, 0, 0], /** Viewport constraints */ maxZoom = 20, minZoom = 0, maxPitch = 60, minPitch = 0, /** Interaction states, required to calculate change during transform */ /* The point on map being grabbed when the operation first started */ startPanLngLat, /* Center of the zoom when the operation first started */ startZoomLngLat, /* Pointer position when rotation started */ startRotatePos, /** Bearing when current perspective rotate operation started */ startBearing, /** Pitch when current perspective rotate operation started */ startPitch, /** Zoom when current zoom operation started */ startZoom, /** Normalize viewport props to fit map height into viewport */ normalize = true } = options; assert(Number.isFinite(longitude)); // `longitude` must be supplied assert(Number.isFinite(latitude)); // `latitude` must be supplied assert(Number.isFinite(zoom)); // `zoom` must be supplied super({ width, height, latitude, longitude, zoom, bearing, pitch, altitude, maxZoom, minZoom, maxPitch, minPitch, normalize, position }, { startPanLngLat, startZoomLngLat, startRotatePos, startBearing, startPitch, startZoom }); this.makeViewport = options.makeViewport; } /** * Start panning * @param {[Number, Number]} pos - position on screen where the pointer grabs */ panStart({ pos }) { return this._getUpdatedState({ startPanLngLat: this._unproject(pos) }); } /** * Pan * @param {[Number, Number]} pos - position on screen where the pointer is * @param {[Number, Number], optional} startPos - where the pointer grabbed at * the start of the operation. Must be supplied of `panStart()` was not called */ pan({ pos, startPos }) { const startPanLngLat = this.getState().startPanLngLat || this._unproject(startPos); if (!startPanLngLat) { return this; } const viewport = this.makeViewport(this.getViewportProps()); const newProps = viewport.panByPosition(startPanLngLat, pos); return this._getUpdatedState(newProps); } /** * End panning * Must call if `panStart()` was called */ panEnd() { return this._getUpdatedState({ startPanLngLat: null }); } /** * Start rotating * @param {[Number, Number]} pos - position on screen where the center is */ rotateStart({ pos }) { return this._getUpdatedState({ startRotatePos: pos, startBearing: this.getViewportProps().bearing, startPitch: this.getViewportProps().pitch }); } /** * Rotate * @param {[Number, Number]} pos - position on screen where the center is */ rotate({ pos, deltaAngleX = 0, deltaAngleY = 0 }) { const { startRotatePos, startBearing, startPitch } = this.getState(); if (!startRotatePos || startBearing === undefined || startPitch === undefined) { return this; } let newRotation; if (pos) { newRotation = this._getNewRotation(pos, startRotatePos, startPitch, startBearing); } else { newRotation = { bearing: startBearing + deltaAngleX, pitch: startPitch + deltaAngleY }; } return this._getUpdatedState(newRotation); } /** * End rotating * Must call if `rotateStart()` was called */ rotateEnd() { return this._getUpdatedState({ startBearing: null, startPitch: null }); } /** * Start zooming * @param {[Number, Number]} pos - position on screen where the center is */ zoomStart({ pos }) { return this._getUpdatedState({ startZoomLngLat: this._unproject(pos), startZoom: this.getViewportProps().zoom }); } /** * Zoom * @param {[Number, Number]} pos - position on screen where the current center is * @param {[Number, Number]} startPos - the center position at * the start of the operation. Must be supplied of `zoomStart()` was not called * @param {Number} scale - a number between [0, 1] specifying the accumulated * relative scale. */ zoom({ pos, startPos, scale }) { // Make sure we zoom around the current mouse position rather than map center let { startZoom, startZoomLngLat } = this.getState(); if (!startZoomLngLat) { // We have two modes of zoom: // scroll zoom that are discrete events (transform from the current zoom level), // and pinch zoom that are continuous events (transform from the zoom level when // pinch started). // If startZoom state is defined, then use the startZoom state; // otherwise assume discrete zooming startZoom = this.getViewportProps().zoom; startZoomLngLat = this._unproject(startPos) || this._unproject(pos); } if (!startZoomLngLat) { return this; } const { maxZoom, minZoom } = this.getViewportProps(); let zoom = startZoom + Math.log2(scale); zoom = clamp(zoom, minZoom, maxZoom); const zoomedViewport = this.makeViewport({ ...this.getViewportProps(), zoom }); return this._getUpdatedState({ zoom, ...zoomedViewport.panByPosition(startZoomLngLat, pos) }); } /** * End zooming * Must call if `zoomStart()` was called */ zoomEnd() { return this._getUpdatedState({ startZoomLngLat: null, startZoom: null }); } zoomIn(speed = 2) { return this._zoomFromCenter(speed); } zoomOut(speed = 2) { return this._zoomFromCenter(1 / speed); } moveLeft(speed = 100) { return this._panFromCenter([speed, 0]); } moveRight(speed = 100) { return this._panFromCenter([-speed, 0]); } moveUp(speed = 100) { return this._panFromCenter([0, speed]); } moveDown(speed = 100) { return this._panFromCenter([0, -speed]); } rotateLeft(speed = 15) { return this._getUpdatedState({ bearing: this.getViewportProps().bearing - speed }); } rotateRight(speed = 15) { return this._getUpdatedState({ bearing: this.getViewportProps().bearing + speed }); } rotateUp(speed = 10) { return this._getUpdatedState({ pitch: this.getViewportProps().pitch + speed }); } rotateDown(speed = 10) { return this._getUpdatedState({ pitch: this.getViewportProps().pitch - speed }); } shortestPathFrom(viewState) { // const endViewStateProps = new this.ControllerState(endProps).shortestPathFrom(startViewstate); const fromProps = viewState.getViewportProps(); const props = { ...this.getViewportProps() }; const { bearing, longitude } = props; if (Math.abs(bearing - fromProps.bearing) > 180) { props.bearing = bearing < 0 ? bearing + 360 : bearing - 360; } if (Math.abs(longitude - fromProps.longitude) > 180) { props.longitude = longitude < 0 ? longitude + 360 : longitude - 360; } return props; } // Apply any constraints (mathematical or defined by _viewportProps) to map state applyConstraints(props) { // Ensure zoom is within specified range const { maxZoom, minZoom, zoom } = props; props.zoom = clamp(zoom, minZoom, maxZoom); // Ensure pitch is within specified range const { maxPitch, minPitch, pitch } = props; props.pitch = clamp(pitch, minPitch, maxPitch); // Normalize viewport props to fit map height into viewport const { normalize = true } = props; if (normalize) { Object.assign(props, normalizeViewportProps(props)); } return props; } /* Private methods */ _zoomFromCenter(scale) { const { width, height } = this.getViewportProps(); return this.zoom({ pos: [width / 2, height / 2], scale }); } _panFromCenter(offset) { const { width, height } = this.getViewportProps(); return this.pan({ startPos: [width / 2, height / 2], pos: [width / 2 + offset[0], height / 2 + offset[1]] }); } _getUpdatedState(newProps) { // @ts-ignore return new this.constructor({ makeViewport: this.makeViewport, ...this.getViewportProps(), ...this.getState(), ...newProps }); } _unproject(pos) { const viewport = this.makeViewport(this.getViewportProps()); // @ts-ignore return pos && viewport.unproject(pos); } _getNewRotation(pos, startPos, startPitch, startBearing) { const deltaX = pos[0] - startPos[0]; const deltaY = pos[1] - startPos[1]; const centerY = pos[1]; const startY = startPos[1]; const { width, height } = this.getViewportProps(); const deltaScaleX = deltaX / width; let deltaScaleY = 0; if (deltaY > 0) { if (Math.abs(height - startY) > PITCH_MOUSE_THRESHOLD) { // Move from 0 to -1 as we drag upwards deltaScaleY = (deltaY / (startY - height)) * PITCH_ACCEL; } } else if (deltaY < 0) { if (startY > PITCH_MOUSE_THRESHOLD) { // Move from 0 to 1 as we drag upwards deltaScaleY = 1 - centerY / startY; } } // clamp deltaScaleY to [-1, 1] so that rotation is constrained between minPitch and maxPitch. // deltaScaleX does not need to be clamped as bearing does not have constraints. deltaScaleY = clamp(deltaScaleY, -1, 1); const { minPitch, maxPitch } = this.getViewportProps(); const bearing = startBearing + 180 * deltaScaleX; let pitch = startPitch; if (deltaScaleY > 0) { // Gradually increase pitch pitch = startPitch + deltaScaleY * (maxPitch - startPitch); } else if (deltaScaleY < 0) { // Gradually decrease pitch pitch = startPitch - deltaScaleY * (minPitch - startPitch); } return { pitch, bearing }; } } export default class MapController extends Controller { constructor() { super(...arguments); this.ControllerState = MapState; this.transition = { transitionDuration: 300, transitionInterpolator: new LinearInterpolator({ transitionProps: { compare: ['longitude', 'latitude', 'zoom', 'bearing', 'pitch', 'position'], required: ['longitude', 'latitude', 'zoom'] } }) }; this.dragMode = 'pan'; } setProps(props) { props.position = props.position || [0, 0, 0]; const oldProps = this.props; super.setProps(props); const dimensionChanged = !oldProps || oldProps.height !== props.height; if (dimensionChanged) { // Dimensions changed, normalize the props this.updateViewport(new this.ControllerState({ makeViewport: this.makeViewport, ...props, ...this.state })); } } } //# sourceMappingURL=map-controller.js.map