chore: move facets to a dedicated module
This commit is contained in:
parent
47c6473285
commit
53f93ee97e
6 changed files with 133 additions and 130 deletions
126
umap/static/umap/js/modules/facets.js
Normal file
126
umap/static/umap/js/modules/facets.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { DomUtil, DomEvent, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
|
import { translate } from './i18n.js'
|
||||||
|
|
||||||
|
export default class Facets {
|
||||||
|
constructor(map) {
|
||||||
|
this.map = map
|
||||||
|
this.selected = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
compute(names, defined) {
|
||||||
|
const properties = {}
|
||||||
|
|
||||||
|
names.forEach((name) => {
|
||||||
|
const type = defined[name]['type']
|
||||||
|
properties[name] = { type: type }
|
||||||
|
this.selected[name] = { type: type }
|
||||||
|
if (!['date', 'datetime', 'number'].includes(type)) {
|
||||||
|
properties[name].choices = []
|
||||||
|
this.selected[name].choices = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.map.eachBrowsableDataLayer((datalayer) => {
|
||||||
|
datalayer.eachFeature((feature) => {
|
||||||
|
names.forEach((name) => {
|
||||||
|
let value = feature.properties[name]
|
||||||
|
const type = defined[name]['type']
|
||||||
|
switch (type) {
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
case 'number':
|
||||||
|
value = type === 'number' ? parseFloat(value) : new Date(value)
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
if (isNaN(properties[name].min) || properties[name].min > value) {
|
||||||
|
properties[name].min = value
|
||||||
|
}
|
||||||
|
if (isNaN(properties[name].max) || properties[name].max < value) {
|
||||||
|
properties[name].max = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
value = String(value || '') || L._('<empty value>')
|
||||||
|
if (!properties[name].choices.includes(value)) {
|
||||||
|
properties[name].choices.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
const container = L.DomUtil.create('div', 'umap-facet-search')
|
||||||
|
const title = L.DomUtil.add(
|
||||||
|
'h3',
|
||||||
|
'umap-filter-title',
|
||||||
|
container,
|
||||||
|
L._('Facet search')
|
||||||
|
)
|
||||||
|
const defined = this.getDefined()
|
||||||
|
const names = Object.keys(defined)
|
||||||
|
const facetProperties = this.compute(names, defined)
|
||||||
|
|
||||||
|
const filterFeatures = function () {
|
||||||
|
let found = false
|
||||||
|
this.map.eachBrowsableDataLayer((datalayer) => {
|
||||||
|
datalayer.resetLayer(true)
|
||||||
|
if (datalayer.hasDataVisible()) found = true
|
||||||
|
})
|
||||||
|
// TODO: display a results counter in the panel instead.
|
||||||
|
if (!found) {
|
||||||
|
this.map.ui.alert({
|
||||||
|
content: L._('No results for these facets'),
|
||||||
|
level: 'info',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = names.map((name) => {
|
||||||
|
let criteria = facetProperties[name]
|
||||||
|
let handler = 'FacetSearchChoices'
|
||||||
|
switch (criteria['type']) {
|
||||||
|
case 'number':
|
||||||
|
handler = 'FacetSearchNumber'
|
||||||
|
break
|
||||||
|
case 'date':
|
||||||
|
handler = 'FacetSearchDate'
|
||||||
|
break
|
||||||
|
case 'datetime':
|
||||||
|
handler = 'FacetSearchDateTime'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let label = defined[name]['label']
|
||||||
|
return [
|
||||||
|
`selected.${name}`,
|
||||||
|
{
|
||||||
|
criteria: criteria,
|
||||||
|
handler: handler,
|
||||||
|
label: label,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const builder = new U.FormBuilder(this, fields, {
|
||||||
|
makeDirty: false,
|
||||||
|
callback: filterFeatures,
|
||||||
|
callbackContext: this,
|
||||||
|
})
|
||||||
|
container.appendChild(builder.build())
|
||||||
|
|
||||||
|
this.map.panel.open({ content: container })
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefined() {
|
||||||
|
const defaultType = 'checkbox'
|
||||||
|
const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime']
|
||||||
|
return (this.map.options.facetKey || '').split(',').reduce((acc, curr) => {
|
||||||
|
let [name, label, type] = curr.split('|')
|
||||||
|
type = allowedTypes.includes(type) ? type : defaultType
|
||||||
|
acc[name] = { label: label || name, type: type }
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import URLs from './urls.js'
|
import URLs from './urls.js'
|
||||||
import Browser from './browser.js'
|
import Browser from './browser.js'
|
||||||
|
import Facets from './facets.js'
|
||||||
import { Panel, EditPanel, FullPanel } from './panel.js'
|
import { Panel, EditPanel, FullPanel } from './panel.js'
|
||||||
import * as Utils from './utils.js'
|
import * as Utils from './utils.js'
|
||||||
import { SCHEMA } from './schema.js'
|
import { SCHEMA } from './schema.js'
|
||||||
|
@ -17,6 +18,7 @@ window.U = {
|
||||||
HTTPError,
|
HTTPError,
|
||||||
NOKError,
|
NOKError,
|
||||||
Browser,
|
Browser,
|
||||||
|
Facets,
|
||||||
Panel,
|
Panel,
|
||||||
EditPanel,
|
EditPanel,
|
||||||
FullPanel,
|
FullPanel,
|
||||||
|
|
|
@ -661,117 +661,7 @@ const ControlsMixin = {
|
||||||
'tilelayers',
|
'tilelayers',
|
||||||
],
|
],
|
||||||
_openFacet: function () {
|
_openFacet: function () {
|
||||||
const container = L.DomUtil.create('div', 'umap-facet-search'),
|
this.facets.open()
|
||||||
title = L.DomUtil.add('h3', 'umap-filter-title', container, L._('Facet search')),
|
|
||||||
facetKeys = this.getFacetKeys(),
|
|
||||||
keys = Object.keys(facetKeys)
|
|
||||||
|
|
||||||
const facetCriteria = {}
|
|
||||||
|
|
||||||
keys.forEach((key) => {
|
|
||||||
const type = facetKeys[key]['type']
|
|
||||||
if (['date', 'datetime', 'number'].includes(type)) {
|
|
||||||
if (!facetCriteria[key])
|
|
||||||
facetCriteria[key] = {
|
|
||||||
type: facetKeys[key]['type'],
|
|
||||||
min: undefined,
|
|
||||||
max: undefined,
|
|
||||||
}
|
|
||||||
if (!this.facets[key])
|
|
||||||
this.facets[key] = {
|
|
||||||
type: facetKeys[key]['type'],
|
|
||||||
min: undefined,
|
|
||||||
max: undefined,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!facetCriteria[key])
|
|
||||||
facetCriteria[key] = {
|
|
||||||
type: facetKeys[key]['type'],
|
|
||||||
choices: [],
|
|
||||||
}
|
|
||||||
if (!this.facets[key])
|
|
||||||
this.facets[key] = {
|
|
||||||
type: facetKeys[key]['type'],
|
|
||||||
choices: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.eachBrowsableDataLayer((datalayer) => {
|
|
||||||
datalayer.eachFeature((feature) => {
|
|
||||||
keys.forEach((key) => {
|
|
||||||
let value = feature.properties[key]
|
|
||||||
const type = facetKeys[key]['type']
|
|
||||||
if (['date', 'datetime', 'number'].includes(type)) {
|
|
||||||
value = value != null ? value : undefined
|
|
||||||
if (['date', 'datetime'].includes(type)) value = new Date(value)
|
|
||||||
if (['number'].includes(type)) value = parseFloat(value)
|
|
||||||
if (
|
|
||||||
!isNaN(value) &&
|
|
||||||
(isNaN(facetCriteria[key]['min']) || facetCriteria[key]['min'] > value)
|
|
||||||
) {
|
|
||||||
facetCriteria[key]['min'] = value
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!isNaN(value) &&
|
|
||||||
(isNaN(facetCriteria[key]['max']) || facetCriteria[key]['max'] < value)
|
|
||||||
) {
|
|
||||||
facetCriteria[key]['max'] = value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = String(value)
|
|
||||||
value = value.length ? value : L._('empty string')
|
|
||||||
if (!!value && !facetCriteria[key]['choices'].includes(value)) {
|
|
||||||
facetCriteria[key]['choices'].push(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 facets'), level: 'info' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const fields = keys.map((key) => {
|
|
||||||
let criteria = facetCriteria[key]
|
|
||||||
let handler = 'FacetSearchChoices'
|
|
||||||
switch (criteria['type']) {
|
|
||||||
case 'number':
|
|
||||||
handler = 'FacetSearchNumber'
|
|
||||||
break
|
|
||||||
case 'date':
|
|
||||||
handler = 'FacetSearchDate'
|
|
||||||
break
|
|
||||||
case 'datetime':
|
|
||||||
handler = 'FacetSearchDateTime'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let label = facetKeys[key]['label']
|
|
||||||
return [
|
|
||||||
`facets.${key}`,
|
|
||||||
{
|
|
||||||
criteria: criteria,
|
|
||||||
handler: handler,
|
|
||||||
label: label,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const builder = new U.FormBuilder(this, fields, {
|
|
||||||
makeDirty: false,
|
|
||||||
callback: filterFeatures,
|
|
||||||
callbackContext: this,
|
|
||||||
})
|
|
||||||
container.appendChild(builder.build())
|
|
||||||
|
|
||||||
this.panel.open({ content: container })
|
|
||||||
},
|
},
|
||||||
|
|
||||||
displayCaption: function () {
|
displayCaption: function () {
|
||||||
|
|
|
@ -73,7 +73,7 @@ L.DomUtil.add = (tagName, className, container, content) => {
|
||||||
if (content.nodeType && content.nodeType === 1) {
|
if (content.nodeType && content.nodeType === 1) {
|
||||||
el.appendChild(content)
|
el.appendChild(content)
|
||||||
} else {
|
} else {
|
||||||
el.innerHTML = content
|
el.textContent = content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return el
|
return el
|
||||||
|
|
|
@ -493,7 +493,7 @@ U.FeatureMixin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
matchFacets: function () {
|
matchFacets: function () {
|
||||||
const facets = this.map.facets
|
const facets = this.map.facets.selected
|
||||||
for (const [property, criteria] of Object.entries(facets)) {
|
for (const [property, criteria] of Object.entries(facets)) {
|
||||||
let value = this.properties[property]
|
let value = this.properties[property]
|
||||||
const type = criteria["type"]
|
const type = criteria["type"]
|
||||||
|
@ -515,8 +515,7 @@ U.FeatureMixin = {
|
||||||
if (!isNaN(max) && !isNaN(value) && max < value) return false
|
if (!isNaN(max) && !isNaN(value) && max < value) return false
|
||||||
} else {
|
} else {
|
||||||
const choices = criteria["choices"]
|
const choices = criteria["choices"]
|
||||||
value = String(value)
|
value = String(value || '') || L._("<empty value>")
|
||||||
value = (value.length ? value : L._("empty string"))
|
|
||||||
if (choices?.length && (!value || !choices.includes(value))) return false
|
if (choices?.length && (!value || !choices.includes(value))) return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,6 @@ U.Map = L.Map.extend({
|
||||||
this.datalayers_index = []
|
this.datalayers_index = []
|
||||||
this.dirty_datalayers = []
|
this.dirty_datalayers = []
|
||||||
this.features_index = {}
|
this.features_index = {}
|
||||||
this.facets = {}
|
|
||||||
|
|
||||||
// Needed for actions labels
|
// Needed for actions labels
|
||||||
this.help = new U.Help(this)
|
this.help = new U.Help(this)
|
||||||
|
@ -377,6 +376,7 @@ U.Map = L.Map.extend({
|
||||||
if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable()
|
if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable()
|
||||||
else this.scrollWheelZoom.disable()
|
else this.scrollWheelZoom.disable()
|
||||||
this.browser = new U.Browser(this)
|
this.browser = new U.Browser(this)
|
||||||
|
this.facets = new U.Facets(this)
|
||||||
this.importer = new U.Importer(this)
|
this.importer = new U.Importer(this)
|
||||||
this.drop = new U.DropControl(this)
|
this.drop = new U.DropControl(this)
|
||||||
this.share = new U.Share(this)
|
this.share = new U.Share(this)
|
||||||
|
@ -1846,20 +1846,6 @@ U.Map = L.Map.extend({
|
||||||
return (this.options.filterKey || this.options.sortKey || 'name').split(',')
|
return (this.options.filterKey || this.options.sortKey || 'name').split(',')
|
||||||
},
|
},
|
||||||
|
|
||||||
getFacetKeys: function () {
|
|
||||||
const defaultType = 'checkbox'
|
|
||||||
const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime']
|
|
||||||
return (this.options.facetKey || '').split(',').reduce((acc, curr) => {
|
|
||||||
let [key, label, type] = curr.split('|')
|
|
||||||
type = allowedTypes.includes(type) ? type : defaultType
|
|
||||||
acc[key] = {
|
|
||||||
label: label || key,
|
|
||||||
type: type,
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
},
|
|
||||||
|
|
||||||
getLayersBounds: function () {
|
getLayersBounds: function () {
|
||||||
const bounds = new L.latLngBounds()
|
const bounds = new L.latLngBounds()
|
||||||
this.eachBrowsableDataLayer((d) => {
|
this.eachBrowsableDataLayer((d) => {
|
||||||
|
|
Loading…
Reference in a new issue