/* 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 synchronizes a collection of layers (e.g. of an OpenLayers.Map)
* with a layer store holding GeoExt.data.model.Layer instances.
*
* @class GeoExt.data.store.Layers
*/
Ext.define('GeoExt.data.store.Layers', {
extend: 'Ext.data.Store',
alternateClassName: ['GeoExt.data.LayerStore'],
requires: ['GeoExt.data.model.Layer'],
mixins: ['GeoExt.mixin.SymbolCheck'],
// <debug>
symbols: [
'ol.Collection#clear',
'ol.Collection#forEach',
'ol.Collection#getArray',
'ol.Collection#insertAt',
'ol.Collection#push',
'ol.Collection#remove',
'ol.layer.Layer',
'ol.layer.Layer#get',
'ol.layer.Layer#set',
'ol.Map',
'ol.Map#getLayers',
],
// </debug>
model: 'GeoExt.data.model.Layer',
config: {
/**
* An OL map instance, whose layers will be managed by the store.
*
* @cfg {ol.Map} map
*/
map: null,
/**
* A collection of ol.layer.Base objects, which will be managed by
* the store.
*
* @cfg {ol.Collection} layers
*/
layers: null,
/**
* An optional function called to filter records used in changeLayer
* function
*
* @cfg {Function} changeLayerFilterFn
*/
changeLayerFilterFn: null,
},
/**
* Constructs an instance of the layer store.
*
* @param {Object} config The configuration object.
*/
constructor: function (config) {
const me = this;
me.onAddLayer = me.onAddLayer.bind(me);
me.onRemoveLayer = me.onRemoveLayer.bind(me);
me.onChangeLayer = me.onChangeLayer.bind(me);
me.callParent([config]);
if (config.map) {
this.bindMap(config.map);
} else if (config.layers) {
this.bindLayers(config.layers);
}
},
/**
* Bind this store to a collection of layers; once bound, the store is
* synchronized with the layer collection and vice-versa.
*
* @param {ol.Collection} layers The layer collection (`ol.layer.Base`).
* @param {ol.Map} map Optional map from which the layers were derived
*/
bindLayers: function (layers, map) {
const me = this;
if (!me.layers) {
me.layers = layers;
}
if (me.layers instanceof ol.layer.Group) {
me.layers = me.layers.getLayers();
}
const mapLayers = me.layers;
mapLayers.forEach(function (layer) {
me.loadRawData(layer, true);
});
mapLayers.forEach(function (layer) {
me.bindLayer(layer, me.getByLayer(layer));
});
mapLayers.on('add', me.onAddLayer);
mapLayers.on('remove', me.onRemoveLayer);
me.on({
load: me.onLoad,
clear: me.onClear,
add: me.onAdd,
remove: me.onRemove,
update: me.onStoreUpdate,
scope: me,
});
me.data.on({
replace: me.onReplace,
scope: me,
});
me.fireEvent('bind', me, map);
},
/**
* Bind this store to a map instance; once bound, the store is synchronized
* with the map and vice-versa.
*
* @param {ol.Map} map The map instance.
*/
bindMap: function (map) {
const me = this;
if (!me.map) {
me.map = map;
}
if (map instanceof ol.Map) {
const mapLayers = map.getLayers();
me.bindLayers(mapLayers, map);
}
},
/**
* Bind the layer to the record and initialize synchronized values.
*
* @param {ol.layer.Base} layer The layer.
* @param {Ext.data.Model} record The record, if not set it will be
* searched for.
*/
bindLayer: function (layer, record) {
const me = this;
layer.on('propertychange', me.onChangeLayer);
Ext.Array.forEach(record.synchronizedProperties, function (prop) {
me.synchronize(record, layer, prop);
});
},
/**
* Unbind this store from the layer collection it is currently bound.
*/
unbindLayers: function () {
const me = this;
if (me.layers) {
me.layers.un('add', me.onAddLayer);
me.layers.un('remove', me.onRemoveLayer);
}
me.un('load', me.onLoad, me);
me.un('clear', me.onClear, me);
me.un('add', me.onAdd, me);
me.un('remove', me.onRemove, me);
me.un('update', me.onStoreUpdate, me);
me.data.un('replace', me.onReplace, me);
},
/**
* Unbind this store from the map it is currently bound.
*/
unbindMap: function () {
const me = this;
me.unbindLayers();
me.map = null;
},
/**
* Handler for layer changes. When layer order changes, this moves the
* appropriate record within the store.
*
* @param {ol.ObjectEvent} evt The emitted `ol.Object` event.
* @private
*/
onChangeLayer: function (evt) {
const layer = evt.target;
const filter = this.changeLayerFilterFn
? this.changeLayerFilterFn.bind(layer)
: undefined;
const record = this.getByLayer(layer, filter);
if (record !== undefined) {
if (evt.key === 'description') {
record.set('qtip', layer.get('description'));
if (record.synchronizedProperties.indexOf('description') > -1) {
this.synchronize(record, layer, 'description');
}
} else if (record.synchronizedProperties.indexOf(evt.key) > -1) {
this.synchronize(record, layer, evt.key);
} else {
this.fireEvent('update', this, record, Ext.data.Record.EDIT, null, {});
}
}
},
/**
* Handler for a layer collection's `add` event.
*
* @param {ol.CollectionEvent} evt The emitted `ol.Collection` event.
* @private
*/
onAddLayer: function (evt) {
const layer = evt.element;
const index = this.layers.getArray().indexOf(layer);
const me = this;
if (!me._adding) {
me._adding = true;
const result = me.proxy.reader.read(layer);
me.insert(index, result.records);
delete me._adding;
}
me.bindLayer(layer, me.getByLayer(layer));
},
/**
* Handler for layer collection's `remove` event.
*
* @param {ol.CollectionEvent} evt The emitted `ol.Collection` event.
* @private
*/
onRemoveLayer: function (evt) {
const me = this;
if (!me._removing) {
const layer = evt.element;
const rec = me.getByLayer(layer);
if (rec) {
me._removing = true;
layer.un('propertychange', me.onChangeLayer);
me.remove(rec);
delete me._removing;
}
}
},
/**
* Handler for a store's `load` event.
*
* @param {Ext.data.Store} store The store that loaded.
* @param {Ext.data.Model | Array<Ext.data.Model>} records An array of loaded model
* instances.
* @param {boolean} successful Whether loading was successful or not.
* @private
*/
onLoad: function (store, records, successful) {
const me = this;
if (successful) {
if (!Ext.isArray(records)) {
records = [records];
}
if (!me._addRecords) {
me._removing = true;
me.layers.forEach(function (layer) {
layer.un('propertychange', me.onChangeLayer);
});
me.layers.getLayers().clear();
delete me._removing;
}
const len = records.length;
if (len > 0) {
const layers = new Array(len);
for (let i = 0; i < len; i++) {
const record = records[i];
layers[i] = record.getOlLayer();
me.bindLayer(layers[i], record);
}
me._adding = true;
me.layers.extend(layers);
delete me._adding;
}
}
delete me._addRecords;
},
/**
* Handler for a store's `clear` event.
*
* @private
*/
onClear: function () {
const me = this;
me._removing = true;
me.layers.forEach(function (layer) {
layer.un('propertychange', me.onChangeLayer);
});
me.layers.clear();
delete me._removing;
},
/**
* Handler for a store's `add` event.
*
* @param {Ext.data.Store} store The store to which a model instance was
* added.
* @param {Array<Ext.data.Model>} records The array of model instances that were
* added.
* @param {number} index The index at which the model instances were added.
* @private
*/
onAdd: function (store, records, index) {
const me = this;
if (!me._adding) {
me._adding = true;
let layer;
for (let i = 0, ii = records.length; i < ii; ++i) {
layer = records[i].getOlLayer();
me.bindLayer(layer, records[i]);
if (index === 0) {
me.layers.push(layer);
} else {
me.layers.insertAt(index, layer);
}
}
delete me._adding;
}
},
/**
* Handler for a store's `remove` event.
*
* @param {Ext.data.Store} store The store from which a model instances
* were removed.
* @param {Array<Ext.data.Model>} records The array of model instances that were
* removed.
* @private
*/
onRemove: function (store, records) {
const me = this;
let record;
let layer;
let found;
let i;
let ii;
if (!me._removing) {
const compareFunc = function (el) {
if (el === layer) {
found = true;
}
};
for (i = 0, ii = records.length; i < ii; ++i) {
record = records[i];
layer = record.getOlLayer();
found = false;
layer.un('propertychange', me.onChangeLayer);
me.layers.forEach(compareFunc);
if (found) {
me._removing = true;
me.removeMapLayer(record);
delete me._removing;
}
}
}
},
/**
* Handler for a store's `update` event.
*
* @param {Ext.data.Store} store The store which was updated.
* @param {Ext.data.Model} record The updated model instance.
* @param {string} operation The operation, either Ext.data.Model.EDIT,
* Ext.data.Model.REJECT or Ext.data.Model.COMMIT.
* @param {Array<string> | null} modifiedFieldNames The fieldnames that were
* modified in this operation.
* @private
*/
onStoreUpdate: function (store, record, operation, modifiedFieldNames) {
const me = this;
if (operation === Ext.data.Record.EDIT) {
if (modifiedFieldNames) {
const layer = record.getOlLayer();
Ext.Array.forEach(modifiedFieldNames, function (prop) {
if (record.synchronizedProperties.indexOf(prop) > -1) {
me.synchronize(layer, record, prop);
}
});
}
}
},
/**
* Removes a record's layer from the bound map.
*
* @param {Ext.data.Model} record The removed model instance.
* @private
*/
removeMapLayer: function (record) {
this.layers.remove(record.getOlLayer());
},
/**
* Handler for a store's data collections' `replace` event.
*
* @param {string} key The associated key.
* @param {Ext.data.Model} oldRecord In this case, a record that has
* been replaced.
* @private
*/
onReplace: function (key, oldRecord) {
this.removeMapLayer(oldRecord);
},
/**
* Get the record for the specified layer.
*
* @param {ol.layer.Base} layer The layer to get a model instance for.
* @param {function(Ext.data.Model): boolean} [filterFn] A filter function
* @return {Ext.data.Model} The corresponding model instance or undefined if
* not found.
*/
getByLayer: function (layer, filterFn) {
const me = this;
let index;
if (me.getData()) {
if (Ext.isFunction(filterFn)) {
index = me.findBy(filterFn);
} else {
index = me.findBy(function (rec) {
return rec.getOlLayer() === layer;
});
}
if (index > -1) {
return me.getAt(index);
}
}
},
/**
* Unbinds listeners by calling #unbindMap (thus #unbindLayers) prior to
* being destroyed.
*
* @private
*/
destroy: function () {
// unbindMap calls unbindLayers
this.unbindMap();
this.callParent();
},
/**
* Overload loadRecords to set a flag if `addRecords` is `true` in the load
* options. ExtJS does not pass the load options to "load" callbacks, so
* this is how we provide that information to `onLoad`.
*
* @param {Array<Ext.data.Model>} records The array of records to load.
* @param {Object} options The loading options.
* @param {boolean} [options.addRecords=false] Pass `true` to add these
* records to the existing records, `false` to remove the Store's
* existing records first.
* @private
*/
loadRecords: function (records, options) {
if (options && options.addRecords) {
this._addRecords = true;
}
this.callParent(arguments);
},
/**
* The event firing behaviour of Ext.4.1 is reestablished here. See also:
* [This discussion on the Sencha forum](http://www.sencha.com/forum/
* showthread.php?253596-beforeload-is-not-fired-by-loadRawData).
*
* @inheritdoc
*/
loadRawData: function (data, append) {
const me = this;
const result = me.proxy.reader.read(data);
const records = result.records;
if (result.success) {
me.totalCount = result.total;
me.loadRecords(records, append ? me.addRecordsOptions : undefined);
me.fireEvent('load', me, records, true);
}
},
/**
* This function synchronizes a value, but only sets it if it is different.
* @param {Ext.data.Model|ol.layer.Base} destination The destination.
* @param {Ext.data.Model|ol.layer.Base} source The source.
* @param {string} prop The property that should get synchronized.
*/
synchronize: function (destination, source, prop) {
const value = source.get(prop);
if (value !== destination.get(prop)) {
destination.set(prop, value);
}
},
});