@deck.gl/core
Version:
deck.gl core library
289 lines • 11 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { deepEqual } from "../utils/deep-equal.js";
import log from "../utils/log.js";
import { flatten } from "../utils/flatten.js";
export default class ViewManager {
constructor(props) {
// List of view descriptors, gets re-evaluated when width/height changes
this.views = [];
this.width = 100;
this.height = 100;
this.viewState = {};
this.controllers = {};
this.timeline = props.timeline;
this._viewports = []; // Generated viewports
this._viewportMap = {};
this._isUpdating = false;
this._needsRedraw = 'First render';
this._needsUpdate = 'Initialize';
this._eventManager = props.eventManager;
this._eventCallbacks = {
onViewStateChange: props.onViewStateChange,
onInteractionStateChange: props.onInteractionStateChange
};
Object.seal(this);
// Init with default map viewport
this.setProps(props);
}
/** Remove all resources and event listeners */
finalize() {
for (const key in this.controllers) {
const controller = this.controllers[key];
if (controller) {
controller.finalize();
}
}
this.controllers = {};
}
/** Check if a redraw is needed */
needsRedraw(opts = { clearRedrawFlags: false }) {
const redraw = this._needsRedraw;
if (opts.clearRedrawFlags) {
this._needsRedraw = false;
}
return redraw;
}
/** Mark the manager as dirty. Will rebuild all viewports and update controllers. */
setNeedsUpdate(reason) {
this._needsUpdate = this._needsUpdate || reason;
this._needsRedraw = this._needsRedraw || reason;
}
/** Checks each viewport for transition updates */
updateViewStates() {
for (const viewId in this.controllers) {
const controller = this.controllers[viewId];
if (controller) {
controller.updateTransition();
}
}
}
/** Get a set of viewports for a given width and height
* TODO - Intention is for deck.gl to autodeduce width and height and drop the need for props
* @param rect (object, optional) - filter the viewports
* + not provided - return all viewports
* + {x, y} - only return viewports that contain this pixel
* + {x, y, width, height} - only return viewports that overlap with this rectangle
*/
getViewports(rect) {
if (rect) {
return this._viewports.filter(viewport => viewport.containsPixel(rect));
}
return this._viewports;
}
/** Get a map of all views */
getViews() {
const viewMap = {};
this.views.forEach(view => {
viewMap[view.id] = view;
});
return viewMap;
}
/** Resolves a viewId string to a View */
getView(viewId) {
return this.views.find(view => view.id === viewId);
}
/** Returns the viewState for a specific viewId. Matches the viewState by
1. view.viewStateId
2. view.id
3. root viewState
then applies the view's filter if any */
getViewState(viewOrViewId) {
const view = typeof viewOrViewId === 'string' ? this.getView(viewOrViewId) : viewOrViewId;
// Backward compatibility: view state for single view
const viewState = (view && this.viewState[view.getViewStateId()]) || this.viewState;
return view ? view.filterViewState(viewState) : viewState;
}
getViewport(viewId) {
return this._viewportMap[viewId];
}
/**
* Unproject pixel coordinates on screen onto world coordinates,
* (possibly [lon, lat]) on map.
* - [x, y] => [lng, lat]
* - [x, y, z] => [lng, lat, Z]
* @param {Array} xyz -
* @param {Object} opts - options
* @param {Object} opts.topLeft=true - Whether origin is top left
* @return {Array|null} - [lng, lat, Z] or [X, Y, Z]
*/
unproject(xyz, opts) {
const viewports = this.getViewports();
const pixel = { x: xyz[0], y: xyz[1] };
for (let i = viewports.length - 1; i >= 0; --i) {
const viewport = viewports[i];
if (viewport.containsPixel(pixel)) {
const p = xyz.slice();
p[0] -= viewport.x;
p[1] -= viewport.y;
return viewport.unproject(p, opts);
}
}
return null;
}
/** Update the manager with new Deck props */
setProps(props) {
if (props.views) {
this._setViews(props.views);
}
if (props.viewState) {
this._setViewState(props.viewState);
}
if ('width' in props || 'height' in props) {
this._setSize(props.width, props.height);
}
// Important: avoid invoking _update() inside itself
// Nested updates result in unexpected side effects inside _rebuildViewports()
// when using auto control in pure-js
if (!this._isUpdating) {
this._update();
}
}
//
// PRIVATE METHODS
//
_update() {
this._isUpdating = true;
// Only rebuild viewports if the update flag is set
if (this._needsUpdate) {
this._needsUpdate = false;
this._rebuildViewports();
}
// If viewport transition(s) are triggered during viewports update, controller(s)
// will immediately call `onViewStateChange` which calls `viewManager.setProps` again.
if (this._needsUpdate) {
this._needsUpdate = false;
this._rebuildViewports();
}
this._isUpdating = false;
}
_setSize(width, height) {
if (width !== this.width || height !== this.height) {
this.width = width;
this.height = height;
this.setNeedsUpdate('Size changed');
}
}
// Update the view descriptor list and set change flag if needed
// Does not actually rebuild the `Viewport`s until `getViewports` is called
_setViews(views) {
views = flatten(views, Boolean);
const viewsChanged = this._diffViews(views, this.views);
if (viewsChanged) {
this.setNeedsUpdate('views changed');
}
this.views = views;
}
_setViewState(viewState) {
if (viewState) {
// depth = 3 when comparing viewStates: viewId.position.0
const viewStateChanged = !deepEqual(viewState, this.viewState, 3);
if (viewStateChanged) {
this.setNeedsUpdate('viewState changed');
}
this.viewState = viewState;
}
else {
log.warn('missing `viewState` or `initialViewState`')();
}
}
_createController(view, props) {
const Controller = props.type;
const controller = new Controller({
timeline: this.timeline,
eventManager: this._eventManager,
// Set an internal callback that calls the prop callback if provided
onViewStateChange: this._eventCallbacks.onViewStateChange,
onStateChange: this._eventCallbacks.onInteractionStateChange,
makeViewport: viewState => this.getView(view.id)?.makeViewport({
viewState,
width: this.width,
height: this.height
})
});
return controller;
}
_updateController(view, viewState, viewport, controller) {
const controllerProps = view.controller;
if (controllerProps && viewport) {
const resolvedProps = {
...viewState,
...controllerProps,
id: view.id,
x: viewport.x,
y: viewport.y,
width: viewport.width,
height: viewport.height
};
// Create controller if not already existing or if the type of the
// controller has changed.
if (!controller || controller.constructor !== controllerProps.type) {
controller = this._createController(view, resolvedProps);
}
if (controller) {
controller.setProps(resolvedProps);
}
return controller;
}
return null;
}
// Rebuilds viewports from descriptors towards a certain window size
_rebuildViewports() {
const { views } = this;
const oldControllers = this.controllers;
this._viewports = [];
this.controllers = {};
let invalidateControllers = false;
// Create controllers in reverse order, so that views on top receive events first
for (let i = views.length; i--;) {
const view = views[i];
const viewState = this.getViewState(view);
const viewport = view.makeViewport({ viewState, width: this.width, height: this.height });
let oldController = oldControllers[view.id];
const hasController = Boolean(view.controller);
if (hasController && !oldController) {
// When a new controller is added, invalidate all controllers below it so that
// events are registered in the correct order
invalidateControllers = true;
}
if ((invalidateControllers || !hasController) && oldController) {
// Remove and reattach invalidated controller
oldController.finalize();
oldController = null;
}
// Update the controller
this.controllers[view.id] = this._updateController(view, viewState, viewport, oldController);
if (viewport) {
this._viewports.unshift(viewport);
}
}
// Remove unused controllers
for (const id in oldControllers) {
const oldController = oldControllers[id];
if (oldController && !this.controllers[id]) {
oldController.finalize();
}
}
this._buildViewportMap();
}
_buildViewportMap() {
// Build a view id to view index
this._viewportMap = {};
this._viewports.forEach(viewport => {
if (viewport.id) {
// TODO - issue warning if multiple viewports use same id
this._viewportMap[viewport.id] = this._viewportMap[viewport.id] || viewport;
}
});
}
// Check if viewport array has changed, returns true if any change
// Note that descriptors can be the same
_diffViews(newViews, oldViews) {
if (newViews.length !== oldViews.length) {
return true;
}
return newViews.some((_, i) => !newViews[i].equals(oldViews[i]));
}
}
//# sourceMappingURL=view-manager.js.map