diff --git a/examples/wfs.html b/examples/wfs.html index 40354befcd..ec6e7ccfeb 100644 --- a/examples/wfs.html +++ b/examples/wfs.html @@ -93,6 +93,7 @@ } view.addLayer({ + type: 'geometry', name: 'lyon_tcl_bus', update: itowns.FeatureProcessing.update, convert: itowns.Feature2Mesh.convert({ diff --git a/jsdoc-config.json b/jsdoc-config.json index 7b42699119..b95bfd1c32 100644 --- a/jsdoc-config.json +++ b/jsdoc-config.json @@ -14,9 +14,17 @@ "src/Core/Geographic/Coordinates.js", "src/Core/Layer/Layer.js", "src/Core/Prefab/GlobeView.js", + "src/Core/Prefab/Globe/GlobeLayer.js", + "src/Core/Prefab/Panorama/PanoramaLayer.js", + "src/Core/Prefab/Planar/PlanarLayer.js", "src/Core/Scheduler/Cache.js", "src/Core/Scheduler/Scheduler.js", + "src/Layer/ColorLayer.js", + "src/Layer/ElevationLayer.js", + "src/Layer/GeometryLayer.js", + "src/Layer/TiledGeometryLayer.js", + "src/Parser/GeoJsonParser.js", "src/Parser/GpxParser.js", "src/Parser/VectorTileParser.js", diff --git a/src/Core/Layer/Layer.js b/src/Core/Layer/Layer.js deleted file mode 100644 index a23bbd9183..0000000000 --- a/src/Core/Layer/Layer.js +++ /dev/null @@ -1,199 +0,0 @@ -import { EventDispatcher } from 'three'; -import Picking from '../Picking'; - -/** - * Fires when layer sequence change (meaning when the order of the layer changes in the view) - * @event Layer#sequence-property-changed - * @property new {object} - * @property new.sequence {number} the new value of the layer sequence - * @property previous {object} - * @property previous.sequence {number} the previous value of the layer sequence - * @property target {Layer} dispatched on layer - * @property type {string} sequence-property-changed -*/ -/** - * Fires when layer opacity change - * @event Layer#opacity-property-changed - * @property new {object} - * @property new.opacity {object} the new value of the layer opacity - * @property previous {object} - * @property previous.opacity {object} the previous value of the layer opacity - * @property target {Layer} dispatched on layer - * @property type {string} opacity-property-changed -*/ -/** - * Fires when layer visibility change - * @event Layer#visible-property-changed - * @property new {object} - * @property new.visible {object} the new value of the layer visibility - * @property previous {object} - * @property previous.visible {object} the previous value of the layer visibility - * @property target {Layer} dispatched on layer - * @property type {string} visible-property-changed -*/ - -export const defineLayerProperty = function defineLayerProperty(layer, propertyName, defaultValue, onChange) { - const existing = Object.getOwnPropertyDescriptor(layer, propertyName); - if (!existing || !existing.set) { - var property = layer[propertyName] == undefined ? defaultValue : layer[propertyName]; - Object.defineProperty(layer, - propertyName, - { get: () => property, - set: (newValue) => { - if (property !== newValue) { - const event = { type: `${propertyName}-property-changed`, previous: {}, new: {} }; - event.previous[propertyName] = property; - event.new[propertyName] = newValue; - property = newValue; - if (onChange) { - onChange(layer, propertyName); - } - layer.dispatchEvent(event); - } - } }); - } -}; - -function GeometryLayer(id, object3d) { - if (!id) { - throw new Error('Missing id parameter (GeometryLayer must have a unique id defined)'); - } - if (!object3d || !object3d.isObject3D) { - throw new Error('Missing/Invalid object3d parameter (must be a three.js Object3D instance)'); - } - this._attachedLayers = []; - - if (object3d && object3d.type === 'Group' && object3d.name === '') { - object3d.name = id; - } - - this.type = 'geometry'; - - Object.defineProperty(this, 'object3d', { - value: object3d, - writable: false, - }); - - Object.defineProperty(this, 'id', { - value: id, - writable: false, - }); - - // Setup default picking method - this.pickObjectsAt = (view, mouse, radius) => Picking.pickObjectsAt(view, mouse, radius, this.object3d); - - // Attached layers expect to receive the visual representation of a layer (= THREE object with a material). - // So if a layer's update function don't process this kind of object, the layer must provide - // a getObjectToUpdateForAttachedLayers function that returns the correct object to update for attached - // layer. - // See 3dtilesProvider or PointCloudProvider for examples. - // eslint-disable-next-line arrow-body-style - this.getObjectToUpdateForAttachedLayers = (obj) => { - if (obj.parent && obj.material) { - return { - element: obj, - parent: obj.parent, - }; - } - }; - - this.postUpdate = () => {}; -} - -GeometryLayer.prototype = Object.create(EventDispatcher.prototype); -GeometryLayer.prototype.constructor = GeometryLayer; - -GeometryLayer.prototype.attach = function attach(layer) { - if (!layer.update) { - throw new Error(`Missing 'update' function -> can't attach layer ${layer.id}`); - } - this._attachedLayers.push(layer); -}; - -GeometryLayer.prototype.detach = function detach(layer) { - const count = this._attachedLayers.length; - this._attachedLayers = this._attachedLayers.filter(attached => attached.id != layer.id); - return this._attachedLayers.length < count; -}; - -/** - * Don't use directly constructor to instance a new Layer - * use addLayer in {@link View} - * @example - * // add and create a new Layer - * const newLayer = view.addLayer({options}); - * - * // Change layer's visibilty - * const layerToChange = view.getLayers(layer => layer.id == 'idLayerToChange')[0]; - * layerToChange.visible = false; - * view.notifyChange(); // update viewer - * - * // Change layer's opacity - * const layerToChange = view.getLayers(layer => layer.id == 'idLayerToChange')[0]; - * layerToChange.opacity = 0.5; - * view.notifyChange(); // update viewer - * - * // Listen properties - * const layerToListen = view.getLayers(layer => layer.id == 'idLayerToListen')[0]; - * layerToListen.addEventListener('visible-property-changed', (event) => console.log(event)); - * layerToListen.addEventListener('opacity-property-changed', (event) => console.log(event)); - * @constructor - * @protected - * @param {String} id - */ -function Layer(id) { - Object.defineProperty(this, 'id', { - value: id, - writable: false, - }); -} - -Layer.prototype = Object.create(EventDispatcher.prototype); -Layer.prototype.constructor = Layer; - -const ImageryLayers = { - // move layer to new index - // After the modification : - // * the minimum sequence will always be 0 - // * the maximum sequence will always be layers.lenght - 1 - // the ordering of all layers (Except that specified) doesn't change - moveLayerToIndex: function moveLayerToIndex(layer, newIndex, imageryLayers) { - newIndex = Math.min(newIndex, imageryLayers.length - 1); - newIndex = Math.max(newIndex, 0); - const oldIndex = layer.sequence; - - for (const imagery of imageryLayers) { - if (imagery.id === layer.id) { - // change index of specified layer - imagery.sequence = newIndex; - } else if (imagery.sequence > oldIndex && imagery.sequence <= newIndex) { - // down all layers between the old index and new index (to compensate the deletion of the old index) - imagery.sequence--; - } else if (imagery.sequence >= newIndex && imagery.sequence < oldIndex) { - // up all layers between the new index and old index (to compensate the insertion of the new index) - imagery.sequence++; - } - } - }, - - moveLayerDown: function moveLayerDown(layer, imageryLayers) { - if (layer.sequence > 0) { - this.moveLayerToIndex(layer, layer.sequence - 1, imageryLayers); - } - }, - - moveLayerUp: function moveLayerUp(layer, imageryLayers) { - const m = imageryLayers.length - 1; - if (layer.sequence < m) { - this.moveLayerToIndex(layer, layer.sequence + 1, imageryLayers); - } - }, - - getColorLayersIdOrderedBySequence: function getColorLayersIdOrderedBySequence(imageryLayers) { - const copy = Array.from(imageryLayers); - copy.sort((a, b) => a.sequence - b.sequence); - return copy.map(l => l.id); - }, -}; - -export { GeometryLayer, Layer, ImageryLayers }; diff --git a/src/Core/MainLoop.js b/src/Core/MainLoop.js index d2a36a9bd3..b3a1175519 100644 --- a/src/Core/MainLoop.js +++ b/src/Core/MainLoop.js @@ -1,5 +1,5 @@ import { EventDispatcher } from 'three'; -import { GeometryLayer, Layer } from './Layer/Layer'; +import Layer from '../Layer/Layer'; import Cache from '../Core/Scheduler/Cache'; export const RENDERING_PAUSED = 0; @@ -77,7 +77,7 @@ function updateElements(context, geometryLayer, elements) { } } // update attached layers - for (const attachedLayer of geometryLayer._attachedLayers) { + for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready) { attachedLayer.update(context, attachedLayer, sub.element, sub.parent); } @@ -90,7 +90,7 @@ function updateElements(context, geometryLayer, elements) { Must be a THREE.Object and have a THREE.Material`); } // update attached layers - for (const attachedLayer of geometryLayer._attachedLayers) { + for (const attachedLayer of geometryLayer.attachedLayers) { if (attachedLayer.ready) { attachedLayer.update(context, attachedLayer, sub.elements[i], sub.parent); } @@ -127,9 +127,10 @@ MainLoop.prototype._update = function _update(view, updateSources, dt) { updateSources.forEach((src) => { const layer = src.layer || src; if (layer instanceof Layer) { - if (!(layer instanceof GeometryLayer)) { + const parentLayer = view.getParentLayer(layer); + if (parentLayer) { // add the parent layer to update sources - updateSources.add(view.getParentLayer(layer)); + updateSources.add(parentLayer); } } }); @@ -143,7 +144,7 @@ MainLoop.prototype._update = function _update(view, updateSources, dt) { const srcs = filterChangeSources(updateSources, geometryLayer); if (srcs.size > 0) { // `preUpdate` returns an array of elements to update - const elementsToUpdate = geometryLayer.preUpdate(context, geometryLayer, srcs); + const elementsToUpdate = geometryLayer.preUpdate(context, srcs); // `update` is called in `updateElements`. updateElements(context, geometryLayer, elementsToUpdate); // `postUpdate` is called when this geom layer update process is finished diff --git a/src/Core/Prefab/Globe/GlobeLayer.js b/src/Core/Prefab/Globe/GlobeLayer.js new file mode 100644 index 0000000000..3cb34b19f8 --- /dev/null +++ b/src/Core/Prefab/Globe/GlobeLayer.js @@ -0,0 +1,66 @@ +import * as THREE from 'three'; + +import TiledGeometryLayer from '../../../Layer/TiledGeometryLayer'; + +import { globeCulling, preGlobeUpdate, globeSubdivisionControl, globeSchemeTileWMTS, globeSchemeTile1 } from '../../../Process/GlobeTileProcessing'; +import BuilderEllipsoidTile from './BuilderEllipsoidTile'; + +class GlobeLayer extends TiledGeometryLayer { + /** + * A {@link TiledGeometryLayer} to use with a {@link GlobeView}. It has + * specific method for updating and subdivising its grid. + * + * @constructor + * @extends TiledGeometryLayer + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {THREE.Object3d} [object3d=THREE.Group] - The object3d used to + * contain the geometry of the TiledGeometryLayer. It is usually a + * THREE.Group, but it can be anything inheriting from a + * THREE.Object3d. + * @param {Object} [config] - Optional configuration, all elements in it + * will be merged as is in the layer. For example, if the configuration + * contains three elements name, protocol, extent, these + * elements will be available using layer.name or something + * else depending on the property name. + * @param {number} [config.maxSubdivisionLevel=18] - Maximum subdivision + * level for this tiled layer. + * @param {number} [config.sseSubdivisionThreshold=1] - Threshold level for + * the SSE. + * @param {number} [config.maxDeltaElevationLevel=4] - Maximum delta between + * two elevations tile. + * + * @throws {Error} object3d must be a valid + * THREE.Object3d. + */ + constructor(id, object3d, config = {}) { + super(id, object3d || new THREE.Group(), config); + + this.options.defaultPickingRadius = 5; + + // Configure tiles + this.schemeTile = globeSchemeTileWMTS(globeSchemeTile1); + this.extent = this.schemeTile[0].clone(); + for (let i = 1; i < this.schemeTile.length; i++) { + this.extent.union(this.schemeTile[i]); + } + + this.culling = globeCulling(2); + this.subdivision = globeSubdivisionControl(2, + config.maxSubdivisionLevel || 18, + config.sseSubdivisionThreshold || 1.0, + config.maxDeltaElevationLevel || 4); + + this.builder = new BuilderEllipsoidTile(); + } + + preUpdate(context, changeSources) { + preGlobeUpdate(context, this); + + return super.preUpdate(context, changeSources); + } +} + +export default GlobeLayer; diff --git a/src/Core/Prefab/GlobeView.js b/src/Core/Prefab/GlobeView.js index a056c297b8..03b941bd6f 100644 --- a/src/Core/Prefab/GlobeView.js +++ b/src/Core/Prefab/GlobeView.js @@ -6,17 +6,11 @@ import { COLOR_LAYERS_ORDER_CHANGED } from '../../Renderer/ColorLayersOrdering'; import RendererConstant from '../../Renderer/RendererConstant'; import GlobeControls from '../../Renderer/ThreeExtended/GlobeControls'; -import { GeometryLayer } from '../Layer/Layer'; - +import GlobeLayer from './Globe/GlobeLayer'; import Atmosphere from './Globe/Atmosphere'; import CoordStars from '../Geographic/CoordStars'; import Coordinates, { C, ellipsoidSizes } from '../Geographic/Coordinates'; -import { processTiledGeometryNode } from '../../Process/TiledNodeProcessing'; -import { globeCulling, preGlobeUpdate, globeSubdivisionControl, globeSchemeTileWMTS, globeSchemeTile1 } from '../../Process/GlobeTileProcessing'; -import BuilderEllipsoidTile from './Globe/BuilderEllipsoidTile'; -import SubdivisionControl from '../../Process/SubdivisionControl'; -import Picking from '../Picking'; /** * Fires when the view is completely loaded. Controls and view's functions can be called then. @@ -71,91 +65,9 @@ export const GLOBE_VIEW_EVENTS = { COLOR_LAYERS_ORDER_CHANGED, }; -export function createGlobeLayer(id, options) { - // Configure tiles - const nodeInitFn = function nodeInitFn(layer, parent, node) { - node.material.setLightingOn(layer.lighting.enable); - node.material.uniforms.lightPosition.value = layer.lighting.position; - if (layer.noTextureColor) { - node.material.uniforms.noTextureColor.value.copy(layer.noTextureColor); - } - - if (__DEBUG__) { - node.material.uniforms.showOutline = { value: layer.showOutline || false }; - node.material.wireframe = layer.wireframe || false; - } - }; - - const wgs84TileLayer = new GeometryLayer(id, options.object3d || new THREE.Group()); - wgs84TileLayer.schemeTile = globeSchemeTileWMTS(globeSchemeTile1); - wgs84TileLayer.extent = wgs84TileLayer.schemeTile[0].clone(); - for (let i = 1; i < wgs84TileLayer.schemeTile.length; i++) { - wgs84TileLayer.extent.union(wgs84TileLayer.schemeTile[i]); - } - wgs84TileLayer.preUpdate = (context, layer, changeSources) => { - SubdivisionControl.preUpdate(context, layer); - - if (__DEBUG__) { - layer._latestUpdateStartingLevel = 0; - } - - preGlobeUpdate(context, layer); - - let commonAncestor; - for (const source of changeSources.values()) { - if (source.isCamera) { - // if the change is caused by a camera move, no need to bother - // to find common ancestor: we need to update the whole tree: - // some invisible tiles may now be visible - return layer.level0Nodes; - } - if (source.layer === layer) { - if (!commonAncestor) { - commonAncestor = source; - } else { - commonAncestor = source.findCommonAncestor(commonAncestor); - if (!commonAncestor) { - return layer.level0Nodes; - } - } - if (commonAncestor.material == null) { - commonAncestor = undefined; - } - } - } - if (commonAncestor) { - if (__DEBUG__) { - layer._latestUpdateStartingLevel = commonAncestor.level; - } - return [commonAncestor]; - } else { - return layer.level0Nodes; - } - }; - - function subdivision(context, layer, node) { - if (SubdivisionControl.hasEnoughTexturesToSubdivide(context, layer, node)) { - return globeSubdivisionControl(2, - options.maxSubdivisionLevel || 18, - options.sseSubdivisionThreshold || 1.0, - options.maxDeltaElevationLevel || 4)(context, layer, node); - } - return false; - } - - wgs84TileLayer.update = processTiledGeometryNode(globeCulling(2), subdivision); - wgs84TileLayer.builder = new BuilderEllipsoidTile(); - wgs84TileLayer.onTileCreated = nodeInitFn; - wgs84TileLayer.protocol = 'tile'; - wgs84TileLayer.visible = true; - wgs84TileLayer.lighting = { - enable: false, - position: { x: -0.5, y: 0.0, z: 1.0 }, - }; - // provide custom pick function - wgs84TileLayer.pickObjectsAt = (_view, mouse, radius = 5) => Picking.pickTilesAt(_view, mouse, radius, wgs84TileLayer); - - return wgs84TileLayer; +export function createGlobeLayer(id, options = {}) { + console.warn('createGlobeLayer is deprecated, use the GlobeLayer class instead.'); + return new GlobeLayer(id, options.object3d, options); } /** @@ -189,7 +101,7 @@ function GlobeView(viewerDiv, coordCarto, options = {}) { this.camera.camera3D.updateProjectionMatrix(); this.camera.camera3D.updateMatrixWorld(true); - const wgs84TileLayer = createGlobeLayer('globe', options); + const wgs84TileLayer = new GlobeLayer('globe', options.object3d); const sun = new THREE.DirectionalLight(); sun.position.set(-0.5, 0, 1); diff --git a/src/Core/Prefab/Panorama/PanoramaLayer.js b/src/Core/Prefab/Panorama/PanoramaLayer.js new file mode 100644 index 0000000000..ff186a19dc --- /dev/null +++ b/src/Core/Prefab/Panorama/PanoramaLayer.js @@ -0,0 +1,112 @@ +import * as THREE from 'three'; + +import TiledGeometryLayer from '../../../Layer/TiledGeometryLayer'; +import Extent from '../../Geographic/Extent'; +import { panoramaCulling, panoramaSubdivisionControl } from '../../../Process/PanoramaTileProcessing'; +import PanoramaTileBuilder from './PanoramaTileBuilder'; +import ProjectionType from './Constants'; + +class PanoramaLayer extends TiledGeometryLayer { + /** + * A {@link TiledGeometryLayer} to use with a {@link PanoramaView}. It has + * specific method for updating and subdivising its grid. + * + * @constructor + * @extends TiledGeometryLayer + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {Coordinates} coordinates - The coordinates of the origin of the + * panorama. + * @param {number} type - The type of projection for the panorama: 1 for a + * cylindrical projection, 2 for a spherical one. + * @param {Object} [config] - Optional configuration, all elements in it + * will be merged as is in the layer. For example, if the configuration + * contains three elements name, protocol, extent, these + * elements will be available using layer.name or something + * else depending on the property name. + * @param {THREE.Object3d} [config.object3d=THREE.Group] - The object3d used to + * contain the geometry of the TiledGeometryLayer. It is usually a + * THREE.Group, but it can be anything inheriting from a + * THREE.Object3d. + * @param {number} [config.maxSubdivisionLevel=10] - Maximum subdivision + * level for this tiled layer. + * @param {number} [config.ratio=1] - Ratio for building the panorama + * sphere. + * + * @throws {Error} object3d must be a valid + * THREE.Object3d. + */ + constructor(id, coordinates, type, config) { + super(id, config.object3d || new THREE.Group(), config); + + coordinates.xyz(this.object3d.position); + this.object3d.quaternion.setFromUnitVectors( + new THREE.Vector3(0, 0, 1), coordinates.geodesicNormal); + this.object3d.updateMatrixWorld(true); + + // FIXME: add CRS = '0' support + this.extent = new Extent('EPSG:4326', { + west: -180, + east: 180, + north: 90, + south: -90, + }); + + if (type === ProjectionType.SPHERICAL) { + // equirectangular -> spherical geometry + this.schemeTile = [ + new Extent('EPSG:4326', { + west: -180, + east: 0, + north: 90, + south: -90, + }), new Extent('EPSG:4326', { + west: 0, + east: 180, + north: 90, + south: -90, + })]; + } else if (type === ProjectionType.CYLINDRICAL) { + // cylindrical geometry + this.schemeTile = [ + new Extent('EPSG:4326', { + west: -180, + east: -90, + north: 90, + south: -90, + }), new Extent('EPSG:4326', { + west: -90, + east: 0, + north: 90, + south: -90, + }), new Extent('EPSG:4326', { + west: 0, + east: 90, + north: 90, + south: -90, + }), new Extent('EPSG:4326', { + west: 90, + east: 180, + north: 90, + south: -90, + })]; + } else { + throw new Error(`Unsupported panorama projection type ${type}. + Only ProjectionType.SPHERICAL and ProjectionType.CYLINDRICAL are supported`); + } + this.disableSkirt = true; + + this.culling = panoramaCulling; + this.subdivision = panoramaSubdivisionControl( + config.maxSubdivisionLevel || 10, + new THREE.Vector2(512, 256)); + + this.builder = new PanoramaTileBuilder(type, config.ratio || 1); + this.options.segments = 8; + this.options.quality = 0.5; + } +} + +export default PanoramaLayer; diff --git a/src/Core/Prefab/PanoramaView.js b/src/Core/Prefab/PanoramaView.js index 44a060a48d..f54a1d71d0 100644 --- a/src/Core/Prefab/PanoramaView.js +++ b/src/Core/Prefab/PanoramaView.js @@ -1,157 +1,11 @@ import * as THREE from 'three'; import View from '../View'; - -import { GeometryLayer } from '../Layer/Layer'; -import Extent from '../Geographic/Extent'; -import { processTiledGeometryNode } from '../../Process/TiledNodeProcessing'; -import { panoramaCulling, panoramaSubdivisionControl } from '../../Process/PanoramaTileProcessing'; -import PanoramaTileBuilder from './Panorama/PanoramaTileBuilder'; -import SubdivisionControl from '../../Process/SubdivisionControl'; -import ProjectionType from './Panorama/Constants'; -import Picking from '../Picking'; +import PanoramaLayer from './Panorama/PanoramaLayer'; export function createPanoramaLayer(id, coordinates, type, options = {}) { - const tileLayer = new GeometryLayer(id, options.object3d || new THREE.Group()); - - coordinates.xyz(tileLayer.object3d.position); - tileLayer.object3d.quaternion.setFromUnitVectors( - new THREE.Vector3(0, 0, 1), coordinates.geodesicNormal); - tileLayer.object3d.updateMatrixWorld(true); - - // FIXME: add CRS = '0' support - tileLayer.extent = new Extent('EPSG:4326', { - west: -180, - east: 180, - north: 90, - south: -90, - }); - - if (type === ProjectionType.SPHERICAL) { - // equirectangular -> spherical geometry - tileLayer.schemeTile = [ - new Extent('EPSG:4326', { - west: -180, - east: 0, - north: 90, - south: -90, - }), new Extent('EPSG:4326', { - west: 0, - east: 180, - north: 90, - south: -90, - })]; - } else if (type === ProjectionType.CYLINDRICAL) { - // cylindrical geometry - tileLayer.schemeTile = [ - new Extent('EPSG:4326', { - west: -180, - east: -90, - north: 90, - south: -90, - }), new Extent('EPSG:4326', { - west: -90, - east: 0, - north: 90, - south: -90, - }), new Extent('EPSG:4326', { - west: 0, - east: 90, - north: 90, - south: -90, - }), new Extent('EPSG:4326', { - west: 90, - east: 180, - north: 90, - south: -90, - })]; - } else { - throw new Error(`Unsupported panorama projection type ${type}. - Only ProjectionType.SPHERICAL and ProjectionType.CYLINDRICAL are supported`); - } - tileLayer.disableSkirt = true; - - // Configure tiles - const nodeInitFn = function nodeInitFn(layer, parent, node) { - if (layer.noTextureColor) { - node.material.uniforms.noTextureColor.value.copy(layer.noTextureColor); - } - node.material.depthWrite = false; - - if (__DEBUG__) { - node.material.uniforms.showOutline = { value: layer.showOutline || false }; - node.material.wireframe = layer.wireframe || false; - } - }; - - tileLayer.preUpdate = (context, layer, changeSources) => { - SubdivisionControl.preUpdate(context, layer); - - if (__DEBUG__) { - layer._latestUpdateStartingLevel = 0; - } - - if (changeSources.has(undefined) || changeSources.size == 0) { - return layer.level0Nodes; - } - - let commonAncestor; - for (const source of changeSources.values()) { - if (source.isCamera) { - // if the change is caused by a camera move, no need to bother - // to find common ancestor: we need to update the whole tree: - // some invisible tiles may now be visible - return layer.level0Nodes; - } - if (source.layer === layer.id) { - if (!commonAncestor) { - commonAncestor = source; - } else { - commonAncestor = source.findCommonAncestor(commonAncestor); - if (!commonAncestor) { - return layer.level0Nodes; - } - } - if (commonAncestor.material == null) { - commonAncestor = undefined; - } - } - } - if (commonAncestor) { - if (__DEBUG__) { - layer._latestUpdateStartingLevel = commonAncestor.level; - } - return [commonAncestor]; - } else { - return layer.level0Nodes; - } - }; - - - function subdivision(context, layer, node) { - if (SubdivisionControl.hasEnoughTexturesToSubdivide(context, layer, node)) { - return panoramaSubdivisionControl( - options.maxSubdivisionLevel || 10, new THREE.Vector2(512, 256))(context, layer, node); - } - return false; - } - - tileLayer.update = processTiledGeometryNode(panoramaCulling, subdivision); - tileLayer.builder = new PanoramaTileBuilder(type, options.ratio); - tileLayer.onTileCreated = nodeInitFn; - tileLayer.protocol = 'tile'; - tileLayer.visible = true; - tileLayer.segments = 8; - tileLayer.quality = 0.5; - tileLayer.lighting = { - enable: false, - position: { x: -0.5, y: 0.0, z: 1.0 }, - }; - // provide custom pick function - tileLayer.pickObjectsAt = (_view, mouse, radius) => Picking.pickTilesAt(_view, mouse, radius, tileLayer); - - - return tileLayer; + console.warn('createPanoramaLayer is deprecated, use the PanoramaLayer class instead.'); + return new PanoramaLayer(id, coordinates, type, options); } function PanoramaView(viewerDiv, coordinates, type, options = {}) { @@ -176,7 +30,7 @@ function PanoramaView(viewerDiv, coordinates, type, options = {}) { } camera.updateMatrixWorld(); - const tileLayer = createPanoramaLayer('panorama', coordinates, type, options); + const tileLayer = new PanoramaLayer('panorama', coordinates, type, options); View.prototype.addLayer.call(this, tileLayer); diff --git a/src/Core/Prefab/Planar/PlanarLayer.js b/src/Core/Prefab/Planar/PlanarLayer.js new file mode 100644 index 0000000000..fc7f559604 --- /dev/null +++ b/src/Core/Prefab/Planar/PlanarLayer.js @@ -0,0 +1,52 @@ +import * as THREE from 'three'; + +import TiledGeometryLayer from '../../../Layer/TiledGeometryLayer'; + +import { planarCulling, planarSubdivisionControl } from '../../../Process/PlanarTileProcessing'; +import PlanarTileBuilder from './PlanarTileBuilder'; + +class PlanarLayer extends TiledGeometryLayer { + /** + * A {@link TiledGeometryLayer} to use with a {@link PlanarView}. It has + * specific method for updating and subdivising its grid. + * + * @constructor + * @extends TiledGeometryLayer + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {Extent} extent - The extent to define the layer within. + * @param {THREE.Object3d} [object3d=THREE.Group] - The object3d used to + * contain the geometry of the TiledGeometryLayer. It is usually a + * THREE.Group, but it can be anything inheriting from a + * THREE.Object3d. + * @param {Object} [config] - Optional configuration, all elements in it + * will be merged as is in the layer. For example, if the configuration + * contains three elements name, protocol, extent, these + * elements will be available using layer.name or something + * else depending on the property name. + * @param {number} [config.maxSubdivisionLevel=5] - Maximum subdivision + * level for this tiled layer. + * @param {number} [config.maxDeltaElevationLevel=4] - Maximum delta between + * two elevations tile. + * + * @throws {Error} object3d must be a valid + * THREE.Object3d. + */ + constructor(id, extent, object3d, config = {}) { + super(id, object3d || new THREE.Group(), config); + + this.extent = extent; + this.schemeTile = [extent]; + + this.culling = planarCulling; + this.subdivision = planarSubdivisionControl( + config.maxSubdivisionLevel || 5, + config.maxDeltaElevationLevel || 4); + + this.builder = new PlanarTileBuilder(); + } +} + +export default PlanarLayer; diff --git a/src/Core/Prefab/PlanarView.js b/src/Core/Prefab/PlanarView.js index 65c774b9a4..b8a8ee4907 100644 --- a/src/Core/Prefab/PlanarView.js +++ b/src/Core/Prefab/PlanarView.js @@ -4,101 +4,11 @@ import View from '../View'; import { RENDERING_PAUSED, MAIN_LOOP_EVENTS } from '../MainLoop'; import RendererConstant from '../../Renderer/RendererConstant'; -import { GeometryLayer } from '../Layer/Layer'; - -import { processTiledGeometryNode } from '../../Process/TiledNodeProcessing'; -import { planarCulling, planarSubdivisionControl, prePlanarUpdate } from '../../Process/PlanarTileProcessing'; -import PlanarTileBuilder from './Planar/PlanarTileBuilder'; -import SubdivisionControl from '../../Process/SubdivisionControl'; -import Picking from '../Picking'; +import PlanarLayer from './Planar/PlanarLayer'; export function createPlanarLayer(id, extent, options) { - const tileLayer = new GeometryLayer(id, options.object3d || new THREE.Group()); - tileLayer.extent = extent; - tileLayer.schemeTile = [extent]; - - // Configure tiles - const nodeInitFn = function nodeInitFn(layer, parent, node) { - node.material.setLightingOn(layer.lighting.enable); - node.material.uniforms.lightPosition.value = layer.lighting.position; - - if (layer.noTextureColor) { - node.material.uniforms.noTextureColor.value.copy(layer.noTextureColor); - } - - if (__DEBUG__) { - node.material.uniforms.showOutline = { value: layer.showOutline || false }; - node.material.wireframe = layer.wireframe || false; - } - }; - - tileLayer.preUpdate = (context, layer, changeSources) => { - SubdivisionControl.preUpdate(context, layer); - - prePlanarUpdate(context, layer); - - if (__DEBUG__) { - layer._latestUpdateStartingLevel = 0; - } - - if (changeSources.has(undefined) || changeSources.size == 0) { - return layer.level0Nodes; - } - - let commonAncestor; - for (const source of changeSources.values()) { - if (source.isCamera) { - // if the change is caused by a camera move, no need to bother - // to find common ancestor: we need to update the whole tree: - // some invisible tiles may now be visible - return layer.level0Nodes; - } - if (source.layer === layer) { - if (!commonAncestor) { - commonAncestor = source; - } else { - commonAncestor = source.findCommonAncestor(commonAncestor); - if (!commonAncestor) { - return layer.level0Nodes; - } - } - if (commonAncestor.material == null) { - commonAncestor = undefined; - } - } - } - if (commonAncestor) { - if (__DEBUG__) { - layer._latestUpdateStartingLevel = commonAncestor.level; - } - return [commonAncestor]; - } else { - return layer.level0Nodes; - } - }; - - - function subdivision(context, layer, node) { - if (SubdivisionControl.hasEnoughTexturesToSubdivide(context, layer, node)) { - return planarSubdivisionControl(options.maxSubdivisionLevel || 5, - options.maxDeltaElevationLevel || 4)(context, layer, node); - } - return false; - } - - tileLayer.update = processTiledGeometryNode(planarCulling, subdivision); - tileLayer.builder = new PlanarTileBuilder(); - tileLayer.onTileCreated = nodeInitFn; - tileLayer.protocol = 'tile'; - tileLayer.visible = true; - tileLayer.lighting = { - enable: false, - position: { x: -0.5, y: 0.0, z: 1.0 }, - }; - // provide custom pick function - tileLayer.pickObjectsAt = (_view, mouse, radius) => Picking.pickTilesAt(_view, mouse, radius, tileLayer); - - return tileLayer; + console.warn('createPlanarLayer is deprecated, use the PlanarLayer class instead.'); + return new PlanarLayer(id, extent, options.object3d, options); } function PlanarView(viewerDiv, extent, options = {}) { @@ -121,7 +31,7 @@ function PlanarView(viewerDiv, extent, options = {}) { this.camera.camera3D.updateProjectionMatrix(); this.camera.camera3D.updateMatrixWorld(true); - const tileLayer = createPlanarLayer('planar', extent, options); + const tileLayer = new PlanarLayer('planar', extent, options.object3d, options); this.addLayer(tileLayer); diff --git a/src/Core/View.js b/src/Core/View.js index 9b683e2bfe..8ce3fa2845 100644 --- a/src/Core/View.js +++ b/src/Core/View.js @@ -1,13 +1,16 @@ /* global window */ -import { Scene, EventDispatcher, Vector2, Object3D } from 'three'; +import * as THREE from 'three'; import Camera from '../Renderer/Camera'; import MainLoop, { MAIN_LOOP_EVENTS, RENDERING_PAUSED } from './MainLoop'; import c3DEngine from '../Renderer/c3DEngine'; -import { STRATEGY_MIN_NETWORK_TRAFFIC } from './Layer/LayerUpdateStrategy'; -import { GeometryLayer, Layer, defineLayerProperty } from './Layer/Layer'; + +import Layer from '../Layer/Layer'; +import ColorLayer from '../Layer/ColorLayer'; +import ElevationLayer from '../Layer/ElevationLayer'; +import GeometryLayer from '../Layer/GeometryLayer'; + import Scheduler from './Scheduler/Scheduler'; import Picking from './Picking'; -import { updateLayeredMaterialNodeImagery, updateLayeredMaterialNodeElevation } from '../Process/LayeredMaterialNodeProcessing'; export const VIEW_EVENTS = { /** @@ -59,7 +62,7 @@ function View(crs, viewerDiv, options = {}) { this.mainLoop = options.mainLoop || new MainLoop(new Scheduler(), engine); - this.scene = options.scene3D || new Scene(); + this.scene = options.scene3D || new THREE.Scene(); if (!options.scene3D) { this.scene.autoUpdate = false; } @@ -76,7 +79,7 @@ function View(crs, viewerDiv, options = {}) { window.addEventListener('resize', () => { // If the user gave us a container (
) then itowns' size is // the container's size. Otherwise we use window' size. - const newSize = new Vector2(viewerDiv.clientWidth, viewerDiv.clientHeight); + const newSize = new THREE.Vector2(viewerDiv.clientWidth, viewerDiv.clientHeight); this.mainLoop.gfxEngine.onWindowResize(newSize.x, newSize.y); this.notifyChange(this.camera.camera3D); }, false); @@ -101,9 +104,25 @@ function View(crs, viewerDiv, options = {}) { }; } -View.prototype = Object.create(EventDispatcher.prototype); +View.prototype = Object.create(THREE.EventDispatcher.prototype); View.prototype.constructor = View; +function _createLayerFromConfig(config) { + switch (config.type) { + case 'color': + return new ColorLayer(config.id, config); + case 'elevation': + return new ElevationLayer(config.id, config); + case 'geometry': + return new GeometryLayer(config.id, new THREE.Group(), config); + case 'debug': + return new Layer(config.id, 'debug', config); + default: + throw new Error(`Unknown layer type ${config.type}: please + specify a valid one`); + } +} + const _syncGeometryLayerVisibility = function _syncGeometryLayerVisibility(layer, view) { if (layer.object3d) { layer.object3d.visible = layer.visible; @@ -119,46 +138,21 @@ const _syncGeometryLayerVisibility = function _syncGeometryLayerVisibility(layer }; function _preprocessLayer(view, layer, provider, parentLayer) { - if (!(layer instanceof Layer) && !(layer instanceof GeometryLayer)) { - const nlayer = new Layer(layer.id); - // nlayer.id is read-only so delete it from layer before Object.assign - const tmp = layer; - delete tmp.id; - layer = Object.assign(nlayer, layer); - // restore layer.id in user provider layer object - tmp.id = layer.id; - } - - layer.options = layer.options || {}; - // TODO remove this warning and fallback after the release following v2.3.0 - if (!layer.format && layer.options.mimetype) { - console.warn('layer.options.mimetype is deprecated, please use layer.format'); - layer.format = layer.options.mimetype; - } - - if (!layer.updateStrategy) { - layer.updateStrategy = { - type: STRATEGY_MIN_NETWORK_TRAFFIC, - }; + if (!(layer instanceof Layer)) { + layer = _createLayerFromConfig(layer); } if (provider) { if (provider.tileInsideLimit) { layer.tileInsideLimit = provider.tileInsideLimit.bind(provider); } - - if (provider.tileTextureCount) { - layer.tileTextureCount = provider.tileTextureCount.bind(provider); - } } if (!layer.whenReady) { - if (layer.type == 'geometry' || layer.type == 'debug') { - if (!layer.object3d) { - // layer.threejsLayer *must* be assigned before preprocessing, - // because TileProvider.preprocessDataLayer function uses it. - layer.threejsLayer = view.mainLoop.gfxEngine.getUniqueThreejsLayer(); - } + if (parentLayer || layer.type == 'debug') { + // layer.threejsLayer *must* be assigned before preprocessing, + // because TileProvider.preprocessDataLayer function uses it. + layer.threejsLayer = view.mainLoop.gfxEngine.getUniqueThreejsLayer(); } let providerPreprocessing = Promise.resolve(); if (provider && provider.preprocessDataLayer) { @@ -175,47 +169,11 @@ function _preprocessLayer(view, layer, provider, parentLayer) { }); } - // probably not the best place to do this - if (layer.type == 'color') { - defineLayerProperty(layer, 'frozen', false); - defineLayerProperty(layer, 'visible', true); - defineLayerProperty(layer, 'opacity', 1.0); - defineLayerProperty(layer, 'sequence', 0); - } else if (layer.type == 'elevation') { - defineLayerProperty(layer, 'frozen', false); - } else if (layer.type == 'geometry' || layer.type == 'debug') { - defineLayerProperty(layer, 'visible', true, () => _syncGeometryLayerVisibility(layer, view)); - defineLayerProperty(layer, 'frozen', false); + if (layer.type == 'geometry' || layer.type == 'debug') { + layer.defineLayerProperty('visible', true, () => _syncGeometryLayerVisibility(layer, view)); _syncGeometryLayerVisibility(layer, view); - - const changeOpacity = (o) => { - if (o.material) { - // != undefined: we want the test to pass if opacity is 0 - if (o.material.opacity != undefined) { - o.material.transparent = layer.opacity < 1.0; - o.material.opacity = layer.opacity; - } - if (o.material.uniforms && o.material.uniforms.opacity != undefined) { - o.material.transparent = layer.opacity < 1.0; - o.material.uniforms.opacity.value = layer.opacity; - } - } - }; - defineLayerProperty(layer, 'opacity', 1.0, () => { - if (layer.object3d) { - layer.object3d.traverse((o) => { - if (o.layer !== layer) { - return; - } - changeOpacity(o); - // 3dtiles layers store scenes in children's content property - if (o.content) { - o.content.traverse(changeOpacity); - } - }); - } - }); } + return layer; } @@ -313,11 +271,6 @@ function _preprocessLayer(view, layer, provider, parentLayer) { * @return {Promise} a promise resolved with the new layer object when it is fully initialized or rejected if any error occurred. */ View.prototype.addLayer = function addLayer(layer, parentLayer) { - if (layer.type == 'color') { - layer.update = layer.update || updateLayeredMaterialNodeImagery; - } else if (layer.type == 'elevation') { - layer.update = layer.update || updateLayeredMaterialNodeElevation; - } return new Promise((resolve, reject) => { if (!layer) { reject(new Error('layer is undefined')); @@ -404,12 +357,12 @@ View.prototype.notifyChange = function notifyChange(changeSource = undefined, ne */ View.prototype.getLayers = function getLayers(filter) { const result = []; - for (const geometryLayer of this._layers) { - if (!filter || filter(geometryLayer)) { - result.push(geometryLayer); + for (const layer of this._layers) { + if (!filter || filter(layer)) { + result.push(layer); } - for (const attached of geometryLayer._attachedLayers) { - if (!filter || filter(attached, geometryLayer)) { + for (const attached of layer.attachedLayers) { + if (!filter || filter(attached, layer)) { result.push(attached); } } @@ -423,7 +376,7 @@ View.prototype.getLayers = function getLayers(filter) { */ View.prototype.getParentLayer = function getParentLayer(layer) { for (const geometryLayer of this._layers) { - for (const attached of geometryLayer._attachedLayers) { + for (const attached of geometryLayer.attachedLayers) { if (attached === layer) { return geometryLayer; } @@ -545,7 +498,7 @@ View.prototype.execFrameRequesters = function execFrameRequesters(when, dt, upda } }; -const _eventCoords = new Vector2(); +const _eventCoords = new THREE.Vector2(); /** * Extract view coordinates from a mouse-event / touch-event * @param {event} event - event can be a MouseEvent or a TouchEvent @@ -575,7 +528,7 @@ View.prototype.eventToNormalizedCoords = function eventToNormalizedCoords(event, /** * Convert view coordinates to normalized coordinates (NDC) - * @param {Vector2} viewCoords (in pixels, 0-0 = top-left of the View) + * @param {THREE.Vector2} viewCoords (in pixels, 0-0 = top-left of the View) * @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1]) */ View.prototype.viewToNormalizedCoords = function viewToNormalizedCoords(viewCoords) { @@ -586,7 +539,7 @@ View.prototype.viewToNormalizedCoords = function viewToNormalizedCoords(viewCoor /** * Convert NDC coordinates to view coordinates - * @param {Vector2} ndcCoords + * @param {THREE.Vector2} ndcCoords * @return {THREE.Vector2} - view coordinates (in pixels, 0-0 = top-left of the View) */ View.prototype.normalizedToViewCoords = function normalizedToViewCoords(ndcCoords) { @@ -632,15 +585,14 @@ View.prototype.pickObjectsAt = function pickObjectsAt(mouseOrEvt, radius, ...whe radius = radius || 0; for (const source of sources) { - if (source instanceof GeometryLayer || - source instanceof Layer || + if (source instanceof Layer || typeof (source) === 'string') { const layer = (typeof (source) === 'string') ? layerIdToLayer(this, source) : source; - // does this layer have a custom picking function? - if (layer.pickObjectsAt) { + const parentLayer = this.getParentLayer(layer); + if (!parentLayer) { const 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 @@ -649,14 +601,6 @@ View.prototype.pickObjectsAt = function pickObjectsAt(mouseOrEvt, radius, ...whe results.push(sp[i]); } } else { - // - it hasn't: this layer is attached to another one - let parentLayer; - this.getLayers((l, p) => { - if (l.id == layer.id) { - parentLayer = p; - } - }); - // raycast using parent layer object3d const obj = Picking.pickObjectsAt( this, @@ -671,7 +615,7 @@ View.prototype.pickObjectsAt = function pickObjectsAt(mouseOrEvt, radius, ...whe } } } - } else if (source instanceof Object3D) { + } else if (source instanceof THREE.Object3D) { Picking.pickObjectsAt( this, mouse, diff --git a/src/Layer/ColorLayer.js b/src/Layer/ColorLayer.js new file mode 100644 index 0000000000..a2ace32bb6 --- /dev/null +++ b/src/Layer/ColorLayer.js @@ -0,0 +1,72 @@ +import Layer from './Layer'; +import { updateLayeredMaterialNodeImagery } from '../Process/LayeredMaterialNodeProcessing'; + +/** + * Fires when the visiblity of the layer has changed. + * @event ColorLayer#visible-property-changed + */ +/** + * Fires when the opacity of the layer has changed. + * @event ColorLayer#opacity-property-changed + */ +/** + * Fires when the sequence of the layer has changed, meaning that the order of + * the layer changed in the view it is attached to. + * @event ColorLayer#sequence-property-changed + */ + +class ColorLayer extends Layer { + /** + * A simple layer, usually managing a texture to display on a view. For example, + * it can be an aerial view of the ground or a simple transparent layer with the + * roads displayed. + * + * @constructor + * @extends Layer + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {Object} [config] - Optional configuration, all elements in it + * will be merged as is in the layer. For example, if the configuration + * contains three elements name, protocol, extent, these + * elements will be available using layer.name or something + * else depending on the property name. + * + * @example + * // Create a ColorLayer + * const color = new ColorLayer('roads', { + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/png', + * transparent: true + * }); + * + * // Add the layer + * view.addLayer(color); + * + * @example + * // Add and create a ColorLayer + * view.addLayer({ + * id: 'roads', + * type: 'color', + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/png', + * transparent: true + * }); + */ + constructor(id, config = {}) { + super(id, 'color', config); + + this.defineLayerProperty('visible', true); + this.defineLayerProperty('opacity', 1.0); + this.defineLayerProperty('sequence', 0); + } + + update(context, layer, node, parent) { + return updateLayeredMaterialNodeImagery(context, this, node, parent); + } +} + +export default ColorLayer; diff --git a/src/Layer/ElevationLayer.js b/src/Layer/ElevationLayer.js new file mode 100644 index 0000000000..06e9b4eb2c --- /dev/null +++ b/src/Layer/ElevationLayer.js @@ -0,0 +1,51 @@ +import Layer from './Layer'; +import { updateLayeredMaterialNodeElevation } from '../Process/LayeredMaterialNodeProcessing'; + +class ElevationLayer extends Layer { + /** + * A simple layer, managing an elevation texture to add some reliefs on the + * plane or globe view for example. + * + * @constructor + * @extends Layer + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {Object} [config] - Optional configuration, all elements in it + * will be merged as is in the layer. For example, if the configuration + * contains three elements name, protocol, extent, these + * elements will be available using layer.name or something + * else depending on the property name. + * + * @example + * // Create an ElevationLayer + * const elevation = new ElevationLayer('IGN_MNT', { + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/x-bil;bits=32', + * }); + * + * // Add the layer + * view.addLayer(elevation); + * + * @example + * // Add and create an ElevationLayer + * view.addLayer({ + * id: 'IGN_MNT', + * type: 'elevation', + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/x-bil;bits=32', + * }); + */ + constructor(id, config = {}) { + super(id, 'elevation', config); + } + + update(context, layer, node, parent) { + return updateLayeredMaterialNodeElevation(context, this, node, parent); + } +} + +export default ElevationLayer; diff --git a/src/Layer/GeometryLayer.js b/src/Layer/GeometryLayer.js new file mode 100644 index 0000000000..485f971385 --- /dev/null +++ b/src/Layer/GeometryLayer.js @@ -0,0 +1,172 @@ +import Layer from './Layer'; +import Picking from '../Core/Picking'; + +/** + * Fires when the opacity of the layer has changed. + * @event GeometryLayer#opacity-property-changed + */ + +class GeometryLayer extends Layer { + /** + * A layer usually managing a geometry to display on a view. For example, it + * can be a layer of buildings extruded from a a WFS stream. + * + * @constructor + * @extends Layer + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {THREE.Object3d} object3d - The object3d used to contain the + * geometry of the GeometryLayer. It is usually a THREE.Group, + * but it can be anything inheriting from a THREE.Object3d. + * @param {Object} [config] - Optional configuration, all elements in it + * will be merged as is in the layer. For example, if the configuration + * contains three elements name, protocol, extent, these + * elements will be available using layer.name or something + * else depending on the property name. + * + * @throws {Error} object3d must be a valid + * THREE.Object3d. + * + * @example + * // Create a GeometryLayer + * const geometry = new GeometryLayer('buildings', { + * url: 'http://server.geo/wfs?', + * protocol: 'wfs', + * format: 'application/json' + * }); + * + * // Add the layer + * view.addLayer(geometry); + * + * @example + * // Add and create a GeometryLayer + * view.addLayer({ + * id: 'buildings', + * type: 'geometry', + * url: 'http://server.geo/wfs?', + * protocol: 'wfs', + * format: 'application/json' + * }); + */ + constructor(id, object3d, config = {}) { + super(id, 'geometry', config); + + if (!object3d || !object3d.isObject3D) { + throw new Error(`Missing/Invalid object3d parameter (must be a + three.js Object3D instance)`); + } + + if (object3d.type === 'Group' && object3d.name === '') { + object3d.name = id; + } + + Object.defineProperty(this, 'object3d', { + value: object3d, + writable: false, + }); + + this.defineLayerProperty('opacity', 1.0, () => { + this.object3d.traverse((object) => { + if (object.layer !== this) { + return; + } + this.changeOpacity(object); + // 3dtiles layers store scenes in children's content property + if (object.content) { + object.content.traverse(this.changeOpacity); + } + }); + }); + + this.attachedLayers = []; + this.visible = true; + + // Attached layers expect to receive the visual representation of a + // layer (= THREE object with a material). So if a layer's update + // function don't process this kind of object, the layer must provide a + // getObjectToUpdateForAttachedLayers function that returns the correct + // object to update for attached layer. + // See 3dtilesProvider or PointCloudProvider for examples. + // eslint-disable-next-line arrow-body-style + this.getObjectToUpdateForAttachedLayers = (obj) => { + if (obj.parent && obj.material) { + return { + element: obj, + parent: obj.parent, + }; + } + }; + + this.postUpdate = () => {}; + } + + /** + * Attach another layer to this one. Layers attached to a GeometryLayer will + * be available in geometryLayer.attachedLayers. + * + * @param {Layer} layer - The layer to attach, that must have an + * update method. + */ + attach(layer) { + if (!layer.update) { + throw new Error(`Missing 'update' function -> can't attach layer + ${layer.id}`); + } + this.attachedLayers.push(layer); + } + + /** + * Detach a layer attached to this one. See {@link attach} to learn how to + * attach a layer. + * + * @param {Layer} layer - The layer to detach. + * + * @return {boolean} Confirmation of the detachment of the layer. + */ + detach(layer) { + const count = this.attachedLayers.length; + this.attachedLayers = this.attachedLayers.filter(attached => attached.id != layer.id); + return this.attachedLayers.length < count; + } + + /** + * Picking method for this layer. It uses the {@link Picking#pickObjectsAt} + * method. + * + * @param {View} view - The view instance. + * @param {Object} coordinates - The coordinates to pick in the view. It + * should have at least x and y properties. + * @param {number} radius - Radius of the picking circle. + * + * @return {Array} An array containing all targets picked under the + * specified coordinates. + */ + pickObjectsAt(view, coordinates, radius = this.options.defaultPickingRadius) { + return Picking.pickObjectsAt(view, coordinates, radius, this.object3d); + } + + /** + * Change the opacity of an object, according to the value of the + * opacity property of this layer. + * + * @param {Object} object - The object to change the opacity from. It is + * usually a THREE.Object3d or an implementation of it. + */ + changeOpacity(object) { + if (object.material) { + // != undefined: we want the test to pass if opacity is 0 + if (object.material.opacity != undefined) { + object.material.transparent = this.opacity < 1.0; + object.material.opacity = this.opacity; + } + if (object.material.uniforms && object.material.uniforms.opacity != undefined) { + object.material.transparent = this.opacity < 1.0; + object.material.uniforms.opacity.value = this.opacity; + } + } + } +} + +export default GeometryLayer; diff --git a/src/Layer/Layer.js b/src/Layer/Layer.js new file mode 100644 index 0000000000..5453866876 --- /dev/null +++ b/src/Layer/Layer.js @@ -0,0 +1,188 @@ +import * as THREE from 'three'; +import { STRATEGY_MIN_NETWORK_TRAFFIC } from './LayerUpdateStrategy'; + +class Layer extends THREE.EventDispatcher { + /** + * Don't use directly constructor to instance a new Layer. Instead, use + * another available type of Layer, implement a new one inheriting from this + * one or use {@link View#addLayer}. + * + * @constructor + * @protected + * + * @param {string} id - The id of the layer, that should be unique. It is + * not mandatory, but an error will be emitted if this layer is added a + * {@link View} that already has a layer going by that id. + * @param {string} type - The type of the layer, used to determine + * operations to do on a layer later in the rendering loop. There are three + * type of layers in itowns: + *