/* 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 mixin for selection model which enables automatic selection of features
* in the map when rows are selected in the grid and vice-versa.
*
* **CAUTION: This class is only usable in applications using the classic
* toolkit of ExtJS 6.**
*
* @class GeoExt.selection.FeatureModelMixin
*/
Ext.define('GeoExt.selection.FeatureModelMixin', {
extend: 'Ext.Mixin',
mixinConfig: {
after: {
bindComponent: 'bindFeatureModel',
},
before: {
destroy: 'unbindOlEvents',
constructor: 'onConstruct',
onSelectChange: 'beforeSelectChange',
},
},
config: {
/**
* The connected vector layer.
* @cfg {ol.layer.Vector} layer The connected vector layer.
* @property {ol.layer.Vector} layer The connected vector layer.
*/
layer: null,
/**
* The OpenLayers map we work with
* @cfg {ol.Map}
* @type {ol.Map}
*/
map: null,
/**
* Set to true to create a click handler on the map selecting a clicked
* object in the #layer.
* @cfg {boolean}
*/
mapSelection: false,
/**
* Set a pixel tolerance for the map selection. Defaults to 12.
*/
selectionTolerance: 12,
/**
* The default style for the selected features.
* @cfg {ol.style.Style}
*/
selectStyle: new ol.style.Style({
image: new ol.style.Circle({
radius: 6,
fill: new ol.style.Fill({
color: 'rgba(255,255,255,0.8)',
}),
stroke: new ol.style.Stroke({
color: 'darkblue',
width: 2,
}),
}),
fill: new ol.style.Fill({
color: 'rgba(255,255,255,0.8)',
}),
stroke: new ol.style.Stroke({
color: 'darkblue',
width: 2,
}),
}),
},
/**
* Lookup to preserve existing feature styles. Used to restore feature style
* when select style is removed.
* @private
* @property {Object} existingFeatStyles Lookup to preserve existing feature styles
*/
existingFeatStyles: {},
/**
* Indicates if a map click handler has been registered on init.
* @private
* @property {boolean} mapClickRegistered Indicates if a map click handler has been registered
*/
mapClickRegistered: false,
/**
* The attribute key to mark an OL feature as selected.
* @cfg {string}
* @property {string} selectedFeatureAttr The attribute key to mark as selected
* @readonly
*/
selectedFeatureAttr: 'gx_selected',
/**
* The currently selected features (`ol.Collection` containing `ol.Feature`
* instances).
* @property {ol.Collection} selectedFeatures Currently selected features
*/
selectedFeatures: null,
onConstruct: function () {
const me = this;
me.onSelectFeatAdd = me.onSelectFeatAdd.bind(me);
me.onSelectFeatRemove = me.onSelectFeatRemove.bind(me);
me.onFeatureClick = me.onFeatureClick.bind(me);
},
/**
* Prepare several connected objects once the selection model is ready.
*
* @private
*/
bindFeatureModel: function () {
const me = this;
// detect a layer from the store if not passed in
if (!me.layer || !(me.layer instanceof ol.layer.Vector)) {
const store = me.getStore();
if (
store &&
store.getLayer &&
store.getLayer() &&
store.getLayer() instanceof ol.layer.Vector
) {
me.layer = store.getLayer();
}
}
// bind several OL events since this is not called while destroying
me.bindOlEvents();
},
/**
* Binds several events on the OL objects used in this class.
*
* @private
*/
bindOlEvents: function () {
if (!this.bound_) {
const me = this;
me.selectedFeatures = new ol.Collection();
// change style of selected feature
me.selectedFeatures.on('add', me.onSelectFeatAdd);
// reset style of no more selected feature
me.selectedFeatures.on('remove', me.onSelectFeatRemove);
// create a map click listener for connected vector layer
if (me.mapSelection && me.layer && me.map) {
me.map.on('singleclick', me.onFeatureClick);
me.mapClickRegistered = true;
}
this.bound_ = true;
}
},
/**
* Unbinds several events that were registered on the OL objects in this
* class (see #bindOlEvents).
*
* @private
*/
unbindOlEvents: function () {
const me = this;
// remove 'add' / 'remove' listener from selected feature collection
if (me.selectedFeatures) {
me.selectedFeatures.un('add', me.onSelectFeatAdd);
me.selectedFeatures.un('remove', me.onSelectFeatRemove);
}
// remove 'singleclick' listener for connected vector layer
if (me.mapClickRegistered) {
me.map.un('singleclick', me.onFeatureClick);
me.mapClickRegistered = false;
}
},
/**
* Handles 'add' event of #selectedFeatures.
* Ensures that added feature gets the #selectStyle and preserves an
* possibly existing feature style.
*
* @private
* @param {ol.Collection.Event} evt OL event object
*/
onSelectFeatAdd: function (evt) {
const me = this;
const feat = evt.element;
if (feat) {
if (feat.getStyle()) {
// we have to preserve the existing feature style
const fid = feat.getId() || me.getRandomFid();
me.existingFeatStyles[fid] = feat.getStyle();
feat.setId(fid);
}
// apply select style
feat.setStyle(me.selectStyle);
}
},
/**
* Handles 'remove' event of #selectedFeatures.
* Ensures that the #selectStyle is reset on the removed feature.
*
* @private
* @param {ol.Collection.Event} evt OL event object
*/
onSelectFeatRemove: function (evt) {
const me = this;
const feat = evt.element;
if (feat) {
const fid = feat.getId();
if (fid && me.existingFeatStyles[fid]) {
// restore existing feature style
feat.setStyle(me.existingFeatStyles[fid]);
delete me.existingFeatStyles[fid];
} else {
// reset feature style, so layer style gets active
feat.setStyle();
}
}
},
/**
* Handles the 'singleclick' event of the #map.
* Detects if a feature of the connected #layer has been clicked and selects
* this feature by selecting its corresponding grid row.
*
* @private
* @param {ol.MapBrowserEvent} evt OL event object
*/
onFeatureClick: function (evt) {
const me = this;
const feat = me.map.forEachFeatureAtPixel(
evt.pixel,
function (feature) {
return feature;
},
{
layerFilter: function (layer) {
return layer === me.layer;
},
hitTolerance: me.selectionTolerance,
},
);
if (feat) {
// select clicked feature in grid
me.selectMapFeature(feat);
}
},
/**
* Selects / deselects a feature by triggering the corresponding actions in
* the grid (e.g. selecting / deselecting a grid row).
*
* @private
* @param {ol.Feature} feature The feature to select
*/
selectMapFeature: function (feature) {
const me = this;
const row = me.store.findBy(function (record, id) {
return record.getFeature() === feature;
});
// deselect all if only one can be selected at a time
if (me.getSelectionMode() === 'SINGLE') {
me.deselectAll();
}
if (feature.get(me.selectedFeatureAttr)) {
// deselect feature by deselecting grid row
me.deselect(row);
} else {
// select the feature by selecting grid row
if (row !== -1 && !me.isSelected(row)) {
me.select(row, !this.singleSelect);
// focus the row in the grid to ensure it is visible
me.view.focusRow(row);
}
}
},
/**
* Is called before the onSelectChange function of the parent class.
* Ensures that the selected feature is added / removed to / from
* #selectedFeatures lookup object.
*
* @private
* @param {GeoExt.data.model.Feature} record Selected / deselected record
* @param {boolean} isSelected Record is selected or deselected
*/
beforeSelectChange: function (record, isSelected) {
const me = this;
const selFeature = record.getFeature();
// toggle feature's selection state
const silent = true;
selFeature.set(me.selectedFeatureAttr, isSelected, silent);
if (isSelected) {
me.selectedFeatures.push(selFeature);
} else {
me.selectedFeatures.remove(selFeature);
}
},
/**
* Returns a random feature ID.
*
* @private
* @return {string} Random feature ID
*/
getRandomFid: function () {
// current timestamp plus a random int between 0 and 10
return new Date().getTime() + '' + Math.floor(Math.random() * 11);
},
});