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.
- umap/.DS_Store +0 -0
- umap/__init__.py +1 -1
- umap/locale/en/LC_MESSAGES/django.po +81 -31
- umap/locale/fr/LC_MESSAGES/django.mo +0 -0
- umap/locale/fr/LC_MESSAGES/django.po +109 -59
- umap/management/commands/run_websocket_server.py +23 -0
- umap/models.py +6 -1
- umap/settings/base.py +11 -3
- umap/static/.DS_Store +0 -0
- umap/static/umap/.DS_Store +0 -0
- umap/static/umap/base.css +53 -162
- umap/static/umap/content.css +3 -2
- umap/static/umap/css/dialog.css +18 -0
- umap/static/umap/css/icon.css +8 -0
- umap/static/umap/css/importers.css +44 -0
- umap/static/umap/css/panel.css +19 -57
- umap/static/umap/css/tooltip.css +59 -0
- umap/static/umap/css/window.css +35 -0
- umap/static/umap/favicons/.DS_Store +0 -0
- umap/static/umap/fonts/.DS_Store +0 -0
- umap/static/umap/img/.DS_Store +0 -0
- umap/static/umap/img/alert-icon-error.svg +8 -0
- umap/static/umap/img/alert-icon-info.svg +4 -0
- umap/static/umap/img/alert-icon-success.svg +3 -0
- umap/static/umap/img/icon-external-link.svg +3 -0
- umap/static/umap/img/importers/communesfr.svg +5 -0
- umap/static/umap/img/importers/datasets.svg +13 -0
- umap/static/umap/img/importers/geodatamine.svg +10 -0
- umap/static/umap/img/importers/overpass.svg +7 -0
- umap/static/umap/img/importers/random.svg +18 -0
- umap/static/umap/img/importers/random1.svg +4 -0
- umap/static/umap/img/importers/random2.svg +4 -0
- umap/static/umap/img/source/.DS_Store +0 -0
- umap/static/umap/js/components/alerts/alert.css +160 -0
- umap/static/umap/js/components/alerts/alert.js +169 -0
- umap/static/umap/js/components/base.js +54 -0
- umap/static/umap/js/modules/autocomplete.js +347 -0
- umap/static/umap/js/modules/browser.js +1 -1
- umap/static/umap/js/modules/caption.js +4 -3
- umap/static/umap/js/modules/global.js +36 -12
- umap/static/umap/js/modules/help.js +255 -0
- umap/static/umap/js/modules/importer.js +280 -0
- umap/static/umap/js/modules/importers/communesfr.js +44 -0
- umap/static/umap/js/modules/importers/datasets.js +41 -0
- umap/static/umap/js/modules/importers/geodatamine.js +95 -0
- umap/static/umap/js/modules/importers/overpass.js +84 -0
- umap/static/umap/js/modules/request.js +12 -14
- umap/static/umap/js/modules/rules.js +241 -0
- umap/static/umap/js/modules/schema.js +63 -14
- umap/static/umap/js/modules/sync/engine.js +93 -0
- umap/static/umap/js/modules/sync/updaters.js +109 -0
- umap/static/umap/js/modules/sync/websocket.js +25 -0
- umap/static/umap/js/modules/ui/dialog.js +52 -0
- umap/static/umap/js/modules/{panel.js → ui/panel.js} +25 -14
- umap/static/umap/js/modules/ui/tooltip.js +116 -0
- umap/static/umap/js/modules/utils.js +25 -18
- umap/static/umap/js/umap.controls.js +13 -14
- umap/static/umap/js/umap.core.js +1 -324
- umap/static/umap/js/umap.features.js +67 -27
- umap/static/umap/js/umap.forms.js +9 -13
- umap/static/umap/js/umap.js +220 -180
- umap/static/umap/js/umap.layer.js +142 -74
- umap/static/umap/js/umap.permissions.js +5 -9
- umap/static/umap/js/umap.tableeditor.js +8 -8
- umap/static/umap/locale/am_ET.js +51 -16
- umap/static/umap/locale/am_ET.json +51 -16
- umap/static/umap/locale/ar.js +51 -16
- umap/static/umap/locale/ar.json +51 -16
- umap/static/umap/locale/ast.js +51 -16
- umap/static/umap/locale/ast.json +51 -16
- umap/static/umap/locale/bg.js +51 -16
- umap/static/umap/locale/bg.json +51 -16
- umap/static/umap/locale/br.js +55 -20
- umap/static/umap/locale/br.json +55 -20
- umap/static/umap/locale/ca.js +51 -16
- umap/static/umap/locale/ca.json +51 -16
- umap/static/umap/locale/cs_CZ.js +93 -58
- umap/static/umap/locale/cs_CZ.json +93 -58
- umap/static/umap/locale/da.js +51 -16
- umap/static/umap/locale/da.json +51 -16
- umap/static/umap/locale/de.js +56 -21
- umap/static/umap/locale/de.json +56 -21
- umap/static/umap/locale/el.js +51 -16
- umap/static/umap/locale/el.json +51 -16
- umap/static/umap/locale/en.js +51 -16
- umap/static/umap/locale/en.json +51 -16
- umap/static/umap/locale/en_US.json +51 -16
- umap/static/umap/locale/es.js +51 -16
- umap/static/umap/locale/es.json +51 -16
- umap/static/umap/locale/et.js +51 -16
- umap/static/umap/locale/et.json +51 -16
- umap/static/umap/locale/eu.js +51 -16
- umap/static/umap/locale/eu.json +51 -16
- umap/static/umap/locale/fa_IR.js +51 -16
- umap/static/umap/locale/fa_IR.json +51 -16
- umap/static/umap/locale/fi.js +51 -16
- umap/static/umap/locale/fi.json +51 -16
- umap/static/umap/locale/fr.js +52 -17
- umap/static/umap/locale/fr.json +52 -17
- umap/static/umap/locale/gl.js +51 -16
- umap/static/umap/locale/gl.json +51 -16
- umap/static/umap/locale/he.js +51 -16
- umap/static/umap/locale/he.json +51 -16
- umap/static/umap/locale/hr.js +51 -16
- umap/static/umap/locale/hr.json +51 -16
- umap/static/umap/locale/hu.js +51 -16
- umap/static/umap/locale/hu.json +51 -16
- umap/static/umap/locale/id.js +51 -16
- umap/static/umap/locale/id.json +51 -16
- umap/static/umap/locale/is.js +51 -16
- umap/static/umap/locale/is.json +51 -16
- umap/static/umap/locale/it.js +51 -16
- umap/static/umap/locale/it.json +51 -16
- umap/static/umap/locale/ja.js +51 -16
- umap/static/umap/locale/ja.json +51 -16
- umap/static/umap/locale/ko.js +51 -16
- umap/static/umap/locale/ko.json +51 -16
- umap/static/umap/locale/lt.js +51 -16
- umap/static/umap/locale/lt.json +51 -16
- umap/static/umap/locale/ms.js +51 -16
- umap/static/umap/locale/ms.json +51 -16
- umap/static/umap/locale/nl.js +51 -16
- umap/static/umap/locale/nl.json +51 -16
- umap/static/umap/locale/no.js +51 -16
- umap/static/umap/locale/no.json +51 -16
- umap/static/umap/locale/pl.js +93 -58
- umap/static/umap/locale/pl.json +93 -58
- umap/static/umap/locale/pl_PL.json +51 -16
- umap/static/umap/locale/pt.js +215 -180
- umap/static/umap/locale/pt.json +215 -180
- umap/static/umap/locale/pt_BR.js +51 -16
- umap/static/umap/locale/pt_BR.json +51 -16
- umap/static/umap/locale/pt_PT.js +51 -16
- umap/static/umap/locale/pt_PT.json +51 -16
- umap/static/umap/locale/ro.js +51 -16
- umap/static/umap/locale/ro.json +51 -16
- umap/static/umap/locale/ru.js +51 -16
- umap/static/umap/locale/ru.json +51 -16
- umap/static/umap/locale/si.js +51 -16
- umap/static/umap/locale/si.json +51 -16
- umap/static/umap/locale/sk_SK.js +51 -16
- umap/static/umap/locale/sk_SK.json +51 -16
- umap/static/umap/locale/sl.js +51 -16
- umap/static/umap/locale/sl.json +51 -16
- umap/static/umap/locale/sr.js +51 -16
- umap/static/umap/locale/sr.json +51 -16
- umap/static/umap/locale/sv.js +51 -16
- umap/static/umap/locale/sv.json +51 -16
- umap/static/umap/locale/th_TH.js +51 -16
- umap/static/umap/locale/th_TH.json +51 -16
- umap/static/umap/locale/tr.js +51 -16
- umap/static/umap/locale/tr.json +51 -16
- umap/static/umap/locale/uk_UA.js +51 -16
- umap/static/umap/locale/uk_UA.json +51 -16
- umap/static/umap/locale/vi.js +51 -16
- umap/static/umap/locale/vi.json +51 -16
- umap/static/umap/locale/vi_VN.json +51 -16
- umap/static/umap/locale/zh.js +51 -16
- umap/static/umap/locale/zh.json +51 -16
- umap/static/umap/locale/zh_CN.json +51 -16
- umap/static/umap/locale/zh_TW.Big5.json +51 -16
- umap/static/umap/locale/zh_TW.js +51 -16
- umap/static/umap/locale/zh_TW.json +51 -16
- umap/static/umap/map.css +27 -41
- umap/static/umap/unittests/sync.js +105 -0
- umap/static/umap/unittests/utils.js +76 -34
- umap/static/umap/vars.css +18 -1
- umap/static/umap/vendors/dompurify/purify.es.js +5 -59
- umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
- umap/templates/umap/components/alerts/alert.html +89 -0
- umap/templates/umap/content.html +4 -3
- umap/templates/umap/css.html +4 -0
- umap/templates/umap/home.html +3 -0
- umap/templates/umap/js.html +0 -3
- umap/templates/umap/map_init.html +2 -8
- umap/templates/umap/messages.html +9 -11
- umap/templates/umap/search.html +3 -0
- umap/tests/.DS_Store +0 -0
- umap/tests/base.py +2 -0
- umap/tests/integration/.DS_Store +0 -0
- umap/tests/integration/conftest.py +30 -0
- umap/tests/integration/test_anonymous_owned_map.py +8 -13
- umap/tests/integration/test_browser.py +1 -1
- umap/tests/integration/test_conditional_rules.py +201 -0
- umap/tests/integration/test_dashboard.py +1 -1
- umap/tests/integration/test_datalayer.py +2 -3
- umap/tests/integration/test_edit_datalayer.py +4 -4
- umap/tests/integration/test_edit_map.py +1 -1
- umap/tests/integration/test_facets_browser.py +3 -3
- umap/tests/integration/test_import.py +138 -49
- umap/tests/integration/test_map.py +2 -2
- umap/tests/integration/{test_collaborative_editing.py → test_optimistic_merge.py} +7 -7
- umap/tests/integration/test_owned_map.py +1 -1
- umap/tests/integration/test_picto.py +2 -2
- umap/tests/integration/test_statics.py +1 -1
- umap/tests/integration/test_websocket_sync.py +283 -0
- umap/tests/settings.py +5 -0
- umap/tests/test_datalayer_views.py +0 -1
- umap/tests/test_views.py +53 -0
- umap/urls.py +5 -0
- umap/views.py +40 -11
- umap/websocket_server.py +92 -0
- {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/METADATA +11 -9
- {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/RECORD +207 -164
- {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/WHEEL +1 -1
- umap/static/umap/js/umap.autocomplete.js +0 -341
- umap/static/umap/js/umap.importer.js +0 -187
- umap/static/umap/js/umap.ui.js +0 -190
- {umap_project-2.3.1.dist-info → umap_project-2.4.0b0.dist-info}/entry_points.txt +0 -0
- {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
|
+
['<', 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
|
-
|
|
4
|
-
|
|
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
|
|
276
|
-
['datalayers', translate('Browser
|
|
277
|
-
['datafilters', translate('Browser
|
|
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:
|
|
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
|
+
}
|