feat: integrate facets into browser filters

This commit is contained in:
Yohan Boniface 2024-05-03 12:53:06 +02:00
parent 8679c0ded8
commit aa78b13f8e
11 changed files with 95 additions and 77 deletions

View file

@ -9,11 +9,24 @@ export default class Browser {
filter: '', filter: '',
inBbox: false, 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) { addFeature(feature, parent) {
const filter = this.options.filter if (feature.isFiltered()) return
if (filter && !feature.matchFilter(filter, this.filterKeys)) return
if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return if (this.options.inBbox && !feature.isOnScreen(this.bounds)) return
const row = DomUtil.create('li', `${feature.getClassName()} feature`) const row = DomUtil.create('li', `${feature.getClassName()} feature`)
const zoom_to = DomUtil.createButtonIcon( const zoom_to = DomUtil.createButtonIcon(
@ -105,6 +118,10 @@ export default class Browser {
}) })
} }
redraw() {
if (this.isOpen()) this.open()
}
isOpen() { isOpen() {
return !!document.querySelector('.umap-browser') 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 // Get once but use it for each feature later
this.filterKeys = this.map.getFilterKeys() this.filterKeys = this.map.getFilterKeys()
const container = DomUtil.create('div') const container = DomUtil.create('div')
@ -136,17 +154,29 @@ export default class Browser {
DomUtil.createTitle(container, translate('Browse data'), 'icon-layers') DomUtil.createTitle(container, translate('Browse data'), 'icon-layers')
this.tabsMenu(container, 'browse') 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) this.dataContainer = DomUtil.create('div', '', container)
const fields = [ let fields = [
['options.filter', { handler: 'Input', placeholder: translate('Filter') }], [
'options.filter',
{ handler: 'Input', placeholder: translate('Search map features…') },
],
['options.inBbox', { handler: 'Switch', label: translate('Current map view') }], ['options.inBbox', { handler: 'Switch', label: translate('Current map view') }],
] ]
const builder = new L.FormBuilder(this, fields, { const builder = new L.FormBuilder(this, fields, {
callback: () => this.onFormChange(), callback: () => this.onFormChange(),
}) })
formContainer.appendChild(builder.build()) 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({ this.map.panel.open({
content: container, content: container,
@ -171,10 +201,6 @@ export default class Browser {
const tabs = L.DomUtil.create('div', 'flat-tabs', container) const tabs = L.DomUtil.create('div', 'flat-tabs', container)
const browse = L.DomUtil.add('button', 'flat tab-browse', tabs, L._('Data')) const browse = L.DomUtil.add('button', 'flat tab-browse', tabs, L._('Data'))
DomEvent.on(browse, 'click', this.open, this) 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')) const info = L.DomUtil.add('button', 'flat tab-info', tabs, L._('About'))
DomEvent.on(info, 'click', this.map.displayCaption, this.map) DomEvent.on(info, 'click', this.map.displayCaption, this.map)
let el = tabs.querySelector(`.tab-${active}`) let el = tabs.querySelector(`.tab-${active}`)

View file

@ -53,23 +53,8 @@ export default class Facets {
return properties return properties
} }
redraw() { build() {
if (this.isOpen()) this.open()
}
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 defined = this.getDefined()
const names = Object.keys(defined) const names = Object.keys(defined)
const facetProperties = this.compute(names, defined) const facetProperties = this.compute(names, defined)
@ -114,13 +99,7 @@ export default class Facets {
] ]
}) })
const builder = new L.FormBuilder(this, fields, { return fields
callback: filterFeatures,
callbackContext: this,
})
container.appendChild(builder.build())
this.map.panel.open({ content: container })
} }
getDefined() { getDefined() {

View file

@ -272,9 +272,9 @@ export const SCHEMA = {
choices: [ choices: [
['none', translate('None')], ['none', translate('None')],
['caption', translate('Caption')], ['caption', translate('Caption')],
['databrowser', translate('Data browser')], ['databrowser', translate('Browser in data mode')],
['datalayers', translate('Layers')], ['datalayers', translate('Browser in layers mode')],
['facet', translate('Facet search')], ['datafilters', translate('Browser in filters mode')],
], ],
default: 'none', default: 'none',
}, },

View file

@ -660,9 +660,6 @@ const ControlsMixin = {
'star', 'star',
'tilelayers', 'tilelayers',
], ],
_openFacet: function () {
this.facets.open()
},
displayCaption: function () { displayCaption: function () {
const container = L.DomUtil.create('div', 'umap-caption') const container = L.DomUtil.create('div', 'umap-caption')

View file

@ -84,6 +84,7 @@ L.DomUtil.createFieldset = (container, legend, options) => {
const fieldset = L.DomUtil.create('div', 'fieldset toggle', container) const fieldset = L.DomUtil.create('div', 'fieldset toggle', container)
const legendEl = L.DomUtil.add('h5', 'legend style_options_toggle', fieldset, legend) const legendEl = L.DomUtil.add('h5', 'legend style_options_toggle', fieldset, legend)
const fieldsEl = L.DomUtil.add('div', 'fields with-transition', fieldset) const fieldsEl = L.DomUtil.add('div', 'fields with-transition', fieldset)
L.DomUtil.classIf(fieldset, 'on', options.on)
L.DomEvent.on(legendEl, 'click', function () { L.DomEvent.on(legendEl, 'click', function () {
if (L.DomUtil.hasClass(fieldset, 'on')) { if (L.DomUtil.hasClass(fieldset, 'on')) {
L.DomUtil.removeClass(fieldset, 'on') L.DomUtil.removeClass(fieldset, 'on')

View file

@ -494,6 +494,14 @@ U.FeatureMixin = {
this.bindTooltip(U.Utils.escapeHTML(displayName), options) 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) { matchFilter: function (filter, keys) {
filter = filter.toLowerCase() filter = filter.toLowerCase()
for (let i = 0, value; i < keys.length; i++) { for (let i = 0, value; i < keys.length; i++) {

View file

@ -211,15 +211,15 @@ U.Map = L.Map.extend({
if (L.Util.queryString('share')) { if (L.Util.queryString('share')) {
this.share.open() this.share.open()
} else if (this.options.onLoadPanel === 'databrowser') { } else if (this.options.onLoadPanel === 'databrowser') {
this.openBrowser('expanded') this.openBrowser('data')
} else if (this.options.onLoadPanel === 'datalayers') { } else if (this.options.onLoadPanel === 'datalayers') {
this.openBrowser('condensed') this.openBrowser('layers')
} else if (this.options.onLoadPanel === 'caption') { } else if (this.options.onLoadPanel === 'caption') {
this.panel.mode = 'condensed' this.panel.mode = 'condensed'
this.displayCaption() this.displayCaption()
} else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) { } else if (['facet', 'datafilters'].includes(this.options.onLoadPanel)) {
this.panel.mode = 'expanded' this.panel.mode = 'expanded'
this.openFacet() this.openBrowser('filters')
} }
if (L.Util.queryString('edit')) { if (L.Util.queryString('edit')) {
if (this.hasEditMode()) this.enableEdit() if (this.hasEditMode()) this.enableEdit()
@ -252,7 +252,7 @@ U.Map = L.Map.extend({
this.initCaptionBar() this.initCaptionBar()
this.renderEditToolbar() this.renderEditToolbar()
this.renderControls() this.renderControls()
this.facets.redraw() this.browser.redraw()
break break
case 'data': case 'data':
this.redrawVisibleDataLayers() this.redrawVisibleDataLayers()
@ -908,15 +908,8 @@ U.Map = L.Map.extend({
}, },
openBrowser: function (mode) { openBrowser: function (mode) {
if (mode) this.panel.mode = mode
this.onceDatalayersLoaded(function () { this.onceDatalayersLoaded(function () {
this.browser.open() this.browser.open(mode)
})
},
openFacet: function () {
this.onceDataLoaded(function () {
this._openFacet()
}) })
}, },
@ -1602,15 +1595,14 @@ U.Map = L.Map.extend({
'umap-open-browser-link flat', 'umap-open-browser-link flat',
container, container,
L._('Browse data'), L._('Browse data'),
() => this.openBrowser('expanded') () => this.openBrowser('data')
) )
if (this.options.facetKey) { if (this.options.facetKey) {
L.DomUtil.createButton( L.DomUtil.createButton(
'umap-open-filter-link flat', 'umap-open-filter-link flat',
container, container,
L._('Select data'), L._('Select data'),
this.openFacet, () => this.openBrowser('filters')
this
) )
} }
} }
@ -1747,17 +1739,17 @@ U.Map = L.Map.extend({
'-', '-',
{ {
text: L._('See layers'), text: L._('See layers'),
callback: () => this.openBrowser('condensed'), callback: () => this.openBrowser('layers'),
}, },
{ {
text: L._('Browse data'), text: L._('Browse data'),
callback: () => this.openBrowser('expanded'), callback: () => this.openBrowser('data'),
} }
) )
if (this.options.facetKey) { if (this.options.facetKey) {
items.push({ items.push({
text: L._('Facet search'), text: L._('Filter data'),
callback: this.openFacet, callback: () => this.openBrowser('filters'),
}) })
} }
items.push( items.push(

View file

@ -875,10 +875,7 @@ U.DataLayer = L.Evented.extend({
}, },
showFeature: function (feature) { showFeature: function (feature) {
const filterKeys = this.map.getFilterKeys(), if (feature.isFiltered()) return
filter = this.map.browser.options.filter
if (filter && !feature.matchFilter(filter, filterKeys)) return
if (!feature.matchFacets()) return
this.layer.addLayer(feature) this.layer.addLayer(feature)
}, },

View file

@ -888,10 +888,10 @@ a.umap-control-caption,
.umap-browser .datalayer i { .umap-browser .datalayer i {
cursor: pointer; cursor: pointer;
} }
.umap-browser ul { .umap-browser .datalayer ul {
display: none; display: none;
} }
.show-list ul { .umap-browser .show-list ul {
display: block; display: block;
} }

View file

@ -77,6 +77,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map):
paths = page.locator(".leaflet-overlay-pane path") paths = page.locator(".leaflet-overlay-pane path")
expect(markers).to_have_count(1) expect(markers).to_have_count(1)
expect(paths).to_have_count(2) expect(paths).to_have_count(2)
page.get_by_role("heading", name="filters").click()
filter_ = page.locator("input[name='filter']") filter_ = page.locator("input[name='filter']")
expect(filter_).to_be_visible() expect(filter_).to_be_visible()
filter_.type("poly") 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): def test_data_browser_can_show_only_visible_features(live_server, page, bootstrap, map):
# Zoom on France # Zoom on France
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") 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") el = page.get_by_text("Current map view")
expect(el).to_be_visible() expect(el).to_be_visible()
el.click() 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): def test_data_browser_can_mix_filter_and_bbox(live_server, page, bootstrap, map):
# Zoom on north west # Zoom on north west
page.goto(f"{live_server.url}{map.get_absolute_url()}#4/61.98/-2.68") 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") el = page.get_by_text("Current map view")
expect(el).to_be_visible() expect(el).to_be_visible()
el.click() 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): def test_data_browser_bbox_limit_should_be_dynamic(live_server, page, bootstrap, map):
# Zoom on Europe # Zoom on Europe
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") 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") el = page.get_by_text("Current map view")
expect(el).to_be_visible() expect(el).to_be_visible()
el.click() el.click()
@ -156,6 +160,7 @@ def test_data_browser_bbox_filter_should_be_persistent(
): ):
# Zoom on Europe # Zoom on Europe
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") 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") el = page.get_by_text("Current map view")
expect(el).to_be_visible() expect(el).to_be_visible()
el.click() el.click()
@ -181,6 +186,7 @@ def test_data_browser_bbox_filtered_is_clickable(live_server, page, bootstrap, m
popup = page.locator(".leaflet-popup") popup = page.locator(".leaflet-popup")
# Zoom on Europe # Zoom on Europe
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/51.000/2.000") 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") el = page.get_by_text("Current map view")
expect(el).to_be_visible() expect(el).to_be_visible()
el.click() 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 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 line in new zeland (line)")).to_be_visible()
expect(page.get_by_text("one polygon in greenland (polygon)")).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']") filter_ = page.locator("input[name='filter']")
expect(filter_).to_be_visible() expect(filter_).to_be_visible()
filter_.type("foobar") # Hide all filter_.type("foobar") # Hide all

View file

@ -93,7 +93,7 @@ DATALAYER_DATA3 = {
def test_simple_facet_search(live_server, page, map): 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"]["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.settings["properties"]["showLabel"] = True map.settings["properties"]["showLabel"] = True
map.save() 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_DATA2)
DataLayerFactory(map=map, data=DATALAYER_DATA3) DataLayerFactory(map=map, data=DATALAYER_DATA3)
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") 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 # From a non browsable datalayer, should not be impacted
paths = page.locator(".leaflet-overlay-pane path") paths = page.locator(".leaflet-overlay-pane path")
expect(paths).to_be_visible() expect(paths).to_be_visible()
@ -117,17 +117,28 @@ def test_simple_facet_search(live_server, page, map):
markers = page.locator(".leaflet-marker-icon") markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(4) expect(markers).to_have_count(4)
# Tooltips # Tooltips
expect(page.get_by_text("Point 1")).to_be_visible() expect(page.get_by_role("tooltip", name="Point 1")).to_be_visible()
expect(page.get_by_text("Point 2")).to_be_visible() expect(page.get_by_role("tooltip", name="Point 2")).to_be_visible()
expect(page.get_by_text("Point 3")).to_be_visible() expect(page.get_by_role("tooltip", name="Point 3")).to_be_visible()
expect(page.get_by_text("Point 4")).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 # Now let's filter
odd.click() odd.click()
expect(markers).to_have_count(2) expect(markers).to_have_count(2)
expect(page.get_by_text("Point 2")).to_be_hidden() expect(page.get_by_role("tooltip", name="Point 2")).to_be_hidden()
expect(page.get_by_text("Point 4")).to_be_hidden() expect(page.get_by_role("tooltip", name="Point 4")).to_be_hidden()
expect(page.get_by_text("Point 1")).to_be_visible() expect(page.get_by_role("tooltip", name="Point 1")).to_be_visible()
expect(page.get_by_text("Point 3")).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 expect(paths).to_be_visible
# Now let's filter # Now let's filter
odd.click() odd.click()
@ -156,7 +167,7 @@ def test_simple_facet_search(live_server, page, map):
def test_date_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.settings["properties"]["facetKey"] = "mydate|Date filter|date"
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1) 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): 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.settings["properties"]["facetKey"] = "mytype|My type"
map.save() map.save()
data = copy.deepcopy(DATALAYER_DATA1) 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): 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.settings["properties"]["facetKey"] = "mynumber|Filter|number"
map.save() map.save()
data = copy.deepcopy(DATALAYER_DATA1) data = copy.deepcopy(DATALAYER_DATA1)