umap-project 3.2.0__py3-none-any.whl → 3.3.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 (173) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.mo +0 -0
  3. umap/locale/en/LC_MESSAGES/django.po +15 -15
  4. umap/settings/base.py +2 -0
  5. umap/static/umap/css/contextmenu.css +58 -2
  6. umap/static/umap/css/form.css +175 -45
  7. umap/static/umap/css/icon.css +20 -0
  8. umap/static/umap/img/16-white.svg +21 -40
  9. umap/static/umap/img/16.svg +1 -1
  10. umap/static/umap/img/24-white.svg +9 -9
  11. umap/static/umap/img/24.svg +23 -10
  12. umap/static/umap/img/source/16-white.svg +23 -41
  13. umap/static/umap/img/source/16.svg +1 -1
  14. umap/static/umap/img/source/24-white.svg +11 -11
  15. umap/static/umap/img/source/24.svg +25 -12
  16. umap/static/umap/js/modules/caption.js +8 -0
  17. umap/static/umap/js/modules/data/features.js +317 -173
  18. umap/static/umap/js/modules/data/layer.js +17 -18
  19. umap/static/umap/js/modules/form/builder.js +11 -7
  20. umap/static/umap/js/modules/form/fields.js +10 -7
  21. umap/static/umap/js/modules/formatter.js +42 -20
  22. umap/static/umap/js/modules/importer.js +6 -1
  23. umap/static/umap/js/modules/importers/opendata.js +125 -37
  24. umap/static/umap/js/modules/importers/openrouteservice.js +140 -0
  25. umap/static/umap/js/modules/managers.js +12 -4
  26. umap/static/umap/js/modules/printer.js +107 -0
  27. umap/static/umap/js/modules/rendering/controls.js +78 -2
  28. umap/static/umap/js/modules/rendering/icon.js +113 -82
  29. umap/static/umap/js/modules/rendering/layers/cluster.js +199 -63
  30. umap/static/umap/js/modules/rendering/map.js +5 -1
  31. umap/static/umap/js/modules/rendering/template.js +71 -1
  32. umap/static/umap/js/modules/rendering/ui.js +98 -34
  33. umap/static/umap/js/modules/schema.js +24 -0
  34. umap/static/umap/js/modules/share.js +19 -12
  35. umap/static/umap/js/modules/ui/bar.js +6 -1
  36. umap/static/umap/js/modules/ui/base.js +24 -9
  37. umap/static/umap/js/modules/ui/contextmenu.js +17 -7
  38. umap/static/umap/js/modules/ui/dialog.js +7 -4
  39. umap/static/umap/js/modules/umap.js +67 -61
  40. umap/static/umap/js/umap.controls.js +22 -57
  41. umap/static/umap/locale/am_ET.js +39 -4
  42. umap/static/umap/locale/am_ET.json +39 -4
  43. umap/static/umap/locale/ar.js +39 -4
  44. umap/static/umap/locale/ar.json +39 -4
  45. umap/static/umap/locale/ast.js +39 -4
  46. umap/static/umap/locale/ast.json +39 -4
  47. umap/static/umap/locale/bg.js +39 -4
  48. umap/static/umap/locale/bg.json +39 -4
  49. umap/static/umap/locale/br.js +39 -4
  50. umap/static/umap/locale/br.json +39 -4
  51. umap/static/umap/locale/ca.js +39 -4
  52. umap/static/umap/locale/ca.json +39 -4
  53. umap/static/umap/locale/cs_CZ.js +39 -4
  54. umap/static/umap/locale/cs_CZ.json +39 -4
  55. umap/static/umap/locale/da.js +47 -12
  56. umap/static/umap/locale/da.json +47 -12
  57. umap/static/umap/locale/de.js +39 -4
  58. umap/static/umap/locale/de.json +39 -4
  59. umap/static/umap/locale/el.js +39 -4
  60. umap/static/umap/locale/el.json +39 -4
  61. umap/static/umap/locale/en.js +39 -4
  62. umap/static/umap/locale/en.json +39 -4
  63. umap/static/umap/locale/en_US.json +39 -4
  64. umap/static/umap/locale/es.js +47 -12
  65. umap/static/umap/locale/es.json +47 -12
  66. umap/static/umap/locale/et.js +39 -4
  67. umap/static/umap/locale/et.json +39 -4
  68. umap/static/umap/locale/eu.js +79 -44
  69. umap/static/umap/locale/eu.json +79 -44
  70. umap/static/umap/locale/fa_IR.js +39 -4
  71. umap/static/umap/locale/fa_IR.json +39 -4
  72. umap/static/umap/locale/fi.js +39 -4
  73. umap/static/umap/locale/fi.json +39 -4
  74. umap/static/umap/locale/fr.js +39 -4
  75. umap/static/umap/locale/fr.json +39 -4
  76. umap/static/umap/locale/gl.js +39 -4
  77. umap/static/umap/locale/gl.json +39 -4
  78. umap/static/umap/locale/he.js +39 -4
  79. umap/static/umap/locale/he.json +39 -4
  80. umap/static/umap/locale/hr.js +39 -4
  81. umap/static/umap/locale/hr.json +39 -4
  82. umap/static/umap/locale/hu.js +39 -4
  83. umap/static/umap/locale/hu.json +39 -4
  84. umap/static/umap/locale/id.js +39 -4
  85. umap/static/umap/locale/id.json +39 -4
  86. umap/static/umap/locale/is.js +39 -4
  87. umap/static/umap/locale/is.json +39 -4
  88. umap/static/umap/locale/it.js +39 -4
  89. umap/static/umap/locale/it.json +39 -4
  90. umap/static/umap/locale/ja.js +39 -4
  91. umap/static/umap/locale/ja.json +39 -4
  92. umap/static/umap/locale/ko.js +39 -4
  93. umap/static/umap/locale/ko.json +39 -4
  94. umap/static/umap/locale/lt.js +39 -4
  95. umap/static/umap/locale/lt.json +39 -4
  96. umap/static/umap/locale/ms.js +39 -4
  97. umap/static/umap/locale/ms.json +39 -4
  98. umap/static/umap/locale/nl.js +39 -4
  99. umap/static/umap/locale/nl.json +39 -4
  100. umap/static/umap/locale/no.js +39 -4
  101. umap/static/umap/locale/no.json +39 -4
  102. umap/static/umap/locale/pl.js +39 -4
  103. umap/static/umap/locale/pl.json +39 -4
  104. umap/static/umap/locale/pl_PL.json +39 -4
  105. umap/static/umap/locale/pt.js +39 -4
  106. umap/static/umap/locale/pt.json +39 -4
  107. umap/static/umap/locale/pt_BR.js +39 -4
  108. umap/static/umap/locale/pt_BR.json +39 -4
  109. umap/static/umap/locale/pt_PT.js +39 -4
  110. umap/static/umap/locale/pt_PT.json +39 -4
  111. umap/static/umap/locale/ro.js +39 -4
  112. umap/static/umap/locale/ro.json +39 -4
  113. umap/static/umap/locale/ru.js +39 -4
  114. umap/static/umap/locale/ru.json +39 -4
  115. umap/static/umap/locale/sk_SK.js +39 -4
  116. umap/static/umap/locale/sk_SK.json +39 -4
  117. umap/static/umap/locale/sl.js +39 -4
  118. umap/static/umap/locale/sl.json +39 -4
  119. umap/static/umap/locale/sr.js +39 -4
  120. umap/static/umap/locale/sr.json +39 -4
  121. umap/static/umap/locale/sv.js +39 -4
  122. umap/static/umap/locale/sv.json +39 -4
  123. umap/static/umap/locale/th_TH.js +39 -4
  124. umap/static/umap/locale/th_TH.json +39 -4
  125. umap/static/umap/locale/tr.js +39 -4
  126. umap/static/umap/locale/tr.json +39 -4
  127. umap/static/umap/locale/uk_UA.js +39 -4
  128. umap/static/umap/locale/uk_UA.json +39 -4
  129. umap/static/umap/locale/vi.js +39 -4
  130. umap/static/umap/locale/vi.json +39 -4
  131. umap/static/umap/locale/vi_VN.json +39 -4
  132. umap/static/umap/locale/zh.js +39 -4
  133. umap/static/umap/locale/zh.json +39 -4
  134. umap/static/umap/locale/zh_CN.json +39 -4
  135. umap/static/umap/locale/zh_TW.Big5.json +39 -4
  136. umap/static/umap/locale/zh_TW.js +98 -63
  137. umap/static/umap/locale/zh_TW.json +98 -63
  138. umap/static/umap/map.css +90 -41
  139. umap/static/umap/vars.css +1 -0
  140. umap/static/umap/vendors/editable/Leaflet.Editable.js +3 -1
  141. umap/static/umap/vendors/openrouteservice/ors-js-client.js +521 -0
  142. umap/static/umap/vendors/openrouteservice/ors-js-client.js.map +1 -0
  143. umap/static/umap/vendors/simple-elevation-chart/elevation.js +63 -0
  144. umap/static/umap/vendors/simple-elevation-chart/elevation.svg +8 -0
  145. umap/static/umap/vendors/snapdom/snapdom.min.mjs +3 -0
  146. umap/storage/staticfiles.py +12 -0
  147. umap/templates/umap/css.html +0 -4
  148. umap/templates/umap/js.html +1 -3
  149. umap/tests/integration/test_basics.py +2 -0
  150. umap/tests/integration/test_conditional_rules.py +17 -17
  151. umap/tests/integration/test_datalayer.py +1 -1
  152. umap/tests/integration/test_draw_polygon.py +3 -5
  153. umap/tests/integration/test_draw_polyline.py +4 -6
  154. umap/tests/integration/test_draw_route.py +178 -0
  155. umap/tests/integration/test_edit_map.py +1 -1
  156. umap/tests/integration/test_edit_marker.py +7 -7
  157. umap/tests/integration/test_edit_polygon.py +2 -2
  158. umap/tests/integration/test_export_map.py +74 -10
  159. umap/tests/integration/test_map_preview.py +1 -1
  160. umap/tests/integration/test_share.py +1 -1
  161. umap/tests/integration/test_tableeditor.py +4 -4
  162. umap/tests/integration/test_websocket_sync.py +4 -4
  163. umap/utils.py +5 -1
  164. umap/views.py +2 -0
  165. {umap_project-3.2.0.dist-info → umap_project-3.3.0.dist-info}/METADATA +8 -8
  166. {umap_project-3.2.0.dist-info → umap_project-3.3.0.dist-info}/RECORD +169 -165
  167. umap/static/umap/vendors/markercluster/MarkerCluster.Default.css +0 -60
  168. umap/static/umap/vendors/markercluster/MarkerCluster.css +0 -14
  169. umap/static/umap/vendors/markercluster/leaflet.markercluster.js +0 -2
  170. umap/static/umap/vendors/markercluster/leaflet.markercluster.js.map +0 -1
  171. {umap_project-3.2.0.dist-info → umap_project-3.3.0.dist-info}/WHEEL +0 -0
  172. {umap_project-3.2.0.dist-info → umap_project-3.3.0.dist-info}/entry_points.txt +0 -0
  173. {umap_project-3.2.0.dist-info → umap_project-3.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,100 @@
1
- import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
2
- // WARNING must be loaded dynamically, or at least after leaflet.markercluster
3
- // Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
1
+ import {
2
+ FeatureGroup,
3
+ LayerGroup,
4
+ Point,
5
+ Marker,
6
+ Rectangle,
7
+ Polyline,
8
+ DomUtil,
9
+ latLngBounds,
10
+ } from '../../../../vendors/leaflet/leaflet-src.esm.js'
4
11
  import { translate } from '../../i18n.js'
5
12
  import * as Utils from '../../utils.js'
6
13
  import { Cluster as ClusterIcon } from '../icon.js'
7
14
  import { LayerMixin } from './base.js'
8
15
 
9
- const MarkerCluster = L.MarkerCluster.extend({
10
- // Custom class so we can call computeTextColor
11
- // when element is already on the DOM.
12
-
16
+ const MarkerCluster = Marker.extend({
13
17
  _initIcon: function () {
14
- L.MarkerCluster.prototype._initIcon.call(this)
15
- const div = this._icon.querySelector('div')
18
+ Marker.prototype._initIcon.call(this)
19
+ const counter = this._icon.querySelector('span')
16
20
  // Compute text color only when icon is added to the DOM.
17
- div.style.color = this._iconObj.computeTextColor(div)
21
+ const bgColor = this.options.icon.options.color
22
+ const textColor = this.options.icon.options.textColor
23
+ counter.style.color =
24
+ textColor || DomUtil.TextColorFromBackgroundColor(counter, bgColor)
25
+ },
26
+
27
+ computeCoverage() {
28
+ if (this._layers.length < 2) return
29
+ if (!this._coverage) {
30
+ const latlngs = this._layers.map((layer) => layer._latlng)
31
+ const bounds = latLngBounds(latlngs)
32
+ this._coverage = new Rectangle(latlngs, {
33
+ color: this.options.icon.options.color,
34
+ stroke: false,
35
+ })
36
+ this._latlng = bounds.getCenter()
37
+ }
38
+ },
39
+
40
+ async zoomToCoverage() {
41
+ let resolve = undefined
42
+ const promise = new Promise((r) => {
43
+ resolve = r
44
+ })
45
+ if (this._map && this._coverage) {
46
+ this._map.once('moveend', () => resolve())
47
+ this._map.fitBounds(this._coverage.getBounds())
48
+ }
49
+ return promise
50
+ },
51
+
52
+ _spiderfyLatLng: function (center, index) {
53
+ const step = 20
54
+ const maxRadius = 150
55
+ const zoom = this._map.getZoom()
56
+ const angle = (index * step * Math.PI) / 180
57
+ const progress = index / this._layers.length
58
+ const radius = maxRadius * (1 - progress) ** 0.4
59
+ const x = radius * Math.cos(angle)
60
+ const y = radius * Math.sin(angle)
61
+ const point = this._map.project([center.lat, center.lng], zoom)
62
+ const latlng = this._map.unproject(new Point(point.x + x, point.y + y), zoom)
63
+ return latlng
64
+ },
65
+
66
+ spiderfy() {
67
+ if (!this._map) return
68
+ const crs = this._map.options.crs
69
+ if (this._spider && this._map.hasLayer(this._spider)) this.unspiderfy()
70
+ this._spider = new LayerGroup()
71
+ let i = 1
72
+ const center = this.getLatLng()
73
+ for (const layer of this._layers) {
74
+ const latlng = this._spiderfyLatLng(center, i++)
75
+ layer._originalLatLng = layer._latlng
76
+ layer.setLatLng(latlng)
77
+ this._spider.addLayer(layer)
78
+ const line = new Polyline([center, latlng], { color: 'black', weight: 1 })
79
+ this._spider.addLayer(line)
80
+ }
81
+ this._map.addLayer(this._spider)
82
+ this._icon.hidden = true
83
+ this._map.once('click zoomstart', this.unspiderfy, this)
84
+ this.once('remove', this.unspiderfy, this)
85
+ },
86
+
87
+ unspiderfy() {
88
+ if (this._icon) this._icon.hidden = false
89
+ if (this._spider) this._spider.remove()
90
+ for (const layer of this._layers) {
91
+ if (layer._originalLatLng) layer.setLatLng(layer._originalLatLng)
92
+ delete layer._originalLatLng
93
+ }
18
94
  },
19
95
  })
20
96
 
21
- export const Cluster = L.MarkerClusterGroup.extend({
97
+ export const Cluster = FeatureGroup.extend({
22
98
  statics: {
23
99
  NAME: translate('Clustered'),
24
100
  TYPE: 'Cluster',
@@ -27,58 +103,132 @@ export const Cluster = L.MarkerClusterGroup.extend({
27
103
 
28
104
  initialize: function (datalayer) {
29
105
  this.datalayer = datalayer
106
+ this._bucket = []
30
107
  if (!Utils.isObject(this.datalayer.properties.cluster)) {
31
108
  this.datalayer.properties.cluster = {}
32
109
  }
33
- const options = {
34
- polygonOptions: {
35
- color: this.datalayer.getColor(),
36
- },
37
- iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster),
110
+ FeatureGroup.prototype.initialize.call(this)
111
+ LayerMixin.onInit.call(this, this.datalayer._leafletMap)
112
+ },
113
+
114
+ dataChanged: function () {
115
+ this.redraw()
116
+ },
117
+
118
+ removeClusters() {
119
+ this.hideCoverage()
120
+ if (this._map) {
121
+ for (const cluster of this._clusters) {
122
+ const layer = cluster._layers.length === 1 ? cluster._layers[0] : cluster
123
+ this._map.removeLayer(layer)
124
+ }
38
125
  }
39
- if (this.datalayer.properties.cluster?.radius) {
40
- options.maxClusterRadius = this.datalayer.properties.cluster.radius
126
+ },
127
+
128
+ addClusters() {
129
+ if (this._map) {
130
+ for (const cluster of this._clusters) {
131
+ const layer = cluster._layers.length === 1 ? cluster._layers[0] : cluster
132
+ this._map.addLayer(layer)
133
+ }
41
134
  }
42
- L.MarkerClusterGroup.prototype.initialize.call(this, options)
43
- LayerMixin.onInit.call(this, this.datalayer._leafletMap)
44
- this._markerCluster = MarkerCluster
45
- this._layers = []
46
135
  },
47
136
 
48
- onAdd: function (map) {
49
- LayerMixin.onAdd.call(this, map)
50
- return L.MarkerClusterGroup.prototype.onAdd.call(this, map)
137
+ redraw: function () {
138
+ this.removeClusters()
139
+ this.compute()
140
+ this.addClusters()
51
141
  },
52
142
 
53
- onRemove: function (map) {
54
- // In some situation, the onRemove is called before the layer is really
55
- // added to the map: basically when combining a defaultView=data + max/minZoom
56
- // and loading the map at a zoom outside of that zoom range.
57
- // FIXME: move this upstream (_unbindEvents should accept a map parameter
58
- // instead of relying on this._map)
59
- this._map = map
60
- LayerMixin.onRemove.call(this, map)
61
- return L.MarkerClusterGroup.prototype.onRemove.call(this, map)
143
+ compute() {
144
+ const radius = this.datalayer.properties.cluster?.radius || 80
145
+ this._clusters = []
146
+ const map = this.datalayer._umap._leafletMap
147
+ const CRS = map.options.crs
148
+ for (const layer of this._bucket) {
149
+ layer._xy = CRS.latLngToPoint(layer._latlng, map.getZoom())
150
+ let cluster = null
151
+ for (const candidate of this._clusters) {
152
+ if (candidate._xy.distanceTo(layer._xy) <= radius) {
153
+ cluster = candidate
154
+ break
155
+ }
156
+ }
157
+ if (!cluster) {
158
+ const icon = new ClusterIcon({
159
+ color: this.datalayer.getColor(),
160
+ textColor: this.datalayer.properties.cluster?.textColor,
161
+ getCounter: () => cluster._layers.length,
162
+ })
163
+ cluster = new MarkerCluster(layer._latlng, { icon })
164
+ cluster.addEventParent(this)
165
+ cluster._xy ??= layer._xy
166
+ cluster._layers = []
167
+ this._clusters.push(cluster)
168
+ }
169
+ cluster._layers.push(layer)
170
+ layer._cluster = cluster
171
+ }
172
+ for (const cluster of this._clusters) {
173
+ cluster.computeCoverage()
174
+ }
62
175
  },
63
176
 
64
177
  addLayer: function (layer) {
65
- this._layers.push(layer)
66
- try {
67
- return L.MarkerClusterGroup.prototype.addLayer.call(this, layer)
68
- } catch (error) {
69
- console.debug(error)
70
- // Certainly a race condition when loading a clustered layer
71
- // while zooming (this for example can happen at load, when the
72
- // initial zoom is changed by uMap).
73
- // FIXME: remove when this is merged:
74
- // https://github.com/Leaflet/Leaflet.markercluster/pull/1048/files
75
- return this
178
+ if (!layer.getLatLng) return FeatureGroup.prototype.addLayer.call(this, layer)
179
+ // Do not add yet the layer to the map
180
+ // wait for datachanged event, so we can compute breaks only once
181
+ this._bucket.push(layer)
182
+ return this
183
+ },
184
+
185
+ onAdd: function (leafletMap) {
186
+ this.on('click', this.onClick)
187
+ this.on('mouseover', this.onMouseOver)
188
+ this.on('mouseout', this.onMouseOut)
189
+ this.compute()
190
+ LayerMixin.onAdd.call(this, leafletMap)
191
+ leafletMap.on('zoomend', this.redraw, this)
192
+ this.addClusters()
193
+ return FeatureGroup.prototype.onAdd.call(this, leafletMap)
194
+ },
195
+
196
+ onRemove: function (leafletMap) {
197
+ leafletMap.off('zoomend', this.redraw, this)
198
+ this.off('click', this.onClick)
199
+ this.off('mouseover', this.onMouseOver)
200
+ this.off('mouseout', this.onMouseOut)
201
+ LayerMixin.onRemove.call(this, leafletMap)
202
+ this.removeClusters()
203
+ return FeatureGroup.prototype.onRemove.call(this, leafletMap)
204
+ },
205
+
206
+ showCoverage(cluster) {
207
+ if (cluster._coverage) {
208
+ this._shownCoverage = cluster._coverage
209
+ this._map.addLayer(this._shownCoverage)
76
210
  }
77
211
  },
78
212
 
79
- removeLayer: function (layer) {
80
- this._layers.splice(this._layers.indexOf(layer), 1)
81
- return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer)
213
+ hideCoverage() {
214
+ if (this._shownCoverage) this._map.removeLayer(this._shownCoverage)
215
+ },
216
+
217
+ onMouseOver(event) {
218
+ event.layer?.computeCoverage?.()
219
+ this.showCoverage(event.layer)
220
+ },
221
+
222
+ onMouseOut(event) {
223
+ this.hideCoverage()
224
+ },
225
+
226
+ onClick(event) {
227
+ if (this._map.getZoom() === this._map.getMaxZoom()) {
228
+ event.layer.spiderfy?.()
229
+ } else {
230
+ event.layer.zoomToCoverage?.()
231
+ }
82
232
  },
83
233
 
84
234
  getEditableProperties: () => [
@@ -101,20 +251,6 @@ export const Cluster = L.MarkerClusterGroup.extend({
101
251
  ],
102
252
 
103
253
  onEdit: function (field, builder) {
104
- if (field === 'properties.cluster.radius') {
105
- // No way to reset radius of an already instanciated MarkerClusterGroup...
106
- this.datalayer.resetLayer(true)
107
- return
108
- }
109
- if (field === 'properties.color') {
110
- this.options.polygonOptions.color = this.datalayer.getColor()
111
- }
112
- },
113
-
114
- _moveChild: (layer, from, to) => {
115
- // Extend parent method, so to remove remove/addLayer,
116
- // to let our own dragend event listener be called
117
- // cf https://github.com/umap-project/umap/issues/2749
118
- layer._latlng = to
254
+ if (field === 'properties.cluster.radius') this.redraw()
119
255
  },
120
256
  })
@@ -22,6 +22,8 @@ import {
22
22
  PermanentCreditsControl,
23
23
  TileLayerChooser,
24
24
  LoadTemplateControl,
25
+ PrintControl,
26
+ SearchControl,
25
27
  } from './controls.js'
26
28
  import * as Utils from '../utils.js'
27
29
  import * as Icon from './icon.js'
@@ -45,6 +47,7 @@ const ControlsMixin = {
45
47
  'locate',
46
48
  'measure',
47
49
  'editinosm',
50
+ 'print',
48
51
  'tilelayers',
49
52
  ],
50
53
 
@@ -84,8 +87,9 @@ const ControlsMixin = {
84
87
  true: translate('Exit Fullscreen'),
85
88
  },
86
89
  })
87
- this._controls.search = new U.SearchControl()
90
+ this._controls.search = new SearchControl(this._umap)
88
91
  this._controls.embed = new EmbedControl(this._umap)
92
+ this._controls.print = new PrintControl(this._umap)
89
93
  this._controls.tilelayersChooser = new TileLayerChooser(this._umap)
90
94
  this._controls.editinosm = new Control.EditInOSM({
91
95
  position: 'topleft',
@@ -1,4 +1,8 @@
1
- import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
1
+ import {
2
+ DomEvent,
3
+ DomUtil,
4
+ CircleMarker,
5
+ } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
6
  import { getLocale, translate } from '../i18n.js'
3
7
  import { Request } from '../request.js'
4
8
  import * as Utils from '../utils.js'
@@ -22,6 +26,9 @@ export default async function loadTemplate(name, feature, container) {
22
26
  case 'Wikipedia':
23
27
  klass = Wikipedia
24
28
  break
29
+ case 'Route':
30
+ klass = Route
31
+ break
25
32
  }
26
33
  const content = new klass()
27
34
  return await content.render(feature, container)
@@ -282,3 +289,66 @@ class Wikipedia extends PopupTemplate {
282
289
  return body
283
290
  }
284
291
  }
292
+
293
+ class Route extends TitleMixin(PopupTemplate) {
294
+ async renderBody(feature) {
295
+ if (feature.type !== 'LineString' || feature.isMulti()) {
296
+ return super.renderBody(feature)
297
+ }
298
+ let prev
299
+ let dist = 0
300
+ const data = []
301
+ const latlngs = feature.ui.getLatLngs()
302
+ const map = feature._umap._leafletMap
303
+ const properties = feature.extendedProperties()
304
+ for (const latlng of latlngs) {
305
+ if (!latlng.alt) {
306
+ continue
307
+ }
308
+ if (prev) {
309
+ dist = map.distance(latlng, prev)
310
+ }
311
+ data.push([latlng.alt, dist])
312
+ prev = latlng
313
+ }
314
+ const [root, { altitude, chart }] = Utils.loadTemplateWithRefs(`
315
+ <div>
316
+ <p>
317
+ ${translate('Distance:')} ${properties.measure} •
318
+ ${translate('Gain:')} ${properties.gain} m ↗ •
319
+ ${translate('Loss:')} ${properties.loss} m ↘ •
320
+ ${translate('Altitude:')} <span data-ref="altitude">—</span> m
321
+ </p>
322
+ <object width="100%"
323
+ data="${feature._umap.getStaticPathFor('../vendors/simple-elevation-chart/elevation.svg')}"
324
+ data-elevation="${JSON.stringify(data)}"
325
+ data-ref="chart"
326
+ type="image/svg+xml">
327
+ </div>
328
+ `)
329
+ let marker
330
+ function removeMarker() {
331
+ if (marker) {
332
+ marker.remove()
333
+ }
334
+ }
335
+ chart.addEventListener('mouseout', removeMarker)
336
+ map.on('popupclose', removeMarker)
337
+ chart.addEventListener('chart:over', (event) => {
338
+ const dataset = event.detail.element.dataset
339
+ if (dataset.ele) {
340
+ altitude.textContent = dataset.ele
341
+ }
342
+ removeMarker()
343
+ const latlng = latlngs[dataset.index]
344
+ if (!latlng) return
345
+ marker = new CircleMarker(latlng, {
346
+ radius: 8,
347
+ fillColor: 'white',
348
+ fillOpacity: 1,
349
+ color: 'orange',
350
+ }).addTo(map)
351
+ })
352
+ return root
353
+ }
354
+ }
@@ -3,6 +3,7 @@ import {
3
3
  CircleMarker as BaseCircleMarker,
4
4
  DomEvent,
5
5
  DomUtil,
6
+ GeoJSON,
6
7
  LatLng,
7
8
  LatLngBounds,
8
9
  LineUtil,
@@ -45,26 +46,29 @@ const FeatureMixin = {
45
46
  this.on('contextmenu editable:vertex:contextmenu', this.onContextMenu)
46
47
  this.on('click', this.onClick)
47
48
  this.on('editable:edited', this.onCommit)
49
+ this.on('mouseover', this.onMouseOver)
50
+ },
51
+
52
+ onMouseOver: function () {
53
+ if (this._map._umap.editEnabled && !this._map._umap.editedFeature) {
54
+ this._map._umap.tooltip.open({
55
+ content: translate('Right-click to edit'),
56
+ anchor: this,
57
+ })
58
+ }
48
59
  },
49
60
 
50
61
  onClick: function (event) {
51
62
  if (this._map.measureTools?.enabled()) return
52
63
  this._popupHandlersAdded = true // Prevent leaflet from managing event
53
- if (!this._map._umap.editEnabled) {
54
- this.feature.view(event)
55
- } else if (!this.feature.isReadOnly()) {
56
- if (event.originalEvent.shiftKey) {
57
- if (event.originalEvent.ctrlKey || event.originalEvent.metaKey) {
58
- this.feature.datalayer.edit(event)
59
- } else {
60
- this.feature.toggleEditing(event)
61
- }
62
- } else if (!this._map.editTools?.drawing()) {
63
- this._map._umap.editContextmenu.open(
64
- event.originalEvent,
65
- this.feature.getInplaceEditMenu(event)
66
- )
64
+ if (event.originalEvent.shiftKey) {
65
+ if (event.originalEvent.ctrlKey || event.originalEvent.metaKey) {
66
+ this.feature.datalayer.edit(event)
67
+ } else if (!this.feature.isReadOnly()) {
68
+ this.feature.toggleEditing(event)
67
69
  }
70
+ } else if (!this._map.editTools?.drawing()) {
71
+ this.feature.view(event)
68
72
  }
69
73
  DomEvent.stop(event)
70
74
  },
@@ -91,8 +95,8 @@ const FeatureMixin = {
91
95
  onContextMenu: function (event) {
92
96
  DomEvent.stop(event)
93
97
  const items = this.feature
94
- .getContextMenuItems(event)
95
- .concat(this._map._umap.getSharedContextMenuItems(event))
98
+ .getContextMenu(event)
99
+ .concat(this._map._umap.getSharedContextMenu(event))
96
100
  this._map._umap.contextmenu.open(event.originalEvent, items)
97
101
  },
98
102
 
@@ -123,6 +127,7 @@ const PointMixin = {
123
127
  },
124
128
 
125
129
  _enableDragging: function () {
130
+ if (this._cluster) return
126
131
  // TODO: start dragging after 1 second on mouse down
127
132
  if (this._map._umap.editEnabled) {
128
133
  if (!this.editEnabled()) this.enableEdit()
@@ -249,17 +254,22 @@ export const LeafletMarker = Marker.extend({
249
254
  })
250
255
 
251
256
  const PathMixin = {
252
- _onMouseOver: function () {
257
+ maxVertex: 100,
258
+ onMouseOver: function () {
253
259
  if (this._map.measureTools?.enabled()) {
254
260
  this._map._umap.tooltip.open({ content: this.getMeasure(), anchor: this })
255
- } else if (this._map._umap.editEnabled && !this._map._umap.editedFeature) {
256
- this._map._umap.tooltip.open({
257
- content: translate('Click to edit'),
258
- anchor: this,
259
- })
261
+ } else {
262
+ FeatureMixin.onMouseOver.call(this)
260
263
  }
261
264
  },
262
265
 
266
+ shouldAllowGeometryEdit: function () {
267
+ const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0)
268
+ return (
269
+ pointsCount < this.maxVertex || this._map.getZoom() === this._map.getMaxZoom()
270
+ )
271
+ },
272
+
263
273
  makeGeometryEditable: function () {
264
274
  // Feature has been removed since then?
265
275
  if (!this._map) return
@@ -268,20 +278,18 @@ const PathMixin = {
268
278
  return
269
279
  }
270
280
  this._map.once('moveend', this.makeGeometryEditable, this)
271
- const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0)
272
- if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) {
281
+ if (this.shouldAllowGeometryEdit()) {
282
+ this.enableEdit()
283
+ } else {
273
284
  this._map._umap.tooltip.open({
274
285
  content: L._('Please zoom in to edit the geometry'),
275
286
  })
276
287
  this.disableEdit()
277
- } else {
278
- this.enableEdit()
279
288
  }
280
289
  },
281
290
 
282
291
  addInteractions: function () {
283
292
  FeatureMixin.addInteractions.call(this)
284
- this.on('mouseover', this._onMouseOver)
285
293
  this.on('drag editable:drag', this._onDrag)
286
294
  this.on('popupopen', this.highlightPath)
287
295
  this.on('popupclose', this._redraw)
@@ -293,6 +301,7 @@ const PathMixin = {
293
301
  },
294
302
 
295
303
  highlightPath: function () {
304
+ this.feature.activate()
296
305
  this.parentClass.prototype.setStyle.call(this, {
297
306
  fillOpacity: Math.sqrt(this.feature.getDynamicOption('fillOpacity', 1.0)),
298
307
  opacity: 1.0,
@@ -336,17 +345,11 @@ const PathMixin = {
336
345
  },
337
346
 
338
347
  _redraw: function () {
348
+ this.feature.deactivate()
339
349
  this.setStyle()
340
350
  this.resetTooltip()
341
351
  },
342
352
 
343
- onVertexRawClick: function (event) {
344
- this._map._umap.editContextmenu.open(
345
- event.originalEvent,
346
- this.feature.getInplaceEditVertexMenu(event)
347
- )
348
- },
349
-
350
353
  isolateShape: function (atLatLng) {
351
354
  if (!this.feature.isMulti()) return
352
355
  const shape = this.enableEdit().deleteShapeAt(atLatLng)
@@ -457,6 +460,67 @@ export const LeafletPolyline = Polyline.extend({
457
460
  },
458
461
  })
459
462
 
463
+ export const RouteEditor = L.Editable.PolylineEditor.extend({
464
+ options: {
465
+ skipMiddleMarkers: true,
466
+ draggable: false,
467
+ },
468
+
469
+ getLatLngs: function () {
470
+ return this.feature._route
471
+ },
472
+ })
473
+
474
+ export const LeafletRoute = LeafletPolyline.extend({
475
+ initialize: function (feature, latlngs) {
476
+ this._route = GeoJSON.coordsToLatLngs(
477
+ feature.properties._umap_options.route?.coordinates
478
+ )
479
+ FeatureMixin.initialize.call(this, feature, latlngs)
480
+ delete this.dragging
481
+ },
482
+
483
+ addInteractions: function () {
484
+ PathMixin.addInteractions.call(this)
485
+ this.on('editable:drawing:clicked', this.onDrawingClick)
486
+ this.on('editable:vertex:dragend editable:vertex:deleted', this.onDrawingMoved)
487
+ },
488
+
489
+ getEditorClass: (tools) => {
490
+ return RouteEditor
491
+ },
492
+
493
+ getClass: () => LeafletRoute,
494
+
495
+ syncRoute() {
496
+ this.feature.properties._umap_options.route.coordinates = GeoJSON.latLngsToCoords(
497
+ this._route
498
+ )
499
+ },
500
+
501
+ onDrawingMoved: function (event) {
502
+ this.syncRoute()
503
+ if (this._route.length >= 2) {
504
+ this.feature.computeRoute()
505
+ }
506
+ },
507
+
508
+ onDrawingClick: function (event) {
509
+ this._route.push(event.latlng)
510
+ this.syncRoute()
511
+ if (this._route.length >= 2) {
512
+ this.feature.computeRoute()
513
+ }
514
+ },
515
+
516
+ shouldAllowGeometryEdit: function () {
517
+ return (
518
+ this._route.length < this.maxVertex ||
519
+ this._map.getZoom() === this._map.getMaxZoom()
520
+ )
521
+ },
522
+ })
523
+
460
524
  export const LeafletPolygon = Polygon.extend({
461
525
  parentClass: Polygon,
462
526
  includes: [FeatureMixin, PathMixin],
@@ -218,6 +218,7 @@ export const SCHEMA = {
218
218
  choices: [
219
219
  ['Default', translate('Default')],
220
220
  ['Circle', translate('Circle')],
221
+ ['LargeCircle', translate('Large Circle')],
221
222
  ['Drop', translate('Drop')],
222
223
  ['Ball', translate('Ball')],
223
224
  ['Raw', translate('None')],
@@ -234,6 +235,17 @@ export const SCHEMA = {
234
235
  inheritable: true,
235
236
  default: 1,
236
237
  },
238
+ iconSize: {
239
+ type: Number,
240
+ impacts: ['data'],
241
+ min: 12,
242
+ max: 64,
243
+ step: 4,
244
+ label: translate('Icon size'),
245
+ helpText: translate('Will only affect raw and large circle icons.'),
246
+ inheritable: true,
247
+ default: 24,
248
+ },
237
249
  iconUrl: {
238
250
  type: String,
239
251
  impacts: ['data'],
@@ -438,9 +450,17 @@ export const SCHEMA = {
438
450
  ['GeoRSSLink', translate('GeoRSS (only link)')],
439
451
  ['OSM', translate('OpenStreetMap')],
440
452
  ['Wikipedia', translate('Wikipedia')],
453
+ ['Route', translate('Route')],
441
454
  ],
442
455
  default: 'Default',
443
456
  },
457
+ printControl: {
458
+ type: Boolean,
459
+ impacts: ['ui'],
460
+ nullable: true,
461
+ label: translate('Display the print control'),
462
+ default: null,
463
+ },
444
464
  rank: {
445
465
  type: Number,
446
466
  impacts: ['datalayer-rank'],
@@ -453,6 +473,10 @@ export const SCHEMA = {
453
473
  type: Object,
454
474
  impacts: ['data'],
455
475
  },
476
+ route: {
477
+ type: Object,
478
+ impacts: ['data'],
479
+ },
456
480
  scaleControl: {
457
481
  type: Boolean,
458
482
  impacts: ['ui'],