From aa78b13f8e4ade77e991ecccf376df8ec53d0883 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 3 May 2024 12:53:06 +0200 Subject: [PATCH] feat: integrate facets into browser filters --- umap/static/umap/js/modules/browser.js | 46 +++++++++++++++---- umap/static/umap/js/modules/facets.js | 25 +--------- umap/static/umap/js/modules/schema.js | 6 +-- umap/static/umap/js/umap.controls.js | 3 -- umap/static/umap/js/umap.core.js | 1 + umap/static/umap/js/umap.features.js | 8 ++++ umap/static/umap/js/umap.js | 30 +++++------- umap/static/umap/js/umap.layer.js | 5 +- umap/static/umap/map.css | 4 +- umap/tests/integration/test_browser.py | 7 +++ umap/tests/integration/test_facets_browser.py | 37 +++++++++------ 11 files changed, 95 insertions(+), 77 deletions(-) diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index f68dc457..2c835431 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -9,11 +9,24 @@ export default class Browser { filter: '', inBbox: false, } + this._mode = 'layers' + } + + set mode(value) { + // Force only if mode is known, otherwise keep current mode. + if (!value) return + // Store the mode so we can respect it when we redraw + if (['data', 'filters'].includes(value)) this.map.panel.mode = 'expanded' + else if (value === 'layers') this.map.panel.mode = 'condensed' + this._mode = value + } + + get mode() { + return this._mode } addFeature(feature, parent) { - const filter = this.options.filter - if (filter && !feature.matchFilter(filter, this.filterKeys)) return + if (feature.isFiltered()) return if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return const row = DomUtil.create('li', `${feature.getClassName()} feature`) const zoom_to = DomUtil.createButtonIcon( @@ -105,6 +118,10 @@ export default class Browser { }) } + redraw() { + if (this.isOpen()) this.open() + } + isOpen() { return !!document.querySelector('.umap-browser') } @@ -126,7 +143,8 @@ export default class Browser { }) } - open() { + open(mode) { + this.mode = mode // Get once but use it for each feature later this.filterKeys = this.map.getFilterKeys() const container = DomUtil.create('div') @@ -136,17 +154,29 @@ export default class Browser { DomUtil.createTitle(container, translate('Browse data'), 'icon-layers') this.tabsMenu(container, 'browse') - const formContainer = DomUtil.create('div', '', container) + const formContainer = DomUtil.createFieldset(container, L._('Filters'), { + on: this.mode === 'filters', + }) this.dataContainer = DomUtil.create('div', '', container) - const fields = [ - ['options.filter', { handler: 'Input', placeholder: translate('Filter') }], + let fields = [ + [ + 'options.filter', + { handler: 'Input', placeholder: translate('Search map features…') }, + ], ['options.inBbox', { handler: 'Switch', label: translate('Current map view') }], ] const builder = new L.FormBuilder(this, fields, { callback: () => this.onFormChange(), }) formContainer.appendChild(builder.build()) + if (this.map.options.facetKey) { + fields = this.map.facets.build() + const builder = new L.FormBuilder(this.map.facets, fields, { + callback: () => this.onFormChange(), + }) + formContainer.appendChild(builder.build()) + } this.map.panel.open({ content: container, @@ -171,10 +201,6 @@ export default class Browser { const tabs = L.DomUtil.create('div', 'flat-tabs', container) const browse = L.DomUtil.add('button', 'flat tab-browse', tabs, L._('Data')) DomEvent.on(browse, 'click', this.open, this) - if (this.map.options.facetKey) { - const facets = L.DomUtil.add('button', 'flat tab-facets', tabs, L._('Filters')) - DomEvent.on(facets, 'click', this.map.facets.open, this.map.facets) - } const info = L.DomUtil.add('button', 'flat tab-info', tabs, L._('About')) DomEvent.on(info, 'click', this.map.displayCaption, this.map) let el = tabs.querySelector(`.tab-${active}`) diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index 00900b7b..8e736fd4 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -53,23 +53,8 @@ export default class Facets { return properties } - redraw() { - if (this.isOpen()) this.open() - } + build() { - isOpen() { - return !!document.querySelector('.umap-facet-search') - } - - open() { - const container = L.DomUtil.create('div', 'umap-facet-search') - const title = L.DomUtil.add( - 'h3', - 'umap-filter-title', - container, - translate('Facet search') - ) - this.map.browser.tabsMenu(container, 'facets') const defined = this.getDefined() const names = Object.keys(defined) const facetProperties = this.compute(names, defined) @@ -114,13 +99,7 @@ export default class Facets { ] }) - const builder = new L.FormBuilder(this, fields, { - callback: filterFeatures, - callbackContext: this, - }) - container.appendChild(builder.build()) - - this.map.panel.open({ content: container }) + return fields } getDefined() { diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js index e733739b..30bc9e3f 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -272,9 +272,9 @@ export const SCHEMA = { choices: [ ['none', translate('None')], ['caption', translate('Caption')], - ['databrowser', translate('Data browser')], - ['datalayers', translate('Layers')], - ['facet', translate('Facet search')], + ['databrowser', translate('Browser in data mode')], + ['datalayers', translate('Browser in layers mode')], + ['datafilters', translate('Browser in filters mode')], ], default: 'none', }, diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 8948489d..0cf90bb0 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -660,9 +660,6 @@ const ControlsMixin = { 'star', 'tilelayers', ], - _openFacet: function () { - this.facets.open() - }, displayCaption: function () { const container = L.DomUtil.create('div', 'umap-caption') diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 156d15c7..988757b9 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -84,6 +84,7 @@ L.DomUtil.createFieldset = (container, legend, options) => { const fieldset = L.DomUtil.create('div', 'fieldset toggle', container) const legendEl = L.DomUtil.add('h5', 'legend style_options_toggle', fieldset, legend) const fieldsEl = L.DomUtil.add('div', 'fields with-transition', fieldset) + L.DomUtil.classIf(fieldset, 'on', options.on) L.DomEvent.on(legendEl, 'click', function () { if (L.DomUtil.hasClass(fieldset, 'on')) { L.DomUtil.removeClass(fieldset, 'on') diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 5d4b0d86..0daaf553 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -494,6 +494,14 @@ U.FeatureMixin = { this.bindTooltip(U.Utils.escapeHTML(displayName), options) }, + isFiltered: function () { + const filterKeys = this.map.getFilterKeys(), + filter = this.map.browser.options.filter + if (filter && !this.matchFilter(filter, filterKeys)) return true + if (!this.matchFacets()) return true + return false + }, + matchFilter: function (filter, keys) { filter = filter.toLowerCase() for (let i = 0, value; i < keys.length; i++) { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index f11054bb..e7e02465 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -211,15 +211,15 @@ U.Map = L.Map.extend({ if (L.Util.queryString('share')) { this.share.open() } else if (this.options.onLoadPanel === 'databrowser') { - this.openBrowser('expanded') + this.openBrowser('data') } else if (this.options.onLoadPanel === 'datalayers') { - this.openBrowser('condensed') + this.openBrowser('layers') } else if (this.options.onLoadPanel === 'caption') { this.panel.mode = 'condensed' this.displayCaption() } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { this.panel.mode = 'expanded' - this.openFacet() + this.openBrowser('filters') } if (L.Util.queryString('edit')) { if (this.hasEditMode()) this.enableEdit() @@ -252,7 +252,7 @@ U.Map = L.Map.extend({ this.initCaptionBar() this.renderEditToolbar() this.renderControls() - this.facets.redraw() + this.browser.redraw() break case 'data': this.redrawVisibleDataLayers() @@ -908,15 +908,8 @@ U.Map = L.Map.extend({ }, openBrowser: function (mode) { - if (mode) this.panel.mode = mode this.onceDatalayersLoaded(function () { - this.browser.open() - }) - }, - - openFacet: function () { - this.onceDataLoaded(function () { - this._openFacet() + this.browser.open(mode) }) }, @@ -1602,15 +1595,14 @@ U.Map = L.Map.extend({ 'umap-open-browser-link flat', container, L._('Browse data'), - () => this.openBrowser('expanded') + () => this.openBrowser('data') ) if (this.options.facetKey) { L.DomUtil.createButton( 'umap-open-filter-link flat', container, L._('Select data'), - this.openFacet, - this + () => this.openBrowser('filters') ) } } @@ -1747,17 +1739,17 @@ U.Map = L.Map.extend({ '-', { text: L._('See layers'), - callback: () => this.openBrowser('condensed'), + callback: () => this.openBrowser('layers'), }, { text: L._('Browse data'), - callback: () => this.openBrowser('expanded'), + callback: () => this.openBrowser('data'), } ) if (this.options.facetKey) { items.push({ - text: L._('Facet search'), - callback: this.openFacet, + text: L._('Filter data'), + callback: () => this.openBrowser('filters'), }) } items.push( diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 8a137e8b..b346f40b 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -875,10 +875,7 @@ U.DataLayer = L.Evented.extend({ }, showFeature: function (feature) { - const filterKeys = this.map.getFilterKeys(), - filter = this.map.browser.options.filter - if (filter && !feature.matchFilter(filter, filterKeys)) return - if (!feature.matchFacets()) return + if (feature.isFiltered()) return this.layer.addLayer(feature) }, diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 666694f1..af5f7e20 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -888,10 +888,10 @@ a.umap-control-caption, .umap-browser .datalayer i { cursor: pointer; } -.umap-browser ul { +.umap-browser .datalayer ul { display: none; } -.show-list ul { +.umap-browser .show-list ul { display: block; } diff --git a/umap/tests/integration/test_browser.py b/umap/tests/integration/test_browser.py index 11a31460..fcb35d96 100644 --- a/umap/tests/integration/test_browser.py +++ b/umap/tests/integration/test_browser.py @@ -77,6 +77,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): paths = page.locator(".leaflet-overlay-pane path") expect(markers).to_have_count(1) expect(paths).to_have_count(2) + page.get_by_role("heading", name="filters").click() filter_ = page.locator("input[name='filter']") expect(filter_).to_be_visible() filter_.type("poly") @@ -103,6 +104,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): def test_data_browser_can_show_only_visible_features(live_server, page, bootstrap, map): # Zoom on France page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -114,6 +116,7 @@ def test_data_browser_can_show_only_visible_features(live_server, page, bootstra def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map): # Zoom on north west page.goto(f"{live_server.url}{map.get_absolute_url()}#4/61.98/-2.68") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -131,6 +134,7 @@ def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map) def test_data_browser_bbox_limit_should_be_dynamic(live_server, page, bootstrap, map): # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -156,6 +160,7 @@ def test_data_browser_bbox_filter_should_be_persistent( ): # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -181,6 +186,7 @@ def test_data_browser_bbox_filtered_is_clickable(live_server, page, bootstrap, m popup = page.locator(".leaflet-popup") # Zoom on Europe page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") + page.get_by_role("heading", name="filters").click() el = page.get_by_text("Current map view") expect(el).to_be_visible() el.click() @@ -202,6 +208,7 @@ def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map): expect(page.get_by_text("one point in france (point)")).to_be_visible() expect(page.get_by_text("one line in new zeland (line)")).to_be_visible() expect(page.get_by_text("one polygon in greenland (polygon)")).to_be_visible() + page.get_by_role("heading", name="filters").click() filter_ = page.locator("input[name='filter']") expect(filter_).to_be_visible() filter_.type("foobar") # Hide all diff --git a/umap/tests/integration/test_facets_browser.py b/umap/tests/integration/test_facets_browser.py index f72d3124..371152b8 100644 --- a/umap/tests/integration/test_facets_browser.py +++ b/umap/tests/integration/test_facets_browser.py @@ -93,7 +93,7 @@ DATALAYER_DATA3 = { def test_simple_facet_search(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number" map.settings["properties"]["showLabel"] = True map.save() @@ -101,7 +101,7 @@ def test_simple_facet_search(live_server, page, map): DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA3) page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") - panel = page.locator(".umap-facet-search") + panel = page.locator(".umap-browser") # From a non browsable datalayer, should not be impacted paths = page.locator(".leaflet-overlay-pane path") expect(paths).to_be_visible() @@ -117,17 +117,28 @@ def test_simple_facet_search(live_server, page, map): markers = page.locator(".leaflet-marker-icon") expect(markers).to_have_count(4) # Tooltips - expect(page.get_by_text("Point 1")).to_be_visible() - expect(page.get_by_text("Point 2")).to_be_visible() - expect(page.get_by_text("Point 3")).to_be_visible() - expect(page.get_by_text("Point 4")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 1")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 2")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 3")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 4")).to_be_visible() + + # Datalist + expect(panel.get_by_text("Point 1")).to_be_visible() + expect(panel.get_by_text("Point 2")).to_be_visible() + expect(panel.get_by_text("Point 3")).to_be_visible() + expect(panel.get_by_text("Point 4")).to_be_visible() + # Now let's filter odd.click() expect(markers).to_have_count(2) - expect(page.get_by_text("Point 2")).to_be_hidden() - expect(page.get_by_text("Point 4")).to_be_hidden() - expect(page.get_by_text("Point 1")).to_be_visible() - expect(page.get_by_text("Point 3")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 2")).to_be_hidden() + expect(page.get_by_role("tooltip", name="Point 4")).to_be_hidden() + expect(page.get_by_role("tooltip", name="Point 1")).to_be_visible() + expect(page.get_by_role("tooltip", name="Point 3")).to_be_visible() + expect(panel.get_by_text("Point 2")).to_be_hidden() + expect(panel.get_by_text("Point 4")).to_be_hidden() + expect(panel.get_by_text("Point 1")).to_be_visible() + expect(panel.get_by_text("Point 3")).to_be_visible() expect(paths).to_be_visible # Now let's filter odd.click() @@ -156,7 +167,7 @@ def test_simple_facet_search(live_server, page, map): def test_date_facet_search(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mydate|Date filter|date" map.save() DataLayerFactory(map=map, data=DATALAYER_DATA1) @@ -174,7 +185,7 @@ def test_date_facet_search(live_server, page, map): def test_choice_with_empty_value(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mytype|My type" map.save() data = copy.deepcopy(DATALAYER_DATA1) @@ -191,7 +202,7 @@ def test_choice_with_empty_value(live_server, page, map): def test_number_with_zero_value(live_server, page, map): - map.settings["properties"]["onLoadPanel"] = "facet" + map.settings["properties"]["onLoadPanel"] = "datafilters" map.settings["properties"]["facetKey"] = "mynumber|Filter|number" map.save() data = copy.deepcopy(DATALAYER_DATA1)