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