diff --git a/src/app/core/editing/editor.js b/src/app/core/editing/editor.js index 1124a3046..141c9f0cc 100644 --- a/src/app/core/editing/editor.js +++ b/src/app/core/editing/editor.js @@ -1,68 +1,104 @@ import Applicationstate from 'store/application-state'; -import ChangesManager from 'services/editing'; +import ChangesManager from 'services/editing'; import SessionsRegistry from 'store/sessions'; const { inherit, base } = require('core/utils/utils'); -const G3WObject = require('core/g3wobject'); -const FeaturesStore = require('core/layers/features/featuresstore'); -const OlFeaturesStore = require('core/layers/features/olfeaturesstore'); -const Layer = require('core/layers/layer'); +const G3WObject = require('core/g3wobject'); +const FeaturesStore = require('core/layers/features/featuresstore'); +const OlFeaturesStore = require('core/layers/features/olfeaturesstore'); +const Layer = require('core/layers/layer'); -// class Editor bind editor to layer to do main actions -function Editor(options={}) { +/** + * Editor Class: bind editor to layer to do main actions + * + * @param config + * + * @constructor + */ +function Editor(options = {}) { + + /** + * Setter hooks. + */ this.setters = { - save() { - this._save(); - }, - addFeature(feature) { - this._addFeature(feature); - }, - updateFeature(feature) { - this._updateFeature(feature); - }, - deleteFeature(feature) { - this._deleteFeature(feature); - }, - setFeatures(features=[]) { - this._setFeatures(features); - }, - getFeatures(options={}) { - return this._getFeatures(options); - } + save: this._save, + addFeature: this._addFeature, + updateFeature: this._updateFeature, + deleteFeature: this._deleteFeature, + setFeatures: this._setFeatures, + getFeatures: this._getFeatures, }; + base(this); - // filter to getFeaturerequest + + /** + * Filter to getFeaturerequest + */ this._filter = { bbox: null }; + /** + * @FIXME add description + */ this._allfeatures = false; - // referred layer + + /** + * Referred layer + */ this._layer = options.layer; - // editing featurestore + + /** + * Store editing features + */ this._featuresstore = this._layer.getType() === Layer.LayerTypes.TABLE ? new FeaturesStore() : new OlFeaturesStore(); - // editor is active or not + + /** + * Whether editor is active or not + * + * @type { boolean } + */ this._started = false; - // not editable fields + + /** + * Not editable fields + */ this._noteditablefileds = this._layer.getEditingNotEditableFields() || []; + } inherit(Editor, G3WObject); const proto = Editor.prototype; -proto._canDoGetFeaturesRequest = function(options={}) { - let doRequest = true; - if (this._layer.getType() === Layer.LayerTypes.VECTOR) { - const {bbox} = options.filter || {}; - if (bbox) { - if (!this._filter.bbox) this._filter.bbox = bbox; - else if (!ol.extent.containsExtent(this._filter.bbox, bbox)) { - this._filter.bbox = ol.extent.extend(this._filter.bbox, bbox); - } else doRequest = false; - } +/** + * Used when vector Layer's bbox is contained into an already requested bbox (so no a new request is done). + * + * @param { number[] } options.filter.bbox bounding box Array [xmin, ymin, xmax, ymax] + * + * @returns { boolean } whether can perform a server request + * + * @private + */ +proto._canDoGetFeaturesRequest = function(options = {}) { + const { bbox } = options.filter || {}; + const is_vector = bbox && Layer.LayerTypes.VECTOR === this._layer.getType(); + + // first request --> need to peform request + if (is_vector && null === this._filter.bbox) { + this._filter.bbox = bbox; // store bbox + return true; } - return doRequest + + // subsequent requests --> check if bbox is contained into an already requested bbox + if (is_vector) { + const is_cached = ol.extent.containsExtent(this._filter.bbox, bbox); + if (!is_cached) this._filter.bbox = ol.extent.extend(this._filter.bbox, bbox); // extend bbox + return is_cached; + } + + // default --> perform request + return true; }; proto.getEditingSource = function() { @@ -76,7 +112,7 @@ proto.getSource = function() { this._layer.getSource(); }; -proto._applyChanges = function(items=[], reverse=true) { +proto._applyChanges = function(items = [], reverse=true) { ChangesManager.execute(this._featuresstore, items, reverse); }; @@ -93,48 +129,61 @@ proto.setLayer = function(layer) { return this._layer; }; -proto.removeNotEditablePropriertiesFromFeature = function(feature){ +proto.removeNotEditablePropriertiesFromFeature = function(feature) { this._noteditablefileds.forEach(field => feature.unset([field])); }; -//clone features method -proto._cloneFeatures = function(features=[]) { - return features.map(feature => feature.clone()); +/** + * @param features features to be cloned + */ +proto._cloneFeatures = function(features = []) { + return features.map(f => f.clone()); }; -proto._addFeaturesFromServer = function(features=[]){ +proto._addFeaturesFromServer = function(features = []) { features = this._cloneFeatures(features); this._featuresstore.addFeatures(features); }; proto._doGetFeaturesRequest = function(options={}) { - const doRequest = Applicationstate.online && !this._allfeatures; - return doRequest && this._canDoGetFeaturesRequest(options) + if (Applicationstate.online && !this._allfeatures) { + return this._canDoGetFeaturesRequest(options); + } + return false; }; -// get features from server method +/** + * get features from server method + */ proto._getFeatures = function(options={}) { - const d = $.Deferred(); + const d = $.Deferred(); const doRequest = this._doGetFeaturesRequest(options); - if (!doRequest) d.resolve(); - else - this._layer.getFeatures(options) - .then(promise => { - promise.then(features => { - this._addFeaturesFromServer(features); - this._allfeatures = !options.filter; - return d.resolve(features); - }).fail(err => d.reject(err)) + if (!doRequest) { + d.resolve(); + } else{ + /** @TODO simplfy nested promises */ + this._layer + .getFeatures(options) + .then(p => { + p + .then(features => { + this._addFeaturesFromServer(features); + this._allfeatures = !options.filter; + return d.resolve(features); + }) + .fail(d.reject) }) - .fail(err => d.reject(err)); + .fail(d.reject); + } return d.promise(); }; -// method to revert (cancel) all changes in history and clean session +/** + * revert (cancel) all changes in history and clean session + */ proto.revert = function() { const d = $.Deferred(); - const features = this._cloneFeatures(this._layer.readFeatures()); - this._featuresstore.setFeatures(features); + this._featuresstore.setFeatures(this._cloneFeatures(this._layer.readFeatures())); d.resolve(); return d.promise(); }; @@ -146,154 +195,248 @@ proto.rollback = function(changes=[]) { return d.promise() }; -proto.applyChangesToNewRelationsAfterCommit = function(relationsResponse) { - for (const relationLayerId in relationsResponse) { - const response = relationsResponse[relationLayerId]; - const layer = this.getLayerById(relationLayerId); - const editingLayerSource = this.getEditingLayer(relationLayerId).getEditingSource(); - const features = editingLayerSource.readFeatures(); - features.forEach(feature => feature.clearState()); +/** + * + * @param relations relations response + */ +proto.applyChangesToNewRelationsAfterCommit = function(relations) { + let layer, source, features; + for (const id in relations) { + layer = this.getLayerById(id); + source = this.getEditingLayer(id).getEditingSource(); + features = source.readFeatures(); + features.forEach(f => f.clearState()); layer.getSource().setFeatures(features); layer.applyCommitResponse({ - response, - result: true + response: relations[id], + result: true, }); - editingLayerSource.setFeatures(layer.getSource().readFeatures()); + source.setFeatures(layer.getSource().readFeatures()); } }; -proto.setFieldValueToRelationField = function({relationId, ids, field, values=[]}={}){ - const editingLayerSource = SessionsRegistry.getSession(relationId).getEditor().getEditingSource(); - ids.forEach(id => { - const feature = editingLayerSource.getFeatureById(id); - if (feature) { - const fieldvalue = feature.get(field); - fieldvalue == values[0] && feature.set(field, values[1]); +/** + * @param opts.relationId + * @param opts.ids + * @param opts.field + * @param opts.values + */ +proto.setFieldValueToRelationField = function({ + relationId, + ids, + field, + values = [] +} = {}) { + const source = SessionsRegistry // get source of editing relation layer. + .getSession(relationId) + .getEditor() + .getEditingSource(); + + ids.forEach(id => { // get relation feature by id. + const feature = source.getFeatureById(id); + if (feature && feature.get(field) == values[0]) { // check field value. + feature.set(field, values[1]); } - }) + }); }; -// apply response data from server in case of new inserted feature -proto.applyCommitResponse = function(response={}, relations=[]) { - if (response && response.result) { - const {response:data} = response; - const ids = data.new; - const lockids = data.new_lockids; - ids.forEach(idobj => { - const feature = this._featuresstore.getFeatureById(idobj.clientid); - feature.setId(idobj.id); - feature.setProperties(idobj.properties); - relations.forEach(relation =>{ - Object.entries(relation).forEach(([relationId, options]) => { - const value = feature.get(options.fatherField); - this.setFieldValueToRelationField({ - relationId, - ids: options.ids, - field: options.childField, - values:[idobj.clientid, value] - }) - }) - }) - }); - const features = this._featuresstore.readFeatures(); - features.forEach(feature => feature.clearState()); - this._layer.setFeatures(features); - this._layer.getSource().addLockIds(lockids); + +/** + * Apply response data from server in case of new inserted feature + * + * @param response + * @param relations + */ +proto.applyCommitResponse = function(response = {}, relations = []) { + + // skip when no response and response.result is false + if (!(response && response.result)) { + return; } + + const ids = response.response.new; // get ids from new attribute of response + const lockids = response.response.new_lockids; // get new lockId + + ids.forEach(({ + clientid, // temporary id created by client __new__ + id, // the new id created and stored on server + properties // properties of the feature saved on server + } = {}) => { + + const feature = this._featuresstore.getFeatureById(clientid); + + feature.setId(id); // set new id + feature.setProperties(properties); + + relations.forEach(relation => { // handle relations (if provided) + Object + .entries(relation) + .forEach(([ relationId, options = {}]) => { + const is_pk = options.fatherField.find(d => this._layer.isPkField(d)); // check if parent field is a Primary Key + if (is_pk) { + this.setFieldValueToRelationField({ // for each field + relationId, // relation layer id + ids: options.ids, // ids of features of relation layers to check + field: options.childField[options.fatherField.indexOf(is_pk)], // relation field to overwrite + values: [clientid, id] // [, ] + }); + } + }); + }); + + }); + + const features = this.readEditingFeatures(); + + features.forEach(f => f.clearState()); // reset state of the editing features (update, new etc..) + + this._layer.setFeatures(features); // substitute layer features with actual editing features + + this.addLockIds(lockids); // add lockIds }; -proto.getLockIds = function(){ +/** + * @param lockids locks be added to current layer + * + * @since 3.9.0 + */ +proto.addLockIds = function(lockids) { + this._layer.getSource().addLockIds(lockids); +} + +/** + * @returns {*} + */ +proto.getLockIds = function() { return this._layer.getSource().getLockIds(); }; -// run after server apply changed to origin resource -proto.commit = function(commitItems) { +/** + * Run after server has applied changes to origin resource + * + * @param commit commit items + * + * @returns jQuery promise + */ +proto.commit = function(commit) { + const d = $.Deferred(); - // in case of relations bind to new feature - const relations = commitItems.add.length ? Object.keys(commitItems.relations).map(relationId => { - const layerRelation = this._layer.getRelations().getRelationByFatherChildren(this._layer.getId(), relationId); - const updates = commitItems.relations[relationId].update.map(relation => relation.id); - const add = commitItems.relations[relationId].add.map(relation => relation.id); - return { - [relationId]:{ - ids: [...add, ...updates], - fatherField: layerRelation.getFatherField(), - childField: layerRelation.getChildField() - } - } - }) : []; - this._layer.commit(commitItems) - .then(promise => { - promise - .then(response => { - this.applyCommitResponse(response, relations); - d.resolve(response); - }) - .fail(err => d.reject(err)) + + let relations = []; + + // check if there are commit relations binded to new feature + if (commit.add.length) { + relations = + Object + .keys(commit.relations) + .map(relationId => { + const relation = this._layer.getRelations().getRelationByFatherChildren(this._layer.getId(), relationId); + return { + [relationId]: { + ids: [ // ids of "added" or "updated" relations + ...commit.relations[relationId].add.map(r => r.id), // added + ...commit.relations[relationId].update.map(r => r.id) // updated + ], + fatherField: relation.getFatherField(), //father Fields + childField: relation.getChildField() //child Fields + } + }; + }); + } + + /** @TODO simplfy nested promises */ + this._layer + .commit(commit) + .then(p => { + p + .then(r => { this.applyCommitResponse(r, relations); d.resolve(r); }) + .fail(e => d.reject(e)) }) - .fail(err => d.reject(err)); + .fail(e => d.reject(e)); + return d.promise(); }; -//start editing function -proto.start = function(options={}) { +/** + * start editing + */ +proto.start = function(options = {}) { const d = $.Deferred(); - // load features of layer based on filter type - this.getFeatures(options) - .then(promise => { - promise + + /** @TODO simplfy nested promises */ + this + .getFeatures(options) // load layer features based on filter type + .then(p => { + p .then(features => { - // the features are already inside featuresstore - d.resolve(features); - //if all ok set to started - this._started = true; + d.resolve(features); // features are already inside featuresstore + this._started = true; // if all ok set to started }) - .fail(err => d.reject(err)) + .fail(d.reject) }) - .fail(err => d.reject(err)); + .fail(d.reject); + return d.promise() }; -//action to layer - +/** + * Add feature (action to layer) + */ proto._addFeature = function(feature) { this._featuresstore.addFeature(feature); }; +/** + * Delete feature (action to layer) + */ proto._deleteFeature = function(feature) { this._featuresstore.deleteFeature(feature); }; +/** + * Update feature (action to layer) + */ proto._updateFeature = function(feature) { this._featuresstore.updateFeature(feature); }; -proto._setFeatures = function(features) { +/** + * Set features (action to layer) + */ +proto._setFeatures = function(features = []) { this._featuresstore.setFeatures(features); }; -proto.readFeatures = function(){ +/** + * Read features (action to layer) + */ +proto.readFeatures = function() { return this._layer.readFeatures(); }; -proto.readEditingFeatures = function(){ +/** + * @returns features stored in editor featurestore + */ +proto.readEditingFeatures = function() { return this._featuresstore.readFeatures() }; -// stop editor +/** + * stop editor + */ proto.stop = function() { const d = $.Deferred(); - this._layer.unlock() - .then(response => { - this.clear(); - d.resolve(response); - }) - .fail(err => d.reject(err)); + this._layer + .unlock() + .then(response => { this.clear(); d.resolve(response); }) + .fail(d.reject); return d.promise(); }; -//run save layer +/** + * run save layer + */ proto._save = function() { this._layer.save(); }; @@ -303,13 +446,17 @@ proto.isStarted = function() { }; proto.clear = function() { - this._started = false; + this._started = false; this._filter.bbox = null; this._allfeatures = false; + this._featuresstore.clear(); this._layer.getFeaturesStore().clear(); - this._layer.getType() === Layer.LayerTypes.VECTOR && this._layer.resetEditingSource( this._featuresstore.getFeaturesCollection()); + + if (Layer.LayerTypes.VECTOR === this._layer.getType()) { + this._layer.resetEditingSource(this._featuresstore.getFeaturesCollection()); + } }; -module.exports = Editor; +module.exports = Editor; \ No newline at end of file diff --git a/src/app/core/editing/session.js b/src/app/core/editing/session.js index 4fd0a818a..2d1d3679b 100644 --- a/src/app/core/editing/session.js +++ b/src/app/core/editing/session.js @@ -379,38 +379,70 @@ proto.set3DGeometryType = function({layerId=this.getId(), commitItems}={}){ })); }; -proto.commit = function({ids=null, items, relations=true}={}) { +/** + * Commit changes on server (save) + * + * @param opts.ids + * @param opts.items + * @param opts.relations + * + * @returns {*} + */ +proto.commit = function({ + ids = null, + items, + relations = true +} = {}) { + const d = $.Deferred(); - let commitItems; + + let commit; // committed items + + // skip when .. if (ids) { - commitItems = this._history.commit(ids); + commit = this._history.commit(ids); this._history.clear(ids); - } else { - if (items) commitItems = items; - else { - commitItems = this._history.commit(); - commitItems = this._serializeCommit(commitItems); - } - if (!relations) commitItems.relations = {}; - this._editor.commit(commitItems) - .then(response => { - if (response && response.result) { - const {response:data} = response; - const {new_relations={}} = data; - for (const id in new_relations) { - const session = SessionsRegistry.getSession(id); - session.getEditor().applyCommitResponse({ - response: new_relations[id], - result: true - }) - } - this._history.clear(); - this.saveChangesOnServer(commitItems); - d.resolve(commitItems, response) - } else d.reject(response); - }) - .fail(err => d.reject(err)); + return d.promise(); + } + + commit = items || this._serializeCommit(this._history.commit()); + + if (!relations) { + commit.relations = {}; } + + this._editor + .commit(commit) + .then(response => { + + // skip when response is null or undefined and response.result is false + if (!(response && response.result)) { + d.reject(response); + return; + } + + const { new_relations = {} } = response.response; // check if new relations are saved on server + + // sync server data with local data + for (const id in new_relations) { + SessionsRegistry + .getSession(id) // get session of relation by id + .getEditor() + .applyCommitResponse({ // apply commit response to current editing relation layer + response: new_relations[id], + result: true + }); + } + + this._history.clear(); // clear history + + this.saveChangesOnServer(commit); // dispatch setter event. + + d.resolve(commit, response); + + }) + .fail(err => d.reject(err)); + return d.promise(); }; @@ -442,7 +474,7 @@ proto.clear = function() { this._editor.clear(); }; -//return l'history +//return history proto.getHistory = function() { return this._history; }; diff --git a/src/app/core/layers/tablelayer.js b/src/app/core/layers/tablelayer.js index d594a90b0..601d5b17d 100644 --- a/src/app/core/layers/tablelayer.js +++ b/src/app/core/layers/tablelayer.js @@ -254,15 +254,26 @@ proto._setOtherConfigParameters = function(config) { // overwrite by vector layer }; -// return layer fields -proto.getEditingFields = function(editable=false) { - let fields = this.config.editing.fields.length ? this.config.editing.fields: this.config.fields; - if (editable) fields = fields.filter(field => field.editable); +/** + * @returns layer fields + */ +proto.getEditingFields = function(editable = false) { + let fields = this.config.editing.fields.length + ? this.config.editing.fields + : this.config.fields; + if (editable) { + fields = fields.filter(field => field.editable); + } return fields; }; -proto.isPkField = function(field){ - const find_field = this.getEditingFields().find(_field => _field.name === field); +/** + * @param field + * + * @returns {boolean} whether field is a Primary Key + */ +proto.isPkField = function(field) { + const find_field = this.getEditingFields().find(f => f.name === field); return find_field && find_field.pk; }; diff --git a/src/app/core/relations/relation.js b/src/app/core/relations/relation.js index a76ef1d75..e43921542 100644 --- a/src/app/core/relations/relation.js +++ b/src/app/core/relations/relation.js @@ -1,22 +1,32 @@ const { base, inherit } = require('core/utils/utils'); const G3WObject = require('core/g3wobject'); -function Relation(config={}) { - const uniqueSuffix = Date.now(); - const id = config.id || `id_${uniqueSuffix}`; - const name = config.name || `name_${uniqueSuffix}`; - const origname = config.origname || `origname_${uniqueSuffix}`; +/** + * Relation Class + * + * @param config + * + * @constructor + */ +function Relation(config = {}) { + + const suffix = Date.now(); + + /** BACKCOMP (g3w-admin < v.3.7.0) */ + const multi_fields = [].concat(config.fieldRef.referencedField); + this.state = { - id, - name, - origname, - father: config.referencedLayer, - child: config.referencingLayer, - fatherField: config.fieldRef.referencedField, - childField: config.fieldRef.referencingField, - type: config.type, - loading: false + id: config.id || `id_${suffix}`, + name: config.name || `name_${suffix}`, + origname: config.origname || `origname_${suffix}`, + father: config.referencedLayer, + child: config.referencingLayer, + fatherField: multi_fields, + childField: multi_fields, + type: config.type, + loading: false }; + base(this); } @@ -24,46 +34,97 @@ inherit(Relation, G3WObject); const proto = Relation.prototype; +/** + * Get relation id + * + * @returns {string} + */ proto.getId = function() { return this.state.id; }; +/** + * Set Relation id + * + * @param id + */ proto.setId = function(id) { this.state.id = id; }; +/** + * Get Relation name + * + * @returns {string} + */ proto.getName = function() { return this.state.name; }; +/** + * Set Relation name + * + * @param name + */ proto.setName = function(name) { this.state.name = name; }; +/** + * @TODO check if deprecated (ie. `this.state.title` is not defined in class constructor) + * + * Get Relation title + * + * @returns { undefined } + */ proto.getTitle = function() { return this.state.title; }; +/** + * @TODO check if deprecated (ie. `this.state.title` is not defined in class constructor) + * + * Set Relation title + * + * @param title + * + * @returns { undefined } + */ proto.setTitle = function(title) { return this.state.title = title; }; +/** + * @returns { string[] } layerId of child relation + */ proto.getChild = function() { return this.state.child; }; +/** + * @returns { string[] } layerId of father relation + */ proto.getFather = function() { return this.state.father; }; +/** + * @returns state Object of relation + */ proto.getState = function() { return this.state; }; +/** + * @returns { 'MANY' | ONE' | string } relation type + */ proto.getType = function() { return this.state.type; }; +/** + * @returns {{ father, child }} relation fields + */ proto.getFields = function() { return { father: this.state.fatherField, @@ -71,28 +132,40 @@ proto.getFields = function() { }; }; +/** + * Return father relation field name + * + * @returns {*} + */ proto.getFatherField = function() { return this.state.fatherField; }; +/** + * Return relation child layer field name + * + * @returns {*} + */ proto.getChildField = function() { return this.state.childField; }; /** - * For editing purpose + * Set Loading state relation (for editing purpose) + * + * @param bool */ - proto.setLoading = function(bool=false){ this.state.loading = bool; }; +/** + * Check Loading state Relation (for editing purpose) + * + * @returns { boolean } + */ proto.isLoading = function(){ return this.state.loading; }; -/** - * End editing loading purpose - */ - -module.exports = Relation; +module.exports = Relation; \ No newline at end of file diff --git a/src/app/core/relations/relations.js b/src/app/core/relations/relations.js index 8406acf76..1380e5745 100644 --- a/src/app/core/relations/relations.js +++ b/src/app/core/relations/relations.js @@ -1,25 +1,34 @@ const { base, inherit } = require('core/utils/utils'); -const G3WObject = require('core/g3wobject'); -const Relation = require('core/relations/relation'); - -// class Relations -function Relations(options={}) { - const {relations} = options; - //store relations +const G3WObject = require('core/g3wobject'); +const Relation = require('core/relations/relation'); + +/** + * Relations Class + * + * @param options + * + * @constructor + */ +function Relations(options = {}) { + + /** + * Relations store + */ this._relations = {}; - this._length = relations ? relations.length : 0; - // to build relations between layers - this._relationsInfo = { - children: {}, // array child (unique ids) - fathers: {}, // array father (unique ids) - father_child: {} // info parent child - }; + + /** + * Number of relations + */ + this._length = options.relations ? options.relations.length : 0; + let relation; - relations.forEach(relationConfig => { + options.relations.forEach(relationConfig => { relation = new Relation(relationConfig); this._relations[relation.getId()] = relation; }); - this._createRelationsInfo(); + + this._reloadRelationsInfo(); + base(this); } @@ -27,74 +36,131 @@ inherit(Relations, G3WObject); const proto = Relations.prototype; +/** + * Populate `this._relationsInfo` object. + */ proto._createRelationsInfo = function() { - let father; - let child; - Object.entries(this._relations).forEach(([relationKey, relation]) => { - father = relation.getFather(); - child = relation.getChild(); - this._relationsInfo.father_child[father+child] = relationKey; - if (!this._relationsInfo.fathers[father]) this._relationsInfo.fathers[father] = []; - if (!this._relationsInfo.children[child]) this._relationsInfo.children[child] = []; - this._relationsInfo.fathers[father].push(child); - this._relationsInfo.children[child].push(father); + + // sanity check + if (!this._relationsInfo) { + this._clearRelationsInfo(); + } + + let f, c; + const { father_child, fathers, children } = this._relationsInfo; + + Object + .entries(this._relations) + .forEach(([relationKey, relation]) => { + + f = relation.getFather(); + c = relation.getChild(); + + father_child[f + c] = relationKey; // relationKey = [father_layerId + child_layerId] + fathers[f] = fathers[f] || []; + children[c] = children[c] || []; + + fathers[f].push(c); + children[c].push(f); }); + }; +/** + * @private + */ proto._clearRelationsInfo = function() { this._relationsInfo = { - children: {}, - fathers: {}, - father_children: {} + children: {}, // hashmap: > + fathers: {}, // hashmap: > + father_child: {}, // hashmap: }; }; +/** + * Build relations between layers. + * + * @private + */ proto._reloadRelationsInfo = function() { this._clearRelationsInfo(); this._createRelationsInfo(); }; -// number of relations +/** + * @returns { number } number of relations + */ proto.getLength = function() { - return this._length + return this._length; }; -proto.getRelations = function({type=null}={}) { - if (!type) return this._relations; - else { - if (['ONE','MANY'].indexOf(type) !== -1) { - const relations = {}; - for (const name in this._relations) { - const relation = this._relations[name]; - if (relation.getType() === type) relations[name] = relation; +/** + * @param relation.type + * + * @returns { {} | Relation[] } relations filtered by type + */ +proto.getRelations = function({ + type = null, +} = {}) { + + // type = null + if (!type) { + return this._relations; + } + + // type = { 'ONE' | 'MANY' } + if (-1 !== ['ONE','MANY'].indexOf(type)) { + const relations = {}; + for (const name in this._relations) { + const relation = this._relations[name]; + if (type === relation.getType()) { + relations[name] = relation; } - return relations; - } else return {}; + } + return relations; } + + return {}; }; -// array of relation +/** + * @returns { Relation[] } + */ proto.getArray = function() { - const relations = []; - Object.entries(this._relations).forEach(([relName, relation]) => { - relations.push(relation); - }); - return relations; + return Object + .entries(this._relations) + .map(([_, relation]) => relation); }; -proto.setRelations = function(relations) { +/** + * @param relations + */ +proto.setRelations = function(relations=[]) { this._relations = Array.isArray(relations) ? relations : []; }; +/** + * @param id + * + * @returns { Relation } + */ proto.getRelationById = function(id) { return this._relations[id]; }; +/** + * @param father father layerId + * @param child child_layerId + * + * @returns { Relation } + */ proto.getRelationByFatherChildren = function(father, child) { - const relationId = this._relationsInfo.father_child[father+child]; - return this.getRelationById(relationId); + return this.getRelationById(this._relationsInfo.father_child[father + child]); }; +/** + * @param relation + */ proto.addRelation = function(relation) { if (relation instanceof Relation) { this._relations[relation.getId()] = relation; @@ -102,41 +168,73 @@ proto.addRelation = function(relation) { } }; +/** + * + * @param relation + */ proto.removeRelation = function(relation) { - let relationId; if (relation instanceof Relation) { - relationId = relation.getId(); - delete this._relations[relationId]; + delete this._relations[relation.getId()]; this._reloadRelationsInfo(); } }; -proto.hasChildren = function(childId) { - const children = this.getChildren(childId); - return children ? !!children.length: false; -}; - -proto.hasFathers = function(fatherId) { - const fathers = this.getFathers(fatherId); - return fathers ? !!fathers.length : false; -}; - -// get children based on father id -proto.getChildren = function(fatherId) { - if (!this.isFather(fatherId)) return null; - return this._relationsInfo.fathers[fatherId]; -}; - -// get fathers based on childId -proto.getFathers = function(childId) { - if (!this.isChild(childId)) return null; - return this._relationsInfo.children[childId]; -}; - +/** + * @param relation_id + * + * @returns { boolean } + */ +proto.hasChildren = function(relation_id) { + const children = this.getChildren(relation_id); + return (children && children.length > 0); +}; + +/** + * @param relation_id + * + * @returns { boolean } + */ +proto.hasFathers = function(relation_id) { + const fathers = this.getFathers(relation_id); + return (fathers && fathers.length > 0); +}; + +/** + * Extract children relations + * + * @param layer_id + * + * @returns { Array | null } child layer (Ids) within same relation + */ +proto.getChildren = function(layer_id) { + return this.isFather(layer_id) ? this._relationsInfo.fathers[layer_id] : null; +}; + +/** + * Extract father relations + * + * @param layer_id + * + * @returns { Array | null } father layer Ids within same relation + */ +proto.getFathers = function(layer_id) { + return this.isChild(layer_id) ? this._relationsInfo.children[layer_id] : null; +}; + +/** + * @param id + * + * @returns { boolean } + */ proto.isChild = function(id) { return !!this._relationsInfo.children[id]; }; +/** + * @param id + * + * @returns { boolean } + */ proto.isFather = function(id) { return !!this._relationsInfo.fathers[id]; }; diff --git a/src/app/g3w-ol/controls/geocodingcontrol.js b/src/app/g3w-ol/controls/geocodingcontrol.js index 3864b3659..2c6b08390 100644 --- a/src/app/g3w-ol/controls/geocodingcontrol.js +++ b/src/app/g3w-ol/controls/geocodingcontrol.js @@ -122,7 +122,7 @@ const utils = { * @return (find_all) {Element} : {Array} */ find(selector, context, find_all) { - if(context === void 0) context = window.document; + if(context === undefined) context = window.document; let simpleRe = /^(#?[\w-]+|\.[\w-.]+)$/, periodRe = /\./g, slice = Array.prototype.slice, diff --git a/src/app/gui/table/tableservice.js b/src/app/gui/table/tableservice.js index 656fc104b..6006c56a2 100644 --- a/src/app/gui/table/tableservice.js +++ b/src/app/gui/table/tableservice.js @@ -1,88 +1,142 @@ import CatalogLayersStoresRegistry from 'store/catalog-layers'; -import DataRouterService from 'services/data'; -import GUI from 'services/gui'; +import DataRouterService from 'services/data'; +import GUI from 'services/gui'; -const { inherit, noop } = require('core/utils/utils'); -const G3WObject = require('core/g3wobject'); -const { t } = require('core/i18n/i18n.service'); -const { coordinatesToGeometry } = require('core/utils/geo'); -const { SELECTION_STATE } = require('core/layers/layer'); +const { inherit, noop } = require('core/utils/utils'); +const G3WObject = require('core/g3wobject'); +const { t } = require('core/i18n/i18n.service'); +const { coordinatesToGeometry } = require('core/utils/geo'); +const { SELECTION_STATE } = require('core/layers/layer'); const PAGELENGTHS = [10, 25, 50]; +/** + * Create a unique feature key + */ +function _createFeatureKey(values) { + return values.join('__'); +} + +/** + * TableService Class + * + * @param options.layer + * @param options.formatter + * + * @constructor + */ const TableService = function(options = {}) { - this.currentPage = 0; // number of pages + + /** + * Number of pages + */ + this.currentPage = 0; + + /** + * @FIXME add description + */ this.layer = options.layer; + + /** + * @FIXME add description + */ this.formatter = options.formatter; - const headers = this.getHeaders(); - this.allfeaturesnumber; + + /** + * @FIXME add description + */ + this.allfeaturesnumber = undefined; + + /** + * @FIXME add description + */ this.nopaginationsfilter = []; + + /** + * @FIXME add description + */ this.selectedfeaturesfid = this.layer.getSelectionFids(); + + /** + * Whether layer has geometry + */ this.geolayer = this.layer.isGeoLayer(); - this.relationsGeometry = []; - !this.geolayer && this.layer.getRelations().getArray().forEach(relation => { - const relationLayer = CatalogLayersStoresRegistry.getLayerById(relation.getChild()); - if (relationLayer.isGeoLayer()) { - this.relationsGeometry.push({ - layer: relationLayer, - child_field: relation.getChildField(), - field: relation.getFatherField(), - features: {} - }) - } - }); - this.projection = this.geolayer ? this.layer.getProjection() : null; + + /** + * @FIXME add description + */ + this.relationsGeometry = this._relationsGeometry(); + + /** + * @FIXME add description + */ + this.projection = this.geolayer ? this.layer.getProjection() : null; + + /** + * @FIXME add description + */ this.mapService = GUI.getService('map'); + + /** + * @FIXME add description + */ this.getAll = false; + + /** + * @FIXME add description + */ this.paginationfilter = false; + + /** + * @FIXME add description + */ this.mapBBoxEventHandlerKey = { key: null, cb: null }; - this.clearAllSelection = () => { - this.state.features.forEach(feature => feature.selected = false); - this.state.tools.show = false; - this.state.selectAll = false; - }; + + + // bind context on event listeners + this.clearAllSelection = this.clearAllSelection.bind(this); + this.filterChangeHandler = this.filterChangeHandler.bind(this); + this.onGUIContent = this.onGUIContent.bind(this); + + /** + * @FIXME add description + */ this.state = { - pageLengths: PAGELENGTHS, - pageLength: this.layer.getAttributeTablePageLength() || PAGELENGTHS[0], - features: [], - title: this.layer.getTitle(), - headers, - geometry: true, - loading: false, - allfeatures: 0, - pagination: !this.getAll, - selectAll: false, + pageLengths: PAGELENGTHS, + pageLength: this.layer.getAttributeTablePageLength() || PAGELENGTHS[0], + features: [], + title: this.layer.getTitle(), + headers: this.getHeaders(), + geometry: true, + loading: false, + allfeatures: 0, + pagination: !this.getAll, + selectAll: false, nofilteredrow: false, tools: { geolayer: { - show: this.geolayer, - active: false, - in_bbox: void 0 + show: this.geolayer, + active: false, + in_bbox: undefined, }, - show: false, - filter: this.layer.state.filter + show: false, + filter: this.layer.state.filter } }; - // pagination filter features + + /** + * Pagination filter features + */ this._async = { state: false, - fnc: noop - }; - GUI.onbefore('setContent', options => { - this._async.state = options.perc === 100; - }); - this.layer.on('unselectionall', this.clearAllSelection); - this.filterChangeHandler = async ({type}={})=>{ - this.allfeaturesnumber = undefined; - let data = []; - // emit redraw if in_bbox filter or not select all - const emitRedraw = type === 'in_bbox' || !this.selectedfeaturesfid.has(SELECTION_STATE.ALL); - if (!this.state.pagination) data = emitRedraw ? await this.reloadData() : []; - emitRedraw && this.emit('redraw', data); + fnc: noop }; + + GUI.onbefore('setContent', this.onGUIContent); + this.layer.on('unselectionall', this.clearAllSelection); this.layer.on('filtertokenchange', this.filterChangeHandler); }; @@ -90,16 +144,81 @@ inherit(TableService, G3WObject); const proto = TableService.prototype; -proto.toggleFilterToken = async function(){ +/** + * @since 3.9.0 + */ +proto._relationsGeometry = function() { + + // layer has geometry + if (this.geolayer) { + return []; + } + + const relations = []; + + this.layer + .getRelations() + .getArray() + .forEach(relation => { + const layer = CatalogLayersStoresRegistry.getLayerById(relation.getFather()); // get project layer + if ( + this.layer.getId() !== relation.getFather() && // current layer is not child layer of relation + layer.isGeoLayer() // relation layer has geometry + ) { + relations.push({ + layer, + father_fields: relation.getFatherField(), // NB: since g3w-admin@v3.7.0 this is an Array value. + fields: relation.getChildField(), // NB: since g3w-admin@v3.7.0 this is an Array value. + features: {}, + }) + } + }); + + return relations; +}; + +/** + * @since 3.9.0 + */ +proto.clearAllSelection = function() { + this.state.features.forEach(feature => feature.selected = false); + this.state.tools.show = false; + this.state.selectAll = false; +}; + +/** + * @since 3.9.0 + */ +proto.filterChangeHandler = async function ({type}={}) { + this.allfeaturesnumber = undefined; + let data = []; + // emit redraw if in_bbox filter or not select all + const emitRedraw = type === 'in_bbox' || !this.selectedfeaturesfid.has(SELECTION_STATE.ALL); + if (!this.state.pagination) data = emitRedraw ? await this.reloadData() : []; + emitRedraw && this.emit('redraw', data); +}; + +/** + * @since 3.9.0 + */ +proto.onGUIContent = function(options) { + this._async.state = (100 === options.perc); +}; + +proto.toggleFilterToken = async function() { await this.layer.toggleFilterToken(); }; +/** + * first value = `null` for DataTable purpose (used to add a custom input selector) + */ proto.getHeaders = function() { - //add null as firs vale of header because need to add a custom input selector fro datatable purpose return [null, ...this.layer.getTableHeaders()]; }; -// function need to work with pagination +/** + * DataTable pagination + */ proto.setDataForDataTable = function() { const data = []; this.state.features.forEach(feature => { @@ -107,10 +226,9 @@ proto.setDataForDataTable = function() { const values = [null]; this.state.headers.forEach(header => { if (header) { - const value = attributes[header.name]; - header.value = value; - //header.label = undefined; // removed label - values.push(value); + header.value = attributes[header.name]; + values.push(header.value); + // header.label = undefined; // removes label. } }); data.push(values) @@ -118,312 +236,432 @@ proto.setDataForDataTable = function() { return data; }; -proto.addRemoveSelectedFeature = function(feature){ +proto.addRemoveSelectedFeature = function(feature) { feature.selected = !feature.selected; - if (this.state.selectAll) { + + const selected = this.selectedfeaturesfid; + const filter = this.nopaginationsfilter; + const count = this.allfeaturesnumber; + + const select_all = this.state.selectAll; + const has_pagination = this.state.pagination; + const features = this.state.features; + const is_active = this.state.tools && this.state.tools.filter && this.state.tools.filter.active + + const is_exclude = !select_all && selected.has(SELECTION_STATE.EXCLUDE); + const is_default = !select_all && !is_exclude; + + /** @FIXME add description */ + if (select_all) { this.state.selectAll = false; - this.layer.excludeSelectionFid(feature.id, this.state.pagination); - } else if (this.selectedfeaturesfid.has(SELECTION_STATE.EXCLUDE)) { - this.layer[feature.selected ? 'includeSelectionFid' : 'excludeSelectionFid'](feature.id); - const size = this.selectedfeaturesfid.size; - if (size === 1) { - !this.state.tools.filter.active && this.layer.setSelectionFidsAll(); - this.state.selectAll = true; - } else if (size -1 === this.state.features.length) this.layer.clearSelectionFids(); - } else { + } + + /** @FIXME add description */ + if (select_all) { + this.layer.excludeSelectionFid(feature.id, has_pagination); + } + + /** @FIXME add description */ + if (is_exclude || is_default) { this.layer[feature.selected ? 'includeSelectionFid' : 'excludeSelectionFid'](feature.id); - const size = this.selectedfeaturesfid.size; - if (size === this.allfeaturesnumber) { - this.state.selectAll = true; - !this.state.tools.filter.active && this.layer.setSelectionFidsAll(); - } } - this.state.tools.show = this.selectedfeaturesfid.size > 0; - if (!this.state.pagination){ - if (this.nopaginationsfilter.length) { - this.state.selectAll = this.state.features.filter(feature => feature.selected).length === this.nopaginationsfilter.length; - } + + /** @FIXME add description */ + if (!is_active && ( (is_exclude && 1 === selected.size) || (is_default && selected.size === count)) ) { + this.layer.setSelectionFidsAll(); } + + /** @FIXME add description */ + if (is_exclude && 1 !== selected.size && selected.size === features.length + 1) { + this.layer.clearSelectionFids(); + } + + /** @FIXME add description */ + this.state.tools.show = selected.size > 0; + + /** @FIXME add description */ + if ( + (is_exclude && 1 === selected.size) || + (is_default && selected.size === count) || + (!has_pagination && filter.length && filter.length === features.filter(f => f.selected).length) + ) { + this.state.selectAll = true; + } + }; -proto.createFeatureForSelection = function(feature){ +proto.createFeatureForSelection = function(feature) { return { attributes: feature.attributes ? feature.attributes : feature.properties, - geometry: this._returnGeometry(feature) + geometry: this._returnGeometry(feature), } }; -proto.getAllFeatures = function(params){ +proto.getAllFeatures = function(params) { GUI.setLoadingContent(true); - return new Promise((resolve, reject) =>{ - this.layer.getDataTable(params || {}) - .then(data =>{ - const {features} = data; - if (this.geolayer && features) { - if (!params){ - const LoadedFeaturesId = this.state.features.map(feature => feature.id); - features.forEach(feature => { - if (LoadedFeaturesId.indexOf(feature.id) === -1) { - feature.geometry && this.layer.addOlSelectionFeature({ - id: feature.id, - feature: this.createFeatureForSelection(feature) - }); - } - }); - this.getAll = true; - } - resolve(features); + return new Promise((resolve, reject) => { + this.layer + .getDataTable(params || {}) + .then(data => { + const is_valid = this.geolayer && data.features; + + if (is_valid && !params) { + const loaded_features = this.state.features.map(f => f.id); + data.features.forEach(f => { + if (-1 === loaded_features.indexOf(f.id) && f.geometry) { + this.layer.addOlSelectionFeature({ + id: f.id, + feature: this.createFeatureForSelection(f), + }); + } + }); + this.getAll = true; } + + if (is_valid) { + resolve(data.features); + } }) - .fail(()=> reject()) - .always(()=>GUI.setLoadingContent(false)) - }) + .fail(() => reject()) + .always(() => GUI.setLoadingContent(false)); + }); }; -proto.switchSelection = async function(){ - if (!this.state.pagination) { // no pagination - if (this.nopaginationsfilter.length) { //filtered - let selected = false; - const filterFeatures = []; - this.state.features.forEach((feature, index) =>{ - if (this.nopaginationsfilter.indexOf(index) !== -1) filterFeatures.push(feature); - feature.selected = !feature.selected; - this.layer[feature.selected ? 'includeSelectionFid' : 'excludeSelectionFid' ](feature.id); - selected = selected || feature.selected; - }); - this.state.tools.show = selected; - this.checkSelectAll(filterFeatures) - } else { // no filter - this.state.features.forEach(feature => { - feature.selected = !feature.selected; - }); - this.layer.invertSelectionFids(); - this.checkSelectAll(); - this.state.tools.show = this.selectedfeaturesfid.size > 0; - } - } else { // pagination - let selected = false; - this.state.features.forEach(feature => { - feature.selected = !feature.selected; - selected = feature.selected; +proto.switchSelection = async function() { + const has_pagination = this.state.pagination; + const filter = this.nopaginationsfilter; + const filtered = !has_pagination && filter.length ? [] : undefined; + let selected = false; + + // pagination + if (has_pagination) { + this.state.features.forEach(f => { + f.selected = !f.selected; + selected = f.selected; }); - !this.getAll && await this.getAllFeatures(); - this.state.selectAll = this.paginationfilter ? selected: this.state.selectAll; + } + + if (has_pagination && !this.getAll) { + await this.getAllFeatures(); + } + + this.state.selectAll = (has_pagination && this.paginationfilter) ? selected : this.state.selectAll; + + // filtered + if (!has_pagination && filter.length) { + this.state.features.forEach((f, i) => { + if (-1 !== filter.indexOf(i)) { + filtered.push(f); + } + f.selected = !f.selected; + this.layer[f.selected ? 'includeSelectionFid' : 'excludeSelectionFid' ](f.id); + selected = selected || f.selected; + }); + this.state.tools.show = selected; + } + + // no filter + if (!has_pagination && !filter.length) { + this.state.features.forEach(f => { f.selected = !f.selected; }); + } + + if (has_pagination || !filter.length) { this.layer.invertSelectionFids(); + } + + if (!has_pagination) { + this.checkSelectAll(filtered); + } + + if (has_pagination || !filter.length) { this.state.tools.show = this.selectedfeaturesfid.size > 0; } + }; -proto.clearLayerSelection = function(){ +proto.clearLayerSelection = function() { this.layer.clearSelectionFids(); }; /** - * Called when alla selected feature is checked + * Called when a selected feature is checked + * * @returns {Promise} */ -proto.selectAllFeatures = async function(){ +proto.selectAllFeatures = async function() { + // set inverse of selectAll this.state.selectAll = !this.state.selectAll; - if (!this.state.pagination) { //no pagination no filter - if (this.nopaginationsfilter.length) { //check if filter is set (no pagination) - let selected = false; - this.state.features.forEach((feature, index) =>{ - if (this.nopaginationsfilter.indexOf(index) !== -1) { - feature.selected = this.state.selectAll; - this.layer[feature.selected ? 'includeSelectionFid': 'excludeSelectionFid'](feature.id); - selected = selected || feature.selected; - } - }); - this.state.tools.show = selected; - } else { - this.state.tools.show = this.state.selectAll; - this.layer[this.state.selectAll ? 'setSelectionFidsAll': 'clearSelectionFids'](); - this.state.features.forEach(feature => feature.selected = this.state.selectAll); - } - } else { //pagination - if (this.paginationfilter) { // filtered - if (this.state.featurescount >= this.state.allfeatures) - this.state.features.forEach(feature => { - feature.selected = this.state.selectAll; - this.layer[feature.selected ? 'includeSelectionFid': 'excludeSelectionFid'](feature.id); - }); - else { - const {search, ordering, formatter, in_bbox } = this.paginationParams; - const features = await this.getAllFeatures({ - search, - ordering, - formatter, - in_bbox + + const has_pagination = this.state.pagination; + const filter = this.nopaginationsfilter; + let selected = false; + + // filtered + if (!has_pagination && filter.length) { + this.state.features.forEach((f, i) =>{ + if (-1 !== filter.indexOf(i)) { + f.selected = this.state.selectAll; + this.layer[f.selected ? 'includeSelectionFid': 'excludeSelectionFid'](f.id); + selected = selected || f.selected; + } + }); + this.state.tools.show = selected; + } + + // no filter + if (!has_pagination && !filter.length) { + this.state.tools.show = this.state.selectAll; + this.layer[this.state.selectAll ? 'setSelectionFidsAll': 'clearSelectionFids'](); + this.state.features.forEach(f => f.selected = this.state.selectAll); + } + + // filtered pagination + if (has_pagination && this.paginationfilter && this.state.featurescount >= this.state.allfeatures) { + this.state.features.forEach(f => { + f.selected = this.state.selectAll; + this.layer[f.selected ? 'includeSelectionFid': 'excludeSelectionFid'](f.id); + }); + } + + if (has_pagination && this.paginationfilter && this.state.featurescount < this.state.allfeatures) { + const features = await this.getAllFeatures({ + search: this.paginationParams.search, + ordering: this.paginationParams.ordering, + formatter: this.paginationParams.formatter, + in_bbox: this.paginationParams.in_bbox, + }); + features.forEach(f => { + if (!this.getAll && this.geolayer && f.geometry) { + this.layer.addOlSelectionFeature({ + id: f.id, + feature: this.createFeatureForSelection(f) }); - features.forEach(feature =>{ - !this.getAll && this.geolayer && feature.geometry && this.layer.addOlSelectionFeature({ - id: feature.id, - feature: this.createFeatureForSelection(feature) - }); - this.layer[this.state.selectAll ? 'includeSelectionFid' : 'excludeSelectionFid'](feature.id); - }) } - this.state.features.forEach(feature => feature.selected = this.state.selectAll); - } else { - this.state.features.forEach(feature => feature.selected = this.state.selectAll); - !this.getAll && await this.getAllFeatures(); - this.layer[this.state.selectAll ? 'setSelectionFidsAll': 'clearSelectionFids'](); - } + this.layer[this.state.selectAll ? 'includeSelectionFid' : 'excludeSelectionFid'](f.id); + }) + } + + if (has_pagination) { + this.state.features.forEach(f => f.selected = this.state.selectAll); + } + + if (has_pagination && !this.paginationfilter && !this.getAll) { + await this.getAllFeatures(); + } + + if (has_pagination && !this.paginationfilter) { + this.layer[this.state.selectAll ? 'setSelectionFidsAll': 'clearSelectionFids'](); + } + + if (has_pagination) { this.state.tools.show = this.state.selectAll || this.selectedfeaturesfid.size > 0; } + }; /** - * Method to set filtered features - * @param featuresIndex + * Set filtered features + * + * @param index features index */ -proto.setFilteredFeature = function(featuresIndex){ - this.nopaginationsfilter = featuresIndex; - this.checkSelectAll((featuresIndex.length === this.allfeaturesnumber || featuresIndex.length === 0) ? undefined - : this.nopaginationsfilter.map(index=> this.state.features[index])); +proto.setFilteredFeature = function(index) { + const filter = this.nopaginationsfilter = index; + if (0 === index.length || index.length === this.allfeaturesnumber) { + this.checkSelectAll(); + } else { + this.checkSelectAll(filter.map(i => this.state.features[i])); + } }; -proto.setAttributeTablePageLength = function(length){ +proto.setAttributeTablePageLength = function(length) { this.layer.setAttributeTablePageLength(length); }; /** - * Main method to get data table layer - * @param start - * @param order - * @param length - * @param columns - * @param search - * @param firstCall - * @returns {Promise} + * Get DataTable layer + * + * @param data.start + * @param data.order + * @param data.length + * @param data.columns + * @param data.search + * @param data.firstCall + * + * @returns {Promise<{{ data: [], recordsTotal: number, recordsFiltered: number }}>} */ -proto.getData = function({start = 0, order = [], length = this.state.pageLength, columns=[], search={value:null}, firstCall=false} = {}) { +proto.getData = function({ + start = 0, + order = [], + length = this.state.pageLength, + columns = [], + search = { value: null }, + firstCall = false +} = {}) { + // reset features before load GUI.setLoadingContent(true); + this.setAttributeTablePageLength(length); + return new Promise((resolve, reject) => { - if (!this.state.headers.length) + + // skip when .. + if (!this.state.headers.length) { resolve({ data: [], recordsTotal: 0, recordsFiltered: 0 }); - else { - let searchText = search.value && search.value.length > 0 ? search.value : null; - this.state.features.splice(0); - if (!order.length) { - order.push({ - column: 1, - dir: 'asc' - }) - } - const ordering = order[0].dir === 'asc' ? this.state.headers[order[0].column].name : '-'+this.state.headers[order[0].column].name; - this.currentPage = start === 0 || (this.state.pagination && this.state.tools.filter.active) ? 1 : (start/length) + 1; - const in_bbox = this.state.tools.geolayer.in_bbox; - const field = this.state.pagination ? columns.filter(column => column.search && column.search.value).map(column => `${column.name}|ilike|${column.search.value}|and`).join(',') : undefined; - this.paginationParams = { - field: field || undefined, - page: this.currentPage, - page_size: length, - search: searchText, - in_bbox, - formatter: this.formatter, - ordering - }; - const getDataPromise = this.state.pagination ? - this.layer.getDataTable(this.paginationParams) : - this.layer.getDataTable({ - ordering, - in_bbox, - formatter: this.formatter + return; + } + + let searchText = search.value && search.value.length > 0 ? search.value : null; + + this.state.features.splice(0); + + if (!order.length) { + order.push({ + column: 1, + dir: 'asc', }); - getDataPromise - .then(data => { - const {features=[]} = data; - this.state.allfeatures = data.count || this.state.features.length; - this.state.featurescount = features.length; - this.allfeaturesnumber = this.allfeaturesnumber === undefined ? data.count : this.allfeaturesnumber; - this.paginationfilter = data.count !== this.allfeaturesnumber; - this.state.pagination = firstCall ? this.state.tools.filter.active || features.length < this.allfeaturesnumber : this.state.pagination; - this.addFeatures(features); - resolve({ - data: this.setDataForDataTable(), - recordsFiltered: data.count, - recordsTotal: data.count - }); - }) - .fail(err => { - GUI.notify.error(t("info.server_error")); - reject(err); - }).always(()=>{ - GUI.setLoadingContent(false); - }) } + + const ordering = ('asc' === order[0].dir ? '' : '-') + this.state.headers[order[0].column].name; + + this.currentPage = 1 + ((0 === start || (this.state.pagination && this.state.tools.filter.active)) ? (start/length) : 0); + + const in_bbox = this.state.tools.geolayer.in_bbox; + + const field = this.state.pagination + ? columns.filter(c => c.search && c.search.value).map(c => `${c.name}|ilike|${c.search.value}|and`).join(',') + : undefined; + + this.paginationParams = { + field: field || undefined, + page: this.currentPage, + page_size: length, + search: searchText, + in_bbox, + formatter: this.formatter, + ordering + }; + + this.layer + .getDataTable( + this.state.pagination + ? this.paginationParams + : ({ ordering, in_bbox, formatter: this.formatter }) + ) + .then(data => { + const { features = [] } = data; + + this.state.allfeatures = data.count || this.state.features.length; + this.state.featurescount = features.length; + this.allfeaturesnumber = (undefined === this.allfeaturesnumber ? data.count : this.allfeaturesnumber); + this.paginationfilter = (data.count !== this.allfeaturesnumber); + this.state.pagination = firstCall + ? this.state.tools.filter.active || features.length < this.allfeaturesnumber + : this.state.pagination; + + this.addFeatures(features); + + resolve({ + data: this.setDataForDataTable(), + recordsFiltered: data.count, + recordsTotal: data.count + }); + }) + .fail(err => { GUI.notify.error(t("info.server_error")); reject(err); }) + .always(() => { GUI.setLoadingContent(false); }) }); }; -proto.setInBBoxParam = function(){ - this.state.tools.geolayer.in_bbox = this.state.tools.geolayer.active ? this.mapService.getMapBBOX().join(',') : void 0; +proto.setInBBoxParam = function() { + const { geolayer } = this.state.tools; + geolayer.in_bbox = geolayer.active ? this.mapService.getMapBBOX().join(',') : undefined; }; -proto.resetMapBBoxEventHandlerKey = function(){ - ol.Observable.unByKey(this.mapBBoxEventHandlerKey.key); - this.mapBBoxEventHandlerKey.key = null; - this.mapBBoxEventHandlerKey.cb = null; +proto.resetMapBBoxEventHandlerKey = function() { + const listener = this.mapBBoxEventHandlerKey; + ol.Observable.unByKey(listener.key); + listener.key = null; + listener.cb = null; }; -proto.getDataFromBBOX = async function(){ - this.state.tools.geolayer.active = !this.state.tools.geolayer.active; - if (this.state.tools.geolayer.active) { - this.mapBBoxEventHandlerKey.cb = this.state.pagination ? () => { +proto.getDataFromBBOX = async function() { + const { geolayer } = this.state.tools; + + geolayer.active = !geolayer.active; + + const is_active = geolayer.active; + const listener = this.mapBBoxEventHandlerKey; + + if (is_active && this.state.pagination) { + listener.cb = () => { this.setInBBoxParam(); this.emit('ajax-reload'); - } : async ()=>{ + }; + } + + if (is_active && !this.state.pagination) { + listener.cb = async () => { this.setInBBoxParam(); - this.filterChangeHandler({ - type: 'in_bbox' - }); + this.filterChangeHandler({ type: 'in_bbox' }); }; - this.mapBBoxEventHandlerKey.key = this.mapService.getMap().on('moveend', this.mapBBoxEventHandlerKey.cb); - this.mapBBoxEventHandlerKey.cb(); - } else { - this.mapBBoxEventHandlerKey.cb && this.mapBBoxEventHandlerKey.cb(); + } + + if (is_active) { + listener.key = this.mapService.getMap().on('moveend', listener.cb); + } + + if (listener.cb) { + listener.cb(); + } + + if (!is_active) { this.resetMapBBoxEventHandlerKey(); } }; proto.addFeature = function(feature) { const tableFeature = { - id: feature.id, - selected: this.state.tools.filter.active || this.layer.hasSelectionFid(feature.id), - attributes: feature.attributes ? feature.attributes : feature.properties + id: feature.id, + selected: this.state.tools.filter.active || this.layer.hasSelectionFid(feature.id), + attributes: feature.attributes || feature.properties, + geometry: this.geolayer && feature.geometry || undefined }; - if (this.geolayer && feature.geometry) { - this.layer.getOlSelectionFeature(tableFeature.id) || this.layer.addOlSelectionFeature({ - id: tableFeature.id, + + const has_geom = this.geolayer && feature.geometry; + const selection = has_geom && this.layer.getOlSelectionFeature(feature.id); + + if (has_geom && !selection) { + this.layer.addOlSelectionFeature({ + id: feature.id, feature: this.createFeatureForSelection(feature) }); - tableFeature.geometry = feature.geometry; } + this.state.features.push(tableFeature); }; -proto.checkSelectAll = function(features=this.state.features){ - this.state.selectAll = this.selectedfeaturesfid.has(SELECTION_STATE.ALL) || (features.length && features.reduce((accumulator, feature) => accumulator && feature.selected, true)); +proto.checkSelectAll = function(features = this.state.features) { + this.state.selectAll = ( + this.selectedfeaturesfid.has(SELECTION_STATE.ALL) || + (features.length && features.reduce((selectAll, f) => selectAll && f.selected, true)) + ); }; proto.addFeatures = function(features=[]) { - features.forEach(feature => this.addFeature(feature)); - this.state.tools.show = this.layer.getFilterActive() || this.selectedfeaturesfid.size > 0; + features.forEach(f => this.addFeature(f)); + this.state.tools.show = this.layer.getFilterActive() || this.selectedfeaturesfid.size > 0; this.checkSelectAll(); }; -proto.reloadData = async function(pagination=false){ +proto.reloadData = async function(pagination=false) { this.state.features.splice(0); this.state.pagination = pagination; - const tabledata = await this.getData(); - const {data=[], reloadData} = tabledata; + const { data = [] } = await this.getData(); return data; }; @@ -432,81 +670,123 @@ proto._setLayout = function() { }; proto._returnGeometry = function(feature) { - let geometry; - if (feature.attributes) geometry = feature.geometry; - else if (feature.geometry) geometry = coordinatesToGeometry(feature.geometry.type, feature.geometry.coordinates); - return geometry; + if (feature.attributes) return feature.geometry; + if (feature.geometry) return coordinatesToGeometry(feature.geometry.type, feature.geometry.coordinates); }; -proto.zoomAndHighLightFeature = function(feature, zoom=true) { - const geometry = feature.geometry; - if (geometry) { - if (this._async.state) this._async.fnc = this.mapService.highlightGeometry.bind(mapService, geometry, {zoom}); - else this.mapService.highlightGeometry(geometry , { zoom }); +proto.zoomAndHighLightFeature = function(feature, zoom = true) { + // async highlight + if (feature.geometry && this._async.state) { + this._async.fnc = this.mapService.highlightGeometry.bind(mapService, feature.geometry, { zoom }); + } + // sync highlight + if (feature.geometry && !this._async.state) { + this.mapService.highlightGeometry(feature.geometry , { zoom }); } }; /** * Zoom to eventually features relation */ -proto.zoomAndHighLightGeometryRelationFeatures = async function(feature, zoom=true){ - if (this.relationsGeometry.length) { - const features = []; - const promises = []; - const values = []; // usefult to check if add or not - this.relationsGeometry.forEach(({layer, child_field, field, features}) =>{ - const value = feature.attributes[field]; - values.push(value); - if (features[value] === undefined) { - let promise; - if (zoom){ - promise = DataRouterService.getData('search:features', { - inputs:{ +proto.zoomAndHighLightGeometryRelationFeatures = async function(feature, zoom = true) { + + // skip when there are no relation features geometry + if (!this.relationsGeometry.length > 0) { + return; + } + + const features = []; + const promises = []; + const field_values = []; // check if add or not + + this.relationsGeometry + .forEach(({ + layer, + father_fields, + fields, + features + }) => { + const values = fields.map(f => feature.attributes[f]); + const k = _createFeatureKey(values); + + field_values.push(values); + + let promise; + + if (zoom && undefined === features[k]) { + promise = DataRouterService + .getData('search:features', { + inputs: { layer, - filter:`${child_field}|eq|${value}`, - formatter: 1, // set formatter to 1 - search_endpoint: 'api' - }, - outputs: false + formatter: 1, + search_endpoint: 'api', + filter: ( + father_fields + .reduce((filter, field, index) => { + filter = `${filter}${index > 0 ? '|AND,' : ''}${field}|eq|${encodeURIComponent(values[index])}` + return filter; + }, '') + ), + }, + outputs: false, // just a request not show on result }); - } else promise = Promise.reject(); - promises.push(promise); - } else promises.push(Promise.resolve( - { - data:[ - { - features: features[value] - } - ] - })) + } + + if (zoom && undefined === features[k]) { + promise = Promise.reject(); + } + + if (undefined !== features[k]) { + promise = Promise.resolve({ data: [{ features: features[k] }] }); + } + + promises.push(promise); }); - const promisesData = await Promise.allSettled(promises); - promisesData.forEach(({status, value}, index) => { - if (status === 'fulfilled'){ - const _features = value.data[0] ? value.data[0].features : []; - _features.forEach(feature => features.push(feature)); - if (this.relationsGeometry[index].features[values[index]] === undefined){ - this.relationsGeometry[index].features[values[index]] = _features; - } + + (await Promise.allSettled(promises)) + .forEach(({ + status, + value + }, index) => { + if ('fulfilled' === status) { + + const relation = this.relationsGeometry[index]; + const k = _createFeatureKey(field_values[index]); + const data = value && value.data[0]; + + if (undefined === relation.features[k]) { + relation.features[k] = data && data.features || []; + } + + relation.features[k].forEach(f => features.push(f)); + } }); - zoom ? this.mapService.zoomToFeatures(features, { - highlight: true - }) : this.mapService.highlightFeatures(features); + + if (zoom) { + this.mapService.zoomToFeatures(features, { highlight: true }); + } else { + this.mapService.highlightFeatures(features); } + }; -proto.clear = function(){ - this.layer.off('unselectionall', this.clearAllSelection); +proto.clear = function() { + this.layer.off('unselectionall', this.clearAllSelection); this.layer.off('filtertokenchange', this.filterChangeHandler); + this.resetMapBBoxEventHandlerKey(); + this.allfeaturesnumber = null; - this.mapService = null; - this._async.state && setTimeout(()=> { - this._async.fnc(); - this._async.state = false; - this._async.fnc = noop; - }); + this.mapService = null; + + if (this._async.state) { + setTimeout(() => { + this._async.fnc(); + this._async.state = false; + this._async.fnc = noop; + }); + } }; module.exports = TableService; diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index df4f326ee..c961ecfc6 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -4,96 +4,166 @@ -->