Merge pull request #1763 from umap-project/flammermann-facet-date
Date and number support for facets
This commit is contained in:
commit
d8c2e14b42
13 changed files with 429 additions and 112 deletions
|
@ -135,7 +135,7 @@ ul {
|
||||||
/* forms */
|
/* forms */
|
||||||
/* *********** */
|
/* *********** */
|
||||||
input[type="text"], input[type="password"], input[type="date"],
|
input[type="text"], input[type="password"], input[type="date"],
|
||||||
input[type="datetime"], input[type="email"], input[type="number"],
|
input[type="datetime-local"], input[type="email"], input[type="number"],
|
||||||
input[type="search"], input[type="tel"], input[type="time"],
|
input[type="search"], input[type="tel"], input[type="time"],
|
||||||
input[type="url"], textarea {
|
input[type="url"], textarea {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
@ -263,6 +263,9 @@ input[type="checkbox"] + label {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
}
|
}
|
||||||
|
label input[type="radio"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
select + .error,
|
select + .error,
|
||||||
input + .error {
|
input + .error {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -343,6 +346,11 @@ input:invalid {
|
||||||
.fieldset.toggle.on .legend:before {
|
.fieldset.toggle.on .legend:before {
|
||||||
background-position: -144px -51px;
|
background-position: -144px -51px;
|
||||||
}
|
}
|
||||||
|
fieldset legend {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Switch */
|
/* Switch */
|
||||||
input.switch:empty {
|
input.switch:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -142,8 +142,7 @@ export default class Browser {
|
||||||
['options.filter', { handler: 'Input', placeholder: translate('Filter') }],
|
['options.filter', { handler: 'Input', placeholder: translate('Filter') }],
|
||||||
['options.inBbox', { handler: 'Switch', label: translate('Current map view') }],
|
['options.inBbox', { handler: 'Switch', label: translate('Current map view') }],
|
||||||
]
|
]
|
||||||
const builder = new U.FormBuilder(this, fields, {
|
const builder = new L.FormBuilder(this, fields, {
|
||||||
makeDirty: false,
|
|
||||||
callback: () => this.onFormChange(),
|
callback: () => this.onFormChange(),
|
||||||
})
|
})
|
||||||
formContainer.appendChild(builder.build())
|
formContainer.appendChild(builder.build())
|
||||||
|
|
148
umap/static/umap/js/modules/facets.js
Normal file
148
umap/static/umap/js/modules/facets.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import { DomUtil, DomEvent, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
|
import { translate } from './i18n.js'
|
||||||
|
import * as Utils from './utils.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']
|
||||||
|
const parser = this.getParser(type)
|
||||||
|
value = parser(value)
|
||||||
|
switch (type) {
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
case 'number':
|
||||||
|
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 = value || translate('<empty value>')
|
||||||
|
if (!properties[name].choices.includes(value)) {
|
||||||
|
properties[name].choices.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw() {
|
||||||
|
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')
|
||||||
|
)
|
||||||
|
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: translate('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 L.FormBuilder(this, fields, {
|
||||||
|
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
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
getParser(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
return parseFloat
|
||||||
|
case 'datetime':
|
||||||
|
return (v) => new Date(v)
|
||||||
|
case 'date':
|
||||||
|
return Utils.parseNaiveDate
|
||||||
|
default:
|
||||||
|
return (v) => String(v || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -27,9 +27,9 @@ export function checkId(string) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the impacts for a given list of fields.
|
* Compute the impacts for a given list of fields.
|
||||||
*
|
*
|
||||||
* Return an array of unique impacts.
|
* Return an array of unique impacts.
|
||||||
*
|
*
|
||||||
* @param {fields} list[fields]
|
* @param {fields} list[fields]
|
||||||
* @returns Array[string]
|
* @returns Array[string]
|
||||||
*/
|
*/
|
||||||
|
@ -356,3 +356,9 @@ export function template(str, data) {
|
||||||
return value
|
return value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseNaiveDate(value) {
|
||||||
|
const naive = new Date(value)
|
||||||
|
// Let's pretend naive date are UTC, and remove time…
|
||||||
|
return new Date(Date.UTC(naive.getFullYear(), naive.getMonth(), naive.getDate()))
|
||||||
|
}
|
||||||
|
|
|
@ -661,55 +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')),
|
|
||||||
keys = Object.keys(this.getFacetKeys())
|
|
||||||
|
|
||||||
const knownValues = {}
|
|
||||||
|
|
||||||
keys.forEach((key) => {
|
|
||||||
knownValues[key] = []
|
|
||||||
if (!this.facets[key]) this.facets[key] = []
|
|
||||||
})
|
|
||||||
|
|
||||||
this.eachBrowsableDataLayer((datalayer) => {
|
|
||||||
datalayer.eachFeature((feature) => {
|
|
||||||
keys.forEach((key) => {
|
|
||||||
let value = feature.properties[key]
|
|
||||||
if (typeof value !== 'undefined' && !knownValues[key].includes(value)) {
|
|
||||||
knownValues[key].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((current) => [
|
|
||||||
`facets.${current}`,
|
|
||||||
{
|
|
||||||
handler: 'FacetSearch',
|
|
||||||
choices: knownValues[current],
|
|
||||||
label: this.getFacetKeys()[current],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
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 () {
|
||||||
|
@ -865,7 +817,7 @@ const ControlsMixin = {
|
||||||
L.DomUtil.createLink(
|
L.DomUtil.createLink(
|
||||||
'umap-user',
|
'umap-user',
|
||||||
rightContainer,
|
rightContainer,
|
||||||
L._(`My Dashboard <span>({username})</span>`, {
|
L._(`My Dashboard ({username})`, {
|
||||||
username: this.options.user.name,
|
username: this.options.user.name,
|
||||||
}),
|
}),
|
||||||
this.options.user.url
|
this.options.user.url
|
||||||
|
|
|
@ -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
|
||||||
|
@ -541,7 +541,7 @@ U.Help = L.Class.extend({
|
||||||
slugKey: L._('The name of the property to use as feature unique identifier.'),
|
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'),
|
filterKey: L._('Comma separated list of properties to use when filtering features'),
|
||||||
facetKey: L._(
|
facetKey: L._(
|
||||||
'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key)'
|
'Comma separated list of properties to use for facet search (eg.: mykey,otherkey). To control label, add it after a | (eg.: mykey|My Key,otherkey|Other Key). To control input field type, add it after another | (eg.: mykey|My Key|checkbox,otherkey|Other Key|datetime). Allowed values for the input field type are checkbox (default), radio, number, date and datetime.'
|
||||||
),
|
),
|
||||||
interactive: L._(
|
interactive: L._(
|
||||||
'If false, the polygon or line will act as a part of the underlying map.'
|
'If false, the polygon or line will act as a part of the underlying map.'
|
||||||
|
|
|
@ -493,11 +493,24 @@ U.FeatureMixin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
matchFacets: function () {
|
matchFacets: function () {
|
||||||
const facets = this.map.facets
|
const selected = this.map.facets.selected
|
||||||
for (const [property, expected] of Object.entries(facets)) {
|
for (let [name, { type, min, max, choices }] of Object.entries(selected)) {
|
||||||
if (expected.length) {
|
let value = this.properties[name]
|
||||||
let value = this.properties[property]
|
let parser = this.map.facets.getParser(type)
|
||||||
if (!value || !expected.includes(value)) return false
|
value = parser(value)
|
||||||
|
switch (type) {
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
case 'number':
|
||||||
|
min = parser(min)
|
||||||
|
max = parser(max)
|
||||||
|
if (!isNaN(min) && !isNaN(value) && min > value) return false
|
||||||
|
if (!isNaN(max) && !isNaN(value) && max < value) return false
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
value = value || L._('<empty value>')
|
||||||
|
if (choices?.length && !choices.includes(value)) return false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -527,11 +527,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const symbol = L.DomUtil.add(
|
const symbol = L.DomUtil.add(
|
||||||
'button',
|
'button',
|
||||||
'flat tab-symbols',
|
'flat tab-symbols',
|
||||||
this.tabs,
|
this.tabs,
|
||||||
L._('Symbol')
|
L._('Symbol')
|
||||||
),
|
),
|
||||||
char = L.DomUtil.add(
|
char = L.DomUtil.add(
|
||||||
'button',
|
'button',
|
||||||
'flat tab-chars',
|
'flat tab-chars',
|
||||||
|
@ -744,34 +744,122 @@ L.FormBuilder.Switch = L.FormBuilder.CheckBox.extend({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
L.FormBuilder.FacetSearch = L.FormBuilder.Element.extend({
|
L.FormBuilder.FacetSearchChoices = L.FormBuilder.Element.extend({
|
||||||
build: function () {
|
build: function () {
|
||||||
this.container = L.DomUtil.create('div', 'umap-facet', this.parentNode)
|
this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode)
|
||||||
|
this.container.appendChild(this.label)
|
||||||
this.ul = L.DomUtil.create('ul', '', this.container)
|
this.ul = L.DomUtil.create('ul', '', this.container)
|
||||||
const choices = this.options.choices
|
this.type = this.options.criteria['type']
|
||||||
|
|
||||||
|
const choices = this.options.criteria['choices']
|
||||||
choices.sort()
|
choices.sort()
|
||||||
choices.forEach((value) => this.buildLi(value))
|
choices.forEach((value) => this.buildLi(value))
|
||||||
},
|
},
|
||||||
|
|
||||||
buildLabel: function () {
|
buildLabel: function () {
|
||||||
this.label = L.DomUtil.add('h5', '', this.parentNode, this.options.label)
|
this.label = L.DomUtil.element('legend', {textContent: this.options.label})
|
||||||
},
|
},
|
||||||
|
|
||||||
buildLi: function (value) {
|
buildLi: function (value) {
|
||||||
const property_li = L.DomUtil.create('li', '', this.ul),
|
const property_li = L.DomUtil.create('li', '', this.ul)
|
||||||
input = L.DomUtil.create('input', '', property_li),
|
const label = L.DomUtil.add('label', '', property_li)
|
||||||
label = L.DomUtil.create('label', '', property_li)
|
const input = L.DomUtil.create('input', '', label)
|
||||||
input.type = 'checkbox'
|
L.DomUtil.add('span', '', label, value)
|
||||||
input.id = `checkbox_${this.name}_${value}`
|
|
||||||
input.checked = this.get().includes(value)
|
input.type = this.type
|
||||||
|
input.name = `${this.type}_${this.name}`
|
||||||
|
input.checked = this.get()['choices'].includes(value)
|
||||||
input.dataset.value = value
|
input.dataset.value = value
|
||||||
label.htmlFor = `checkbox_${this.name}_${value}`
|
|
||||||
label.innerHTML = value
|
|
||||||
L.DomEvent.on(input, 'change', (e) => this.sync())
|
L.DomEvent.on(input, 'change', (e) => this.sync())
|
||||||
},
|
},
|
||||||
|
|
||||||
toJS: function () {
|
toJS: function () {
|
||||||
return [...this.ul.querySelectorAll('input:checked')].map((i) => i.dataset.value)
|
return {
|
||||||
|
type: this.type,
|
||||||
|
choices: [...this.ul.querySelectorAll('input:checked')].map(
|
||||||
|
(i) => i.dataset.value
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
L.FormBuilder.MinMaxBase = L.FormBuilder.Element.extend({
|
||||||
|
getInputType: function (type) {
|
||||||
|
return type
|
||||||
|
},
|
||||||
|
|
||||||
|
getLabels: function () {
|
||||||
|
return [L._('Min'), L._('Max')]
|
||||||
|
},
|
||||||
|
|
||||||
|
castValue: function (value) {
|
||||||
|
return value.valueOf()
|
||||||
|
},
|
||||||
|
|
||||||
|
build: function () {
|
||||||
|
this.container = L.DomUtil.create('fieldset', 'umap-facet', this.parentNode)
|
||||||
|
this.container.appendChild(this.label)
|
||||||
|
const {min, max, type} = this.options.criteria
|
||||||
|
this.type = type
|
||||||
|
this.inputType = this.getInputType(this.type)
|
||||||
|
|
||||||
|
const [minLabel, maxLabel] = this.getLabels()
|
||||||
|
|
||||||
|
this.minLabel = L.DomUtil.create('label', '', this.container)
|
||||||
|
this.minLabel.innerHTML = minLabel
|
||||||
|
|
||||||
|
this.minInput = L.DomUtil.create('input', '', this.minLabel)
|
||||||
|
this.minInput.type = this.inputType
|
||||||
|
this.minInput.step = 'any'
|
||||||
|
if (min != null) {
|
||||||
|
this.minInput.valueAsNumber = this.castValue(min)
|
||||||
|
this.minInput.dataset.value = min
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.maxLabel = L.DomUtil.create('label', '', this.container)
|
||||||
|
this.maxLabel.innerHTML = maxLabel
|
||||||
|
|
||||||
|
this.maxInput = L.DomUtil.create('input', '', this.maxLabel)
|
||||||
|
this.maxInput.type = this.inputType
|
||||||
|
this.maxInput.step = 'any'
|
||||||
|
if (max != null) {
|
||||||
|
this.maxInput.valueAsNumber = this.castValue(max)
|
||||||
|
this.maxInput.dataset.value = max
|
||||||
|
}
|
||||||
|
|
||||||
|
L.DomEvent.on(this.minInput, 'change', (e) => this.sync())
|
||||||
|
L.DomEvent.on(this.maxInput, 'change', (e) => this.sync())
|
||||||
|
},
|
||||||
|
|
||||||
|
buildLabel: function () {
|
||||||
|
this.label = L.DomUtil.element('legend', {textContent: this.options.label})
|
||||||
|
},
|
||||||
|
|
||||||
|
toJS: function () {
|
||||||
|
return {
|
||||||
|
type: this.type,
|
||||||
|
min: this.minInput.value,
|
||||||
|
max: this.maxInput.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
L.FormBuilder.FacetSearchNumber = L.FormBuilder.MinMaxBase.extend({})
|
||||||
|
|
||||||
|
L.FormBuilder.FacetSearchDate = L.FormBuilder.MinMaxBase.extend({
|
||||||
|
castValue: function (value) {
|
||||||
|
return value.valueOf() - value.getTimezoneOffset() * 60000
|
||||||
|
},
|
||||||
|
getLabels: function () {
|
||||||
|
return [L._('From'), L._('Until')]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
L.FormBuilder.FacetSearchDateTime = L.FormBuilder.FacetSearchDate.extend({
|
||||||
|
getInputType: function (type) {
|
||||||
|
return 'datetime-local'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1029,10 +1117,8 @@ U.FormBuilder = L.FormBuilder.extend({
|
||||||
|
|
||||||
setter: function (field, value) {
|
setter: function (field, value) {
|
||||||
L.FormBuilder.prototype.setter.call(this, field, value)
|
L.FormBuilder.prototype.setter.call(this, field, value)
|
||||||
if (this.options.makeDirty !== false) {
|
this.obj.isDirty = true
|
||||||
this.obj.isDirty = true
|
if ('render' in this.obj) this.obj.render([field], this)
|
||||||
if ('render' in this.obj) this.obj.render([field], this)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
finish: function () {
|
finish: function () {
|
||||||
|
|
|
@ -106,17 +106,19 @@ U.Map = L.Map.extend({
|
||||||
this.options.slideshow &&
|
this.options.slideshow &&
|
||||||
this.options.slideshow.delay &&
|
this.options.slideshow.delay &&
|
||||||
this.options.slideshow.active === undefined
|
this.options.slideshow.active === undefined
|
||||||
)
|
) {
|
||||||
this.options.slideshow.active = true
|
this.options.slideshow.active = true
|
||||||
if (this.options.advancedFilterKey)
|
}
|
||||||
|
if (this.options.advancedFilterKey) {
|
||||||
this.options.facetKey = this.options.advancedFilterKey
|
this.options.facetKey = this.options.advancedFilterKey
|
||||||
|
delete this.options.advancedFilterKey
|
||||||
|
}
|
||||||
|
|
||||||
// Global storage for retrieving datalayers and features
|
// Global storage for retrieving datalayers and features
|
||||||
this.datalayers = {}
|
this.datalayers = {}
|
||||||
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)
|
||||||
|
@ -218,6 +220,7 @@ U.Map = L.Map.extend({
|
||||||
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.openFacet()
|
this.openFacet()
|
||||||
}
|
}
|
||||||
if (L.Util.queryString('edit')) {
|
if (L.Util.queryString('edit')) {
|
||||||
|
@ -251,6 +254,7 @@ U.Map = L.Map.extend({
|
||||||
this.initCaptionBar()
|
this.initCaptionBar()
|
||||||
this.renderEditToolbar()
|
this.renderEditToolbar()
|
||||||
this.renderControls()
|
this.renderControls()
|
||||||
|
this.facets.redraw()
|
||||||
break
|
break
|
||||||
case 'data':
|
case 'data':
|
||||||
this.redrawVisibleDataLayers()
|
this.redrawVisibleDataLayers()
|
||||||
|
@ -376,6 +380,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)
|
||||||
|
@ -1234,9 +1239,9 @@ U.Map = L.Map.extend({
|
||||||
[
|
[
|
||||||
'options.facetKey',
|
'options.facetKey',
|
||||||
{
|
{
|
||||||
handler: 'Input',
|
handler: 'BlurInput',
|
||||||
helpEntries: 'facetKey',
|
helpEntries: 'facetKey',
|
||||||
placeholder: L._('Example: key1,key2,key3'),
|
placeholder: L._('Example: key1,key2|Label 2,key3|Label 3|checkbox'),
|
||||||
label: L._('Facet keys'),
|
label: L._('Facet keys'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1845,14 +1850,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 () {
|
|
||||||
return (this.options.facetKey || '').split(',').reduce((acc, curr) => {
|
|
||||||
const els = curr.split('|')
|
|
||||||
acc[els[0]] = els[1] || els[0]
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
},
|
|
||||||
|
|
||||||
getLayersBounds: function () {
|
getLayersBounds: function () {
|
||||||
const bounds = new L.latLngBounds()
|
const bounds = new L.latLngBounds()
|
||||||
this.eachBrowsableDataLayer((d) => {
|
this.eachBrowsableDataLayer((d) => {
|
||||||
|
|
|
@ -927,7 +927,6 @@ a.umap-control-caption,
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.umap-facet-search li:nth-child(even),
|
|
||||||
.umap-browser .datalayer li:nth-child(even) {
|
.umap-browser .datalayer li:nth-child(even) {
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
}
|
}
|
||||||
|
|
|
@ -590,4 +590,21 @@ describe('Utils', function () {
|
||||||
assert.deepEqual(getImpactsFromSchema(['foo', 'bar', 'baz'], schema), ['A', 'B'])
|
assert.deepEqual(getImpactsFromSchema(['foo', 'bar', 'baz'], schema), ['A', 'B'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('parseNaiveDate', () => {
|
||||||
|
it('should parse a date', () => {
|
||||||
|
assert.equal(Utils.parseNaiveDate("2024/03/04").toISOString(), "2024-03-04T00:00:00.000Z")
|
||||||
|
})
|
||||||
|
it('should parse a datetime', () => {
|
||||||
|
assert.equal(Utils.parseNaiveDate("2024/03/04 12:13:14").toISOString(), "2024-03-04T00:00:00.000Z")
|
||||||
|
})
|
||||||
|
it('should parse an iso datetime', () => {
|
||||||
|
assert.equal(Utils.parseNaiveDate("2024-03-04T00:00:00.000Z").toISOString(), "2024-03-04T00:00:00.000Z")
|
||||||
|
})
|
||||||
|
it('should parse a GMT time', () => {
|
||||||
|
assert.equal(Utils.parseNaiveDate("04 Mar 2024 00:12:00 GMT").toISOString(), "2024-03-04T00:00:00.000Z")
|
||||||
|
})
|
||||||
|
it('should parse a GMT time with explicit timezone', () => {
|
||||||
|
assert.equal(Utils.parseNaiveDate("Thu, 04 Mar 2024 00:00:00 GMT+0300").toISOString(), "2024-03-03T00:00:00.000Z")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from playwright.sync_api import expect
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
@ -11,12 +13,22 @@ DATALAYER_DATA1 = {
|
||||||
"features": [
|
"features": [
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {"mytype": "even", "name": "Point 2"},
|
"properties": {
|
||||||
|
"mytype": "even",
|
||||||
|
"name": "Point 2",
|
||||||
|
"mynumber": 10,
|
||||||
|
"mydate": "2024/04/14 12:19:17",
|
||||||
|
},
|
||||||
"geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]},
|
"geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {"mytype": "odd", "name": "Point 1"},
|
"properties": {
|
||||||
|
"mytype": "odd",
|
||||||
|
"name": "Point 1",
|
||||||
|
"mynumber": 12,
|
||||||
|
"mydate": "2024/03/13 12:20:20",
|
||||||
|
},
|
||||||
"geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
|
"geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -31,12 +43,22 @@ DATALAYER_DATA2 = {
|
||||||
"features": [
|
"features": [
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {"mytype": "even", "name": "Point 4"},
|
"properties": {
|
||||||
|
"mytype": "even",
|
||||||
|
"name": "Point 4",
|
||||||
|
"mynumber": 10,
|
||||||
|
"mydate": "2024/08/18 13:14:15",
|
||||||
|
},
|
||||||
"geometry": {"type": "Point", "coordinates": [0.856934, 45.290347]},
|
"geometry": {"type": "Point", "coordinates": [0.856934, 45.290347]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"properties": {"mytype": "odd", "name": "Point 3"},
|
"properties": {
|
||||||
|
"mytype": "odd",
|
||||||
|
"name": "Point 3",
|
||||||
|
"mynumber": 14,
|
||||||
|
"mydate": "2024-04-14T10:19:17.000Z",
|
||||||
|
},
|
||||||
"geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
|
"geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -70,23 +92,19 @@ DATALAYER_DATA3 = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def test_simple_facet_search(live_server, page, map):
|
||||||
def bootstrap(map, live_server):
|
|
||||||
map.settings["properties"]["onLoadPanel"] = "facet"
|
map.settings["properties"]["onLoadPanel"] = "facet"
|
||||||
map.settings["properties"]["facetKey"] = "mytype|My type"
|
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()
|
||||||
DataLayerFactory(map=map, data=DATALAYER_DATA1)
|
DataLayerFactory(map=map, data=DATALAYER_DATA1)
|
||||||
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")
|
||||||
|
|
||||||
def test_simple_facet_search(live_server, page, bootstrap, map):
|
|
||||||
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
|
||||||
panel = page.locator(".umap-facet-search")
|
panel = page.locator(".umap-facet-search")
|
||||||
# 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()
|
||||||
expect(panel).to_be_visible()
|
expect(panel).to_be_visible()
|
||||||
# Facet name
|
# Facet name
|
||||||
expect(page.get_by_text("My type")).to_be_visible()
|
expect(page.get_by_text("My type")).to_be_visible()
|
||||||
|
@ -95,7 +113,7 @@ def test_simple_facet_search(live_server, page, bootstrap, map):
|
||||||
odd = page.get_by_text("odd")
|
odd = page.get_by_text("odd")
|
||||||
expect(oven).to_be_visible()
|
expect(oven).to_be_visible()
|
||||||
expect(odd).to_be_visible()
|
expect(odd).to_be_visible()
|
||||||
expect(paths).to_be_visible
|
expect(paths).to_be_visible()
|
||||||
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
|
||||||
|
@ -114,4 +132,76 @@ def test_simple_facet_search(live_server, page, bootstrap, map):
|
||||||
# Now let's filter
|
# Now let's filter
|
||||||
odd.click()
|
odd.click()
|
||||||
expect(markers).to_have_count(4)
|
expect(markers).to_have_count(4)
|
||||||
expect(paths).to_be_visible
|
expect(paths).to_be_visible()
|
||||||
|
|
||||||
|
# Let's filter using the number facet
|
||||||
|
expect(page.get_by_text("My Number")).to_be_visible()
|
||||||
|
expect(page.get_by_label("Min")).to_have_value("10")
|
||||||
|
expect(page.get_by_label("Max")).to_have_value("14")
|
||||||
|
page.get_by_label("Min").fill("11")
|
||||||
|
page.keyboard.press("Tab") # Move out of the input, so the "change" event is sent
|
||||||
|
expect(markers).to_have_count(2)
|
||||||
|
expect(paths).to_be_visible()
|
||||||
|
page.get_by_label("Max").fill("13")
|
||||||
|
page.keyboard.press("Tab")
|
||||||
|
expect(markers).to_have_count(1)
|
||||||
|
|
||||||
|
# Now let's combine
|
||||||
|
page.get_by_label("Min").fill("10")
|
||||||
|
page.keyboard.press("Tab")
|
||||||
|
expect(markers).to_have_count(3)
|
||||||
|
odd.click()
|
||||||
|
expect(markers).to_have_count(1)
|
||||||
|
expect(paths).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_facet_search(live_server, page, map):
|
||||||
|
map.settings["properties"]["onLoadPanel"] = "facet"
|
||||||
|
map.settings["properties"]["facetKey"] = "mydate|Date filter|date"
|
||||||
|
map.save()
|
||||||
|
DataLayerFactory(map=map, data=DATALAYER_DATA1)
|
||||||
|
DataLayerFactory(map=map, data=DATALAYER_DATA2)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
markers = page.locator(".leaflet-marker-icon")
|
||||||
|
expect(markers).to_have_count(4)
|
||||||
|
expect(page.get_by_text("Date Filter")).to_be_visible()
|
||||||
|
expect(page.get_by_label("From")).to_have_value("2024-03-13")
|
||||||
|
expect(page.get_by_label("Until")).to_have_value("2024-08-18")
|
||||||
|
page.get_by_label("From").fill("2024-03-14")
|
||||||
|
expect(markers).to_have_count(3)
|
||||||
|
page.get_by_label("Until").fill("2024-08-17")
|
||||||
|
expect(markers).to_have_count(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_choice_with_empty_value(live_server, page, map):
|
||||||
|
map.settings["properties"]["onLoadPanel"] = "facet"
|
||||||
|
map.settings["properties"]["facetKey"] = "mytype|My type"
|
||||||
|
map.save()
|
||||||
|
data = copy.deepcopy(DATALAYER_DATA1)
|
||||||
|
data["features"][0]["properties"]["mytype"] = ""
|
||||||
|
del data["features"][1]["properties"]["mytype"]
|
||||||
|
DataLayerFactory(map=map, data=data)
|
||||||
|
DataLayerFactory(map=map, data=DATALAYER_DATA2)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
expect(page.get_by_text("<empty value>")).to_be_visible()
|
||||||
|
markers = page.locator(".leaflet-marker-icon")
|
||||||
|
expect(markers).to_have_count(4)
|
||||||
|
page.get_by_text("<empty value>").click()
|
||||||
|
expect(markers).to_have_count(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_number_with_zero_value(live_server, page, map):
|
||||||
|
map.settings["properties"]["onLoadPanel"] = "facet"
|
||||||
|
map.settings["properties"]["facetKey"] = "mynumber|Filter|number"
|
||||||
|
map.save()
|
||||||
|
data = copy.deepcopy(DATALAYER_DATA1)
|
||||||
|
data["features"][0]["properties"]["mynumber"] = 0
|
||||||
|
DataLayerFactory(map=map, data=data)
|
||||||
|
DataLayerFactory(map=map, data=DATALAYER_DATA2)
|
||||||
|
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
||||||
|
expect(page.get_by_label("Min")).to_have_value("0")
|
||||||
|
expect(page.get_by_label("Max")).to_have_value("14")
|
||||||
|
page.get_by_label("Min").fill("1")
|
||||||
|
page.keyboard.press("Tab") # Move out of the input, so the "change" event is sent
|
||||||
|
markers = page.locator(".leaflet-marker-icon")
|
||||||
|
expect(markers).to_have_count(3)
|
||||||
|
|
Loading…
Reference in a new issue