diff --git a/src/app/application.js b/src/app/application.js index 90d9e0f7df..bd89b52a7e 100644 --- a/src/app/application.js +++ b/src/app/application.js @@ -656,9 +656,91 @@ export class App { this.#createLayerGroups(configs); } + /** + * Add a data view config. + * + * @param {number} dataId The data id. + * @param {object} config The view configuration. + */ + addDataViewConfig(dataId, config) { + // add to list + const configs = this.#options.dataViewConfigs; + if (typeof configs[dataId] === 'undefined') { + configs[dataId] = []; + } + const equalDivId = function (item) { + return item.divId === config.divId; + }; + const itemIndex = configs[dataId].findIndex(equalDivId); + if (itemIndex === -1) { + this.#options.dataViewConfigs[dataId].push(config); + } else { + throw new Error('Duplicate view sconfig for data ' + dataId + + ' and div ' + config.divId); + } + + // data is loaded, create view + if (typeof this.#dataController.get(dataId) !== 'undefined') { + const lg = this.#stage.getLayerGroupByDivId(config.divId); + if (typeof lg === 'undefined') { + // create layer group + this.#createLayerGroup(config); + } else { + // add view + this.#addViewLayer(dataId, config); + } + } + } + + /** + * Remove a data view config. + * + * @param {number} dataId The data id. + * @param {object} config The view configuration. + */ + removeDataViewConfig(dataId, config) { + // remove from list + const configs = this.#options.dataViewConfigs; + if (typeof configs[dataId] === 'undefined') { + // no config for dataId + return; + } + const equalDivId = function (item) { + return item.divId === config.divId; + }; + const itemIndex = configs[dataId].findIndex(equalDivId); + if (itemIndex === -1) { + // no config for divId + return; + } + configs[dataId].splice(itemIndex, 1); + + // data is loaded, remove view + if (typeof this.#dataController.get(dataId) !== 'undefined') { + const lg = this.#stage.getLayerGroupByDivId(config.divId); + if (typeof lg !== 'undefined') { + const vls = lg.getViewLayersByDataIndex(dataId); + if (vls.length === 1) { + lg.removeLayer(vls[0]); + } else { + throw new Error('Expected one view layer, got ' + vls.length); + } + const dls = lg.getDrawLayersByDataIndex(dataId); + if (dls.length === 1) { + lg.removeLayer(dls[0]); + } else { + throw new Error('Expected one draw layer, got ' + dls.length); + } + if (lg.getNumberOfLayers() === 0) { + this.#stage.removeLayerGroup(lg); + } + } + } + } + /** * Create layer groups according to a data view config: - * adds them to stage and bind them. + * adds them to stage and binds them. * * @param {object} dataViewConfigs The data view config. */ @@ -671,22 +753,32 @@ export class App { const viewConfig = dataConfigs[j]; // view configs can contain the same divIds, avoid duplicating if (!divIds.includes(viewConfig.divId)) { - // create new layer group - const element = document.getElementById(viewConfig.divId); - const layerGroup = this.#stage.addLayerGroup(element); - // bind events - this.#bindLayerGroupToApp(layerGroup); - // optional orientation - if (typeof viewConfig.orientation !== 'undefined') { - layerGroup.setTargetOrientation( - getMatrixFromName(viewConfig.orientation)); - } + this.#createLayerGroup(viewConfig); divIds.push(viewConfig.divId); } } } } + /** + * Create a layer group according to a view config: + * adds it to stage and binds it. + * + * @param {object} viewConfig The view config. + */ + #createLayerGroup(viewConfig) { + // create new layer group + const element = document.getElementById(viewConfig.divId); + const layerGroup = this.#stage.addLayerGroup(element); + // bind events + this.#bindLayerGroupToApp(layerGroup); + // optional orientation + if (typeof viewConfig.orientation !== 'undefined') { + layerGroup.setTargetOrientation( + getMatrixFromName(viewConfig.orientation)); + } + } + /** * Set the layer groups binders. * diff --git a/src/gui/layerGroup.js b/src/gui/layerGroup.js index 3ad9a72039..f5cf92091b 100644 --- a/src/gui/layerGroup.js +++ b/src/gui/layerGroup.js @@ -554,6 +554,24 @@ export class LayerGroup { viewLayer.addEventListener('renderend', this.#fireEvent); } + /** + * Un-bind a view layer events to this. + * + * @param {ViewLayer} viewLayer The view layer to unbind. + */ + #unbindViewLayer(viewLayer) { + // listen to position change to update other group layers + viewLayer.removeEventListener( + 'positionchange', this.updateLayersToPositionChange); + // propagate view viewLayer-layer events + for (let j = 0; j < viewEventNames.length; ++j) { + viewLayer.removeEventListener(viewEventNames[j], this.#fireEvent); + } + // propagate viewLayer events + viewLayer.removeEventListener('renderstart', this.#fireEvent); + viewLayer.removeEventListener('renderend', this.#fireEvent); + } + /** * Bind draw layer events to this. * @@ -565,6 +583,17 @@ export class LayerGroup { drawLayer.addEventListener('drawdelete', this.#fireEvent); } + /** + * Un-bind a draw layer events to this. + * + * @param {DrawLayer} drawLayer The draw layer to unbind. + */ + #unbindDrawLayer(drawLayer) { + // propagate drawLayer events + drawLayer.removeEventListener('drawcreate', this.#fireEvent); + drawLayer.removeEventListener('drawdelete', this.#fireEvent); + } + /** * Get the next layer DOM div. * @@ -595,6 +624,38 @@ export class LayerGroup { } } + /** + * Remove a layer from this layer group. + * + * @param {ViewLayer | DrawLayer} layer The layer to remove. + */ + removeLayer(layer) { + // find layer + const index = this.#layers.findIndex((item) => item === layer); + if (index === -1) { + throw new Error('Cannot find layer'); + } + // unbind and update active index + if (layer instanceof ViewLayer) { + this.#unbindViewLayer(layer); + if (this.#activeViewLayerIndex === index) { + this.#activeViewLayerIndex = undefined; + } + } else { + this.#unbindDrawLayer(layer); + if (this.#activeDrawLayerIndex === index) { + this.#activeDrawLayerIndex = undefined; + } + } + // remove from storage + this.#layers.splice(index, 1); + // update html + const layerDiv = document.getElementById(layer.getId()); + if (layerDiv) { + layerDiv.remove(); + } + } + /** * Show a crosshair at a given position. * diff --git a/src/gui/stage.js b/src/gui/stage.js index 730f7aadec..d0e0eaebcb 100644 --- a/src/gui/stage.js +++ b/src/gui/stage.js @@ -263,6 +263,31 @@ export class Stage { this.#activeLayerGroupIndex = null; } + /** + * Remove a layer group from this stage. + * + * @param {LayerGroup} layerGroup The layer group to remove. + */ + removeLayerGroup(layerGroup) { + // find layer + const index = this.#layerGroups.findIndex((item) => item === layerGroup); + if (index === -1) { + throw new Error('Cannot find layerGroup'); + } + // unbind + this.unbindLayerGroups(); + // empty layer group + layerGroup.empty(); + // remove from storage + this.#layerGroups.splice(index, 1); + // update active index + if (this.#activeLayerGroupIndex === index) { + this.#activeLayerGroupIndex = undefined; + } + // bind + this.bindLayerGroups(); + } + /** * Reset the stage: calls reset on all layer groups. */ diff --git a/tests/gui/layerGroup.test.js b/tests/gui/layerGroup.test.js index 64300264ed..d16999f279 100644 --- a/tests/gui/layerGroup.test.js +++ b/tests/gui/layerGroup.test.js @@ -1,4 +1,5 @@ import { + LayerGroup, getLayerDivId, getLayerDetailsFromLayerDivId } from '../../src/gui/layerGroup'; @@ -39,3 +40,61 @@ QUnit.test('Test LayerGroup string id.', function (assert) { assert.equal(details01.layerId, theoDetails01.layerId, 'getLayerDetailsFromLayerDivId layerId #01'); }); + +/** + * Tests for {@link LayerGroup} creation. + * + * @function module:tests/gui~LayerGroup + */ +QUnit.test('Test LayerGroup.', function (assert) { + const element00 = document.createElement('div'); + element00.id = 'layerGroup00'; + const layerGroup00 = new LayerGroup(element00); + assert.equal(layerGroup00.getNumberOfLayers(), 0, + 'new layerGroup has no layers'); + assert.equal(layerGroup00.getDivId(), element00.id, + 'new layerGroup div id'); +}); + + +/** + * Tests for {@link LayerGroup} add/remove view layer. + * + * @function module:tests/gui~LayerGroup + */ +QUnit.test('Test LayerGroup add/remove view layer.', function (assert) { + const element00 = document.createElement('div'); + element00.id = 'layerGroup00'; + const layerGroup00 = new LayerGroup(element00); + assert.equal(layerGroup00.getNumberOfLayers(), 0, + 'new layerGroup has no layers'); + + const vl00 = layerGroup00.addViewLayer(); + assert.equal(layerGroup00.getNumberOfLayers(), 1, + 'layerGroup has one view layers after add'); + + layerGroup00.removeLayer(vl00); + assert.equal(layerGroup00.getNumberOfLayers(), 0, + 'layerGroup has no view layers after remove'); +}); + +/** + * Tests for {@link LayerGroup} add/remove draw layer. + * + * @function module:tests/gui~LayerGroup + */ +QUnit.test('Test LayerGroup add/remove draw layer.', function (assert) { + const element00 = document.createElement('div'); + element00.id = 'layerGroup00'; + const layerGroup00 = new LayerGroup(element00); + assert.equal(layerGroup00.getNumberOfLayers(), 0, + 'new layerGroup has no layers'); + + const dl00 = layerGroup00.addDrawLayer(); + assert.equal(layerGroup00.getNumberOfLayers(), 1, + 'layerGroup has one draw layers after add'); + + layerGroup00.removeLayer(dl00); + assert.equal(layerGroup00.getNumberOfLayers(), 0, + 'layerGroup has no view layers after remove'); +});