umap-project 2.3.1__py3-none-any.whl → 2.4.0b0__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 (210) hide show
  1. umap/.DS_Store +0 -0
  2. umap/__init__.py +1 -1
  3. umap/locale/en/LC_MESSAGES/django.po +81 -31
  4. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/fr/LC_MESSAGES/django.po +109 -59
  6. umap/management/commands/run_websocket_server.py +23 -0
  7. umap/models.py +6 -1
  8. umap/settings/base.py +11 -3
  9. umap/static/.DS_Store +0 -0
  10. umap/static/umap/.DS_Store +0 -0
  11. umap/static/umap/base.css +53 -162
  12. umap/static/umap/content.css +3 -2
  13. umap/static/umap/css/dialog.css +18 -0
  14. umap/static/umap/css/icon.css +8 -0
  15. umap/static/umap/css/importers.css +44 -0
  16. umap/static/umap/css/panel.css +19 -57
  17. umap/static/umap/css/tooltip.css +59 -0
  18. umap/static/umap/css/window.css +35 -0
  19. umap/static/umap/favicons/.DS_Store +0 -0
  20. umap/static/umap/fonts/.DS_Store +0 -0
  21. umap/static/umap/img/.DS_Store +0 -0
  22. umap/static/umap/img/alert-icon-error.svg +8 -0
  23. umap/static/umap/img/alert-icon-info.svg +4 -0
  24. umap/static/umap/img/alert-icon-success.svg +3 -0
  25. umap/static/umap/img/icon-external-link.svg +3 -0
  26. umap/static/umap/img/importers/communesfr.svg +5 -0
  27. umap/static/umap/img/importers/datasets.svg +13 -0
  28. umap/static/umap/img/importers/geodatamine.svg +10 -0
  29. umap/static/umap/img/importers/overpass.svg +7 -0
  30. umap/static/umap/img/importers/random.svg +18 -0
  31. umap/static/umap/img/importers/random1.svg +4 -0
  32. umap/static/umap/img/importers/random2.svg +4 -0
  33. umap/static/umap/img/source/.DS_Store +0 -0
  34. umap/static/umap/js/components/alerts/alert.css +160 -0
  35. umap/static/umap/js/components/alerts/alert.js +169 -0
  36. umap/static/umap/js/components/base.js +54 -0
  37. umap/static/umap/js/modules/autocomplete.js +347 -0
  38. umap/static/umap/js/modules/browser.js +1 -1
  39. umap/static/umap/js/modules/caption.js +4 -3
  40. umap/static/umap/js/modules/global.js +36 -12
  41. umap/static/umap/js/modules/help.js +255 -0
  42. umap/static/umap/js/modules/importer.js +280 -0
  43. umap/static/umap/js/modules/importers/communesfr.js +44 -0
  44. umap/static/umap/js/modules/importers/datasets.js +41 -0
  45. umap/static/umap/js/modules/importers/geodatamine.js +95 -0
  46. umap/static/umap/js/modules/importers/overpass.js +84 -0
  47. umap/static/umap/js/modules/request.js +12 -14
  48. umap/static/umap/js/modules/rules.js +241 -0
  49. umap/static/umap/js/modules/schema.js +63 -14
  50. umap/static/umap/js/modules/sync/engine.js +93 -0
  51. umap/static/umap/js/modules/sync/updaters.js +109 -0
  52. umap/static/umap/js/modules/sync/websocket.js +25 -0
  53. umap/static/umap/js/modules/ui/dialog.js +52 -0
  54. umap/static/umap/js/modules/{panel.js → ui/panel.js} +25 -14
  55. umap/static/umap/js/modules/ui/tooltip.js +116 -0
  56. umap/static/umap/js/modules/utils.js +25 -18
  57. umap/static/umap/js/umap.controls.js +13 -14
  58. umap/static/umap/js/umap.core.js +1 -324
  59. umap/static/umap/js/umap.features.js +67 -27
  60. umap/static/umap/js/umap.forms.js +9 -13
  61. umap/static/umap/js/umap.js +220 -180
  62. umap/static/umap/js/umap.layer.js +142 -74
  63. umap/static/umap/js/umap.permissions.js +5 -9
  64. umap/static/umap/js/umap.tableeditor.js +8 -8
  65. umap/static/umap/locale/am_ET.js +51 -16
  66. umap/static/umap/locale/am_ET.json +51 -16
  67. umap/static/umap/locale/ar.js +51 -16
  68. umap/static/umap/locale/ar.json +51 -16
  69. umap/static/umap/locale/ast.js +51 -16
  70. umap/static/umap/locale/ast.json +51 -16
  71. umap/static/umap/locale/bg.js +51 -16
  72. umap/static/umap/locale/bg.json +51 -16
  73. umap/static/umap/locale/br.js +55 -20
  74. umap/static/umap/locale/br.json +55 -20
  75. umap/static/umap/locale/ca.js +51 -16
  76. umap/static/umap/locale/ca.json +51 -16
  77. umap/static/umap/locale/cs_CZ.js +93 -58
  78. umap/static/umap/locale/cs_CZ.json +93 -58
  79. umap/static/umap/locale/da.js +51 -16
  80. umap/static/umap/locale/da.json +51 -16
  81. umap/static/umap/locale/de.js +56 -21
  82. umap/static/umap/locale/de.json +56 -21
  83. umap/static/umap/locale/el.js +51 -16
  84. umap/static/umap/locale/el.json +51 -16
  85. umap/static/umap/locale/en.js +51 -16
  86. umap/static/umap/locale/en.json +51 -16
  87. umap/static/umap/locale/en_US.json +51 -16
  88. umap/static/umap/locale/es.js +51 -16
  89. umap/static/umap/locale/es.json +51 -16
  90. umap/static/umap/locale/et.js +51 -16
  91. umap/static/umap/locale/et.json +51 -16
  92. umap/static/umap/locale/eu.js +51 -16
  93. umap/static/umap/locale/eu.json +51 -16
  94. umap/static/umap/locale/fa_IR.js +51 -16
  95. umap/static/umap/locale/fa_IR.json +51 -16
  96. umap/static/umap/locale/fi.js +51 -16
  97. umap/static/umap/locale/fi.json +51 -16
  98. umap/static/umap/locale/fr.js +52 -17
  99. umap/static/umap/locale/fr.json +52 -17
  100. umap/static/umap/locale/gl.js +51 -16
  101. umap/static/umap/locale/gl.json +51 -16
  102. umap/static/umap/locale/he.js +51 -16
  103. umap/static/umap/locale/he.json +51 -16
  104. umap/static/umap/locale/hr.js +51 -16
  105. umap/static/umap/locale/hr.json +51 -16
  106. umap/static/umap/locale/hu.js +51 -16
  107. umap/static/umap/locale/hu.json +51 -16
  108. umap/static/umap/locale/id.js +51 -16
  109. umap/static/umap/locale/id.json +51 -16
  110. umap/static/umap/locale/is.js +51 -16
  111. umap/static/umap/locale/is.json +51 -16
  112. umap/static/umap/locale/it.js +51 -16
  113. umap/static/umap/locale/it.json +51 -16
  114. umap/static/umap/locale/ja.js +51 -16
  115. umap/static/umap/locale/ja.json +51 -16
  116. umap/static/umap/locale/ko.js +51 -16
  117. umap/static/umap/locale/ko.json +51 -16
  118. umap/static/umap/locale/lt.js +51 -16
  119. umap/static/umap/locale/lt.json +51 -16
  120. umap/static/umap/locale/ms.js +51 -16
  121. umap/static/umap/locale/ms.json +51 -16
  122. umap/static/umap/locale/nl.js +51 -16
  123. umap/static/umap/locale/nl.json +51 -16
  124. umap/static/umap/locale/no.js +51 -16
  125. umap/static/umap/locale/no.json +51 -16
  126. umap/static/umap/locale/pl.js +93 -58
  127. umap/static/umap/locale/pl.json +93 -58
  128. umap/static/umap/locale/pl_PL.json +51 -16
  129. umap/static/umap/locale/pt.js +215 -180
  130. umap/static/umap/locale/pt.json +215 -180
  131. umap/static/umap/locale/pt_BR.js +51 -16
  132. umap/static/umap/locale/pt_BR.json +51 -16
  133. umap/static/umap/locale/pt_PT.js +51 -16
  134. umap/static/umap/locale/pt_PT.json +51 -16
  135. umap/static/umap/locale/ro.js +51 -16
  136. umap/static/umap/locale/ro.json +51 -16
  137. umap/static/umap/locale/ru.js +51 -16
  138. umap/static/umap/locale/ru.json +51 -16
  139. umap/static/umap/locale/si.js +51 -16
  140. umap/static/umap/locale/si.json +51 -16
  141. umap/static/umap/locale/sk_SK.js +51 -16
  142. umap/static/umap/locale/sk_SK.json +51 -16
  143. umap/static/umap/locale/sl.js +51 -16
  144. umap/static/umap/locale/sl.json +51 -16
  145. umap/static/umap/locale/sr.js +51 -16
  146. umap/static/umap/locale/sr.json +51 -16
  147. umap/static/umap/locale/sv.js +51 -16
  148. umap/static/umap/locale/sv.json +51 -16
  149. umap/static/umap/locale/th_TH.js +51 -16
  150. umap/static/umap/locale/th_TH.json +51 -16
  151. umap/static/umap/locale/tr.js +51 -16
  152. umap/static/umap/locale/tr.json +51 -16
  153. umap/static/umap/locale/uk_UA.js +51 -16
  154. umap/static/umap/locale/uk_UA.json +51 -16
  155. umap/static/umap/locale/vi.js +51 -16
  156. umap/static/umap/locale/vi.json +51 -16
  157. umap/static/umap/locale/vi_VN.json +51 -16
  158. umap/static/umap/locale/zh.js +51 -16
  159. umap/static/umap/locale/zh.json +51 -16
  160. umap/static/umap/locale/zh_CN.json +51 -16
  161. umap/static/umap/locale/zh_TW.Big5.json +51 -16
  162. umap/static/umap/locale/zh_TW.js +51 -16
  163. umap/static/umap/locale/zh_TW.json +51 -16
  164. umap/static/umap/map.css +27 -41
  165. umap/static/umap/unittests/sync.js +105 -0
  166. umap/static/umap/unittests/utils.js +76 -34
  167. umap/static/umap/vars.css +18 -1
  168. umap/static/umap/vendors/dompurify/purify.es.js +5 -59
  169. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  170. umap/templates/umap/components/alerts/alert.html +89 -0
  171. umap/templates/umap/content.html +4 -3
  172. umap/templates/umap/css.html +4 -0
  173. umap/templates/umap/home.html +3 -0
  174. umap/templates/umap/js.html +0 -3
  175. umap/templates/umap/map_init.html +2 -8
  176. umap/templates/umap/messages.html +9 -11
  177. umap/templates/umap/search.html +3 -0
  178. umap/tests/.DS_Store +0 -0
  179. umap/tests/base.py +2 -0
  180. umap/tests/integration/.DS_Store +0 -0
  181. umap/tests/integration/conftest.py +30 -0
  182. umap/tests/integration/test_anonymous_owned_map.py +8 -13
  183. umap/tests/integration/test_browser.py +1 -1
  184. umap/tests/integration/test_conditional_rules.py +201 -0
  185. umap/tests/integration/test_dashboard.py +1 -1
  186. umap/tests/integration/test_datalayer.py +2 -3
  187. umap/tests/integration/test_edit_datalayer.py +4 -4
  188. umap/tests/integration/test_edit_map.py +1 -1
  189. umap/tests/integration/test_facets_browser.py +3 -3
  190. umap/tests/integration/test_import.py +138 -49
  191. umap/tests/integration/test_map.py +2 -2
  192. umap/tests/integration/{test_collaborative_editing.py → test_optimistic_merge.py} +7 -7
  193. umap/tests/integration/test_owned_map.py +1 -1
  194. umap/tests/integration/test_picto.py +2 -2
  195. umap/tests/integration/test_statics.py +1 -1
  196. umap/tests/integration/test_websocket_sync.py +283 -0
  197. umap/tests/settings.py +5 -0
  198. umap/tests/test_datalayer_views.py +0 -1
  199. umap/tests/test_views.py +53 -0
  200. umap/urls.py +5 -0
  201. umap/views.py +40 -11
  202. umap/websocket_server.py +92 -0
  203. {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/METADATA +11 -9
  204. {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/RECORD +207 -164
  205. {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/WHEEL +1 -1
  206. umap/static/umap/js/umap.autocomplete.js +0 -341
  207. umap/static/umap/js/umap.importer.js +0 -187
  208. umap/static/umap/js/umap.ui.js +0 -190
  209. {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/entry_points.txt +0 -0
  210. {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -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
  }
@@ -0,0 +1,93 @@
1
+ import { WebSocketTransport } from './websocket.js'
2
+ import { MapUpdater, DataLayerUpdater, FeatureUpdater } from './updaters.js'
3
+
4
+ export class SyncEngine {
5
+ constructor(map) {
6
+ this.updaters = {
7
+ map: new MapUpdater(map),
8
+ feature: new FeatureUpdater(map),
9
+ datalayer: new DataLayerUpdater(map),
10
+ }
11
+ this.transport = undefined
12
+ }
13
+
14
+ async authenticate(tokenURI, webSocketURI, server) {
15
+ const [response, _, error] = await server.get(tokenURI)
16
+ if (!error) {
17
+ this.start(webSocketURI, response.token)
18
+ }
19
+ }
20
+
21
+ start(webSocketURI, authToken) {
22
+ this.transport = new WebSocketTransport(webSocketURI, authToken, this)
23
+ }
24
+
25
+ stop() {
26
+ if (this.transport) this.transport.close()
27
+ this.transport = undefined
28
+ }
29
+
30
+ _getUpdater(subject, metadata) {
31
+ if (Object.keys(this.updaters).includes(subject)) {
32
+ return this.updaters[subject]
33
+ }
34
+ throw new Error(`Unknown updater ${subject}, ${metadata}`)
35
+ }
36
+
37
+ // This method is called by the transport layer on new messages
38
+ receive({ kind, ...payload }) {
39
+ if (kind == 'operation') {
40
+ let updater = this._getUpdater(payload.subject, payload.metadata)
41
+ updater.applyMessage(payload)
42
+ } else {
43
+ throw new Error(`Unknown dispatch kind: ${kind}`)
44
+ }
45
+ }
46
+
47
+ _send(message) {
48
+ if (this.transport) {
49
+ this.transport.send('operation', message)
50
+ }
51
+ }
52
+
53
+ upsert(subject, metadata, value) {
54
+ this._send({ verb: 'upsert', subject, metadata, value })
55
+ }
56
+
57
+ update(subject, metadata, key, value) {
58
+ this._send({ verb: 'update', subject, metadata, key, value })
59
+ }
60
+
61
+ delete(subject, metadata, key) {
62
+ this._send({ verb: 'delete', subject, metadata, key })
63
+ }
64
+
65
+ /**
66
+ * Create a proxy for this sync engine.
67
+ *
68
+ * The proxy will automatically call `object.getSyncMetadata` and inject the returned
69
+ * `subject` and `metadata`` to the `upsert`, `update` and `delete` calls.
70
+ *
71
+ * The proxy can be used as follows:
72
+ *
73
+ * ```
74
+ * const proxy = sync.proxy(object)
75
+ * proxy.update('key', 'value')
76
+ *```
77
+ */
78
+ proxy(object) {
79
+ const handler = {
80
+ get(target, prop) {
81
+ // Only proxy these methods
82
+ if (['upsert', 'update', 'delete'].includes(prop)) {
83
+ const { subject, metadata } = object.getSyncMetadata()
84
+ // Reflect.get is calling the original method.
85
+ // .bind is adding the parameters automatically
86
+ return Reflect.get(...arguments).bind(target, subject, metadata)
87
+ }
88
+ return Reflect.get(...arguments)
89
+ },
90
+ }
91
+ return new Proxy(this, handler)
92
+ }
93
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * This file contains the updaters: classes that are able to convert messages
3
+ * received from another party (or the server) to changes on the map.
4
+ */
5
+
6
+ class BaseUpdater {
7
+ constructor(map) {
8
+ this.map = map
9
+ }
10
+
11
+ updateObjectValue(obj, key, value) {
12
+ const parts = key.split('.')
13
+ const lastKey = parts.pop()
14
+
15
+ // Reduce the current list of attributes,
16
+ // to find the object to set the property onto
17
+ const objectToSet = parts.reduce((currentObj, part) => {
18
+ if (currentObj !== undefined && part in currentObj) return currentObj[part]
19
+ }, obj)
20
+
21
+ // In case the given path doesn't exist, stop here
22
+ if (objectToSet === undefined) return
23
+
24
+ // Set the value (or delete it)
25
+ if (typeof value === 'undefined') {
26
+ delete objectToSet[lastKey]
27
+ } else {
28
+ objectToSet[lastKey] = value
29
+ }
30
+ }
31
+
32
+ getDataLayerFromID(layerId) {
33
+ if (layerId) return this.map.getDataLayerByUmapId(layerId)
34
+ return this.map.defaultEditDataLayer()
35
+ }
36
+
37
+ applyMessage(payload) {
38
+ let { verb } = payload
39
+ return this[verb](payload)
40
+ }
41
+ }
42
+
43
+ export class MapUpdater extends BaseUpdater {
44
+ update({ key, value }) {
45
+ this.updateObjectValue(this.map, key, value)
46
+ this.map.render([key])
47
+ }
48
+ }
49
+
50
+ export class DataLayerUpdater extends BaseUpdater {
51
+ upsert({ value }) {
52
+ // Inserts does not happen (we use multiple updates instead).
53
+ this.map.createDataLayer(value, false)
54
+ this.map.render([])
55
+ }
56
+
57
+ update({ key, metadata, value }) {
58
+ const datalayer = this.getDataLayerFromID(metadata.id)
59
+ this.updateObjectValue(datalayer, key, value)
60
+ datalayer.render([key])
61
+ }
62
+ }
63
+
64
+ export class FeatureUpdater extends BaseUpdater {
65
+ getFeatureFromMetadata({ id, layerId }) {
66
+ const datalayer = this.getDataLayerFromID(layerId)
67
+ return datalayer.getFeatureById(id)
68
+ }
69
+
70
+ // Create or update an object at a specific position
71
+ upsert({ metadata, value }) {
72
+ let { id, layerId } = metadata
73
+ const datalayer = this.getDataLayerFromID(layerId)
74
+ let feature = this.getFeatureFromMetadata(metadata, value)
75
+
76
+ feature = datalayer.geoJSONToLeaflet({
77
+ geometry: value.geometry,
78
+ geojson: value,
79
+ id,
80
+ feature,
81
+ })
82
+ datalayer.addLayer(feature)
83
+ }
84
+
85
+ // Update a property of an object
86
+ update({ key, metadata, value }) {
87
+ let feature = this.getFeatureFromMetadata(metadata)
88
+ if (feature === undefined) {
89
+ console.error(`Unable to find feature with id = ${metadata.id}.`)
90
+ }
91
+ switch (key) {
92
+ case 'geometry':
93
+ const datalayer = this.getDataLayerFromID(metadata.layerId)
94
+ datalayer.geoJSONToLeaflet({ geometry: value, id: metadata.id, feature })
95
+ default:
96
+ this.updateObjectValue(feature, key, value)
97
+ feature.datalayer.indexProperties(feature)
98
+ }
99
+
100
+ feature.render([key])
101
+ }
102
+
103
+ delete({ metadata }) {
104
+ // XXX Distinguish between properties getting deleted
105
+ // and the wole feature getting deleted
106
+ let feature = this.getFeatureFromMetadata(metadata)
107
+ if (feature) feature.del(false)
108
+ }
109
+ }
@@ -0,0 +1,25 @@
1
+ export class WebSocketTransport {
2
+ constructor(webSocketURI, authToken, messagesReceiver) {
3
+ this.websocket = new WebSocket(webSocketURI)
4
+ this.websocket.onopen = () => {
5
+ this.send('join', { token: authToken })
6
+ }
7
+ this.websocket.addEventListener('message', this.onMessage.bind(this))
8
+ this.receiver = messagesReceiver
9
+ }
10
+
11
+ onMessage(wsMessage) {
12
+ this.receiver.receive(JSON.parse(wsMessage.data))
13
+ }
14
+
15
+ send(kind, payload) {
16
+ const message = { ...payload }
17
+ message.kind = kind
18
+ let encoded = JSON.stringify(message)
19
+ this.websocket.send(encoded)
20
+ }
21
+
22
+ close() {
23
+ this.websocket.close()
24
+ }
25
+ }
@@ -0,0 +1,52 @@
1
+ import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
+ import { translate } from '../i18n.js'
3
+
4
+ export default class Dialog {
5
+ constructor(parent) {
6
+ this.parent = parent
7
+ this.className = 'umap-dialog window'
8
+ this.container = DomUtil.create('dialog', this.className, this.parent)
9
+ DomEvent.disableClickPropagation(this.container)
10
+ DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu.
11
+ DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation)
12
+ DomEvent.on(this.container, 'MozMousePixelScroll', DomEvent.stopPropagation)
13
+ }
14
+
15
+ get visible() {
16
+ return this.container.open
17
+ }
18
+
19
+ close() {
20
+ this.container.close()
21
+ }
22
+
23
+ currentZIndex() {
24
+ return Math.max(
25
+ ...Array.from(document.querySelectorAll('dialog')).map(
26
+ (el) => window.getComputedStyle(el).getPropertyValue('z-index') || 0
27
+ )
28
+ )
29
+ }
30
+
31
+ open({ className, content, modal } = {}) {
32
+ this.container.innerHTML = ''
33
+ const currentZIndex = this.currentZIndex()
34
+ if (currentZIndex) this.container.style.zIndex = currentZIndex + 1
35
+ if (modal) this.container.showModal()
36
+ else this.container.show()
37
+ if (className) {
38
+ // Reset
39
+ this.container.className = this.className
40
+ this.container.classList.add(...className.split(' '))
41
+ }
42
+ const buttonsContainer = DomUtil.create('ul', 'buttons', this.container)
43
+ const closeButton = DomUtil.createButtonIcon(
44
+ DomUtil.create('li', '', buttonsContainer),
45
+ 'icon-close',
46
+ translate('Close')
47
+ )
48
+ DomEvent.on(closeButton, 'click', this.close, this)
49
+ this.container.appendChild(buttonsContainer)
50
+ this.container.appendChild(content)
51
+ }
52
+ }