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 (
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:
+ * color
, used for simple layer containing a textureelevation
, used for adding an elevation to the globe or
+ * plane the layer is attached togeometry
, used for complex layer containing meshes,
+ * like a WFS layer with extruded buildingsname, protocol, extent
, these
+ * elements will be available using layer.name
or something
+ * else depending on the property name.
+ *
+ * @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(id, type, config = {}) {
+ super();
+
+ Object.assign(this, config);
+
+ Object.defineProperty(this, 'id', {
+ value: id,
+ writable: false,
+ });
+
+ Object.defineProperty(this, 'type', {
+ value: type,
+ writable: false,
+ });
+
+ // Default properties
+ this.options = config.options || {};
+
+ if (!this.updateStrategy) {
+ this.updateStrategy = {
+ type: STRATEGY_MIN_NETWORK_TRAFFIC,
+ };
+ }
+
+ // TODO remove this warning and fallback after the release following v2.3.0
+ if (!this.format && this.options.mimetype) {
+ console.warn('layer.options.mimetype is deprecated, please use layer.format');
+ this.format = this.options.mimetype;
+ }
+
+ this.defineLayerProperty('frozen', false);
+ }
+
+ /**
+ * Defines a property for this layer, with a default value and a callback
+ * executed when the property changes.
+ * ${propertyName}-property-changed
, with
+ * ${propertyName}
being replaced by the name of the property.
+ * For example, if the added property name is frozen
, it will
+ * emit a frozen-property-changed
.
+ * + * event = { + * new: { + * ${propertyName}: * // the new value of the property + * }, + * previous: { + * ${propertyName}: * // the previous value of the property + * }, + * target: Layer // the layer it has been dispatched from + * type: string // the name of the emitted event + * } + *+ * + * @param {string} propertyName - The name of the property, also used in + * the emitted event name. + * @param {*} defaultValue - The default set value. + * @param {function} [onChange] - The function executed when the property is + * changed. Parameters are the layer the property is defined on, and the + * name of the property. + */ + defineLayerProperty(propertyName, defaultValue, onChange) { + const existing = Object.getOwnPropertyDescriptor(this, propertyName); + if (!existing || !existing.set) { + let property = this[propertyName] == undefined ? defaultValue : this[propertyName]; + + Object.defineProperty( + this, + 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(this, propertyName); + } + this.dispatchEvent(event); + } + }, + }); + } + } +} + +export default Layer; + +export const ImageryLayers = { + // move this 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); + }, +}; + diff --git a/src/Core/Layer/LayerUpdateState.js b/src/Layer/LayerUpdateState.js similarity index 100% rename from src/Core/Layer/LayerUpdateState.js rename to src/Layer/LayerUpdateState.js diff --git a/src/Core/Layer/LayerUpdateStrategy.js b/src/Layer/LayerUpdateStrategy.js similarity index 97% rename from src/Core/Layer/LayerUpdateStrategy.js rename to src/Layer/LayerUpdateStrategy.js index c4a5b43e04..32c77291d9 100644 --- a/src/Core/Layer/LayerUpdateStrategy.js +++ b/src/Layer/LayerUpdateStrategy.js @@ -1,4 +1,4 @@ -import { EMPTY_TEXTURE_ZOOM } from '../../Renderer/LayeredMaterialConstants'; +import { EMPTY_TEXTURE_ZOOM } from '../Renderer/LayeredMaterialConstants'; /** * This modules implements various layer update strategies. * diff --git a/src/Layer/TiledGeometryLayer.js b/src/Layer/TiledGeometryLayer.js new file mode 100644 index 0000000000..ec7ee72764 --- /dev/null +++ b/src/Layer/TiledGeometryLayer.js @@ -0,0 +1,168 @@ +import GeometryLayer from './GeometryLayer'; +import Picking from '../Core/Picking'; +import { processTiledGeometryNode } from '../Process/TiledNodeProcessing'; + +class TiledGeometryLayer extends GeometryLayer { + /** + * A layer extending the {@link GeometryLayer}, but with a tiling notion. + * + * @constructor + * @extends GeometryLayer + * + * @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 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.
+ *
+ * @throws {Error} object3d
must be a valid
+ * THREE.Object3d
.
+ */
+ constructor(id, object3d, config) {
+ super(id, object3d, config);
+
+ this.protocol = 'tile';
+ this.lighting = {
+ enable: false,
+ position: { x: -0.5, y: 0.0, z: 1.0 },
+ };
+ }
+
+ /**
+ * Picking method for this layer. It uses the {@link Picking#pickTilesAt}
+ * 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.pickTilesAt(view, coordinates, radius, this);
+ }
+
+ preUpdate(context, changeSources) {
+ if (changeSources.has(undefined) || changeSources.size == 0) {
+ return this.level0Nodes;
+ }
+
+ if (__DEBUG__) {
+ this._latestUpdateStartingLevel = 0;
+ }
+
+ context.colorLayers = context.view.getLayers(
+ (l, a) => a && a.id == this.id && l.type == 'color');
+ context.elevationLayers = context.view.getLayers(
+ (l, a) => a && a.id == this.id && l.type == 'elevation');
+
+ context.maxElevationLevel = -1;
+ for (const e of context.elevationLayers) {
+ context.maxElevationLevel = Math.max(e.options.zoom.max, context.maxElevationLevel);
+ }
+ if (context.maxElevationLevel == -1) {
+ context.maxElevationLevel = Infinity;
+ }
+
+ 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 this.level0Nodes;
+ }
+ if (source.this === this) {
+ if (!commonAncestor) {
+ commonAncestor = source;
+ } else {
+ commonAncestor = source.findCommonAncestor(commonAncestor);
+ if (!commonAncestor) {
+ return this.level0Nodes;
+ }
+ }
+ if (commonAncestor.material == null) {
+ commonAncestor = undefined;
+ }
+ }
+ }
+ if (commonAncestor) {
+ if (__DEBUG__) {
+ this._latestUpdateStartingLevel = commonAncestor.level;
+ }
+ return [commonAncestor];
+ } else {
+ return this.level0Nodes;
+ }
+ }
+
+ update(context, layer, node) {
+ return processTiledGeometryNode(this.culling, this.subdivision)(context, this, node);
+ }
+
+ onTileCreated(node) {
+ node.material.setLightingOn(this.lighting.enable);
+ node.material.uniforms.lightPosition.value = this.lighting.position;
+
+ if (this.noTextureColor) {
+ node.material.uniforms.noTextureColor.value.copy(this.noTextureColor);
+ }
+
+ if (__DEBUG__) {
+ node.material.uniforms.showOutline = { value: this.showOutline || false };
+ node.material.wireframe = this.wireframe || false;
+ }
+ }
+
+ /**
+ * Tell if a node has enough elevation or color textures to subdivide.
+ * Subdivision is prevented if:
+ *