itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
1,288 lines (1,111 loc) • 46.9 kB
JavaScript
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = exports.VIEW_EVENTS = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var THREE = _interopRequireWildcard(require("three"));
var _Camera = _interopRequireDefault(require("../Renderer/Camera"));
var _MainLoop = _interopRequireWildcard(require("./MainLoop"));
var _ColorLayersOrdering = require("../Renderer/ColorLayersOrdering");
var _c3DEngine = _interopRequireDefault(require("../Renderer/c3DEngine"));
var _RenderMode = _interopRequireDefault(require("../Renderer/RenderMode"));
var _Crs = _interopRequireDefault(require("./Geographic/Crs"));
var _Coordinates = _interopRequireDefault(require("./Geographic/Coordinates"));
var _FeaturesUtils = _interopRequireDefault(require("../Utils/FeaturesUtils"));
var _LayeredMaterial = require("../Renderer/LayeredMaterial");
var _Scheduler = _interopRequireDefault(require("./Scheduler/Scheduler"));
var _Picking = _interopRequireDefault(require("./Picking"));
// TEMPORY fix, on waiting THREE v110.
THREE.ShaderChunk.logdepthbuf_fragment = THREE.ShaderChunk.logdepthbuf_fragment.replace('== 1.0', '> 0.5');
var VIEW_EVENTS = {
/**
* Fires when all the layers of the view are considered initialized.
* Initialized in this context means: all layers are ready to be
* displayed (no pending network access, no visual improvement to be
* expected, ...).
* If you add new layers, the event will be fired again when all
* layers are ready.
* @event View#layers-initialized
* @property type {string} layers-initialized
*/
LAYERS_INITIALIZED: 'layers-initialized',
LAYER_REMOVED: 'layer-removed',
LAYER_ADDED: 'layer-added',
INITIALIZED: 'initialized',
COLOR_LAYERS_ORDER_CHANGED: _ColorLayersOrdering.COLOR_LAYERS_ORDER_CHANGED
};
exports.VIEW_EVENTS = VIEW_EVENTS;
var _syncGeometryLayerVisibility = function (layer, view) {
if (layer.object3d) {
layer.object3d.visible = layer.visible;
}
if (layer.threejsLayer) {
if (layer.visible) {
view.camera.camera3D.layers.enable(layer.threejsLayer);
} else {
view.camera.camera3D.layers.disable(layer.threejsLayer);
}
}
};
function _preprocessLayer(view, layer, provider, parentLayer) {
var source = layer.source;
if (parentLayer && !layer.extent) {
layer.extent = parentLayer.extent;
if (source && !source.extent) {
source.extent = parentLayer.extent;
}
}
if (layer.isGeometryLayer) {
if (parentLayer) {
// layer.threejsLayer *must* be assigned before preprocessing,
// because TileProvider.preprocessDataLayer function uses it.
layer.threejsLayer = view.mainLoop.gfxEngine.getUniqueThreejsLayer();
}
layer.defineLayerProperty('visible', true, function () {
return _syncGeometryLayerVisibility(layer, view);
});
_syncGeometryLayerVisibility(layer, view); // Find projection layer, this is projection destination
layer.projection = view.referenceCrs;
} else if (parentLayer.tileMatrixSets.includes(_Crs["default"].formatToTms(source.projection))) {
layer.projection = source.projection;
} else {
layer.projection = parentLayer.extent.crs;
}
if (!layer.whenReady) {
if (provider && provider.preprocessDataLayer) {
layer.whenReady = provider.preprocessDataLayer(layer, view, view.mainLoop.scheduler, parentLayer);
} else if (source && source.whenReady) {
layer.whenReady = source.whenReady;
} else {
layer.whenReady = Promise.resolve();
}
}
layer.whenReady = layer.whenReady.then(function () {
layer.ready = true;
return layer;
});
return layer;
}
var _eventCoords = new THREE.Vector2();
var matrix = new THREE.Matrix4();
var screen = new THREE.Vector2();
var ray = new THREE.Ray();
var direction = new THREE.Vector3();
var positionVector = new THREE.Vector3();
var coordinates = new _Coordinates["default"]('EPSG:4326');
var View =
/*#__PURE__*/
function (_THREE$EventDispatche) {
(0, _inherits2["default"])(View, _THREE$EventDispatche);
/**
* Constructs an Itowns View instance
*
* @param {string} crs - The default CRS of Three.js coordinates. Should be a cartesian CRS.
* @param {HTMLElement} viewerDiv - Where to instanciate the Three.js scene in the DOM
* @param {Object=} options - Optional properties.
* @param {?MainLoop} options.mainLoop - {@link MainLoop} instance to use, otherwise a default one will be constructed
* @param {?(WebGLRenderer|object)} options.renderer - {@link WebGLRenderer} instance to use, otherwise
* a default one will be constructed. In this case, if options.renderer is an object, it will be used to
* configure the renderer (see {@link c3DEngine}. If not present, a new <canvas> will be created and
* added to viewerDiv (mutually exclusive with mainLoop)
* @param {?Scene} options.scene3D - {@link Scene} instance to use, otherwise a default one will be constructed
* @constructor
*/
function View(crs, viewerDiv) {
var _this;
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
(0, _classCallCheck2["default"])(this, View);
if (!viewerDiv) {
throw new Error('Invalid viewerDiv parameter (must non be null/undefined)');
}
_this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(View).call(this));
_this.referenceCrs = crs;
coordinates.crs = crs;
var engine; // options.renderer can be 2 separate things:
// - an actual renderer (in this case we don't use viewerDiv)
// - options for the renderer to be created
if (options.renderer && options.renderer.domElement) {
engine = new _c3DEngine["default"](options.renderer);
} else {
engine = new _c3DEngine["default"](viewerDiv, options.renderer);
}
_this.mainLoop = options.mainLoop || new _MainLoop["default"](new _Scheduler["default"](), engine);
_this.scene = options.scene3D || new THREE.Scene();
if (!options.scene3D) {
_this.scene.autoUpdate = false;
}
_this.camera = new _Camera["default"](_this.referenceCrs, _this.mainLoop.gfxEngine.getWindowSize().x, _this.mainLoop.gfxEngine.getWindowSize().y, options);
_this._frameRequesters = {};
_this._layers = [];
window.addEventListener('resize', function () {
// If the user gave us a container (<div>) then itowns' size is
// the container's size. Otherwise we use window' size.
_this.mainLoop.gfxEngine.onWindowResize(viewerDiv.clientWidth, viewerDiv.clientHeight);
_this.camera.resize(viewerDiv.clientWidth, viewerDiv.clientHeight);
_this.notifyChange(_this.camera.camera3D);
}, false);
_this._changeSources = new Set();
_this._delayedFrameRequesterRemoval = [];
_this._allLayersAreReadyCallback = function () {
// all layers must be ready
var allReady = _this.getLayers().every(function (layer) {
return layer.ready;
});
if (allReady && _this.mainLoop.scheduler.commandsWaitingExecutionCount() == 0 && _this.mainLoop.renderingState == _MainLoop.RENDERING_PAUSED) {
_this.dispatchEvent({
type: VIEW_EVENTS.LAYERS_INITIALIZED
});
_this.removeFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END, _this._allLayersAreReadyCallback);
}
};
_this.camera.resize(viewerDiv.clientWidth, viewerDiv.clientHeight);
var fn = function () {
_this.removeEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn);
_this.dispatchEvent({
type: VIEW_EVENTS.INITIALIZED
});
};
_this.addEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn);
_this._fullSizeDepthBuffer = null;
_this.addFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.BEFORE_RENDER, function () {
if (_this._fullSizeDepthBuffer != null && _this._fullSizeDepthBuffer.needsUpdate) {
// clean depth buffer
_this._fullSizeDepthBuffer = null;
}
}); // Focus needed to capture some key events.
viewerDiv.focus();
return _this;
}
/**
* Add layer in viewer.
* The layer id must be unique.
*
* This function calls `preprocessDataLayer` of the relevant provider with this
* layer and set `layer.whenReady` to a promise that resolves when
* the preprocessing operation is done. This promise is also returned by
* `addLayer` allowing to chain call.
*
* @param {LayerOptions|Layer|GeometryLayer} layer
* @param {Layer=} parentLayer
* @return {Promise} a promise resolved with the new layer object when it is fully initialized or rejected if any error occurred.
*/
(0, _createClass2["default"])(View, [{
key: "addLayer",
value: function addLayer(layer, parentLayer) {
var _this2 = this;
return new Promise(function (resolve, reject) {
if (!layer) {
reject(new Error('layer is undefined'));
return;
}
var duplicate = _this2.getLayerById(layer.id);
if (duplicate) {
reject(new Error("Invalid id '".concat(layer.id, "': id already used")));
return;
}
var protocol = layer.source ? layer.source.protocol : layer.protocol;
var provider = _this2.mainLoop.scheduler.getProtocolProvider(protocol);
if (layer.protocol && !provider) {
reject(new Error("".concat(layer.protocol, " is not a recognized protocol name.")));
return;
}
layer = _preprocessLayer(_this2, layer, provider, parentLayer);
if (parentLayer) {
if (layer.isColorLayer) {
var layerColors = _this2.getLayers(function (l) {
return l.isColorLayer;
});
var sumColorLayers = parentLayer.countColorLayersTextures.apply(parentLayer, (0, _toConsumableArray2["default"])(layerColors).concat([layer]));
if (sumColorLayers <= (0, _LayeredMaterial.getMaxColorSamplerUnitsCount)()) {
parentLayer.attach(layer);
} else {
reject(new Error("Cant add color layer ".concat(layer.id, ": the maximum layer is reached")));
return;
}
} else {
parentLayer.attach(layer);
}
} else {
if (typeof layer.update !== 'function') {
reject(new Error('Cant add GeometryLayer: missing a update function'));
return;
}
if (typeof layer.preUpdate !== 'function') {
reject(new Error('Cant add GeometryLayer: missing a preUpdate function'));
return;
}
_this2._layers.push(layer);
}
if (layer.object3d && !layer.object3d.parent && layer.object3d !== _this2.scene) {
_this2.scene.add(layer.object3d);
}
layer.whenReady.then(function (layer) {
_this2.notifyChange(parentLayer || layer, false);
if (!_this2._frameRequesters[_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END] || !_this2._frameRequesters[_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END].includes(_this2._allLayersAreReadyCallback)) {
_this2.addFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END, _this2._allLayersAreReadyCallback);
}
resolve(layer);
});
_this2.dispatchEvent({
type: VIEW_EVENTS.LAYER_ADDED,
layerId: layer.id
});
});
}
/**
* Removes a specific imagery layer from the current layer list. This removes layers inserted with attach().
* @example
* view.removeLayer('layerId');
* @param {string} layerId The identifier
* @return {boolean}
*/
}, {
key: "removeLayer",
value: function removeLayer(layerId) {
var layer = this.getLayerById(layerId);
if (layer) {
var parentLayer = layer.parent; // Remove and dispose all nodes
layer["delete"](); // Detach layer if it's attached
if (parentLayer && !parentLayer.detach(layer)) {
throw new Error("Error to detach ".concat(layerId, " from ").concat(parentLayer.id));
} else if (parentLayer == undefined) {
// Remove layer from viewer
this._layers.splice(this._layers.findIndex(function (l) {
return l.id == layerId;
}), 1);
}
if (layer.isColorLayer) {
// Update color layers sequence
var imageryLayers = this.getLayers(function (l) {
return l.isColorLayer;
});
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = imageryLayers[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var color = _step.value;
if (color.sequence > layer.sequence) {
color.sequence--;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator["return"] != null) {
_iterator["return"]();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
this.notifyChange(this.camera);
this.dispatchEvent({
type: VIEW_EVENTS.LAYER_REMOVED,
layerId: layerId
});
return true;
} else {
throw new Error("".concat(layerId, " doesn't exist"));
}
}
/**
* Notifies the scene it needs to be updated due to changes exterior to the
* scene itself (e.g. camera movement).
* non-interactive events (e.g: texture loaded)
* @param {*} changeSource
* @param {boolean} needsRedraw - indicates if notified change requires a full scene redraw.
*/
}, {
key: "notifyChange",
value: function notifyChange() {
var changeSource = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
var needsRedraw = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
if (changeSource) {
this._changeSources.add(changeSource);
if ((changeSource.isTileMesh || changeSource.isCamera) && this._fullSizeDepthBuffer) {
this._fullSizeDepthBuffer.needsUpdate = true;
}
}
this.mainLoop.scheduleViewUpdate(this, needsRedraw);
}
/**
* Get all layers, with an optionnal filter applied.
* The filter method will be called with 2 args:
* - 1st: current layer
* - 2nd: (optional) the geometry layer to which the current layer is attached
* @example
* // get all layers
* view.getLayers();
* // get all color layers
* view.getLayers(layer => layer.isColorLayer);
* // get all elevation layers
* view.getLayers(layer => layer.isElevationLayer);
* // get all geometry layers
* view.getLayers(layer => layer.isGeometryLayer);
* // get one layer with id
* view.getLayers(layer => layer.id === 'itt');
* @param {function(Layer):boolean} filter
* @returns {Array<Layer>}
*/
}, {
key: "getLayers",
value: function getLayers(filter) {
var result = [];
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = this._layers[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var layer = _step2.value;
if (!filter || filter(layer)) {
result.push(layer);
}
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = layer.attachedLayers[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var attached = _step3.value;
if (!filter || filter(attached, layer)) {
result.push(attached);
}
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) {
_iterator3["return"]();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
_iterator2["return"]();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return result;
}
/**
* Gets the layer by identifier.
*
* @param {String} layerId The layer identifier
* @return {Layer} The layer by identifier.
*/
}, {
key: "getLayerById",
value: function getLayerById(layerId) {
var layers = this.getLayers(function (l) {
return l.id === layerId;
});
if (layers.length) {
return layers[0];
}
}
/**
* @param {Layer} layer
* @returns {GeometryLayer} the parent layer of the given layer or undefined.
*/
}, {
key: "getParentLayer",
value: function getParentLayer(layer) {
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = this._layers[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var geometryLayer = _step4.value;
var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;
try {
for (var _iterator5 = geometryLayer.attachedLayers[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
var attached = _step5.value;
if (attached === layer) {
return geometryLayer;
}
}
} catch (err) {
_didIteratorError5 = true;
_iteratorError5 = err;
} finally {
try {
if (!_iteratorNormalCompletion5 && _iterator5["return"] != null) {
_iterator5["return"]();
}
} finally {
if (_didIteratorError5) {
throw _iteratorError5;
}
}
}
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) {
_iterator4["return"]();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
}
/**
* @name FrameRequester
* @function
*
* @description
* Method that will be called each time the `MainLoop` updates. This function
* will be given as parameter the delta (in ms) between this update and the
* previous one, and whether or not we just started to render again. This update
* is considered as the "next" update if `view.notifyChange` was called during a
* precedent update. If `view.notifyChange` has been called by something else
* (other micro/macrotask, UI events etc...), then this update is considered as
* being the "first". It can also receive optional arguments, depending on the
* attach point of this function. Currently only `BEFORE_LAYER_UPDATE /
* AFTER_LAYER_UPDATE` attach points provide an additional argument: the layer
* being updated.
* <br><br>
*
* This means that if a `frameRequester` function wants to animate something, it
* should keep on calling `view.notifyChange` until its task is done.
* <br><br>
*
* Implementors of `frameRequester` should keep in mind that this function will
* be potentially called at each frame, thus care should be given about
* performance.
* <br><br>
*
* Typical frameRequesters are controls, module wanting to animate moves or UI
* elements etc... Basically anything that would want to call
* requestAnimationFrame.
*
* @param {number} dt
* @param {boolean} updateLoopRestarted
* @param {...*} args
*/
/**
* Add a frame requester to this view.
*
* FrameRequesters can activate the MainLoop update by calling view.notifyChange.
*
* @param {String} when - decide when the frameRequester should be called during
* the update cycle. Can be any of {@link MAIN_LOOP_EVENTS}.
* @param {FrameRequester} frameRequester - this function will be called at each
* MainLoop update with the time delta between last update, or 0 if the MainLoop
* has just been relaunched.
*/
}, {
key: "addFrameRequester",
value: function addFrameRequester(when, frameRequester) {
if (typeof frameRequester !== 'function') {
throw new Error('frameRequester must be a function');
}
if (!this._frameRequesters[when]) {
this._frameRequesters[when] = [frameRequester];
} else {
this._frameRequesters[when].push(frameRequester);
}
}
/**
* Remove a frameRequester.
* The effective removal will happen either later; at worst it'll be at
* the beginning of the next frame.
*
* @param {String} when - attach point of this requester. Can be any of
* {@link MAIN_LOOP_EVENTS}.
* @param {FrameRequester} frameRequester
*/
}, {
key: "removeFrameRequester",
value: function removeFrameRequester(when, frameRequester) {
if (this._frameRequesters[when].includes(frameRequester)) {
this._delayedFrameRequesterRemoval.push({
when: when,
frameRequester: frameRequester
});
} else {
console.error('Invalid call to removeFrameRequester: frameRequester isn\'t registered');
}
}
}, {
key: "_executeFrameRequestersRemovals",
value: function _executeFrameRequestersRemovals() {
var _iteratorNormalCompletion6 = true;
var _didIteratorError6 = false;
var _iteratorError6 = undefined;
try {
for (var _iterator6 = this._delayedFrameRequesterRemoval[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
var toDelete = _step6.value;
var index = this._frameRequesters[toDelete.when].indexOf(toDelete.frameRequester);
if (index >= 0) {
this._frameRequesters[toDelete.when].splice(index, 1);
} else {
console.warn('FrameReq has already been removed');
}
}
} catch (err) {
_didIteratorError6 = true;
_iteratorError6 = err;
} finally {
try {
if (!_iteratorNormalCompletion6 && _iterator6["return"] != null) {
_iterator6["return"]();
}
} finally {
if (_didIteratorError6) {
throw _iteratorError6;
}
}
}
this._delayedFrameRequesterRemoval.length = 0;
}
/**
* Execute a frameRequester.
*
* @param {String} when - attach point of this (these) requester(s). Can be any
* of {@link MAIN_LOOP_EVENTS}.
* @param {Number} dt - delta between this update and the previous one
* @param {boolean} updateLoopRestarted
* @param {...*} args - optional arguments
*/
}, {
key: "execFrameRequesters",
value: function execFrameRequesters(when, dt, updateLoopRestarted) {
if (!this._frameRequesters[when]) {
return;
}
if (this._delayedFrameRequesterRemoval.length > 0) {
this._executeFrameRequestersRemovals();
}
for (var _len = arguments.length, args = new Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
args[_key - 3] = arguments[_key];
}
var _iteratorNormalCompletion7 = true;
var _didIteratorError7 = false;
var _iteratorError7 = undefined;
try {
for (var _iterator7 = this._frameRequesters[when][Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
var frameRequester = _step7.value;
if (frameRequester.update) {
frameRequester.update(dt, updateLoopRestarted, args);
} else {
frameRequester(dt, updateLoopRestarted, args);
}
}
} catch (err) {
_didIteratorError7 = true;
_iteratorError7 = err;
} finally {
try {
if (!_iteratorNormalCompletion7 && _iterator7["return"] != null) {
_iterator7["return"]();
}
} finally {
if (_didIteratorError7) {
throw _iteratorError7;
}
}
}
}
/**
* Extract view coordinates from a mouse-event / touch-event
* @param {event} event - event can be a MouseEvent or a TouchEvent
* @param {THREE.Vector2} target - the target to set the view coords in
* @param {number} [touchIdx=0] - finger index when using a TouchEvent
* @return {THREE.Vector2} - view coordinates (in pixels, 0-0 = top-left of the View)
*/
}, {
key: "eventToViewCoords",
value: function eventToViewCoords(event) {
var target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _eventCoords;
var touchIdx = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
if (event.touches === undefined || !event.touches.length) {
return target.set(event.offsetX, event.offsetY);
} else {
var br = this.mainLoop.gfxEngine.renderer.domElement.getBoundingClientRect();
return target.set(event.touches[touchIdx].clientX - br.x, event.touches[touchIdx].clientY - br.y);
}
}
/**
* Extract normalized coordinates (NDC) from a mouse-event / touch-event
* @param {event} event - event can be a MouseEvent or a TouchEvent
* @param {number} touchIdx - finger index when using a TouchEvent (default: 0)
* @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1])
*/
}, {
key: "eventToNormalizedCoords",
value: function eventToNormalizedCoords(event) {
var touchIdx = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
return this.viewToNormalizedCoords(this.eventToViewCoords(event, _eventCoords, touchIdx));
}
/**
* Convert view coordinates to normalized coordinates (NDC)
* @param {THREE.Vector2} viewCoords (in pixels, 0-0 = top-left of the View)
* @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1])
*/
}, {
key: "viewToNormalizedCoords",
value: function viewToNormalizedCoords(viewCoords) {
_eventCoords.x = 2 * (viewCoords.x / this.camera.width) - 1;
_eventCoords.y = -2 * (viewCoords.y / this.camera.height) + 1;
return _eventCoords;
}
/**
* Convert NDC coordinates to view coordinates
* @param {THREE.Vector2} ndcCoords
* @return {THREE.Vector2} - view coordinates (in pixels, 0-0 = top-left of the View)
*/
}, {
key: "normalizedToViewCoords",
value: function normalizedToViewCoords(ndcCoords) {
_eventCoords.x = (ndcCoords.x + 1) * 0.5 * this.camera.width;
_eventCoords.y = (ndcCoords.y - 1) * -0.5 * this.camera.height;
return _eventCoords;
}
/**
* Return objects from some layers/objects3d under the mouse in this view.
*
* @param {Object} mouseOrEvt - mouse position in window coordinates (0, 0 = top-left)
* or MouseEvent or TouchEvent
* @param {number} radius - picking will happen in a circle centered on mouseOrEvt. Radius
* is the radius of this circle, in pixels
* @param {...*} where - where to look for objects. Can be either: empty (= look
* in all layers with type == 'geometry'), layer ids or layers or a mix of all
* the above.
* @return {Array} - an array of objects. Each element contains at least an object
* property which is the Object3D under the cursor. Then depending on the queried
* layer/source, there may be additionnal properties (coming from THREE.Raycaster
* for instance).
*
* @example
* view.pickObjectsAt({ x, y })
* view.pickObjectsAt({ x, y }, 1, 'wfsBuilding')
* view.pickObjectsAt({ x, y }, 3, 'wfsBuilding', myLayer)
*/
}, {
key: "pickObjectsAt",
value: function pickObjectsAt(mouseOrEvt) {
var radius = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var results = [];
for (var _len2 = arguments.length, where = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
where[_key2 - 2] = arguments[_key2];
}
var sources = where.length == 0 ? this.getLayers(function (l) {
return l.isGeometryLayer;
}) : [].concat(where);
var mouse = mouseOrEvt instanceof Event ? this.eventToViewCoords(mouseOrEvt) : mouseOrEvt;
var _iteratorNormalCompletion8 = true;
var _didIteratorError8 = false;
var _iteratorError8 = undefined;
try {
for (var _iterator8 = sources[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
var source = _step8.value;
if (source.isLayer || typeof source === 'string') {
var layer = typeof source === 'string' ? this.getLayerById(source) : source;
if (!layer || !layer.ready) {
console.warn('view.pickObjectAt : layer is not ready : ', source);
continue;
}
var parentLayer = this.getParentLayer(layer);
if (!parentLayer) {
var sp = layer.pickObjectsAt(this, mouse, radius); // warning: sp might be very large, so we can't use '...sp' (we'll hit
// 'javascript maximum call stack size exceeded' error) nor
// Array.prototype.push.apply(result, sp)
for (var i = 0; i < sp.length; i++) {
results.push(sp[i]);
}
} else {
// raycast using parent layer object3d
var obj = _Picking["default"].pickObjectsAt(this, mouse, radius, parentLayer.object3d); // then filter the results
var _iteratorNormalCompletion9 = true;
var _didIteratorError9 = false;
var _iteratorError9 = undefined;
try {
for (var _iterator9 = obj[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
var o = _step9.value;
if (o.layer === layer) {
results.push(o);
}
}
} catch (err) {
_didIteratorError9 = true;
_iteratorError9 = err;
} finally {
try {
if (!_iteratorNormalCompletion9 && _iterator9["return"] != null) {
_iterator9["return"]();
}
} finally {
if (_didIteratorError9) {
throw _iteratorError9;
}
}
}
}
} else if (source.isObject3D) {
_Picking["default"].pickObjectsAt(this, mouse, radius, source, results);
} else {
throw new Error("Invalid where arg (value = ".concat(where, "). Expected layers, layer ids or Object3Ds"));
}
}
} catch (err) {
_didIteratorError8 = true;
_iteratorError8 = err;
} finally {
try {
if (!_iteratorNormalCompletion8 && _iterator8["return"] != null) {
_iterator8["return"]();
}
} finally {
if (_didIteratorError8) {
throw _iteratorError8;
}
}
}
return results;
}
/**
* Return the current zoom scale at the central point of the view. This
* function compute the scale of a map.
*
* @param {number} pitch - Screen pitch, in millimeters ; 0.28 by default
*
* @return {number} The zoom scale.
*/
}, {
key: "getScale",
value: function getScale() {
var pitch = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0.28;
return this.getScaleFromDistance(pitch, this.getDistanceFromCamera());
}
}, {
key: "getScaleFromDistance",
value: function getScaleFromDistance() {
var pitch = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0.28;
var distance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
pitch /= 1000;
var fov = THREE.Math.degToRad(this.camera.camera3D.fov);
var unit = this.camera.height / (2 * distance * Math.tan(fov * 0.5));
return pitch * unit;
}
/**
* Given a screen coordinates, get the distance between the projected
* coordinates and the camera associated to this view.
*
* @param {THREE.Vector2} [screenCoord] - The screen coordinate to get the
* distance at. By default this is the middle of the screen.
*
* @return {number} The distance in meters.
*/
}, {
key: "getDistanceFromCamera",
value: function getDistanceFromCamera(screenCoord) {
this.getPickingPositionFromDepth(screenCoord, positionVector);
return this.camera.camera3D.position.distanceTo(positionVector);
}
/**
* Get, for a specific screen coordinate, the projected distance on the
* surface of the main layer of the view.
*
* @param {number} [pixels=1] - The size, in pixels, to get in meters.
* @param {THREE.Vector2} [screenCoord] - The screen coordinate to get the
* projected distance at. By default, this is the middle of the screen.
*
* @return {number} The projected distance in meters.
*/
}, {
key: "getPixelsToMeters",
value: function getPixelsToMeters() {
var pixels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var screenCoord = arguments.length > 1 ? arguments[1] : undefined;
return this.getPixelsToMetersFromDistance(pixels, this.getDistanceFromCamera(screenCoord));
}
}, {
key: "getPixelsToMetersFromDistance",
value: function getPixelsToMetersFromDistance() {
var pixels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var distance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
return pixels * distance / this.camera._preSSE;
}
/**
* Get, for a specific screen coordinate, this size in pixels of a projected
* distance on the surface of the main layer of the view.
*
* @param {number} [meters=1] - The size, in meters, to get in pixels.
* @param {THREE.Vector2} [screenCoord] - The screen coordinate to get the
* projected distance at. By default, this is the middle of the screen.
*
* @return {number} The projected distance in meters.
*/
}, {
key: "getMetersToPixels",
value: function getMetersToPixels() {
var meters = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var screenCoord = arguments.length > 1 ? arguments[1] : undefined;
return this.getMetersToPixelsFromDistance(meters, this.getDistanceFromCamera(screenCoord));
}
}, {
key: "getMetersToPixelsFromDistance",
value: function getMetersToPixelsFromDistance() {
var meters = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var distance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
return this.camera._preSSE * meters / distance;
}
/**
* Returns features under the mouse, for a set of specified layers.
*
* @param {MouseEvent|Object} mouseOrEvt - A MouseEvent, or a screen
* coordinates.
* @param {number} [radius=3] - The precision of the picking, in pixels.
* @param {Layer[]} [where] - The layers to look into. If not specified, all
* `GeometryLayer` and `ColorLayer` layers of this view will be looked in.
*
* @return {Object} - An object, with a property per layer. For example,
* looking for features on layers `wfsBuilding` and `wfsRoads` will give an
* object like `{ wfsBuilding: [...], wfsRoads: [] }`. Each property is made
* of an array, that can be empty or filled with found features.
*
* @example
* view.pickFeaturesAt({ x, y });
* view.pickFeaturesAt({ x, y }, 1, ['wfsBuilding']);
* view.pickFeaturesAt({ x, y }, 3, ['wfsBuilding', myLayer]);
*/
}, {
key: "pickFeaturesAt",
value: function pickFeaturesAt(mouseOrEvt) {
var radius = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 3;
var where = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var result = {};
var layers = where.length == 0 ? this.getLayers(function (l) {
return l.isGeometryLayer || l.isColorLayer;
}) : where;
layers = layers.map(function (l) {
var id = l.isLayer ? l.id : l;
result[id] = [];
return id;
}); // Get the mouse coordinates to the correct system
var mouse = mouseOrEvt instanceof Event ? this.eventToViewCoords(mouseOrEvt, _eventCoords) : mouseOrEvt;
this.getPickingPositionFromDepth(mouse, positionVector);
var distance = this.camera.camera3D.position.distanceTo(positionVector);
coordinates.setFromVector3(positionVector); // Get the correct precision; the position variable will be set in this
// function.
var precision;
var precisions = {
M: this.getPixelsToMetersFromDistance(radius, distance),
D: 0.001 * radius
};
if (this.isPlanarView) {
precisions.D = precisions.M;
} else if (this.getPixelsToDegrees) {
precisions.D = this.getMetersToDegrees(precisions.M);
} // Get the tile corresponding to where the cursor is
var tiles = _Picking["default"].pickTilesAt(this, mouse, radius, this.tileLayer);
var _iteratorNormalCompletion10 = true;
var _didIteratorError10 = false;
var _iteratorError10 = undefined;
try {
for (var _iterator10 = tiles[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
var tile = _step10.value;
if (!tile.object.material) {
continue;
}
var _iteratorNormalCompletion11 = true;
var _didIteratorError11 = false;
var _iteratorError11 = undefined;
try {
for (var _iterator11 = tile.object.material.getLayers(layers)[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) {
var materialLayer = _step11.value;
var _iteratorNormalCompletion12 = true;
var _didIteratorError12 = false;
var _iteratorError12 = undefined;
try {
for (var _iterator12 = materialLayer.textures[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) {
var texture = _step12.value;
if (!texture.parsedData) {
continue;
}
precision = _Crs["default"].isMetricUnit(texture.parsedData.crs) ? precisions.M : precisions.D;
result[materialLayer.id] = result[materialLayer.id].concat(_FeaturesUtils["default"].filterFeaturesUnderCoordinate(coordinates, texture.parsedData, precision));
}
} catch (err) {
_didIteratorError12 = true;
_iteratorError12 = err;
} finally {
try {
if (!_iteratorNormalCompletion12 && _iterator12["return"] != null) {
_iterator12["return"]();
}
} finally {
if (_didIteratorError12) {
throw _iteratorError12;
}
}
}
}
} catch (err) {
_didIteratorError11 = true;
_iteratorError11 = err;
} finally {
try {
if (!_iteratorNormalCompletion11 && _iterator11["return"] != null) {
_iterator11["return"]();
}
} finally {
if (_didIteratorError11) {
throw _iteratorError11;
}
}
}
}
} catch (err) {
_didIteratorError10 = true;
_iteratorError10 = err;
} finally {
try {
if (!_iteratorNormalCompletion10 && _iterator10["return"] != null) {
_iterator10["return"]();
}
} finally {
if (_didIteratorError10) {
throw _iteratorError10;
}
}
}
return result;
}
}, {
key: "readDepthBuffer",
value: function readDepthBuffer(x, y, width, height) {
var g = this.mainLoop.gfxEngine;
var currentWireframe = this.tileLayer.wireframe;
var currentOpacity = this.tileLayer.opacity;
var currentVisibility = this.tileLayer.visible;
if (currentWireframe) {
this.tileLayer.wireframe = false;
}
if (currentOpacity < 1.0) {
this.tileLayer.opacity = 1.0;
}
if (!currentVisibility) {
this.tileLayer.visible = true;
}
var restore = this.tileLayer.level0Nodes.map(function (n) {
return _RenderMode["default"].push(n, _RenderMode["default"].MODES.DEPTH);
});
var buffer = g.renderViewToBuffer({
camera: this.camera,
scene: this.tileLayer.object3d
}, {
x: x,
y: y,
width: width,
height: height
});
restore.forEach(function (r) {
return r();
});
if (this.tileLayer.wireframe !== currentWireframe) {
this.tileLayer.wireframe = currentWireframe;
}
if (this.tileLayer.opacity !== currentOpacity) {
this.tileLayer.opacity = currentOpacity;
}
if (this.tileLayer.visible !== currentVisibility) {
this.tileLayer.visible = currentVisibility;
}
return buffer;
}
/**
* Returns the world position (view's crs: referenceCrs) under view coordinates.
* This position is computed with depth buffer.
*
* @param {THREE.Vector2} mouse position in view coordinates (in pixel), if it's null so it's view's center.
* @param {THREE.Vector3} [target=THREE.Vector3()] target. the result will be copied into this Vector3. If not present a new one will be created.
* @return {THREE.Vector3} the world position in view's crs: referenceCrs.
*/
}, {
key: "getPickingPositionFromDepth",
value: function getPickingPositionFromDepth(mouse) {
var target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new THREE.Vector3();
if (!this.tileLayer || this.tileLayer.level0Nodes.length == 0 || !this.tileLayer.level0Nodes[0]) {
target = undefined;
return;
}
var l = this.mainLoop;
var viewPaused = l.scheduler.commandsWaitingExecutionCount() == 0 && l.renderingState == _MainLoop.RENDERING_PAUSED;
var g = l.gfxEngine;
var dim = g.getWindowSize();
var camera = this.camera.camera3D;
mouse = mouse || dim.clone().multiplyScalar(0.5);
mouse.x = Math.floor(mouse.x);
mouse.y = Math.floor(mouse.y); // Prepare state
var prev = camera.layers.mask;
camera.layers.mask = 1 << this.tileLayer.threejsLayer; // Render/Read to buffer
var buffer;
if (viewPaused) {
this._fullSizeDepthBuffer = this._fullSizeDepthBuffer || this.readDepthBuffer(0, 0, dim.x, dim.y);
var id = ((dim.y - mouse.y - 1) * dim.x + mouse.x) * 4;
buffer = this._fullSizeDepthBuffer.slice(id, id + 4);
} else {
buffer = this.readDepthBuffer(mouse.x, mouse.y, 1, 1);
}
screen.x = mouse.x / dim.x * 2 - 1;
screen.y = -(mouse.y / dim.y) * 2 + 1; // Origin
ray.origin.copy(camera.position); // Direction
ray.direction.set(screen.x, screen.y, 0.5); // Unproject
matrix.multiplyMatrices(camera.matrixWorld, matrix.getInverse(camera.projectionMatrix));
ray.direction.applyMatrix4(matrix);
ray.direction.sub(ray.origin);
direction.set(0, 0, 1.0);
direction.applyMatrix4(matrix);
direction.sub(ray.origin);
var angle = direction.angleTo(ray.direction);
var orthoZ = g.depthBufferRGBAValueToOrthoZ(buffer, camera);
var length = orthoZ / Math.cos(angle);
target.addVectors(camera.position, ray.direction.setLength(length));
camera.layers.mask = prev;
if (target.length() > 10000000) {
return undefined;
}
return target;
}
}]);
return View;
}(THREE.EventDispatcher);
var _default = View;
exports["default"] = _default;