From d69f965f790653bcc6358694f8dd9eb496aa1dd6 Mon Sep 17 00:00:00 2001 From: 3st3ban3 Date: Sat, 14 Jan 2023 22:31:39 +0100 Subject: [PATCH] Filters checkboxes using features' properties --- umap/static/umap/base.css | 1 + umap/static/umap/js/umap.controls.js | 110 ++++++++++++++++++++++++++- umap/static/umap/js/umap.core.js | 2 + umap/static/umap/js/umap.forms.js | 3 +- umap/static/umap/js/umap.js | 32 +++++++- umap/static/umap/map.css | 13 +++- 6 files changed, 153 insertions(+), 8 deletions(-) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index e4d9eed4..8e4c928f 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -508,6 +508,7 @@ i.info { .umap-layer-properties-container, .umap-footer-container, .umap-browse-data, +.umap-filter-data, .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 b7b9bff7..4884304a 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -695,7 +695,7 @@ L.U.Map.include({ var build = function () { ul.innerHTML = ''; datalayer.eachFeature(function (feature) { - if (filterValue && !feature.matchFilter(filterValue, filterKeys)) return; + if ((filterValue && !feature.matchFilter(filterValue, filterKeys)) || feature.properties.isVisible === false) return; ul.appendChild(addFeature(feature)); }); }; @@ -732,6 +732,114 @@ L.U.Map.include({ label.textContent = label.title = L._('About'); L.DomEvent.on(link, 'click', this.displayCaption, this); this.ui.openPanel({data: {html: browserContainer}, actions: [link]}); + }, + + _openFilter: function () { + var filterContainer = L.DomUtil.create('div', 'umap-filter-data'), + title = L.DomUtil.add('h3', 'umap-filter-title', filterContainer, this.options.name), + propertiesContainer = L.DomUtil.create('div', 'umap-filter-properties', filterContainer), + advancedFilterKeys = this.getAdvancedFilterKeys(); + + var advancedFiltersFull = {}; + var filtersAlreadyLoaded = true; + if (!this.getMap().options.advancedFilters) { + this.getMap().options.advancedFilters = {}; + filtersAlreadyLoaded = false; + } + advancedFilterKeys.forEach(property => { + advancedFiltersFull[property] = []; + if (!filtersAlreadyLoaded) { + this.getMap().options.advancedFilters[property] = []; + } + }); + this.eachDataLayer(function (datalayer) { + datalayer.eachFeature(function (feature) { + advancedFilterKeys.forEach(property => { + if (feature.properties[property]) { + if (!advancedFiltersFull[property].includes(feature.properties[property])) { + advancedFiltersFull[property].push(feature.properties[property]); + } + } + }); + }); + }); + + var addPropertyValue = function (property, value) { + var 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.getMap().options.advancedFilters[property] && this.getMap().options.advancedFilters[property].includes(value); + filter_check.setAttribute('data-property', property); + filter_check.setAttribute('data-value', value); + filter_label.htmlFor = `checkbox_${property}_${value}`; + filter_label.innerHTML = value; + L.DomEvent.on(filter_check, 'change', function (e) { + var property = e.srcElement.dataset.property; + var value = e.srcElement.dataset.value; + if (e.srcElement.checked) { + this.getMap().options.advancedFilters[property].push(value); + } else { + this.getMap().options.advancedFilters[property].splice(this.getMap().options.advancedFilters[property].indexOf(value), 1); + } + L.bind(filterFeatures, this)(); + }, this); + return property_li + }; + + var addProperty = function (property) { + var container = L.DomUtil.create('div', 'property-container', propertiesContainer), + headline = L.DomUtil.add('h5', '', container, property); + var ul = L.DomUtil.create('ul', '', container); + var orderedValues = advancedFiltersFull[property]; + orderedValues.sort(); + orderedValues.forEach(value => { + ul.appendChild(L.bind(addPropertyValue, this)(property, value)); + }); + }; + + var filterFeatures = function () { + var noResults = true; + this.eachDataLayer(function (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'); + } + }); + }); + if (noResults) { + this.help.show('advancedFiltersNoResults'); + } else { + this.help.hide(); + } + }; + + propertiesContainer.innerHTML = ''; + advancedFilterKeys.forEach(property => { + L.bind(addProperty, this)(property); + }); + + var link = L.DomUtil.create('li', ''); + L.DomUtil.create('i', 'umap-icon-16 umap-caption', link); + var 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] }); } }); diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 02afe2cd..a6201360 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -455,6 +455,8 @@ L.U.Help = L.Class.extend({ sortKey: L._('Property to use for sorting features'), 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'), + 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.forms.js b/umap/static/umap/js/umap.forms.js index 88c03703..0067b4c6 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -287,7 +287,8 @@ L.FormBuilder.onLoadPanel = L.FormBuilder.Select.extend({ selectOptions: [ ['none', L._('None')], ['caption', L._('Caption')], - ['databrowser', L._('Data browser')] + ['databrowser', L._('Data browser')], + ['datafilters', L._('Data filters')] ] }); diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 92ca44a2..4222ef49 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -210,6 +210,7 @@ L.U.Map.include({ this.onceDatalayersLoaded(function () { if (this.options.onLoadPanel === 'databrowser') this.openBrowser(); else if (this.options.onLoadPanel === 'caption') this.displayCaption(); + else if (this.options.onLoadPanel === 'datafilters') this.openFilter(); }); this.onceDataLoaded(function () { const slug = L.Util.queryString('feature'); @@ -893,6 +894,12 @@ L.U.Map.include({ }); }, + openFilter: function () { + this.onceDatalayersLoaded(function () { + this._openFilter(); + }); + }, + displayCaption: function () { var container = L.DomUtil.create('div', 'umap-caption'), title = L.DomUtil.create('h3', '', container); @@ -948,10 +955,15 @@ L.U.Map.include({ umapCredit.innerHTML = L._('Powered by Leaflet and Django, glued by uMap project.', urls); var browser = L.DomUtil.create('li', ''); L.DomUtil.create('i', 'umap-icon-16 umap-list', browser); - var label = L.DomUtil.create('span', '', browser); - label.textContent = label.title = L._('Browse data'); + var labelBrowser = L.DomUtil.create('span', '', browser); + labelBrowser.textContent = labelBrowser.title = L._('Browse data'); L.DomEvent.on(browser, 'click', this.openBrowser, this); - this.ui.openPanel({data: {html: container}, actions: [browser]}); + var filter = L.DomUtil.create('li', ''); + L.DomUtil.create('i', 'umap-icon-16 umap-add', filter); + var labelFilter = L.DomUtil.create('span', '', filter); + labelFilter.textContent = labelFilter.title = L._('Filter data'); + L.DomEvent.on(filter, 'click', this.openFilter, this); + this.ui.openPanel({data: {html: container}, actions: [browser, filter]}); }, eachDataLayer: function (method, context) { @@ -1057,6 +1069,7 @@ L.U.Map.include({ 'sortKey', 'labelKey', 'filterKey', + 'advancedFilterKey', 'slugKey', 'showLabel', 'labelDirection', @@ -1253,6 +1266,7 @@ L.U.Map.include({ 'options.labelKey', ['options.sortKey', {handler: 'BlurInput', helpEntries: 'sortKey', placeholder: L._('Default: name'), label: L._('Sort key'), inheritable: true}], ['options.filterKey', {handler: 'Input', helpEntries: 'filterKey', placeholder: L._('Default: name'), label: L._('Filter keys'), inheritable: true}], + ['options.advancedFilterKey', {handler: 'Input', helpEntries: 'advancedFilterKey', placeholder: L._('Example: key1,key2,key3'), label: L._('Advanced filter keys'), inheritable: true}], ['options.slugKey', {handler: 'BlurInput', helpEntries: 'slugKey', placeholder: L._('Default: name'), label: L._('Feature identifier key')}] ]; @@ -1435,6 +1449,10 @@ L.U.Map.include({ browser.href = '#'; L.DomEvent.on(browser, 'click', L.DomEvent.stop) .on(browser, 'click', this.openBrowser, this); + var filter = L.DomUtil.add('a', 'umap-open-filter-link', container, ' | ' + L._('Filter data')); + filter.href = '#'; + L.DomEvent.on(filter, 'click', L.DomEvent.stop) + .on(filter, 'click', this.openFilter, this); var setName = function () { name.textContent = this.getDisplayName(); }; @@ -1620,6 +1638,10 @@ L.U.Map.include({ text: L._('Browse data'), callback: this.openBrowser }, + { + text: L._('Filter data'), + callback: this.openFilter + }, { text: L._('About'), callback: this.displayCaption @@ -1698,6 +1720,10 @@ L.U.Map.include({ getFilterKeys: function () { return (this.options.filterKey || this.options.sortKey || 'name').split(','); + }, + + getAdvancedFilterKeys: function () { + return (this.options.advancedFilterKey || '').split(","); } }); diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 707821c7..c57f4596 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -729,20 +729,24 @@ a.add-datalayer:hover, margin-bottom: 14px; border-radius: 2px; } +.umap-browse-features h5, .umap-filter-data h5 { + margin-bottom: 0; + overflow: hidden; + padding-left: 5px; +} .umap-browse-features h5 { height: 30px; line-height: 30px; background-color: #eeeee0; - margin-bottom: 0; color: #666; - overflow: hidden; - padding-left: 5px; } .umap-browse-features h5 span { margin-left: 10px; } .umap-browse-features li { padding: 2px 0; +} +.umap-browse-features li, .umap-filter-data li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -775,6 +779,9 @@ a.add-datalayer:hover, .umap-browse-features .polygon .feature-color { background-position: -32px -16px; } +.umap-filter-data .property-container:not(:first-child) { + margin-top: 14px; +} .show-on-edit { display: none!important; }