umap-project 3.3.6__py3-none-any.whl → 3.4.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. umap/__init__.py +1 -1
  2. umap/context_processors.py +4 -1
  3. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/cs_CZ/LC_MESSAGES/django.po +43 -33
  5. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/da/LC_MESSAGES/django.po +43 -33
  7. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/de/LC_MESSAGES/django.po +35 -29
  9. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/el/LC_MESSAGES/django.po +35 -29
  11. umap/locale/en/LC_MESSAGES/django.po +34 -28
  12. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/es/LC_MESSAGES/django.po +43 -33
  14. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/et/LC_MESSAGES/django.po +58 -54
  16. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  17. umap/locale/eu/LC_MESSAGES/django.po +43 -33
  18. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/fa_IR/LC_MESSAGES/django.po +43 -33
  20. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/fr/LC_MESSAGES/django.po +36 -30
  22. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/gl/LC_MESSAGES/django.po +43 -33
  24. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/hu/LC_MESSAGES/django.po +35 -29
  26. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/is/LC_MESSAGES/django.po +43 -33
  28. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/it/LC_MESSAGES/django.po +43 -33
  30. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/nl/LC_MESSAGES/django.po +35 -29
  32. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/pl/LC_MESSAGES/django.po +43 -33
  34. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/pt/LC_MESSAGES/django.po +43 -33
  36. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/th_TH/LC_MESSAGES/django.po +310 -109
  38. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/zh_TW/LC_MESSAGES/django.po +80 -70
  40. umap/management/commands/switch_user.py +2 -2
  41. umap/static/umap/base.css +89 -32
  42. umap/static/umap/content.css +129 -33
  43. umap/static/umap/css/bar.css +82 -20
  44. umap/static/umap/css/browser.css +163 -0
  45. umap/static/umap/css/contextmenu.css +15 -0
  46. umap/static/umap/css/dialog.css +36 -16
  47. umap/static/umap/css/form.css +122 -32
  48. umap/static/umap/css/icon.css +46 -3
  49. umap/static/umap/css/panel.css +7 -3
  50. umap/static/umap/css/popup.css +34 -8
  51. umap/static/umap/css/tooltip.css +8 -4
  52. umap/static/umap/img/16-white.svg +26 -8
  53. umap/static/umap/img/16.svg +1 -1
  54. umap/static/umap/img/source/16-white.svg +36 -18
  55. umap/static/umap/img/source/16.svg +1 -1
  56. umap/static/umap/js/components/alerts/alert.css +69 -31
  57. umap/static/umap/js/components/alerts/alert.js +20 -2
  58. umap/static/umap/js/modules/browser.js +63 -55
  59. umap/static/umap/js/modules/caption.js +10 -7
  60. umap/static/umap/js/modules/data/features.js +82 -59
  61. umap/static/umap/js/modules/data/layer.js +56 -157
  62. umap/static/umap/js/modules/domutils.js +109 -0
  63. umap/static/umap/js/modules/filters.js +807 -0
  64. umap/static/umap/js/modules/form/builder.js +8 -5
  65. umap/static/umap/js/modules/form/fields.js +110 -220
  66. umap/static/umap/js/modules/formatter.js +24 -1
  67. umap/static/umap/js/modules/help.js +3 -2
  68. umap/static/umap/js/modules/importers/opendata.js +5 -0
  69. umap/static/umap/js/modules/importers/openrouteservice.js +6 -1
  70. umap/static/umap/js/modules/managers.js +265 -1
  71. umap/static/umap/js/modules/permissions.js +39 -31
  72. umap/static/umap/js/modules/rendering/controls.js +7 -7
  73. umap/static/umap/js/modules/rendering/icon.js +3 -8
  74. umap/static/umap/js/modules/rendering/layers/classified.js +17 -10
  75. umap/static/umap/js/modules/rendering/layers/cluster.js +5 -3
  76. umap/static/umap/js/modules/rendering/template.js +44 -8
  77. umap/static/umap/js/modules/rendering/ui.js +29 -23
  78. umap/static/umap/js/modules/rules.js +4 -3
  79. umap/static/umap/js/modules/schema.js +3 -6
  80. umap/static/umap/js/modules/share.js +4 -3
  81. umap/static/umap/js/modules/tableeditor.js +50 -38
  82. umap/static/umap/js/modules/templates.js +2 -3
  83. umap/static/umap/js/modules/ui/bar.js +42 -18
  84. umap/static/umap/js/modules/ui/dialog.js +33 -31
  85. umap/static/umap/js/modules/ui/panel.js +21 -7
  86. umap/static/umap/js/modules/ui/tooltip.js +6 -5
  87. umap/static/umap/js/modules/umap.js +149 -51
  88. umap/static/umap/js/modules/utils.js +23 -1
  89. umap/static/umap/js/umap.core.js +1 -110
  90. umap/static/umap/locale/am_ET.js +40 -14
  91. umap/static/umap/locale/am_ET.json +40 -14
  92. umap/static/umap/locale/ar.js +40 -14
  93. umap/static/umap/locale/ar.json +40 -14
  94. umap/static/umap/locale/ast.js +40 -14
  95. umap/static/umap/locale/ast.json +40 -14
  96. umap/static/umap/locale/bg.js +40 -14
  97. umap/static/umap/locale/bg.json +40 -14
  98. umap/static/umap/locale/br.js +47 -21
  99. umap/static/umap/locale/br.json +47 -21
  100. umap/static/umap/locale/ca.js +40 -14
  101. umap/static/umap/locale/ca.json +40 -14
  102. umap/static/umap/locale/cs_CZ.js +40 -14
  103. umap/static/umap/locale/cs_CZ.json +40 -14
  104. umap/static/umap/locale/da.js +40 -14
  105. umap/static/umap/locale/da.json +40 -14
  106. umap/static/umap/locale/de.js +39 -13
  107. umap/static/umap/locale/de.json +39 -13
  108. umap/static/umap/locale/el.js +40 -14
  109. umap/static/umap/locale/el.json +40 -14
  110. umap/static/umap/locale/en.js +39 -13
  111. umap/static/umap/locale/en.json +39 -13
  112. umap/static/umap/locale/en_US.json +40 -14
  113. umap/static/umap/locale/es.js +40 -14
  114. umap/static/umap/locale/es.json +40 -14
  115. umap/static/umap/locale/et.js +79 -53
  116. umap/static/umap/locale/et.json +79 -53
  117. umap/static/umap/locale/eu.js +72 -46
  118. umap/static/umap/locale/eu.json +72 -46
  119. umap/static/umap/locale/fa_IR.js +40 -14
  120. umap/static/umap/locale/fa_IR.json +40 -14
  121. umap/static/umap/locale/fi.js +40 -14
  122. umap/static/umap/locale/fi.json +40 -14
  123. umap/static/umap/locale/fr.js +39 -13
  124. umap/static/umap/locale/fr.json +39 -13
  125. umap/static/umap/locale/gl.js +40 -14
  126. umap/static/umap/locale/gl.json +40 -14
  127. umap/static/umap/locale/he.js +40 -14
  128. umap/static/umap/locale/he.json +40 -14
  129. umap/static/umap/locale/hr.js +40 -14
  130. umap/static/umap/locale/hr.json +40 -14
  131. umap/static/umap/locale/hu.js +40 -14
  132. umap/static/umap/locale/hu.json +40 -14
  133. umap/static/umap/locale/id.js +40 -14
  134. umap/static/umap/locale/id.json +40 -14
  135. umap/static/umap/locale/is.js +40 -14
  136. umap/static/umap/locale/is.json +40 -14
  137. umap/static/umap/locale/it.js +40 -14
  138. umap/static/umap/locale/it.json +40 -14
  139. umap/static/umap/locale/ja.js +40 -14
  140. umap/static/umap/locale/ja.json +40 -14
  141. umap/static/umap/locale/ko.js +40 -14
  142. umap/static/umap/locale/ko.json +40 -14
  143. umap/static/umap/locale/lt.js +40 -14
  144. umap/static/umap/locale/lt.json +40 -14
  145. umap/static/umap/locale/ms.js +40 -14
  146. umap/static/umap/locale/ms.json +40 -14
  147. umap/static/umap/locale/nl.js +40 -14
  148. umap/static/umap/locale/nl.json +40 -14
  149. umap/static/umap/locale/no.js +40 -14
  150. umap/static/umap/locale/no.json +40 -14
  151. umap/static/umap/locale/pl.js +40 -14
  152. umap/static/umap/locale/pl.json +40 -14
  153. umap/static/umap/locale/pl_PL.json +40 -14
  154. umap/static/umap/locale/pt.js +40 -14
  155. umap/static/umap/locale/pt.json +40 -14
  156. umap/static/umap/locale/pt_BR.js +40 -14
  157. umap/static/umap/locale/pt_BR.json +40 -14
  158. umap/static/umap/locale/pt_PT.js +40 -14
  159. umap/static/umap/locale/pt_PT.json +40 -14
  160. umap/static/umap/locale/ro.js +40 -14
  161. umap/static/umap/locale/ro.json +40 -14
  162. umap/static/umap/locale/ru.js +40 -14
  163. umap/static/umap/locale/ru.json +40 -14
  164. umap/static/umap/locale/sk_SK.js +40 -14
  165. umap/static/umap/locale/sk_SK.json +40 -14
  166. umap/static/umap/locale/sl.js +40 -14
  167. umap/static/umap/locale/sl.json +40 -14
  168. umap/static/umap/locale/sr.js +40 -14
  169. umap/static/umap/locale/sr.json +40 -14
  170. umap/static/umap/locale/sv.js +40 -14
  171. umap/static/umap/locale/sv.json +40 -14
  172. umap/static/umap/locale/th_TH.js +40 -14
  173. umap/static/umap/locale/th_TH.json +40 -14
  174. umap/static/umap/locale/tr.js +40 -14
  175. umap/static/umap/locale/tr.json +40 -14
  176. umap/static/umap/locale/uk_UA.js +40 -14
  177. umap/static/umap/locale/uk_UA.json +40 -14
  178. umap/static/umap/locale/vi.js +40 -14
  179. umap/static/umap/locale/vi.json +40 -14
  180. umap/static/umap/locale/vi_VN.json +40 -14
  181. umap/static/umap/locale/zh.js +40 -14
  182. umap/static/umap/locale/zh.json +40 -14
  183. umap/static/umap/locale/zh_CN.json +40 -14
  184. umap/static/umap/locale/zh_TW.Big5.json +40 -14
  185. umap/static/umap/locale/zh_TW.js +39 -13
  186. umap/static/umap/locale/zh_TW.json +39 -13
  187. umap/static/umap/map.css +60 -223
  188. umap/static/umap/unittests/utils.js +18 -0
  189. umap/static/umap/vars.css +23 -5
  190. umap/templates/umap/components/alerts/alert.html +32 -29
  191. umap/templates/umap/css.html +2 -1
  192. umap/templates/umap/login_popup_end.html +18 -9
  193. umap/templates/umap/user_map_table.html +7 -2
  194. umap/tests/integration/conftest.py +2 -6
  195. umap/tests/integration/test_anonymous_owned_map.py +89 -36
  196. umap/tests/integration/test_basics.py +25 -1
  197. umap/tests/integration/test_browser.py +37 -0
  198. umap/tests/integration/test_draw_polygon.py +2 -0
  199. umap/tests/integration/test_edit_marker.py +1 -1
  200. umap/tests/integration/test_export_map.py +19 -0
  201. umap/tests/integration/test_fields.py +522 -0
  202. umap/tests/integration/test_filters.py +617 -0
  203. umap/tests/integration/test_import.py +15 -42
  204. umap/tests/integration/test_remote_data.py +60 -4
  205. umap/tests/integration/test_share.py +4 -4
  206. umap/tests/integration/test_tableeditor.py +31 -7
  207. umap/tests/integration/test_websocket_sync.py +3 -1
  208. umap/tests/test_dashboard.py +10 -0
  209. umap/tests/test_utils.py +15 -1
  210. umap/urls.py +1 -0
  211. umap/utils.py +6 -0
  212. umap/views.py +5 -0
  213. {umap_project-3.3.6.dist-info → umap_project-3.4.0b1.dist-info}/METADATA +12 -12
  214. {umap_project-3.3.6.dist-info → umap_project-3.4.0b1.dist-info}/RECORD +217 -214
  215. umap/static/umap/js/modules/facets.js +0 -164
  216. umap/tests/integration/test_facets_browser.py +0 -279
  217. {umap_project-3.3.6.dist-info → umap_project-3.4.0b1.dist-info}/WHEEL +0 -0
  218. {umap_project-3.3.6.dist-info → umap_project-3.4.0b1.dist-info}/entry_points.txt +0 -0
  219. {umap_project-3.3.6.dist-info → umap_project-3.4.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,807 @@
1
+ import { translate } from './i18n.js'
2
+ import { Form } from './form/builder.js'
3
+ import * as Utils from './utils.js'
4
+ import Orderable from './orderable.js'
5
+ import { Fields } from './form/fields.js'
6
+
7
+ const EMPTY_VALUE = translate('<empty value>')
8
+
9
+ const getParser = (type) => {
10
+ switch (type) {
11
+ case 'Number':
12
+ return Number.parseFloat
13
+ case 'Datetime':
14
+ return (v) => new Date(v)
15
+ case 'Date':
16
+ return Utils.parseNaiveDate
17
+ case 'Boolean':
18
+ return Boolean
19
+ case 'Enum':
20
+ return (v) => {
21
+ if (!v) return [EMPTY_VALUE]
22
+ return String(v || '')
23
+ .split(',')
24
+ .map((s) => s.trim())
25
+ }
26
+ default:
27
+ return (v) => String(v || '')
28
+ }
29
+ }
30
+ const Widgets = {}
31
+
32
+ class BaseWidget {
33
+ constructor(parent, label, field) {
34
+ this.parent = parent
35
+ this.label = label
36
+ this.field = field
37
+ }
38
+
39
+ get userData() {
40
+ return this.parent.userData[this.field] || {}
41
+ }
42
+
43
+ dumps() {
44
+ const props = {
45
+ widget: this.KEY,
46
+ fieldKey: this.field,
47
+ }
48
+ if (this.label) {
49
+ props.label = this.label
50
+ }
51
+ return props
52
+ }
53
+
54
+ getFormField(field) {
55
+ return 'FilterByCheckbox'
56
+ }
57
+
58
+ computeInitialData(data, value) {}
59
+ }
60
+
61
+ Widgets.MinMax = class extends BaseWidget {
62
+ constructor(parent, label, field) {
63
+ super(parent, label, field)
64
+ // FIXME make it dynamic from class name
65
+ this.KEY = 'MinMax'
66
+ }
67
+ match(value) {
68
+ if (this.userData.min > value) return true
69
+ if (this.userData.max < value) return true
70
+ return false
71
+ }
72
+ isActive() {
73
+ return this.userData.min !== undefined || this.userData.max !== undefined
74
+ }
75
+ getFormField(field) {
76
+ if (field.type === 'Number') {
77
+ return 'FilterByNumber'
78
+ }
79
+ if (field.type === 'Date') {
80
+ return 'FilterByDate'
81
+ }
82
+ if (field.type === 'Datetime') {
83
+ return 'FilterByDateTime'
84
+ }
85
+ return super.getFormField(field)
86
+ }
87
+ computeInitialData(data, value) {
88
+ if (value === undefined || value === null) return
89
+ if (data.min === undefined || data.min > value) {
90
+ data.min = value
91
+ }
92
+ if (data.max === undefined || data.max < value) {
93
+ data.max = value
94
+ }
95
+ }
96
+ }
97
+ // Can't use static properties yet (baseline >= 2022)
98
+ Widgets.MinMax.NAME = translate('Min/Max')
99
+
100
+ class Choices extends BaseWidget {
101
+ match(value) {
102
+ if (!this.userData.selected?.length) return false
103
+ if (Array.isArray(value)) {
104
+ const intersection = value.filter((item) => this.userData.selected.includes(item))
105
+ if (intersection.length !== this.userData.selected.length) return true
106
+ } else {
107
+ value = value || EMPTY_VALUE
108
+ if (!this.userData.selected.includes(value)) return true
109
+ }
110
+ return false
111
+ }
112
+ isActive() {
113
+ return !!this.userData.selected?.length
114
+ }
115
+
116
+ computeInitialData(data, value) {
117
+ data.choices ??= new Set()
118
+ if (Array.isArray(value)) {
119
+ data.choices = new Set([...data.choices, ...value])
120
+ } else {
121
+ value = value || EMPTY_VALUE
122
+ data.choices.add(value)
123
+ }
124
+ }
125
+ }
126
+
127
+ Widgets.Checkbox = class extends Choices {
128
+ constructor(parent, label, field) {
129
+ super(parent, label, field)
130
+ this.KEY = 'Checkbox'
131
+ }
132
+ }
133
+ Widgets.Checkbox.NAME = translate('Multiple choices')
134
+
135
+ Widgets.Radio = class extends Choices {
136
+ constructor(parent, label, field) {
137
+ super(parent, label, field)
138
+ this.KEY = 'Radio'
139
+ }
140
+ getFormField(field) {
141
+ return 'FilterByRadio'
142
+ }
143
+ }
144
+ Widgets.Radio.NAME = translate('Exclusive choice')
145
+
146
+ Widgets.Switch = class extends BaseWidget {
147
+ constructor(parent, label, field) {
148
+ super(parent, label, field)
149
+ this.KEY = 'Switch'
150
+ }
151
+ match(value) {
152
+ if (this.userData.wanted === undefined) return false
153
+ return !!value !== this.userData.wanted
154
+ }
155
+ isActive() {
156
+ return this.userData.wanted !== undefined
157
+ }
158
+ getFormField(field) {
159
+ return 'FilterBySwitch'
160
+ }
161
+ }
162
+ Widgets.Switch.NAME = translate('Yes/No')
163
+
164
+ const loadWidget = (key) => {
165
+ return Widgets[key] || Widgets.Checkbox
166
+ }
167
+
168
+ export class Filters {
169
+ constructor(parent, umap) {
170
+ this._parent = parent
171
+ this._umap = umap
172
+ this.available = new Map()
173
+ this.userData = {}
174
+ this.load()
175
+ }
176
+
177
+ get size() {
178
+ return this.available.size
179
+ }
180
+
181
+ isActive() {
182
+ return this.available.values().some((obj) => obj.isActive())
183
+ }
184
+
185
+ // Loop on the data to compute the list of choices, min
186
+ // and max values.
187
+ computeInitialData() {
188
+ const initialData = Object.fromEntries(
189
+ this.available.keys().map((name) => [name, {}])
190
+ )
191
+
192
+ for (const [name, filter] of this.available.entries()) {
193
+ const field = this._parent.fields.get(name)
194
+ if (!field) continue
195
+ const parser = getParser(field.type)
196
+ this._parent.eachFeature((feature) => {
197
+ let value = feature.properties[name]
198
+ value = parser(value)
199
+ filter.computeInitialData(initialData[name], value)
200
+ })
201
+ }
202
+ return initialData
203
+ }
204
+
205
+ buildFormFields() {
206
+ const initialData = this.computeInitialData()
207
+
208
+ const formFields = []
209
+ for (const [name, filter] of this.available.entries()) {
210
+ const field = this._parent.fields.get(name)
211
+ if (!field) continue
212
+ formFields.push([
213
+ `userData.${name}`,
214
+ {
215
+ initialData: initialData[name] || {},
216
+ handler: filter.getFormField(field),
217
+ label: Utils.escapeHTML(this.available.get(name).label || field.key),
218
+ onClick: () => {
219
+ this._parent
220
+ .edit()
221
+ .then((panel) => panel.scrollTo('details#fields-management'))
222
+ this._parent.filters.createFilterForm(name)
223
+ },
224
+ },
225
+ ])
226
+ }
227
+ return formFields
228
+ }
229
+
230
+ load() {
231
+ let filters = this._parent.properties.filters || []
232
+ // TMP fix for dev server to update map created before changing
233
+ // filters to be an array
234
+ if (typeof filters === 'object' && !Array.isArray(filters) && filters !== null) {
235
+ filters = Object.entries(filters).map(([fieldKey, props]) => {
236
+ return { fieldKey, ...props }
237
+ })
238
+ }
239
+ for (const filter of filters) {
240
+ this._add({ ...filter })
241
+ }
242
+ }
243
+
244
+ dumps(sync = true) {
245
+ const oldValue = this._parent.properties.filters
246
+ this._parent.properties.filters = Array.from(
247
+ this.available.entries().map(([key, filter]) => filter.dumps())
248
+ )
249
+ if (sync) {
250
+ this._parent.sync.update(
251
+ 'properties.filters',
252
+ this._parent.properties.filters,
253
+ oldValue
254
+ )
255
+ this._parent.render(['properties.filters'])
256
+ }
257
+ }
258
+
259
+ has(fieldKey) {
260
+ return this.available.has(fieldKey)
261
+ }
262
+
263
+ get(fieldKey) {
264
+ return this.available.get(fieldKey)
265
+ }
266
+
267
+ add({ fieldKey, label, widget }) {
268
+ if (!this.available.has(fieldKey)) {
269
+ this.update({ fieldKey, label, widget })
270
+ }
271
+ }
272
+
273
+ _add({ fieldKey, label, widget }) {
274
+ const klass = loadWidget(widget)
275
+ const inst = new klass(this, label, fieldKey)
276
+ this.available.set(fieldKey, inst)
277
+ }
278
+
279
+ update({ fieldKey, label, widget }) {
280
+ this._add({ fieldKey, label, widget })
281
+ this.dumps()
282
+ }
283
+
284
+ remove(fieldKey) {
285
+ this.available.delete(fieldKey)
286
+ this.dumps()
287
+ }
288
+
289
+ edit() {
290
+ const template = `
291
+ <div>
292
+ <h3>${translate('Manage filters')}</h3>
293
+ </div>
294
+ `
295
+ const body = Utils.loadTemplate(template)
296
+ this._listFilters(this._umap.filters, body, translate('Map (all layers)'))
297
+ this._umap.datalayers.active().forEach((datalayer) => {
298
+ this._listFilters(
299
+ datalayer.filters,
300
+ body,
301
+ `${datalayer.getName()} (${translate('single layer')})`
302
+ )
303
+ })
304
+ this._umap.dialog.open({ template: body })
305
+ }
306
+
307
+ _listFilters(filters, container, title) {
308
+ const template = `
309
+ <details open>
310
+ <summary>${title}</summary>
311
+ <ul data-ref=ul></ul>
312
+ <div>
313
+ <button type="button" data-ref=add>${translate('Add filter')}</button>
314
+ </div>
315
+ </details>
316
+ `
317
+ const [body, { ul, add }] = Utils.loadTemplateWithRefs(template)
318
+ if (!filters._parent.fields.size) {
319
+ add.disabled = true
320
+ ul.appendChild(
321
+ Utils.loadTemplate(
322
+ `<li>${translate('Add a field prior to create a filter.')}</li>`
323
+ )
324
+ )
325
+ }
326
+ if (!filters.size) {
327
+ body.open = false
328
+ }
329
+ filters.available.forEach((filter, fieldKey) => {
330
+ const [li, { edit, remove }] = Utils.loadTemplateWithRefs(
331
+ `<li class="orderable with-toolbox" data-fieldkey="${fieldKey}">
332
+ <span>
333
+ ${filter.label || fieldKey}
334
+ </span>
335
+ <span>
336
+ <button class="icon icon-16 icon-edit" data-ref="edit" title="${translate('Edit this filter')}"></button>
337
+ <button class="icon icon-16 icon-delete" data-ref="remove" title="${translate('Remove this filter')}"></button>
338
+ <i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i>
339
+ </span>
340
+ </li>`
341
+ )
342
+ ul.appendChild(li)
343
+ remove.addEventListener('click', () => {
344
+ filters.remove(fieldKey)
345
+ filters._parent
346
+ .edit()
347
+ .then((panel) => panel.scrollTo('details#fields-management'))
348
+ })
349
+ edit.addEventListener('click', () => {
350
+ filters.createFilterForm(fieldKey)
351
+ })
352
+ })
353
+ add.addEventListener('click', () => filters.createFilterForm())
354
+ const onReorder = (src, dst, initialIndex, finalIndex) => {
355
+ const orderedKeys = Array.from(ul.querySelectorAll('li')).map(
356
+ (el) => el.dataset.fieldkey
357
+ )
358
+ const oldValue = Utils.CopyJSON(filters._parent.properties.filters)
359
+ const copy = filters.available.entries().reduce((acc, [key, filter]) => {
360
+ acc[key] = filter.dumps()
361
+ return acc
362
+ }, {})
363
+
364
+ filters.available.clear()
365
+ for (const fieldKey of orderedKeys) {
366
+ filters.add({ ...copy[fieldKey] })
367
+ }
368
+ filters._parent.sync.update(
369
+ 'properties.filters',
370
+ filters._parent.properties.filters,
371
+ oldValue
372
+ )
373
+ }
374
+ const orderable = new Orderable(ul, onReorder)
375
+ container.appendChild(body)
376
+ }
377
+
378
+ createFilterForm(fieldKey) {
379
+ let widget = 'Checkbox'
380
+ const field = this._parent.fields.get(fieldKey)
381
+ if (['Number', 'Date', 'Datetime'].includes(field?.type)) {
382
+ widget = 'MinMax'
383
+ } else if (field?.type === 'Boolean') {
384
+ widget = 'Switch'
385
+ }
386
+ const properties = {
387
+ target: this._parent.fields.size ? this._parent : null,
388
+ fieldKey,
389
+ widget,
390
+ ...(this.available?.get(fieldKey)?.dumps() || {}),
391
+ }
392
+ const fieldKeys = fieldKey
393
+ ? [fieldKey]
394
+ : [
395
+ '',
396
+ ...this._parent.fieldKeys.filter((fieldKey) => !this.available.has(fieldKey)),
397
+ ]
398
+ const metadata = [
399
+ [
400
+ 'target',
401
+ {
402
+ handler: 'FilterTargetSelect',
403
+ label: translate('Apply filter to'),
404
+ disabled: Boolean(fieldKey),
405
+ },
406
+ ],
407
+ [
408
+ 'fieldKey',
409
+ {
410
+ handler: 'Select',
411
+ selectOptions: fieldKeys,
412
+ label: translate('Filter on'),
413
+ },
414
+ ],
415
+ [
416
+ 'label',
417
+ { handler: 'Input', label: translate('Human readable name of the filter') },
418
+ ],
419
+ [
420
+ 'widget',
421
+ {
422
+ handler: 'MultiChoice',
423
+ choices: Object.entries(Widgets).map(([key, klass]) => [key, klass.NAME]),
424
+ label: translate('Widget for the filter'),
425
+ },
426
+ ],
427
+ ]
428
+ const form = new Form(properties, metadata, { umap: this._umap })
429
+ let label
430
+ if (fieldKey) {
431
+ label = translate('Edit filter')
432
+ } else {
433
+ label = translate('Add filter')
434
+ }
435
+
436
+ const [container, { body, editField }] = Utils.loadTemplateWithRefs(`
437
+ <div>
438
+ <h3>${label}</h3>
439
+ <div data-ref=body></div>
440
+ <button type="button" data-ref=editField><i class="icon icon-16 icon-edit"></i>${translate('Edit this field')}</button>
441
+ </div>
442
+ `)
443
+ body.appendChild(form.build())
444
+ editField.addEventListener('click', () => {
445
+ this._umap.dialog.accept()
446
+ this._parent.fields.editField(fieldKey)
447
+ })
448
+
449
+ return this._umap.dialog.open({ template: container }).then(() => {
450
+ const target = properties.target
451
+ if (!target) return
452
+ if (!properties.fieldKey) return
453
+ if (fieldKey) {
454
+ target.filters.update({ ...properties })
455
+ } else {
456
+ target.filters.add({ ...properties })
457
+ }
458
+ target.filters._parent
459
+ .edit()
460
+ .then((panel) => panel.scrollTo('details#fields-management'))
461
+ })
462
+ }
463
+
464
+ buildForm(container) {
465
+ const form = new FiltersForm(this, this.buildFormFields(), { className: 'formbox' })
466
+ container.appendChild(form.build())
467
+ return form
468
+ }
469
+
470
+ matchFeature(feature) {
471
+ for (const [fieldKey, obj] of this.available.entries()) {
472
+ if (!obj.isActive()) continue
473
+ const field = this._parent.fields.get(fieldKey)
474
+ // This field may only exist on another layer.
475
+ if (!field) continue
476
+ let value = feature.properties[fieldKey]
477
+ const parser = getParser(field.type)
478
+ value = parser(value)
479
+ if (obj.match(value)) return true
480
+ }
481
+ return false
482
+ }
483
+ }
484
+
485
+ class FiltersForm extends Form {
486
+ buildField(field) {
487
+ const [root, elements] = field.buildTemplate()
488
+ elements.editFilter.addEventListener('click', field.properties.onClick)
489
+ field.build()
490
+ }
491
+
492
+ getHelperTemplate(helper) {
493
+ return helper.getTemplate()
494
+ }
495
+
496
+ getTemplate(helper) {
497
+ return `
498
+ <fieldset class="umap-filter">
499
+ <legend data-ref=label>
500
+ <span>${helper.properties.label}</span>
501
+ <span class="filter-toolbox">
502
+ <button type="button" class="icon icon-16 icon-edit show-on-edit" data-ref=editFilter title="${translate('Edit filter')}"></button>
503
+ </span>
504
+ </legend>
505
+ <div data-ref="container">${helper.getTemplate()}</div>
506
+ </fieldset>
507
+ `
508
+ }
509
+ }
510
+
511
+ const FilterBase = class extends Fields.Base {
512
+ buildLabel() {}
513
+ }
514
+
515
+ const FilterByChoices = class extends FilterBase {
516
+ getTemplate() {
517
+ return '<ul data-ref=ul></ul>'
518
+ }
519
+
520
+ build() {
521
+ this.type = this.getType()
522
+
523
+ const choices = Array.from(this.properties.initialData.choices || [])
524
+ choices.sort()
525
+ choices.forEach((value) => this.buildLi(value))
526
+ super.build()
527
+ }
528
+
529
+ buildLi(value) {
530
+ const name = `${this.type}_${this.name}`
531
+ const [li, { input, label }] = Utils.loadTemplateWithRefs(`
532
+ <li>
533
+ <label>
534
+ <input type="${this.type}" name="${name}" data-ref=input />
535
+ <span data-ref=label></span>
536
+ </label>
537
+ </li>
538
+ `)
539
+ label.textContent = value
540
+ input.checked = this.get()?.selected?.includes(value)
541
+ input.dataset.value = value
542
+ input.addEventListener('change', () => this.sync())
543
+ this.elements.ul.appendChild(li)
544
+ }
545
+
546
+ toJS() {
547
+ return {
548
+ selected: [...this.elements.ul.querySelectorAll('input:checked')].map(
549
+ (i) => i.dataset.value
550
+ ),
551
+ }
552
+ }
553
+ }
554
+
555
+ Fields.FilterByCheckbox = class extends FilterByChoices {
556
+ getType() {
557
+ return 'checkbox'
558
+ }
559
+ }
560
+
561
+ Fields.FilterByRadio = class extends FilterByChoices {
562
+ getType() {
563
+ return 'radio'
564
+ }
565
+ }
566
+
567
+ Fields.MinMaxBase = class extends FilterBase {
568
+ getLabels() {
569
+ return [translate('Min'), translate('Max')]
570
+ }
571
+
572
+ prepareForHTML(value) {
573
+ return value?.valueOf() ?? null
574
+ }
575
+
576
+ getTemplate() {
577
+ const [minLabel, maxLabel] = this.getLabels()
578
+ const { min, max } = this.properties.initialData
579
+ const inputType = this.getInputType()
580
+ const minHTML = this.prepareForHTML(min)
581
+ const maxHTML = this.prepareForHTML(max)
582
+ return `
583
+ <label>${minLabel}<input min="${minHTML}" max="${maxHTML}" step=any type="${inputType}" data-ref=minInput /></label>
584
+ <label>${maxLabel}<input min="${minHTML}" max="${maxHTML}" step=any type="${inputType}" data-ref=maxInput /></label>
585
+ `
586
+ }
587
+
588
+ build() {
589
+ this.minInput = this.elements.minInput
590
+ this.maxInput = this.elements.maxInput
591
+ const { min, max, type } = this.properties.initialData
592
+ const { min: userMin, max: userMax } = this.get() || {}
593
+
594
+ const currentMin = userMin !== undefined ? userMin : min
595
+ const currentMax = userMax !== undefined ? userMax : max
596
+ if (min != null) {
597
+ // The value stored using setAttribute is not modified by
598
+ // user input, and will be used as initial value when calling
599
+ // form.reset(), and can also be retrieve later on by using
600
+ // getAttributing, to compare with current value and know
601
+ // if this value has been modified by the user
602
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset
603
+ this.minInput.setAttribute('value', this.prepareForHTML(min))
604
+ this.minInput.value = this.prepareForHTML(currentMin)
605
+ }
606
+
607
+ if (max != null) {
608
+ // Cf comment above about setAttribute vs value
609
+ this.maxInput.setAttribute('value', this.prepareForHTML(max))
610
+ this.maxInput.value = this.prepareForHTML(currentMax)
611
+ }
612
+ this.toggleStatus()
613
+
614
+ this.minInput.addEventListener('change', () => this.sync())
615
+ this.maxInput.addEventListener('change', () => this.sync())
616
+ super.build()
617
+ }
618
+
619
+ toggleStatus() {
620
+ this.minInput.dataset.modified = this.isMinModified()
621
+ this.maxInput.dataset.modified = this.isMaxModified()
622
+ }
623
+
624
+ sync() {
625
+ super.sync()
626
+ this.toggleStatus()
627
+ }
628
+
629
+ isMinModified() {
630
+ const default_ = this.minInput.getAttribute('value')
631
+ const current = this.minInput.value
632
+ return current !== default_
633
+ }
634
+
635
+ isMaxModified() {
636
+ const default_ = this.maxInput.getAttribute('value')
637
+ const current = this.maxInput.value
638
+ return current !== default_
639
+ }
640
+
641
+ toJS() {
642
+ const opts = {}
643
+ if (this.minInput.value !== '' && this.isMinModified()) {
644
+ opts.min = this.prepareForJS(this.minInput.value)
645
+ }
646
+ if (this.maxInput.value !== '' && this.isMaxModified()) {
647
+ opts.max = this.prepareForJS(this.maxInput.value)
648
+ }
649
+ return opts
650
+ }
651
+ }
652
+
653
+ Fields.FilterByNumber = class extends Fields.MinMaxBase {
654
+ getInputType(type) {
655
+ return 'number'
656
+ }
657
+
658
+ prepareForJS(value) {
659
+ return new Number(value)
660
+ }
661
+ }
662
+
663
+ Fields.FilterByDate = class extends Fields.MinMaxBase {
664
+ getInputType(type) {
665
+ return 'date'
666
+ }
667
+
668
+ prepareForJS(value) {
669
+ return new Date(value)
670
+ }
671
+
672
+ toLocaleDateTime(dt) {
673
+ return new Date(dt.valueOf() - dt.getTimezoneOffset() * 60000)
674
+ }
675
+
676
+ prepareForHTML(value) {
677
+ // Value must be in local time
678
+ if (!value || isNaN(value)) return
679
+ return this.toLocaleDateTime(value).toISOString().substr(0, 10)
680
+ }
681
+
682
+ getLabels() {
683
+ return [translate('From'), translate('Until')]
684
+ }
685
+ }
686
+
687
+ Fields.FilterByDateTime = class extends Fields.FilterByDate {
688
+ getInputType() {
689
+ return 'datetime-local'
690
+ }
691
+
692
+ prepareForHTML(value) {
693
+ // Value must be in local time
694
+ if (Number.isNaN(value)) return
695
+ return this.toLocaleDateTime(value).toISOString().slice(0, -1)
696
+ }
697
+ }
698
+
699
+ Fields.FilterTargetSelect = class extends Fields.Select {
700
+ getOptions() {
701
+ const options = []
702
+ if (this.builder.properties.umap.fields.size) {
703
+ if (!this.obj.target) {
704
+ this.obj.target = this.builder.properties.umap
705
+ }
706
+ options.push([
707
+ `map:${this.builder.properties.umap.id}`,
708
+ `${this.builder.properties.umap.properties.name} (${translate('all layers')})`,
709
+ ])
710
+ }
711
+ this.builder.properties.umap.datalayers.reverse().map((datalayer) => {
712
+ if (datalayer.isBrowsable() && datalayer.fields.size) {
713
+ if (!this.obj.target) {
714
+ this.obj.target = datalayer
715
+ }
716
+ options.push([
717
+ `layer:${datalayer.id}`,
718
+ `${datalayer.getName()} (${translate('single layer')})`,
719
+ ])
720
+ }
721
+ })
722
+ return options
723
+ }
724
+
725
+ toHTML() {
726
+ if (!this.obj.target) return null
727
+ // TODO: better way to check for class
728
+ // Importing DataLayer will end in circular import
729
+ const type = this.obj.target._umap ? 'layer' : 'map'
730
+ return `${type}:${this.obj.target?.id}`
731
+ }
732
+
733
+ toJS() {
734
+ const value = this.value()
735
+ if (!value) return null
736
+ const [type, id] = value.split(':')
737
+ if (type === 'map') {
738
+ return this.builder.properties.umap
739
+ }
740
+ return this.builder.properties.umap.datalayers[id]
741
+ }
742
+ }
743
+
744
+ Fields.FilterBySwitch = class extends FilterBase {
745
+ getTemplate() {
746
+ return `
747
+ <div class="ternary-switch">
748
+ <input type="radio" id="${this.id}.1" name="${this.name}" value="true" />
749
+ <label tabindex="0" for="${this.id}.1">${translate('yes')}</label>
750
+ <input type="radio" id="${this.id}.2" name="${this.name}" value="unset" checked />
751
+ <label tabindex="0" for="${this.id}.2">${translate('unset')}</label>
752
+ <input type="radio" id="${this.id}.3" name="${this.name}" value="false" />
753
+ <label tabindex="0" for="${this.id}.3">${translate('no')}</label>
754
+ </div>
755
+ `
756
+ }
757
+
758
+ build() {
759
+ super.build()
760
+ this.inputs = Array.from(this.form[this.name])
761
+ for (const input of this.inputs) {
762
+ input.addEventListener('change', () => this.sync())
763
+ }
764
+ }
765
+
766
+ value() {
767
+ return this.form[this.name].value
768
+ }
769
+
770
+ toJS() {
771
+ if (this.value() === 'unset') return {}
772
+ return { wanted: this.value() === 'true' }
773
+ }
774
+ }
775
+
776
+ export const migrateLegacyFilters = (properties) => {
777
+ const legacy = properties.advancedFilterKey || properties.facetKey
778
+ if (!legacy) return false
779
+ properties.filters ??= []
780
+ properties.fields ??= []
781
+ for (const filter of legacy.split(',')) {
782
+ let [fieldKey, label, widget] = filter.split('|')
783
+ let type = 'String'
784
+ if (['number', 'date', 'datetime'].includes(widget)) {
785
+ // Retrocompat
786
+ if (widget === 'number') {
787
+ type = 'Number'
788
+ } else if (widget === 'datetime') {
789
+ type = 'Datetime'
790
+ } else if (widget === 'date') {
791
+ type = 'Date'
792
+ }
793
+ widget = 'MinMax'
794
+ }
795
+ if (widget === 'radio') {
796
+ widget = 'Radio'
797
+ }
798
+ if (!(widget in Widgets)) {
799
+ widget = 'Checkbox'
800
+ }
801
+ properties.filters.push({ fieldKey, label, widget })
802
+ properties.fields.push({ key: fieldKey, type })
803
+ }
804
+ delete properties.facetKey
805
+ delete properties.advancedFilterKey
806
+ return true
807
+ }