UNPKG

@geoext/geoext

Version:

GeoExt is Open Source and enables building desktop-like GIS applications through the web. It is a JavaScript framework that combines the GIS functionality of OpenLayers with the user interface savvy of the ExtJS library provided by Sencha.

497 lines (455 loc) 15.2 kB
/* Copyright (c) 2015-present The Open Source Geospatial Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * A store that is synchronized with a GeoExt.data.store.Layers. It can be * used by an {Ext.tree.Panel}. * * @class GeoExt.data.store.LayersTree */ Ext.define('GeoExt.data.store.LayersTree', { extend: 'Ext.data.TreeStore', alternateClassName: ['GeoExt.data.TreeStore'], requires: ['GeoExt.util.Layer'], mixins: ['GeoExt.mixin.SymbolCheck'], // <debug> symbols: [ 'ol.Collection', 'ol.Collection#getArray', 'ol.Collection#once', 'ol.Collection#un', 'ol.layer.Base', 'ol.layer.Base#get', 'ol.layer.Group', 'ol.layer.Group#get', 'ol.layer.Group#getLayers', ], // </debug> model: 'GeoExt.data.model.LayerTreeNode', config: { /** * The ol.layer.Group that the tree is derived from. * * @cfg {ol.layer.Group} */ layerGroup: null, /** * Configures the behaviour of the checkbox of an `ol.layer.Group` * (folder). Possible values are `'classic'` or `'ol3'`. * * * `'classic'` forwards the checkstate to the children of the folder. * * Check a leaf => all parent nodes are checked * * Uncheck all leafs in a folder => parent node is unchecked * * Check a folder Node => all children are checked * * Uncheck a folder Node => all children are unchecked * * `'ol3'` emulates the behaviour of `ol.layer.Group`. So a layerGroup * can be invisible but can have visible children. * * Emulates the behaviour of an `ol.layer.Group,` so a parentfolder * can be unchecked but still contain checked leafs and vice versa. * * @cfg */ folderToggleMode: 'classic', }, statics: { /** * A string which we'll us for child nodes to detect if they are removed * because their parent collapsed just recently. See the private * method #onBeforeGroupNodeToggle for an explanation. * * @private */ KEY_COLLAPSE_REMOVE_OPT_OUT: '__remove_by_collapse__', }, /** * Defines if the order of the layers added to the store will be * reversed. The default behaviour and what most users expect is * that mapLayers on top are also on top in the tree. * * @property {boolean} inverseLayerOrder Reverse order of layers in tree */ inverseLayerOrder: true, /** * Whether the treestore currently shall handle openlayers collection * change events. See #suspendCollectionEvents and #resumeCollectionEvents. * * @property {boolean} collectionEventsSuspended Suspend OL collection events * @private */ collectionEventsSuspended: false, /** * @cfg * @inheritdoc */ proxy: { type: 'memory', reader: { type: 'json', }, }, root: { expanded: true, }, /** * Constructs a LayersTree store. */ constructor: function () { const me = this; me.onLayerCollectionRemove = me.onLayerCollectionRemove.bind(me); me.onLayerCollectionAdd = me.onLayerCollectionAdd.bind(me); me.bindGroupLayerCollectionEvents = me.bindGroupLayerCollectionEvents.bind(me); me.unbindGroupLayerCollectionEvents = me.unbindGroupLayerCollectionEvents.bind(me); me.callParent(arguments); const collection = me.layerGroup.getLayers(); Ext.each( collection.getArray(), function (layer) { me.addLayerNode(layer); }, me, me.inverseLayerOrder, ); me.bindGroupLayerCollectionEvents(me.layerGroup); me.on({ remove: me.handleRemove, noderemove: me.handleNodeRemove, nodeappend: me.handleNodeAppend, nodeinsert: me.handleNodeInsert, scope: me, }); }, /** * Applies the #folderToggleMode to the treenodes. * * @param {string} folderToggleMode The folderToggleMode that was set. * @return {string} The folderToggleMode that was set. * @private */ applyFolderToggleMode: function (folderToggleMode) { if (folderToggleMode === 'classic' || folderToggleMode === 'ol3') { const rootNode = this.getRootNode(); if (rootNode) { rootNode.cascadeBy({ before: function (child) { child.set('__toggleMode', folderToggleMode); }, }); } return folderToggleMode; } Ext.raise( 'Invalid folderToggleMode set in ' + this.self.getName() + ': ' + folderToggleMode + "; 'classic' or 'ol3' are valid.", ); }, /** * Listens to the `remove` event and syncs the attached layergroup. * * @param {GeoExt.data.store.LayersTree} store The layer store. * @param {Array<GeoExt.data.model.LayerTreeNode>} records An array of the * removed nodes. * @private */ handleRemove: function (store, records) { const me = this; const keyRemoveOptOut = me.self.KEY_COLLAPSE_REMOVE_OPT_OUT; me.suspendCollectionEvents(); Ext.each(records, function (record) { if (keyRemoveOptOut in record && record[keyRemoveOptOut] === true) { delete record[keyRemoveOptOut]; return; } const layerOrGroup = record.getOlLayer(); if (layerOrGroup instanceof ol.layer.Group) { me.unbindGroupLayerCollectionEvents(layerOrGroup); } let group = GeoExt.util.Layer.findParentGroup( layerOrGroup, me.getLayerGroup(), ); if (!group) { group = me.getLayerGroup(); } if (group) { group.getLayers().remove(layerOrGroup); } }); me.resumeCollectionEvents(); }, /** * Listens to the `noderemove` event. Updates the tree with the current * map state. * * @param {GeoExt.data.model.LayerTreeNode} parentNode The parent node. * @param {GeoExt.data.model.LayerTreeNode} removedNode The removed node. * @private */ handleNodeRemove: function (parentNode, removedNode) { const me = this; let layerOrGroup = removedNode.getOlLayer(); if (!layerOrGroup) { layerOrGroup = me.getLayerGroup(); } if (layerOrGroup instanceof ol.layer.Group) { removedNode.un('beforeexpand', me.onBeforeGroupNodeToggle); removedNode.un('beforecollapse', me.onBeforeGroupNodeToggle); me.unbindGroupLayerCollectionEvents(layerOrGroup); } const group = GeoExt.util.Layer.findParentGroup( layerOrGroup, me.getLayerGroup(), ); if (group) { me.suspendCollectionEvents(); group.getLayers().remove(layerOrGroup); me.resumeCollectionEvents(); } }, /** * Listens to the `nodeappend` event. Updates the tree with the current * map state. * * @param {GeoExt.data.model.LayerTreeNode} parentNode The parent node. * @param {GeoExt.data.model.LayerTreeNode} appendedNode The appended node. * @private */ handleNodeAppend: function (parentNode, appendedNode) { const me = this; let group = parentNode.getOlLayer(); const layer = appendedNode.getOlLayer(); if (!group) { group = me.getLayerGroup(); } // check if the layer is possibly already at the desired index: const layerInGroupIdx = GeoExt.util.Layer.getLayerIndex(layer, group); if (layerInGroupIdx === -1) { me.suspendCollectionEvents(); if (me.inverseLayerOrder) { group.getLayers().insertAt(0, layer); } else { group.getLayers().push(layer); } me.resumeCollectionEvents(); } }, /** * Listens to the `nodeinsert` event. Updates the tree with the current * map state. * * @param {GeoExt.data.model.LayerTreeNode} parentNode The parent node. * @param {GeoExt.data.model.LayerTreeNode} insertedNode The inserted node. * @param {GeoExt.data.model.LayerTreeNode} insertedBefore The node we were * inserted before. * @private */ handleNodeInsert: function (parentNode, insertedNode, insertedBefore) { const me = this; let group = parentNode.getOlLayer(); if (!group) { // can only happen if a node was dragged before the visible root. group = me.getLayerGroup(); } const layer = insertedNode.getOlLayer(); const beforeLayer = insertedBefore.getOlLayer(); const groupLayers = group.getLayers(); const beforeIdx = GeoExt.util.Layer.getLayerIndex(beforeLayer, group); let insertIdx = beforeIdx; if (me.inverseLayerOrder) { insertIdx += 1; } // check if the layer is possibly already at the desired index: const currentLayerInGroupIdx = GeoExt.util.Layer.getLayerIndex( layer, group, ); if ( currentLayerInGroupIdx !== insertIdx && !Ext.Array.contains(groupLayers.getArray(), layer) ) { me.suspendCollectionEvents(); groupLayers.insertAt(insertIdx, layer); me.resumeCollectionEvents(); } }, /** * Adds a layer as a node to the store. It can be an `ol.layer.Base`. * * @param {ol.layer.Base} layerOrGroup The layer or layer group to add. */ addLayerNode: function (layerOrGroup) { const me = this; // 2. get group to which the layer was added const group = GeoExt.util.Layer.findParentGroup( layerOrGroup, me.getLayerGroup(), ); // 3. get index of layer in that group let layerIdx = GeoExt.util.Layer.getLayerIndex(layerOrGroup, group); // 3.1 the index must probably be changed because of inverseLayerOrder // TODO Check if (me.inverseLayerOrder) { const totalInGroup = group.getLayers().getLength(); layerIdx = totalInGroup - layerIdx - 1; } // 4. find the node that represents the group let parentNode; if (group === me.getLayerGroup()) { parentNode = me.getRootNode(); } else { parentNode = me.getRootNode().findChildBy( function (candidate) { return candidate.getOlLayer() === group; }, me, true, ); } if (!parentNode) { return; } // 5. insert a new layer node at the specified index to that node const layerNode = parentNode.insertChild(layerIdx, layerOrGroup); if (layerOrGroup instanceof ol.layer.Group) { // See onBeforeGroupNodeToggle for an explanation why we have this layerNode.on('beforeexpand', me.onBeforeGroupNodeToggle, me); layerNode.on('beforecollapse', me.onBeforeGroupNodeToggle, me); const childLayers = layerOrGroup.getLayers().getArray(); Ext.each(childLayers, me.addLayerNode, me, me.inverseLayerOrder); } }, /** * Bound as an eventlistener for layer nodes which are a folder / group on * the beforecollapse event. Whenever a folder gets collapsed, ExtJS seems * to actually remove the children from the store, triggering the removal * of the actual layers in the map. This is an undesired behaviour. We * handle this as follows: Before the collapsing happens, we mark the * childNodes, so we effectively opt-out in #handleRemove. * * @param {Ext.data.NodeInterface} node The collapsible folder node. * @private */ onBeforeGroupNodeToggle: function (node) { const keyRemoveOptOut = this.self.KEY_COLLAPSE_REMOVE_OPT_OUT; node.cascadeBy(function (child) { child[keyRemoveOptOut] = true; }); }, /** * A utility method which binds collection change events to the passed layer * if it is a `ol.layer.Group`. * * @param {ol.layer.Base} layerOrGroup The layer to probably bind event * listeners for collection change events to. * @private */ bindGroupLayerCollectionEvents: function (layerOrGroup) { const me = this; if (layerOrGroup instanceof ol.layer.Group) { const collection = layerOrGroup.getLayers(); collection.on('remove', me.onLayerCollectionRemove); collection.on('add', me.onLayerCollectionAdd); collection.forEach(me.bindGroupLayerCollectionEvents); } }, /** * A utility method which unbinds collection change events from the passed * layer if it is a `ol.layer.Group`. * * @param {ol.layer.Base} layerOrGroup The layer to probably unbind event * listeners for collection change events from. * @private */ unbindGroupLayerCollectionEvents: function (layerOrGroup) { const me = this; if (layerOrGroup instanceof ol.layer.Group) { const collection = layerOrGroup.getLayers(); collection.un('remove', me.onLayerCollectionRemove); collection.un('add', me.onLayerCollectionAdd); collection.forEach(me.unbindGroupLayerCollectionEvents); } }, /** * Handles the `add` event of a managed `ol.layer.Group` and eventually * removes the appropriate node. * * @param {ol.CollectionEvent} evt The event object holding a reference to * the relevant `ol.layer.Base`. * @private */ onLayerCollectionAdd: function (evt) { const me = this; if (me.collectionEventsSuspended) { return; } const layerOrGroup = evt.element; me.addLayerNode(layerOrGroup); me.bindGroupLayerCollectionEvents(layerOrGroup); }, /** * Handles the `remove` event of a managed `ol.layer.Group` and eventually * removes the appropriate node. * * @param {ol.CollectionEvent} evt The event object holding a reference to * the relevant `ol.layer.Base`. * @private */ onLayerCollectionRemove: function (evt) { const me = this; if (me.collectionEventsSuspended) { return; } const layerOrGroup = evt.element; // 1. find the node that existed for that layer const node = me.getRootNode().findChildBy( function (candidate) { return candidate.getOlLayer() === layerOrGroup; }, me, true, ); if (!node) { return; } // 2. if grouplayer: cascade down and remove any possible listeners if (layerOrGroup instanceof ol.layer.Group) { me.unbindGroupLayerCollectionEvents(layerOrGroup); } // 3. find the parent const parent = node.parentNode; // 4. remove the node from the parent parent.removeChild(node); }, /** * Allows for temporarily unlistening to change events on the underlying * OpenLayers collections. Use #resumeCollectionEvents to start listening * again. */ suspendCollectionEvents: function () { this.collectionEventsSuspended = true; }, /** * Undoes the effect of #suspendCollectionEvents; so that the store is now * listening to change events on the underlying OpenLayers collections * again. */ resumeCollectionEvents: function () { this.collectionEventsSuspended = false; }, });