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: '',
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}`)

View file

@ -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() {

View file

@ -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',
},

View file

@ -660,9 +660,6 @@ const ControlsMixin = {
'star',
'tilelayers',
],
_openFacet: function () {
this.facets.open()
},
displayCaption: function () {
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 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')

View file

@ -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++) {

View file

@ -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(

View file

@ -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)
},

View file

@ -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;
}

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")
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

View file

@ -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)