From ac30e71e74d250a3259ce70ca460e2692ec2629e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 1 Aug 2023 14:09:09 +0200 Subject: [PATCH 01/10] Do not let advancedFilters control add/remove features from map This should be done by the datalayer itself, which is now the case. --- umap/static/umap/js/umap.controls.js | 34 ++++------------------------ umap/static/umap/js/umap.core.js | 1 - umap/static/umap/js/umap.features.js | 11 +++++++++ umap/static/umap/js/umap.layer.js | 5 ++++ 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 2b8d20f0..da996445 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -885,38 +885,12 @@ L.U.Map.include({ } const filterFeatures = function () { - let noResults = true + let found = false this.eachBrowsableDataLayer((datalayer) => { - datalayer.eachFeature(function (feature) { - feature.properties.isVisible = true - for (const [property, values] of Object.entries( - this.map.options.advancedFilters - )) { - if (values.length > 0) { - if ( - !feature.properties[property] || - !values.includes(feature.properties[property]) - ) { - feature.properties.isVisible = false - } - } - } - if (feature.properties.isVisible) { - noResults = false - if (!this.isLoaded()) this.fetchData() - this.map.addLayer(feature) - this.fire('show') - } else { - this.map.removeLayer(feature) - this.fire('hide') - } - }) + datalayer.resetLayer(true) + if (datalayer.hasDataVisible()) found = true }) - if (noResults) { - this.help.show('advancedFiltersNoResults') - } else { - this.help.hide() - } + if (!found) this.ui.alert({content: L._('No results for these filters'), level: 'info'}) } propertiesContainer.innerHTML = '' diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 0a3cb7ac..910c4ae4 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -571,7 +571,6 @@ L.U.Help = L.Class.extend({ advancedFilterKey: L._( 'Comma separated list of properties to use for checkbox filtering' ), - advancedFiltersNoResults: L._('No results for these filters'), interactive: L._('If false, the polygon will act as a part of the underlying map.'), outlink: L._('Define link to open in a new window on polygon click.'), dynamicRemoteData: L._('Fetch data each time map view changes.'), diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 56cd620e..cf5aa938 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -469,6 +469,17 @@ L.U.FeatureMixin = { return false }, + matchAdvancedFilters: function () { + const filters = this.map.options.advancedFilters + for (const [property, expected] of Object.entries(filters)) { + if (expected.length) { + let value = this.properties[property] + if (!value || !expected.includes(value)) return false + } + } + return true + }, + onVertexRawClick: function (e) { new L.Toolbar.Popup(e.latlng, { className: 'leaflet-inplace-toolbar', diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 09317e7e..68a42837 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -285,6 +285,10 @@ L.U.DataLayer = L.Evented.extend({ this.parentPane.appendChild(this.pane) }, + hasDataVisible: function () { + return !!Object.keys(this.layer._layers).length + }, + resetLayer: function (force) { if (this.layer && this.options.type === this.layer._type && !force) return const visible = this.isVisible() @@ -297,6 +301,7 @@ L.U.DataLayer = L.Evented.extend({ filter = this.map.options.filter this.eachLayer(function (layer) { if (filter && !layer.matchFilter(filter, filterKeys)) return + if (!layer.matchAdvancedFilters()) return this.layer.addLayer(layer) }) if (visible) this.map.addLayer(this.layer) From 3d4524268415c278b7e6c24d309032a84ec6e515 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 1 Aug 2023 14:27:23 +0200 Subject: [PATCH 02/10] Remove use of getMap on the map itself --- umap/static/umap/js/umap.controls.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index da996445..cc702182 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -813,14 +813,14 @@ L.U.Map.include({ const advancedFiltersFull = {} let filtersAlreadyLoaded = true - if (!this.getMap().options.advancedFilters) { - this.getMap().options.advancedFilters = {} + if (!this.options.advancedFilters) { + this.options.advancedFilters = {} filtersAlreadyLoaded = false } advancedFilterKeys.forEach((property) => { advancedFiltersFull[property] = [] - if (!filtersAlreadyLoaded || !this.getMap().options.advancedFilters[property]) { - this.getMap().options.advancedFilters[property] = [] + if (!filtersAlreadyLoaded || !this.options.advancedFilters[property]) { + this.options.advancedFilters[property] = [] } }) this.eachBrowsableDataLayer((datalayer) => { @@ -842,10 +842,10 @@ L.U.Map.include({ filter_check.type = 'checkbox' filter_check.id = `checkbox_${property}_${value}` filter_check.checked = - this.getMap().options.advancedFilters[property] && - this.getMap().options.advancedFilters[property].includes(value) - filter_check.setAttribute('data-property', property) - filter_check.setAttribute('data-value', value) + this.options.advancedFilters[property] && + this.options.advancedFilters[property].includes(value) + filter_check.dataset.property = property + filter_check.dataset.value = value filter_label.htmlFor = `checkbox_${property}_${value}` filter_label.innerHTML = value L.DomEvent.on( @@ -855,10 +855,10 @@ L.U.Map.include({ const property = e.srcElement.dataset.property const value = e.srcElement.dataset.value if (e.srcElement.checked) { - this.getMap().options.advancedFilters[property].push(value) + this.options.advancedFilters[property].push(value) } else { - this.getMap().options.advancedFilters[property].splice( - this.getMap().options.advancedFilters[property].indexOf(value), + this.options.advancedFilters[property].splice( + this.options.advancedFilters[property].indexOf(value), 1 ) } From 56cb4b44d057f6729a03e0471287017333ca2309 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 1 Aug 2023 14:42:12 +0200 Subject: [PATCH 03/10] Factorize about link --- umap/static/umap/js/umap.controls.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index cc702182..15d357bb 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -788,12 +788,8 @@ L.U.Map.include({ L.bind(appendAll, this)() L.DomEvent.on(filter, 'input', appendAll, this) L.DomEvent.on(filter, 'input', resetLayers, this) - const link = L.DomUtil.create('li', '') - L.DomUtil.create('i', 'umap-icon-16 umap-caption', link) - const label = L.DomUtil.create('span', '', link) - label.textContent = label.title = L._('About') - L.DomEvent.on(link, 'click', this.displayCaption, this) - this.ui.openPanel({ data: { html: browserContainer }, actions: [link] }) + + this.ui.openPanel({ data: { html: browserContainer }, actions: [this._aboutLink()] }) }, _openFilter: function () { @@ -890,7 +886,8 @@ L.U.Map.include({ datalayer.resetLayer(true) if (datalayer.hasDataVisible()) found = true }) - if (!found) this.ui.alert({content: L._('No results for these filters'), level: 'info'}) + if (!found) + this.ui.alert({ content: L._('No results for these filters'), level: 'info' }) } propertiesContainer.innerHTML = '' @@ -898,12 +895,16 @@ L.U.Map.include({ L.bind(addProperty, this)(property) }) + this.ui.openPanel({ data: { html: filterContainer }, actions: [this._aboutLink()] }) + }, + + _aboutLink: function () { const link = L.DomUtil.create('li', '') L.DomUtil.create('i', 'umap-icon-16 umap-caption', link) const label = L.DomUtil.create('span', '', link) label.textContent = label.title = L._('About') L.DomEvent.on(link, 'click', this.displayCaption, this) - this.ui.openPanel({ data: { html: filterContainer }, actions: [link] }) + return link }, displayCaption: function () { From 9abbfbc01ecd0f9a37132ec2fbdcaf0071cafe6b Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 2 Aug 2023 07:59:37 +0200 Subject: [PATCH 04/10] Use Leaflet.FormBuilder to build advanced filters form --- umap/static/umap/js/umap.controls.js | 113 ++++++++------------------- umap/static/umap/js/umap.features.js | 7 +- umap/static/umap/js/umap.forms.js | 30 ++++++- 3 files changed, 69 insertions(+), 81 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 15d357bb..8550c098 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -789,113 +789,68 @@ L.U.Map.include({ L.DomEvent.on(filter, 'input', appendAll, this) L.DomEvent.on(filter, 'input', resetLayers, this) - this.ui.openPanel({ data: { html: browserContainer }, actions: [this._aboutLink()] }) + this.ui.openPanel({ + data: { html: browserContainer }, + actions: [this._aboutLink()], + }) }, _openFilter: function () { - const filterContainer = L.DomUtil.create('div', 'umap-filter-data'), - title = L.DomUtil.add( - 'h3', - 'umap-filter-title', - filterContainer, - this.options.name - ), + const container = L.DomUtil.create('div', 'umap-filter-data'), + title = L.DomUtil.add('h3', 'umap-filter-title', container, this.options.name), propertiesContainer = L.DomUtil.create( 'div', 'umap-filter-properties', - filterContainer + container ), - advancedFilterKeys = this.getAdvancedFilterKeys() + keys = this.getAdvancedFilterKeys() - const advancedFiltersFull = {} - let filtersAlreadyLoaded = true - if (!this.options.advancedFilters) { - this.options.advancedFilters = {} - filtersAlreadyLoaded = false - } - advancedFilterKeys.forEach((property) => { - advancedFiltersFull[property] = [] - if (!filtersAlreadyLoaded || !this.options.advancedFilters[property]) { - this.options.advancedFilters[property] = [] - } + const knownValues = {} + if (!this.options.advancedFilters) this.options.advancedFilters = {} + + keys.forEach((key) => { + knownValues[key] = [] + if (!this.options.advancedFilters[key]) + this.options.advancedFilters[key] = [] }) + this.eachBrowsableDataLayer((datalayer) => { datalayer.eachFeature((feature) => { - advancedFilterKeys.forEach((property) => { - if (feature.properties[property]) { - if (!advancedFiltersFull[property].includes(feature.properties[property])) { - advancedFiltersFull[property].push(feature.properties[property]) - } + keys.forEach((key) => { + let value = feature.properties[key] + if (typeof value !== 'undefined' && !knownValues[key].includes(value)) { + knownValues[key].push(value) } }) }) }) - const addPropertyValue = function (property, value) { - const property_li = L.DomUtil.create('li', ''), - filter_check = L.DomUtil.create('input', '', property_li), - filter_label = L.DomUtil.create('label', '', property_li) - filter_check.type = 'checkbox' - filter_check.id = `checkbox_${property}_${value}` - filter_check.checked = - this.options.advancedFilters[property] && - this.options.advancedFilters[property].includes(value) - filter_check.dataset.property = property - filter_check.dataset.value = value - filter_label.htmlFor = `checkbox_${property}_${value}` - filter_label.innerHTML = value - L.DomEvent.on( - filter_check, - 'change', - function (e) { - const property = e.srcElement.dataset.property - const value = e.srcElement.dataset.value - if (e.srcElement.checked) { - this.options.advancedFilters[property].push(value) - } else { - this.options.advancedFilters[property].splice( - this.options.advancedFilters[property].indexOf(value), - 1 - ) - } - L.bind(filterFeatures, this)() - }, - this - ) - return property_li - } - - const addProperty = function (property) { - const container = L.DomUtil.create( - 'div', - 'property-container', - propertiesContainer - ), - headline = L.DomUtil.add('h5', '', container, property) - const ul = L.DomUtil.create('ul', '', container) - const orderedValues = advancedFiltersFull[property] - orderedValues.sort() - orderedValues.forEach((value) => { - ul.appendChild(L.bind(addPropertyValue, this)(property, value)) - }) - } - const filterFeatures = function () { let found = false this.eachBrowsableDataLayer((datalayer) => { datalayer.resetLayer(true) if (datalayer.hasDataVisible()) found = true }) + // TODO: display a results counter in the panel instead. if (!found) this.ui.alert({ content: L._('No results for these filters'), level: 'info' }) } - propertiesContainer.innerHTML = '' - advancedFilterKeys.forEach((property) => { - L.bind(addProperty, this)(property) + const fields = keys.map((current) => [ + `options.advancedFilters.${current}`, + { + handler: 'AdvancedFilter', + choices: knownValues[current], + }, + ]) + const builder = new L.U.FormBuilder(this, fields, { + makeDirty: false, + callback: filterFeatures, + callbackContext: this, }) + container.appendChild(builder.build()) - this.ui.openPanel({ data: { html: filterContainer }, actions: [this._aboutLink()] }) + this.ui.openPanel({ data: { html: container }, actions: [this._aboutLink()] }) }, _aboutLink: function () { diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index cf5aa938..2eddf185 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -87,7 +87,12 @@ L.U.FeatureMixin = { edit: function (e) { if (!this.map.editEnabled || this.isReadOnly()) return const container = L.DomUtil.create('div', 'umap-feature-container') - L.DomUtil.add('h3', `umap-feature-properties ${this.getClassName()}`, container, L._('Feature properties')) + L.DomUtil.add( + 'h3', + `umap-feature-properties ${this.getClassName()}`, + container, + L._('Feature properties') + ) let builder = new L.U.FormBuilder(this, ['datalayer'], { callback: function () { diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index bb8e73a4..5bf8eb32 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -684,6 +684,34 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ }, }) +L.FormBuilder.AdvancedFilter = L.FormBuilder.Element.extend({ + build: function () { + this.container = L.DomUtil.create('div', 'property-container', this.parentNode) + this.headline = L.DomUtil.add('h5', '', this.container, this.name) + this.ul = L.DomUtil.create('ul', '', this.container) + const choices = this.options.choices + choices.sort() + choices.forEach((value) => this.buildLi(value)) + }, + + buildLi: function (value) { + const property_li = L.DomUtil.create('li', '', this.ul), + input = L.DomUtil.create('input', '', property_li), + label = L.DomUtil.create('label', '', property_li) + input.type = 'checkbox' + input.id = `checkbox_${this.name}_${value}` + input.checked = this.get().includes(value) + input.dataset.value = value + label.htmlFor = `checkbox_${this.name}_${value}` + label.innerHTML = value + L.DomEvent.on(input, 'change', (e) => this.sync()) + }, + + toJS: function () { + return [...this.ul.querySelectorAll('input:checked')].map(i => i.dataset.value) + }, +}) + L.FormBuilder.MultiChoice = L.FormBuilder.Element.extend({ default: 'null', className: 'umap-multiplechoice', @@ -1125,7 +1153,7 @@ L.U.FormBuilder = L.FormBuilder.extend({ setter: function (field, value) { L.FormBuilder.prototype.setter.call(this, field, value) - this.obj.isDirty = true + if (this.options.makeDirty !== false) this.obj.isDirty = true }, finish: function () { From 81a7bdcd6afcf13648db32892df5c099f8ab28ed Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 2 Aug 2023 08:47:06 +0200 Subject: [PATCH 05/10] Rename advancedFilters to facet search --- umap/static/umap/base.css | 2 +- umap/static/umap/js/umap.controls.js | 33 ++++++++++------------------ umap/static/umap/js/umap.core.js | 4 ++-- umap/static/umap/js/umap.features.js | 6 ++--- umap/static/umap/js/umap.forms.js | 4 ++-- umap/static/umap/js/umap.js | 31 +++++++++++++------------- umap/static/umap/js/umap.layer.js | 2 +- umap/static/umap/map.css | 6 ++--- umap/static/umap/test/DataLayer.js | 6 ++--- 9 files changed, 42 insertions(+), 52 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 32ddccdb..6e3828f7 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -481,7 +481,7 @@ i.info { .umap-datalayer-container, .umap-layer-properties-container, .umap-browse-data, -.umap-filter-data, +.umap-facet-search, .umap-browse-datalayers { padding: 0 10px; } diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 8550c098..63566a60 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -753,11 +753,7 @@ L.U.Map.include({ const build = () => { ul.innerHTML = '' datalayer.eachFeature((feature) => { - if ( - (filterValue && !feature.matchFilter(filterValue, filterKeys)) || - feature.properties.isVisible === false - ) - return + if (filterValue && !feature.matchFilter(filterValue, filterKeys)) return ul.appendChild(addFeature(feature)) }) } @@ -795,23 +791,16 @@ L.U.Map.include({ }) }, - _openFilter: function () { - const container = L.DomUtil.create('div', 'umap-filter-data'), - title = L.DomUtil.add('h3', 'umap-filter-title', container, this.options.name), - propertiesContainer = L.DomUtil.create( - 'div', - 'umap-filter-properties', - container - ), - keys = this.getAdvancedFilterKeys() + _openFacet: function () { + const container = L.DomUtil.create('div', 'umap-facet-search'), + title = L.DomUtil.add('h3', 'umap-filter-title', container, L._('Facet search')), + keys = this.getFacetKeys() const knownValues = {} - if (!this.options.advancedFilters) this.options.advancedFilters = {} keys.forEach((key) => { knownValues[key] = [] - if (!this.options.advancedFilters[key]) - this.options.advancedFilters[key] = [] + if (!this.facets[key]) this.facets[key] = [] }) this.eachBrowsableDataLayer((datalayer) => { @@ -833,13 +822,13 @@ L.U.Map.include({ }) // TODO: display a results counter in the panel instead. if (!found) - this.ui.alert({ content: L._('No results for these filters'), level: 'info' }) + this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) } const fields = keys.map((current) => [ - `options.advancedFilters.${current}`, + `facets.${current}`, { - handler: 'AdvancedFilter', + handler: 'FacetSearch', choices: knownValues[current], }, ]) @@ -941,11 +930,11 @@ L.U.Map.include({ labelBrowser.textContent = labelBrowser.title = L._('Browse data') L.DomEvent.on(browser, 'click', this.openBrowser, this) const actions = [browser] - if (this.options.advancedFilterKey) { + if (this.options.facetKey) { const filter = L.DomUtil.create('li', '') L.DomUtil.create('i', 'umap-icon-16 umap-add', filter) const labelFilter = L.DomUtil.create('span', '', filter) - labelFilter.textContent = labelFilter.title = L._('Select data') + labelFilter.textContent = labelFilter.title = L._('Facet search') L.DomEvent.on(filter, 'click', this.openFilter, this) actions.push(filter) } diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 910c4ae4..77afe2b2 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -568,8 +568,8 @@ L.U.Help = L.Class.extend({ ), slugKey: L._('The name of the property to use as feature unique identifier.'), filterKey: L._('Comma separated list of properties to use when filtering features'), - advancedFilterKey: L._( - 'Comma separated list of properties to use for checkbox filtering' + facetKey: L._( + 'Comma separated list of properties to use for facet search' ), interactive: L._('If false, the polygon will act as a part of the underlying map.'), outlink: L._('Define link to open in a new window on polygon click.'), diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 2eddf185..b22e1491 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -474,9 +474,9 @@ L.U.FeatureMixin = { return false }, - matchAdvancedFilters: function () { - const filters = this.map.options.advancedFilters - for (const [property, expected] of Object.entries(filters)) { + matchFacets: function () { + const facets = this.map.facets + for (const [property, expected] of Object.entries(facets)) { if (expected.length) { let value = this.properties[property] if (!value || !expected.includes(value)) return false diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 5bf8eb32..e825d400 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -431,7 +431,7 @@ L.FormBuilder.OnLoadPanel = L.FormBuilder.Select.extend({ ['none', L._('None')], ['caption', L._('Caption')], ['databrowser', L._('Data browser')], - ['datafilters', L._('Data filters')], + ['facet', L._('Facet search')], ], }) @@ -684,7 +684,7 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ }, }) -L.FormBuilder.AdvancedFilter = L.FormBuilder.Element.extend({ +L.FormBuilder.FacetSearch = L.FormBuilder.Element.extend({ build: function () { this.container = L.DomUtil.create('div', 'property-container', this.parentNode) this.headline = L.DomUtil.add('h5', '', this.container, this.name) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 10f82db3..1412eb04 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -151,12 +151,14 @@ L.U.Map.include({ this.options.slideshow.active === undefined ) this.options.slideshow.active = true + if (this.options.advancedFilterKey) this.options.facetKey = this.options.advancedFilterKey // Global storage for retrieving datalayers and features this.datalayers = {} this.datalayers_index = [] this.dirty_datalayers = [] this.features_index = {} + this.facets = {} if (this.options.hash) this.addHash() this.initControls() @@ -250,7 +252,7 @@ L.U.Map.include({ if (L.Util.queryString('share')) this.renderShareBox() else if (this.options.onLoadPanel === 'databrowser') this.openBrowser() else if (this.options.onLoadPanel === 'caption') this.displayCaption() - else if (this.options.onLoadPanel === 'datafilters') this.openFilter() + else if (this.options.onLoadPanel === 'facet' || this.options.onLoadPanel === 'datafilters') this.openFacet() }) this.onceDataLoaded(function () { const slug = L.Util.queryString('feature') @@ -953,9 +955,9 @@ L.U.Map.include({ }) }, - openFilter: function () { + openFacet: function () { this.onceDatalayersLoaded(function () { - this._openFilter() + this._openFacet() }) }, @@ -1067,7 +1069,7 @@ L.U.Map.include({ 'sortKey', 'labelKey', 'filterKey', - 'advancedFilterKey', + 'facetKey', 'slugKey', 'showLabel', 'labelDirection', @@ -1394,13 +1396,12 @@ L.U.Map.include({ }, ], [ - 'options.advancedFilterKey', + 'options.facetKey', { handler: 'Input', - helpEntries: 'advancedFilterKey', + helpEntries: 'facetKey', placeholder: L._('Example: key1,key2,key3'), - label: L._('Advanced filter keys'), - inheritable: true, + label: L._('Facet keys') }, ], [ @@ -1785,7 +1786,7 @@ L.U.Map.include({ this.openBrowser, this ) - if (this.options.advancedFilterKey) { + if (this.options.facetKey) { const filter = L.DomUtil.add( 'a', 'umap-open-filter-link', @@ -1796,7 +1797,7 @@ L.U.Map.include({ L.DomEvent.on(filter, 'click', L.DomEvent.stop).on( filter, 'click', - this.openFilter, + this.openFacet, this ) } @@ -2014,10 +2015,10 @@ L.U.Map.include({ text: L._('Browse data'), callback: this.openBrowser, }) - if (this.options.advancedFilterKey) { + if (this.options.facetKey) { items.push({ - text: L._('Select data'), - callback: this.openFilter, + text: L._('Facet search'), + callback: this.openFacet, }) } items.push( @@ -2102,8 +2103,8 @@ L.U.Map.include({ return (this.options.filterKey || this.options.sortKey || 'name').split(',') }, - getAdvancedFilterKeys: function () { - return (this.options.advancedFilterKey || '').split(',') + getFacetKeys: function () { + return (this.options.facetKey || '').split(',') }, getLayersBounds: function () { diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 68a42837..f6aecced 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -301,7 +301,7 @@ L.U.DataLayer = L.Evented.extend({ filter = this.map.options.filter this.eachLayer(function (layer) { if (filter && !layer.matchFilter(filter, filterKeys)) return - if (!layer.matchAdvancedFilters()) return + if (!layer.matchFacets()) return this.layer.addLayer(layer) }) if (visible) this.map.addLayer(this.layer) diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index ef430474..ceacc7e9 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -805,7 +805,7 @@ a.add-datalayer:hover, margin-bottom: 14px; border-radius: 2px; } -.umap-browse-features h5, .umap-filter-data h5 { +.umap-browse-features h5, .umap-facet-search h5 { margin-bottom: 0; overflow: hidden; padding-left: 5px; @@ -822,7 +822,7 @@ a.add-datalayer:hover, .umap-browse-features li { padding: 2px 0; } -.umap-browse-features li, .umap-filter-data li { +.umap-browse-features li, .umap-facet-search li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -855,7 +855,7 @@ a.add-datalayer:hover, .umap-browse-features .polygon .feature-color { background-position: -32px -16px; } -.umap-filter-data .property-container:not(:first-child) { +.umap-facet-search .property-container:not(:first-child) { margin-top: 14px; } .show-on-edit { diff --git a/umap/static/umap/test/DataLayer.js b/umap/static/umap/test/DataLayer.js index 59d9b1ad..db92bec6 100644 --- a/umap/static/umap/test/DataLayer.js +++ b/umap/static/umap/test/DataLayer.js @@ -409,7 +409,7 @@ describe('L.U.DataLayer', function () { /\/datalayer\/63\/\?.*/, JSON.stringify(RESPONSES.datalayer63_GET) ) - this.map.options.advancedFilterKey = 'name' + this.map.options.facetKey = 'name' this.map.createDataLayer(RESPONSES.datalayer63_GET._umap_options) this.server.respond() }) @@ -417,8 +417,8 @@ describe('L.U.DataLayer', function () { assert.ok(qs('path[fill="SteelBlue"]')) }) it('should allow advanced filter', function () { - this.map.openFilter() - assert.ok(qs('div.umap-filter-properties')) + this.map.openFacet() + assert.ok(qs('div.umap-facet-search')) // This one if from the normal datalayer // it's name is "test", so it should be hidden // by the filter From d188525c7416567cf279ca8ef028c575643acc6f Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 10 Aug 2023 08:16:03 +0200 Subject: [PATCH 06/10] Compute _layers for MarkerCluster, so to easily know if layer has data --- umap/static/umap/js/umap.layer.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index f6aecced..06035165 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -10,6 +10,10 @@ L.U.Layer = { }, postUpdate: function () {}, + + hasDataVisible: function () { + return !!Object.keys(this._layers).length + }, } L.U.Layer.Default = L.FeatureGroup.extend({ @@ -53,6 +57,17 @@ L.U.Layer.Cluster = L.MarkerClusterGroup.extend({ } L.MarkerClusterGroup.prototype.initialize.call(this, options) this._markerCluster = L.U.MarkerCluster + this._layers = [] + }, + + addLayer: function (layer) { + this._layers.push(layer) + return L.MarkerClusterGroup.prototype.addLayer.call(this, layer) + }, + + removeLayer: function (layer) { + this._layers.splice(this._layers.indexOf(layer), 1) + return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer) }, getEditableOptions: function () { @@ -286,7 +301,7 @@ L.U.DataLayer = L.Evented.extend({ }, hasDataVisible: function () { - return !!Object.keys(this.layer._layers).length + return this.layer.hasDataVisible() }, resetLayer: function (force) { From 80e62f3046d1387ebaf29886ab1945f99d0b62c8 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Thu, 10 Aug 2023 10:10:00 +0200 Subject: [PATCH 07/10] Better CSS for facet search --- umap/static/umap/map.css | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index ceacc7e9..4abeb02c 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -800,6 +800,7 @@ a.add-datalayer:hover, /* Features browser panel */ /* ********************************* */ +.umap-facet-search .formbox, .umap-browse-features > div { border: 1px solid #d3d3d3; margin-bottom: 14px; @@ -809,8 +810,6 @@ a.add-datalayer:hover, margin-bottom: 0; overflow: hidden; padding-left: 5px; -} -.umap-browse-features h5 { height: 30px; line-height: 30px; background-color: #eeeee0; @@ -819,14 +818,13 @@ a.add-datalayer:hover, .umap-browse-features h5 span { margin-left: 10px; } -.umap-browse-features li { - padding: 2px 0; -} .umap-browse-features li, .umap-facet-search li { + padding: 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.umap-facet-search li:nth-child(even), .umap-browse-features li:nth-child(even) { background-color: #f8f8f3; } From b013692527c4ea07546655053ac4165a32407a95 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 8 Aug 2023 08:20:34 +0200 Subject: [PATCH 08/10] Make sure filters are called before actually displaying a feature on the map fix #1005 --- umap/static/umap/js/umap.layer.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 06035165..590d69d3 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -312,13 +312,7 @@ L.U.DataLayer = L.Evented.extend({ if (visible) this.map.removeLayer(this.layer) const Class = L.U.Layer[this.options.type] || L.U.Layer.Default this.layer = new Class(this) - const filterKeys = this.map.getFilterKeys(), - filter = this.map.options.filter - this.eachLayer(function (layer) { - if (filter && !layer.matchFilter(filter, filterKeys)) return - if (!layer.matchFacets()) return - this.layer.addLayer(layer) - }) + this.eachLayer((feature) => this.showFeature(feature)) if (visible) this.map.addLayer(this.layer) this.propagateRemote() }, @@ -498,15 +492,23 @@ L.U.DataLayer = L.Evented.extend({ return this.options.type === 'Cluster' }, + showFeature: function (feature) { + const filterKeys = this.map.getFilterKeys(), + filter = this.map.options.filter + if (filter && !feature.matchFilter(filter, filterKeys)) return + if (!feature.matchFacets()) return + this.layer.addLayer(feature) + }, + addLayer: function (feature) { const id = L.stamp(feature) feature.connectToDataLayer(this) this._index.push(id) this._layers[id] = feature - this.layer.addLayer(feature) this.indexProperties(feature) - if (this.hasDataLoaded()) this.fire('datachanged') this.map.features_index[feature.getSlug()] = feature + this.showFeature(feature) + if (this.hasDataLoaded()) this.fire('datachanged') }, removeLayer: function (feature) { From d2c3b8694b41d4ff37fe81ad18fe02889f7d734c Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 11 Aug 2023 06:39:14 +0200 Subject: [PATCH 09/10] Allow to control facet labels --- umap/static/umap/js/umap.controls.js | 3 ++- umap/static/umap/js/umap.core.js | 2 +- umap/static/umap/js/umap.forms.js | 7 +++++-- umap/static/umap/js/umap.js | 6 +++++- umap/static/umap/map.css | 3 --- umap/static/umap/test/DataLayer.js | 14 +++++++++----- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 63566a60..43260c1f 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -794,7 +794,7 @@ L.U.Map.include({ _openFacet: function () { const container = L.DomUtil.create('div', 'umap-facet-search'), title = L.DomUtil.add('h3', 'umap-filter-title', container, L._('Facet search')), - keys = this.getFacetKeys() + keys = Object.keys(this.getFacetKeys()) const knownValues = {} @@ -830,6 +830,7 @@ L.U.Map.include({ { handler: 'FacetSearch', choices: knownValues[current], + label: this.getFacetKeys()[current], }, ]) const builder = new L.U.FormBuilder(this, fields, { diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 77afe2b2..f1827966 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -569,7 +569,7 @@ L.U.Help = L.Class.extend({ slugKey: L._('The name of the property to use as feature unique identifier.'), filterKey: L._('Comma separated list of properties to use when filtering features'), facetKey: L._( - 'Comma separated list of properties to use for facet search' + 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkeyOther Key)' ), interactive: L._('If false, the polygon will act as a part of the underlying map.'), outlink: L._('Define link to open in a new window on polygon click.'), diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index e825d400..e3569a2d 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -686,14 +686,17 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({ L.FormBuilder.FacetSearch = L.FormBuilder.Element.extend({ build: function () { - this.container = L.DomUtil.create('div', 'property-container', this.parentNode) - this.headline = L.DomUtil.add('h5', '', this.container, this.name) + this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode) this.ul = L.DomUtil.create('ul', '', this.container) const choices = this.options.choices choices.sort() choices.forEach((value) => this.buildLi(value)) }, + buildLabel: function () { + this.label = L.DomUtil.add('h5', '', this.parentNode, this.options.label); + }, + buildLi: function (value) { const property_li = L.DomUtil.create('li', '', this.ul), input = L.DomUtil.create('input', '', property_li), diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 1412eb04..184e33e9 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -2104,7 +2104,11 @@ L.U.Map.include({ }, getFacetKeys: function () { - return (this.options.facetKey || '').split(',') + return (this.options.facetKey || '').split(',').reduce((acc, curr) => { + const els = curr.split("|") + acc[els[0]] = els[1] || els[0] + return acc + }, {}) }, getLayersBounds: function () { diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 4abeb02c..852e4bdc 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -853,9 +853,6 @@ a.add-datalayer:hover, .umap-browse-features .polygon .feature-color { background-position: -32px -16px; } -.umap-facet-search .property-container:not(:first-child) { - margin-top: 14px; -} .show-on-edit { display: none!important; } diff --git a/umap/static/umap/test/DataLayer.js b/umap/static/umap/test/DataLayer.js index db92bec6..85c91628 100644 --- a/umap/static/umap/test/DataLayer.js +++ b/umap/static/umap/test/DataLayer.js @@ -403,7 +403,7 @@ describe('L.U.DataLayer', function () { assert.ok(qs('path[fill="DarkGoldenRod"]')) }) }) - describe('#advanced-filters()', function () { + describe('#facet-search()', function () { before(function () { this.server.respondWith( /\/datalayer\/63\/\?.*/, @@ -413,7 +413,7 @@ describe('L.U.DataLayer', function () { this.map.createDataLayer(RESPONSES.datalayer63_GET._umap_options) this.server.respond() }) - it('should show non browsable layer', function () { + it('should not impact non browsable layer', function () { assert.ok(qs('path[fill="SteelBlue"]')) }) it('should allow advanced filter', function () { @@ -428,10 +428,15 @@ describe('L.U.DataLayer', function () { // This one comes from a non browsable layer // so it should not be affected by the filter assert.ok(qs('path[fill="SteelBlue"]')) - happen.click(qs('input[data-value="name poly"]')) // Undo + happen.click(qs('input[data-value="name poly"]')) // Undo + }) + it('should allow to control facet label', function () { + this.map.options.facetKey = 'name|Nom' + this.map.openFacet() + assert.ok(qs('div.umap-facet-search h5')) + assert.equal(qs('div.umap-facet-search h5').textContent, 'Nom') }) }) - describe('#zoomEnd', function () { it('should honour the fromZoom option', function () { this.map.setZoom(6, {animate: false}) @@ -453,5 +458,4 @@ describe('L.U.DataLayer', function () { assert.ok(qs('path[fill="none"]')) }) }) - }) From 379d75239dd250f1cb4b3420521eb422766a1990 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 12 Aug 2023 06:31:06 +0200 Subject: [PATCH 10/10] Update umap/static/umap/js/umap.core.js Co-authored-by: David Larlet <3556+davidbgk@users.noreply.github.com> --- umap/static/umap/js/umap.core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index f1827966..2a9aeb03 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -569,7 +569,7 @@ L.U.Help = L.Class.extend({ slugKey: L._('The name of the property to use as feature unique identifier.'), filterKey: L._('Comma separated list of properties to use when filtering features'), facetKey: L._( - 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkeyOther Key)' + 'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key)' ), interactive: L._('If false, the polygon will act as a part of the underlying map.'), outlink: L._('Define link to open in a new window on polygon click.'),