umap-project 2.3.1__py3-none-any.whl → 2.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.

Potentially problematic release.


This version of umap-project might be problematic. Click here for more details.

Files changed (204) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.po +81 -31
  3. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/fr/LC_MESSAGES/django.po +117 -66
  5. umap/management/commands/run_websocket_server.py +23 -0
  6. umap/models.py +6 -1
  7. umap/settings/base.py +11 -3
  8. umap/static/umap/base.css +64 -184
  9. umap/static/umap/content.css +3 -2
  10. umap/static/umap/css/dialog.css +18 -0
  11. umap/static/umap/css/icon.css +8 -0
  12. umap/static/umap/css/importers.css +51 -0
  13. umap/static/umap/css/panel.css +18 -57
  14. umap/static/umap/css/tooltip.css +59 -0
  15. umap/static/umap/css/window.css +35 -0
  16. umap/static/umap/img/16-white.svg +1 -3
  17. umap/static/umap/img/alert-icon-error.svg +8 -0
  18. umap/static/umap/img/alert-icon-info.svg +4 -0
  19. umap/static/umap/img/alert-icon-success.svg +3 -0
  20. umap/static/umap/img/icon-external-link.svg +3 -0
  21. umap/static/umap/img/importers/communesfr.svg +5 -0
  22. umap/static/umap/img/importers/datasets.svg +13 -0
  23. umap/static/umap/img/importers/geodatamine.svg +10 -0
  24. umap/static/umap/img/importers/overpass.svg +7 -0
  25. umap/static/umap/img/importers/random.svg +18 -0
  26. umap/static/umap/img/importers/random1.svg +4 -0
  27. umap/static/umap/img/importers/random2.svg +4 -0
  28. umap/static/umap/img/source/16-white.svg +2 -4
  29. umap/static/umap/js/components/alerts/alert.css +160 -0
  30. umap/static/umap/js/components/alerts/alert.js +169 -0
  31. umap/static/umap/js/components/base.js +54 -0
  32. umap/static/umap/js/modules/autocomplete.js +347 -0
  33. umap/static/umap/js/modules/browser.js +6 -6
  34. umap/static/umap/js/modules/caption.js +5 -4
  35. umap/static/umap/js/modules/global.js +36 -12
  36. umap/static/umap/js/modules/help.js +255 -0
  37. umap/static/umap/js/modules/importer.js +308 -0
  38. umap/static/umap/js/modules/importers/communesfr.js +44 -0
  39. umap/static/umap/js/modules/importers/datasets.js +42 -0
  40. umap/static/umap/js/modules/importers/geodatamine.js +95 -0
  41. umap/static/umap/js/modules/importers/overpass.js +84 -0
  42. umap/static/umap/js/modules/request.js +12 -14
  43. umap/static/umap/js/modules/rules.js +241 -0
  44. umap/static/umap/js/modules/schema.js +63 -14
  45. umap/static/umap/js/modules/sync/engine.js +93 -0
  46. umap/static/umap/js/modules/sync/updaters.js +109 -0
  47. umap/static/umap/js/modules/sync/websocket.js +25 -0
  48. umap/static/umap/js/modules/ui/dialog.js +52 -0
  49. umap/static/umap/js/modules/{panel.js → ui/panel.js} +25 -14
  50. umap/static/umap/js/modules/ui/tooltip.js +116 -0
  51. umap/static/umap/js/modules/utils.js +25 -18
  52. umap/static/umap/js/umap.controls.js +13 -14
  53. umap/static/umap/js/umap.core.js +1 -324
  54. umap/static/umap/js/umap.features.js +77 -29
  55. umap/static/umap/js/umap.forms.js +9 -13
  56. umap/static/umap/js/umap.js +254 -215
  57. umap/static/umap/js/umap.layer.js +152 -74
  58. umap/static/umap/js/umap.permissions.js +5 -9
  59. umap/static/umap/js/umap.popup.js +1 -1
  60. umap/static/umap/js/umap.tableeditor.js +8 -8
  61. umap/static/umap/locale/am_ET.js +51 -16
  62. umap/static/umap/locale/am_ET.json +51 -16
  63. umap/static/umap/locale/ar.js +51 -16
  64. umap/static/umap/locale/ar.json +51 -16
  65. umap/static/umap/locale/ast.js +51 -16
  66. umap/static/umap/locale/ast.json +51 -16
  67. umap/static/umap/locale/bg.js +51 -16
  68. umap/static/umap/locale/bg.json +51 -16
  69. umap/static/umap/locale/br.js +55 -20
  70. umap/static/umap/locale/br.json +55 -20
  71. umap/static/umap/locale/ca.js +51 -16
  72. umap/static/umap/locale/ca.json +51 -16
  73. umap/static/umap/locale/cs_CZ.js +93 -58
  74. umap/static/umap/locale/cs_CZ.json +93 -58
  75. umap/static/umap/locale/da.js +51 -16
  76. umap/static/umap/locale/da.json +51 -16
  77. umap/static/umap/locale/de.js +56 -21
  78. umap/static/umap/locale/de.json +56 -21
  79. umap/static/umap/locale/el.js +51 -16
  80. umap/static/umap/locale/el.json +51 -16
  81. umap/static/umap/locale/en.js +52 -16
  82. umap/static/umap/locale/en.json +52 -16
  83. umap/static/umap/locale/en_US.json +51 -16
  84. umap/static/umap/locale/es.js +51 -16
  85. umap/static/umap/locale/es.json +51 -16
  86. umap/static/umap/locale/et.js +51 -16
  87. umap/static/umap/locale/et.json +51 -16
  88. umap/static/umap/locale/eu.js +51 -16
  89. umap/static/umap/locale/eu.json +51 -16
  90. umap/static/umap/locale/fa_IR.js +51 -16
  91. umap/static/umap/locale/fa_IR.json +51 -16
  92. umap/static/umap/locale/fi.js +51 -16
  93. umap/static/umap/locale/fi.json +51 -16
  94. umap/static/umap/locale/fr.js +61 -25
  95. umap/static/umap/locale/fr.json +61 -25
  96. umap/static/umap/locale/gl.js +51 -16
  97. umap/static/umap/locale/gl.json +51 -16
  98. umap/static/umap/locale/he.js +51 -16
  99. umap/static/umap/locale/he.json +51 -16
  100. umap/static/umap/locale/hr.js +51 -16
  101. umap/static/umap/locale/hr.json +51 -16
  102. umap/static/umap/locale/hu.js +51 -16
  103. umap/static/umap/locale/hu.json +51 -16
  104. umap/static/umap/locale/id.js +51 -16
  105. umap/static/umap/locale/id.json +51 -16
  106. umap/static/umap/locale/is.js +51 -16
  107. umap/static/umap/locale/is.json +51 -16
  108. umap/static/umap/locale/it.js +51 -16
  109. umap/static/umap/locale/it.json +51 -16
  110. umap/static/umap/locale/ja.js +51 -16
  111. umap/static/umap/locale/ja.json +51 -16
  112. umap/static/umap/locale/ko.js +51 -16
  113. umap/static/umap/locale/ko.json +51 -16
  114. umap/static/umap/locale/lt.js +51 -16
  115. umap/static/umap/locale/lt.json +51 -16
  116. umap/static/umap/locale/ms.js +51 -16
  117. umap/static/umap/locale/ms.json +51 -16
  118. umap/static/umap/locale/nl.js +51 -16
  119. umap/static/umap/locale/nl.json +51 -16
  120. umap/static/umap/locale/no.js +51 -16
  121. umap/static/umap/locale/no.json +51 -16
  122. umap/static/umap/locale/pl.js +93 -58
  123. umap/static/umap/locale/pl.json +93 -58
  124. umap/static/umap/locale/pl_PL.json +51 -16
  125. umap/static/umap/locale/pt.js +215 -180
  126. umap/static/umap/locale/pt.json +215 -180
  127. umap/static/umap/locale/pt_BR.js +51 -16
  128. umap/static/umap/locale/pt_BR.json +51 -16
  129. umap/static/umap/locale/pt_PT.js +51 -16
  130. umap/static/umap/locale/pt_PT.json +51 -16
  131. umap/static/umap/locale/ro.js +51 -16
  132. umap/static/umap/locale/ro.json +51 -16
  133. umap/static/umap/locale/ru.js +51 -16
  134. umap/static/umap/locale/ru.json +51 -16
  135. umap/static/umap/locale/si.js +51 -16
  136. umap/static/umap/locale/si.json +51 -16
  137. umap/static/umap/locale/sk_SK.js +51 -16
  138. umap/static/umap/locale/sk_SK.json +51 -16
  139. umap/static/umap/locale/sl.js +51 -16
  140. umap/static/umap/locale/sl.json +51 -16
  141. umap/static/umap/locale/sr.js +51 -16
  142. umap/static/umap/locale/sr.json +51 -16
  143. umap/static/umap/locale/sv.js +51 -16
  144. umap/static/umap/locale/sv.json +51 -16
  145. umap/static/umap/locale/th_TH.js +51 -16
  146. umap/static/umap/locale/th_TH.json +51 -16
  147. umap/static/umap/locale/tr.js +51 -16
  148. umap/static/umap/locale/tr.json +51 -16
  149. umap/static/umap/locale/uk_UA.js +51 -16
  150. umap/static/umap/locale/uk_UA.json +51 -16
  151. umap/static/umap/locale/vi.js +51 -16
  152. umap/static/umap/locale/vi.json +51 -16
  153. umap/static/umap/locale/vi_VN.json +51 -16
  154. umap/static/umap/locale/zh.js +51 -16
  155. umap/static/umap/locale/zh.json +51 -16
  156. umap/static/umap/locale/zh_CN.json +51 -16
  157. umap/static/umap/locale/zh_TW.Big5.json +51 -16
  158. umap/static/umap/locale/zh_TW.js +51 -16
  159. umap/static/umap/locale/zh_TW.json +51 -16
  160. umap/static/umap/map.css +40 -53
  161. umap/static/umap/unittests/sync.js +105 -0
  162. umap/static/umap/unittests/utils.js +78 -36
  163. umap/static/umap/vars.css +19 -1
  164. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +2 -2
  165. umap/templates/umap/components/alerts/alert.html +89 -0
  166. umap/templates/umap/content.html +4 -3
  167. umap/templates/umap/css.html +4 -0
  168. umap/templates/umap/home.html +3 -0
  169. umap/templates/umap/js.html +0 -3
  170. umap/templates/umap/map_init.html +2 -8
  171. umap/templates/umap/messages.html +9 -11
  172. umap/templates/umap/search.html +3 -0
  173. umap/tests/base.py +2 -0
  174. umap/tests/integration/conftest.py +30 -0
  175. umap/tests/integration/test_anonymous_owned_map.py +8 -13
  176. umap/tests/integration/test_browser.py +77 -4
  177. umap/tests/integration/test_conditional_rules.py +201 -0
  178. umap/tests/integration/test_dashboard.py +1 -1
  179. umap/tests/integration/test_datalayer.py +2 -3
  180. umap/tests/integration/test_edit_datalayer.py +4 -4
  181. umap/tests/integration/test_edit_map.py +1 -1
  182. umap/tests/integration/test_facets_browser.py +3 -3
  183. umap/tests/integration/test_import.py +185 -49
  184. umap/tests/integration/test_map.py +31 -2
  185. umap/tests/integration/{test_collaborative_editing.py → test_optimistic_merge.py} +7 -7
  186. umap/tests/integration/test_owned_map.py +1 -1
  187. umap/tests/integration/test_picto.py +2 -2
  188. umap/tests/integration/test_statics.py +1 -1
  189. umap/tests/integration/test_view_marker.py +2 -2
  190. umap/tests/integration/test_websocket_sync.py +283 -0
  191. umap/tests/settings.py +5 -0
  192. umap/tests/test_datalayer_views.py +0 -1
  193. umap/tests/test_views.py +53 -0
  194. umap/urls.py +5 -0
  195. umap/views.py +40 -11
  196. umap/websocket_server.py +92 -0
  197. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/METADATA +10 -8
  198. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/RECORD +201 -167
  199. umap/static/umap/js/umap.autocomplete.js +0 -341
  200. umap/static/umap/js/umap.importer.js +0 -187
  201. umap/static/umap/js/umap.ui.js +0 -190
  202. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/WHEEL +0 -0
  203. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/entry_points.txt +0 -0
  204. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,95 @@
1
+ import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
+ import { BaseAjax, SingleMixin } from '../autocomplete.js'
3
+ import { translate } from '../i18n.js'
4
+ import * as Utils from '../utils.js'
5
+ import { uMapAlert as Alert } from '../../components/alerts/alert.js'
6
+
7
+ const BOUNDARY_TYPES = {
8
+ admin_6: 'département',
9
+ admin_7: 'pays (loi Voynet)',
10
+ admin_8: 'commune',
11
+ admin_9: 'quartier, hameau, arrondissement',
12
+ political: 'canton',
13
+ local_authority: 'EPCI',
14
+ }
15
+
16
+ const TEMPLATE = `
17
+ <h3>GeoDataMine</h3>
18
+ <p>${translate('GeoDataMine: thematic data from OpenStreetMap')}.</p>
19
+ <select name="theme">
20
+ <option value="">${translate('Choose a theme')}</option>
21
+ </select>
22
+ <label>
23
+ <input type="checkbox" name="aspoint" />
24
+ ${translate('Symplify all geometries to points')}
25
+ </label>
26
+ <label id="boundary">
27
+ </label>
28
+ <button class="button">${translate('Choose this data')}</button>
29
+ `
30
+
31
+ class Autocomplete extends SingleMixin(BaseAjax) {
32
+ createResult(item) {
33
+ return super.createResult({
34
+ value: item.id,
35
+ label: `${item.name} (${BOUNDARY_TYPES[item.type]} — ${item.ref})`,
36
+ })
37
+ }
38
+ }
39
+
40
+ export class Importer {
41
+ constructor(map, options = {}) {
42
+ this.map = map
43
+ this.name = options.name || 'GeoDataMine'
44
+ this.baseUrl = options?.url || 'https://geodatamine.fr'
45
+ this.id = 'geodatamine'
46
+ }
47
+
48
+ async open(importer) {
49
+ let boundary = null
50
+ let boundaryName = null
51
+ const container = DomUtil.create('div')
52
+ container.innerHTML = TEMPLATE
53
+ const response = await importer.map.request.get(`${this.baseUrl}/themes`)
54
+ const select = container.querySelector('select')
55
+ if (response && response.ok) {
56
+ const { themes } = await response.json()
57
+ themes.sort((a, b) => Utils.naturalSort(a['name:fr'], b ['name:fr']))
58
+ for (const theme of themes) {
59
+ DomUtil.element({
60
+ tagName: 'option',
61
+ value: theme.id,
62
+ textContent: theme['name:fr'],
63
+ parent: select,
64
+ })
65
+ }
66
+ } else {
67
+ console.error(response)
68
+ }
69
+ const asPoint = container.querySelector('[name=aspoint]')
70
+ this.autocomplete = new Autocomplete(container.querySelector('#boundary'), {
71
+ placeholder: translate('Search admin boundary'),
72
+ url: `${this.baseUrl}/boundaries/search?text={q}`,
73
+ on_select: (choice) => {
74
+ boundary = choice.item.value
75
+ boundaryName = choice.item.label
76
+ },
77
+ })
78
+ const confirm = () => {
79
+ if (!boundary || !select.value) {
80
+ Alert.error(translate('Please choose a theme and a boundary first.'))
81
+ return
82
+ }
83
+ importer.url = `${this.baseUrl}/data/${select.value}/${boundary}?format=geojson&aspoint=${asPoint.checked}`
84
+ importer.format = 'geojson'
85
+ importer.layerName = `${boundaryName} — ${select.options[select.selectedIndex].textContent}`
86
+ importer.dialog.close()
87
+ }
88
+ DomEvent.on(container.querySelector('button'), 'click', confirm)
89
+
90
+ importer.dialog.open({
91
+ content: container,
92
+ className: `${this.id} importer dark`,
93
+ })
94
+ }
95
+ }
@@ -0,0 +1,84 @@
1
+ import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
+ import { BaseAjax, SingleMixin } from '../autocomplete.js'
3
+ import { translate } from '../i18n.js'
4
+ import { uMapAlert as Alert } from '../../components/alerts/alert.js'
5
+
6
+ const TEMPLATE = `
7
+ <h3>Overpass</h3>
8
+ <label>
9
+ <span data-help="overpassImporter">${translate('Expression')}</span>
10
+ <input type="text" placeholder="amenity=drinking_water" name="tags" />
11
+ </label>
12
+ <label>
13
+ ${translate('Geometry mode')}
14
+ <select name="out-mode">
15
+ <option value="geom" selected>${translate('Default')}</option>
16
+ <option value="center">${translate('Only geometry centers')}</option>
17
+ </select>
18
+ </label>
19
+ <label id="area"><span>${translate('Search area')}</span></label>
20
+ `
21
+
22
+ class Autocomplete extends SingleMixin(BaseAjax) {
23
+ handleResults(data) {
24
+ return super.handleResults(data.features)
25
+ }
26
+
27
+ createResult(item) {
28
+ return super.createResult({
29
+ // Overpass convention to get their id from an osm one.
30
+ value: item.properties.osm_id + 3600000000,
31
+ label: `${item.properties.name}`,
32
+ })
33
+ }
34
+ }
35
+
36
+ export class Importer {
37
+ constructor(map, options) {
38
+ this.map = map
39
+ this.name = options.name || 'Overpass'
40
+ this.baseUrl = options?.url || 'https://overpass-api.de/api/interpreter'
41
+ this.id = 'overpass'
42
+ }
43
+
44
+ async open(importer) {
45
+ let boundary = null
46
+ let boundaryName = null
47
+ const container = DomUtil.create('div')
48
+ container.innerHTML = TEMPLATE
49
+ this.autocomplete = new Autocomplete(container.querySelector('#area'), {
50
+ url: 'https://photon.komoot.io/api?q={q}&osm_tag=place',
51
+ placeholder: translate(
52
+ 'Type area name, or let empty to load data in current map view'
53
+ ),
54
+ on_select: (choice) => {
55
+ boundary = choice.item.value
56
+ boundaryName = choice.item.label
57
+ },
58
+ })
59
+ this.map.help.parse(container)
60
+
61
+ const confirm = () => {
62
+ let tags = container.querySelector('[name=tags]').value
63
+ if (!tags) {
64
+ Alert.error(translate('Please define an expression for the query first'))
65
+ return
66
+ }
67
+ const outMode = container.querySelector('[name=out-mode]').value
68
+ if (!tags.startsWith('[')) tags = `[${tags}]`
69
+ let area = '{south},{west},{north},{east}'
70
+ if (boundary) area = `area:${boundary}`
71
+ let query = `[out:json];nwr${tags}(${area});out ${outMode};`
72
+ importer.url = `${this.baseUrl}?data=${query}`
73
+ if (boundary) importer.layerName = boundaryName
74
+ importer.format = 'osm'
75
+ importer.dialog.close()
76
+ }
77
+ L.DomUtil.createButton('', container, translate('Choose this data'), confirm)
78
+
79
+ importer.dialog.open({
80
+ content: container,
81
+ className: `${this.id} importer dark`,
82
+ })
83
+ }
84
+ }
@@ -1,5 +1,5 @@
1
- // Uses `L._`` from Leaflet.i18n which we cannot import as a module yet
2
- import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
1
+ import { translate } from './i18n.js'
2
+ import { uMapAlert as Alert } from '../components/alerts/alert.js'
3
3
 
4
4
  export class RequestError extends Error {}
5
5
 
@@ -47,14 +47,13 @@ class BaseRequest {
47
47
  // In case of error, an alert is sent, but non 20X status are not handled
48
48
  // The consumer must check the response status by hand
49
49
  export class Request extends BaseRequest {
50
- constructor(ui) {
51
- super()
52
- this.ui = ui
50
+ fire(name, params) {
51
+ document.body.dispatchEvent(new CustomEvent(name, params))
53
52
  }
54
53
 
55
54
  async _fetch(method, uri, headers, data) {
56
55
  const id = Math.random()
57
- this.ui.fire('dataloading', { id: id })
56
+ this.fire('dataloading', { id: id })
58
57
  try {
59
58
  const response = await BaseRequest.prototype._fetch.call(
60
59
  this,
@@ -68,7 +67,7 @@ export class Request extends BaseRequest {
68
67
  if (error instanceof NOKError) return this._onNOK(error)
69
68
  return this._onError(error)
70
69
  } finally {
71
- this.ui.fire('dataload', { id: id })
70
+ this.fire('dataload', { id: id })
72
71
  }
73
72
  }
74
73
 
@@ -81,7 +80,7 @@ export class Request extends BaseRequest {
81
80
  }
82
81
 
83
82
  _onError(error) {
84
- this.ui.alert({ content: L._('Problem in the response'), level: 'error' })
83
+ Alert.error(translate('Problem in the response'))
85
84
  }
86
85
 
87
86
  _onNOK(error) {
@@ -127,9 +126,9 @@ export class ServerRequest extends Request {
127
126
  try {
128
127
  const data = await response.json()
129
128
  if (data.info) {
130
- this.ui.alert({ content: data.info, level: 'info' })
129
+ Alert.info(data.info)
131
130
  } else if (data.error) {
132
- this.ui.alert({ content: data.error, level: 'error' })
131
+ Alert.error(data.error)
133
132
  return this._onError(new Error(data.error))
134
133
  }
135
134
  return [data, response, null]
@@ -144,10 +143,9 @@ export class ServerRequest extends Request {
144
143
 
145
144
  _onNOK(error) {
146
145
  if (error.status === 403) {
147
- this.ui.alert({
148
- content: error.message || L._('Action not allowed :('),
149
- level: 'error',
150
- })
146
+ Alert.error(error.message || translate('Action not allowed :('))
147
+ } else {
148
+ super._onError(error)
151
149
  }
152
150
  return [{}, error.response, error]
153
151
  }
@@ -0,0 +1,241 @@
1
+ import { DomUtil, DomEvent, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
2
+ import * as Utils from './utils.js'
3
+ import { translate } from './i18n.js'
4
+
5
+ class Rule {
6
+
7
+ get condition() {
8
+ return this._condition
9
+ }
10
+
11
+ set condition(value) {
12
+ this._condition = value
13
+ this.parse()
14
+ }
15
+
16
+
17
+ get isDirty() {
18
+ return this._isDirty
19
+ }
20
+
21
+ set isDirty(status) {
22
+ this._isDirty = status
23
+ if (status) this.map.isDirty = status
24
+ }
25
+
26
+ constructor(map, condition = '', options = {}) {
27
+ // TODO make this public properties when browser coverage is ok
28
+ // cf https://caniuse.com/?search=public%20class%20field
29
+ this._condition = null
30
+ this._isDirty = false
31
+ this.OPERATORS = [
32
+ ['>', this.gt],
33
+ ['<', this.lt],
34
+ // When sent by Django
35
+ ['&lt;', this.lt],
36
+ ['!=', this.not_equal],
37
+ ['=', this.equal],
38
+ ]
39
+ this.map = map
40
+ this.active = true
41
+ this.options = options
42
+ this.condition = condition
43
+ }
44
+
45
+ render(fields) {
46
+ this.map.render(fields)
47
+ }
48
+
49
+ equal(other) {
50
+ return this.expected === other
51
+ }
52
+
53
+ not_equal(other) {
54
+ return this.expected != other
55
+ }
56
+
57
+ gt(other) {
58
+ return other > this.expected
59
+ }
60
+
61
+ lt(other) {
62
+ return other < this.expected
63
+ }
64
+
65
+ parse() {
66
+ let vars = []
67
+ this.cast = (v) => v
68
+ this.operator = undefined
69
+ for (const [sign, func] of this.OPERATORS) {
70
+ if (this.condition.includes(sign)) {
71
+ this.operator = func
72
+ vars = this.condition.split(sign)
73
+ break
74
+ }
75
+ }
76
+ if (vars.length != 2) return
77
+ this.key = vars[0]
78
+ this.expected = vars[1]
79
+ if (!isNaN(this.expected)) this.cast = parseFloat
80
+ else if (['true', 'false'].includes(this.expected)) this.cast = (v) => !!v
81
+ this.expected = this.cast(this.expected)
82
+ }
83
+
84
+ match(props) {
85
+ if (!this.operator || !this.active) return false
86
+ return this.operator(this.cast(props[this.key]))
87
+ }
88
+
89
+ getMap() {
90
+ return this.map
91
+ }
92
+
93
+ getOption(option) {
94
+ return this.options[option]
95
+ }
96
+
97
+ edit() {
98
+ const options = [
99
+ [
100
+ 'condition',
101
+ {
102
+ handler: 'BlurInput',
103
+ label: translate('Condition'),
104
+ placeholder: translate('key=value or key!=value'),
105
+ },
106
+ ],
107
+ 'options.color',
108
+ 'options.iconClass',
109
+ 'options.iconUrl',
110
+ 'options.iconOpacity',
111
+ 'options.opacity',
112
+ 'options.weight',
113
+ 'options.fill',
114
+ 'options.fillColor',
115
+ 'options.fillOpacity',
116
+ 'options.smoothFactor',
117
+ 'options.dashArray',
118
+ ]
119
+ const container = DomUtil.create('div')
120
+ const builder = new U.FormBuilder(this, options)
121
+ const defaultShapeProperties = DomUtil.add('div', '', container)
122
+ defaultShapeProperties.appendChild(builder.build())
123
+
124
+ this.map.editPanel.open({ content: container })
125
+ }
126
+
127
+ renderToolbox(row) {
128
+ row.classList.toggle('off', !this.active)
129
+ const toggle = DomUtil.createButtonIcon(
130
+ row,
131
+ 'icon-eye',
132
+ translate('Show/hide layer')
133
+ )
134
+ const edit = DomUtil.createButtonIcon(
135
+ row,
136
+ 'icon-edit show-on-edit',
137
+ translate('Edit')
138
+ )
139
+ const remove = DomUtil.createButtonIcon(
140
+ row,
141
+ 'icon-delete show-on-edit',
142
+ translate('Delete layer')
143
+ )
144
+ DomEvent.on(edit, 'click', this.edit, this)
145
+ DomEvent.on(
146
+ remove,
147
+ 'click',
148
+ function () {
149
+ if (!confirm(translate('Are you sure you want to delete this rule?'))) return
150
+ this._delete()
151
+ this.map.editPanel.close()
152
+ },
153
+ this
154
+ )
155
+ DomUtil.add('span', '', row, this.condition || translate('empty rule'))
156
+ DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
157
+ row.dataset.id = stamp(this)
158
+ DomEvent.on(toggle, 'click', () => {
159
+ this.active = !this.active
160
+ row.classList.toggle('off', !this.active)
161
+ this.map.render(['rules'])
162
+ })
163
+ }
164
+
165
+ _delete() {
166
+ this.map.rules.rules = this.map.rules.rules.filter((rule) => rule != this)
167
+ }
168
+ }
169
+
170
+ export default class Rules {
171
+ constructor(map) {
172
+ this.map = map
173
+ this.rules = []
174
+ this.loadRules()
175
+ }
176
+
177
+ loadRules() {
178
+ if (!this.map.options.rules?.length) return
179
+ for (const { condition, options } of this.map.options.rules) {
180
+ if (!condition) continue
181
+ this.rules.push(new Rule(this.map, condition, options))
182
+ }
183
+ }
184
+
185
+ onReorder(src, dst, initialIndex, finalIndex) {
186
+ const moved = this.rules.find((rule) => stamp(rule) == src.dataset.id)
187
+ const reference = this.rules.find((rule) => stamp(rule) == dst.dataset.id)
188
+ const movedIdx = this.rules.indexOf(moved)
189
+ let referenceIdx = this.rules.indexOf(reference)
190
+ const minIndex = Math.min(movedIdx, referenceIdx)
191
+ const maxIndex = Math.max(movedIdx, referenceIdx)
192
+ moved._delete() // Remove from array
193
+ referenceIdx = this.rules.indexOf(reference)
194
+ let newIdx
195
+ if (finalIndex === 0) newIdx = 0
196
+ else if (finalIndex > initialIndex) newIdx = referenceIdx
197
+ else newIdx = referenceIdx + 1
198
+ this.rules.splice(newIdx, 0, moved)
199
+ moved.isDirty = true
200
+ this.map.render(['rules'])
201
+ }
202
+
203
+ edit(container) {
204
+ const body = DomUtil.createFieldset(container, translate('Conditional style rules'))
205
+ if (this.rules.length) {
206
+ const ul = DomUtil.create('ul', '', body)
207
+ for (const rule of this.rules) {
208
+ rule.renderToolbox(DomUtil.create('li', 'orderable', ul))
209
+ }
210
+
211
+ const orderable = new U.Orderable(ul, this.onReorder.bind(this))
212
+ }
213
+
214
+ DomUtil.createButton('umap-add', body, translate('Add rule'), this.addRule, this)
215
+ }
216
+
217
+ addRule() {
218
+ const rule = new Rule(this.map)
219
+ rule.isDirty = true
220
+ this.rules.push(rule)
221
+ rule.edit(map)
222
+ }
223
+
224
+ commit() {
225
+ this.map.options.rules = this.rules.map((rule) => {
226
+ return {
227
+ condition: rule.condition,
228
+ options: rule.options,
229
+ }
230
+ })
231
+ }
232
+
233
+ getOption(option, feature) {
234
+ for (const rule of this.rules) {
235
+ if (rule.match(feature.properties)) {
236
+ if (Utils.usableOption(rule.options, option)) return rule.options[option]
237
+ break
238
+ }
239
+ }
240
+ }
241
+ }
@@ -1,12 +1,25 @@
1
1
  import { translate } from './i18n.js'
2
2
 
3
- // Possible impacts
4
- // ['ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', 'background']
3
+ /**
4
+ * This SCHEMA defines metadata about properties.
5
+ *
6
+ * This is here in order to have a centered place where all properties are specified.
7
+ *
8
+ * Each property defines:
9
+ *
10
+ * - `type`: The type of the data
11
+ * - `impacts`: A list of impacts than happen when this property is updated, among
12
+ * 'ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data',
13
+ * 'background' 'sync'.
14
+ *
15
+ * - Extra keys are being passed to the FormBuilder automatically.
16
+ */
5
17
 
18
+ // This is sorted alphabetically
6
19
  export const SCHEMA = {
7
20
  browsable: {
8
- impacts: ['ui'],
9
21
  type: Boolean,
22
+ impacts: ['ui'],
10
23
  },
11
24
  captionBar: {
12
25
  type: Boolean,
@@ -14,6 +27,13 @@ export const SCHEMA = {
14
27
  label: translate('Do you want to display a caption bar?'),
15
28
  default: false,
16
29
  },
30
+ captionControl: {
31
+ type: Boolean,
32
+ impacts: ['ui'],
33
+ nullable: true,
34
+ label: translate('Display the caption control'),
35
+ default: true,
36
+ },
17
37
  captionMenus: {
18
38
  type: Boolean,
19
39
  impacts: ['ui'],
@@ -37,6 +57,10 @@ export const SCHEMA = {
37
57
  type: Object,
38
58
  impacts: ['data'],
39
59
  },
60
+ condition: {
61
+ type: String,
62
+ impacts: ['data'],
63
+ },
40
64
  dashArray: {
41
65
  type: String,
42
66
  impacts: ['data'],
@@ -146,6 +170,10 @@ export const SCHEMA = {
146
170
  label: translate('Display the fullscreen control'),
147
171
  default: true,
148
172
  },
173
+ geometry: {
174
+ type: Object,
175
+ impacts: ['data'],
176
+ },
149
177
  heat: {
150
178
  type: Object,
151
179
  impacts: ['data'],
@@ -184,7 +212,6 @@ export const SCHEMA = {
184
212
  type: Boolean,
185
213
  impacts: ['ui'],
186
214
  },
187
-
188
215
  interactive: {
189
216
  type: Boolean,
190
217
  impacts: ['data'],
@@ -272,9 +299,9 @@ export const SCHEMA = {
272
299
  choices: [
273
300
  ['none', translate('None')],
274
301
  ['caption', translate('Caption')],
275
- ['databrowser', translate('Browser in data mode')],
276
- ['datalayers', translate('Browser in layers mode')],
277
- ['datafilters', translate('Browser in filters mode')],
302
+ ['databrowser', translate('Browser: data')],
303
+ ['datalayers', translate('Browser: layers')],
304
+ ['datafilters', translate('Browser: filters')],
278
305
  ],
279
306
  default: 'none',
280
307
  },
@@ -290,6 +317,7 @@ export const SCHEMA = {
290
317
  },
291
318
  outlink: {
292
319
  type: String,
320
+ impacts: [],
293
321
  label: translate('Link to…'),
294
322
  helpEntries: 'outlink',
295
323
  placeholder: 'http://...',
@@ -362,6 +390,10 @@ export const SCHEMA = {
362
390
  type: Object,
363
391
  impacts: ['remote-data'],
364
392
  },
393
+ rules: {
394
+ type: Object,
395
+ impacts: ['data'],
396
+ },
365
397
  scaleControl: {
366
398
  type: Boolean,
367
399
  impacts: ['ui'],
@@ -373,12 +405,6 @@ export const SCHEMA = {
373
405
  impacts: ['ui'],
374
406
  label: translate('Allow scroll wheel zoom?'),
375
407
  },
376
- captionControl: {
377
- type: Boolean,
378
- nullable: true,
379
- label: translate('Display the caption control'),
380
- default: true,
381
- },
382
408
  searchControl: {
383
409
  type: Boolean,
384
410
  impacts: ['ui'],
@@ -437,6 +463,13 @@ export const SCHEMA = {
437
463
  inheritable: true,
438
464
  default: true,
439
465
  },
466
+ syncEnabled: {
467
+ type: Boolean,
468
+ impacts: ['sync', 'ui'],
469
+ label: translate('Enable real-time collaboration'),
470
+ helpEntries: 'sync',
471
+ default: false,
472
+ },
440
473
  tilelayer: {
441
474
  type: Object,
442
475
  impacts: ['background'],
@@ -453,8 +486,19 @@ export const SCHEMA = {
453
486
  label: translate('To zoom'),
454
487
  helpText: translate('Optional.'),
455
488
  },
489
+ ttl: {
490
+ type: Number,
491
+ label: translate('Cache proxied request'),
492
+ choices: [
493
+ ['', translate('No cache')],
494
+ ['300', translate('5 min')],
495
+ ['3600', translate('1 hour')],
496
+ ['86400', translate('1 day')],
497
+ ],
498
+ default: '300',
499
+ },
456
500
  type: {
457
- type: 'String',
501
+ type: String,
458
502
  impacts: ['data'],
459
503
  },
460
504
  weight: {
@@ -486,4 +530,9 @@ export const SCHEMA = {
486
530
  label: translate('Default zoom level'),
487
531
  inheritable: true,
488
532
  },
533
+ // FIXME This is an internal Leaflet property, we might want to do this differently.
534
+ _latlng: {
535
+ type: Object,
536
+ impacts: ['data'],
537
+ },
489
538
  }