/**
* @module olcs.AbstractSynchronizer
*/
import {unByKey as olObservableUnByKey} from 'ol/Observable.js';
import olLayerGroup from 'ol/layer/Group.js';
import {olcsListen, getUid} from './util.js';
class AbstractSynchronizer {
/**
* @param {!ol.Map} map
* @param {!Cesium.Scene} scene
* @template T
* @abstract
* @api
*/
constructor(map, scene) {
/**
* @type {!ol.Map}
* @protected
*/
this.map = map;
/**
* @type {ol.View}
* @protected
*/
this.view = map.getView();
/**
* @type {!Cesium.Scene}
* @protected
*/
this.scene = scene;
/**
* @type {ol.Collection.
}
* @protected
*/
this.olLayers = map.getLayerGroup().getLayers();
/**
* @type {ol.layer.Group}
*/
this.mapLayerGroup = map.getLayerGroup();
/**
* Map of OpenLayers layer ids (from getUid) to the Cesium ImageryLayers.
* Null value means, that we are unable to create equivalent layers.
* @type {Object.>}
* @protected
*/
this.layerMap = {};
/**
* Map of listen keys for OpenLayers layer layers ids (from getUid).
* @type {!Object.>}
* @protected
*/
this.olLayerListenKeys = {};
/**
* Map of listen keys for OpenLayers layer groups ids (from getUid).
* @type {!Object.>}
* @private
*/
this.olGroupListenKeys_ = {};
}
/**
* Destroy all and perform complete synchronization of the layers.
* @api
*/
synchronize() {
this.destroyAll();
this.addLayers_(this.mapLayerGroup);
}
/**
* Order counterparts using the same algorithm as the Openlayers renderer:
* z-index then original sequence order.
* @protected
*/
orderLayers() {
// Ordering logics is handled in subclasses.
}
/**
* Add a layer hierarchy.
* @param {ol.layer.Base} root
* @private
*/
addLayers_(root) {
/** @type {Array} */
const fifo = [{
layer: root,
parents: []
}];
while (fifo.length > 0) {
const olLayerWithParents = fifo.splice(0, 1)[0];
const olLayer = olLayerWithParents.layer;
const olLayerId = getUid(olLayer).toString();
this.olLayerListenKeys[olLayerId] = [];
console.assert(!this.layerMap[olLayerId]);
let cesiumObjects = null;
if (olLayer instanceof olLayerGroup) {
this.listenForGroupChanges_(olLayer);
if (olLayer !== this.mapLayerGroup) {
cesiumObjects = this.createSingleLayerCounterparts(olLayerWithParents);
}
if (!cesiumObjects) {
olLayer.getLayers().forEach((l) => {
if (l) {
const newOlLayerWithParents = {
layer: l,
parents: olLayer === this.mapLayerGroup ?
[] :
[olLayerWithParents.layer].concat(olLayerWithParents.parents)
};
fifo.push(newOlLayerWithParents);
}
});
}
} else {
cesiumObjects = this.createSingleLayerCounterparts(olLayerWithParents);
if (!cesiumObjects) {
// keep an eye on the layers that once failed to be added (might work when the layer is updated)
// for example when a source is set after the layer is added to the map
const layerId = olLayerId;
const layerWithParents = olLayerWithParents;
const onLayerChange = (e) => {
const cesiumObjs = this.createSingleLayerCounterparts(layerWithParents);
if (cesiumObjs) {
// unsubscribe event listener
layerWithParents.layer.un('change', onLayerChange);
this.addCesiumObjects_(cesiumObjs, layerId, layerWithParents.layer);
this.orderLayers();
}
};
this.olLayerListenKeys[olLayerId].push(olcsListen(layerWithParents.layer, 'change', onLayerChange));
}
}
// add Cesium layers
if (cesiumObjects) {
this.addCesiumObjects_(cesiumObjects, olLayerId, olLayer);
}
}
this.orderLayers();
}
/**
* Add Cesium objects.
* @param {Array.} cesiumObjects
* @param {string} layerId
* @param {ol.layer.Base} layer
* @private
*/
addCesiumObjects_(cesiumObjects, layerId, layer) {
this.layerMap[layerId] = cesiumObjects;
this.olLayerListenKeys[layerId].push(olcsListen(layer, 'change:zIndex', () => this.orderLayers()));
cesiumObjects.forEach((cesiumObject) => {
this.addCesiumObject(cesiumObject);
});
}
/**
* Remove and destroy a single layer.
* @param {ol.layer.Layer} layer
* @return {boolean} counterpart destroyed
* @private
*/
removeAndDestroySingleLayer_(layer) {
const uid = getUid(layer).toString();
const counterparts = this.layerMap[uid];
if (!!counterparts) {
counterparts.forEach((counterpart) => {
this.removeSingleCesiumObject(counterpart, false);
this.destroyCesiumObject(counterpart);
});
this.olLayerListenKeys[uid].forEach(olObservableUnByKey);
delete this.olLayerListenKeys[uid];
}
delete this.layerMap[uid];
return !!counterparts;
}
/**
* Unlisten a single layer group.
* @param {ol.layer.Group} group
* @private
*/
unlistenSingleGroup_(group) {
if (group === this.mapLayerGroup) {
return;
}
const uid = getUid(group).toString();
const keys = this.olGroupListenKeys_[uid];
keys.forEach((key) => {
olObservableUnByKey(key);
});
delete this.olGroupListenKeys_[uid];
delete this.layerMap[uid];
}
/**
* Remove layer hierarchy.
* @param {ol.layer.Base} root
* @private
*/
removeLayer_(root) {
if (!!root) {
const fifo = [root];
while (fifo.length > 0) {
const olLayer = fifo.splice(0, 1)[0];
const done = this.removeAndDestroySingleLayer_(olLayer);
if (olLayer instanceof olLayerGroup) {
this.unlistenSingleGroup_(olLayer);
if (!done) {
// No counterpart for the group itself so removing
// each of the child layers.
olLayer.getLayers().forEach((l) => {
fifo.push(l);
});
}
}
}
}
}
/**
* Register listeners for single layer group change.
* @param {ol.layer.Group} group
* @private
*/
listenForGroupChanges_(group) {
const uuid = getUid(group).toString();
console.assert(this.olGroupListenKeys_[uuid] === undefined);
const listenKeyArray = [];
this.olGroupListenKeys_[uuid] = listenKeyArray;
// only the keys that need to be relistened when collection changes
let contentKeys = [];
const listenAddRemove = (function() {
const collection = group.getLayers();
if (collection) {
contentKeys = [
collection.on('add', (event) => {
this.addLayers_(event.element);
}),
collection.on('remove', (event) => {
this.removeLayer_(event.element);
})
];
listenKeyArray.push(...contentKeys);
}
}).bind(this);
listenAddRemove();
listenKeyArray.push(group.on('change:layers', (e) => {
contentKeys.forEach((el) => {
const i = listenKeyArray.indexOf(el);
if (i >= 0) {
listenKeyArray.splice(i, 1);
}
olObservableUnByKey(el);
});
listenAddRemove();
}));
}
/**
* Destroys all the created Cesium objects.
* @protected
*/
destroyAll() {
this.removeAllCesiumObjects(true); // destroy
let objKey;
for (objKey in this.olGroupListenKeys_) {
const keys = this.olGroupListenKeys_[objKey];
keys.forEach(olObservableUnByKey);
}
for (objKey in this.olLayerListenKeys) {
this.olLayerListenKeys[objKey].forEach(olObservableUnByKey);
}
this.olGroupListenKeys_ = {};
this.olLayerListenKeys = {};
this.layerMap = {};
}
/**
* Adds a single Cesium object to the collection.
* @param {!T} object
* @abstract
* @protected
*/
addCesiumObject(object) {}
/**
* @param {!T} object
* @abstract
* @protected
*/
destroyCesiumObject(object) {}
/**
* Remove single Cesium object from the collection.
* @param {!T} object
* @param {boolean} destroy
* @abstract
* @protected
*/
removeSingleCesiumObject(object, destroy) {}
/**
* Remove all Cesium objects from the collection.
* @param {boolean} destroy
* @abstract
* @protected
*/
removeAllCesiumObjects(destroy) {}
/**
* @param {import('olsc/core.js').LayerWithParents} olLayerWithParents
* @return {?Array.}
* @abstract
* @protected
*/
createSingleLayerCounterparts(olLayerWithParents) {}
}
export default AbstractSynchronizer;