@record-evolution/widget-mapbox 1.4.11 → 1.4.12

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.
@@ -1,550 +1,591 @@
1
- import { html, css, LitElement } from 'lit';
2
- import { property, state } from 'lit/decorators.js';
1
+ import { html, css, LitElement } from 'lit'
2
+ import { property, state } from 'lit/decorators.js'
3
3
  import { repeat } from 'lit/directives/repeat.js'
4
4
  // @ts-ignore
5
5
  // import mapboxgl from 'https://cdn.skypack.dev/-/mapbox-gl@v2.15.0-iKfohePv9lgutCMNih0d/dist=es2020,mode=imports,min/optimized/mapbox-gl.js'
6
- import mapboxgl from 'https://esm.run/mapbox-gl@3.0.1';
7
- import * as GeoJSON from 'geojson';
8
- import tinycolor from "tinycolor2";
9
- import { InputData, Dataseries, Point } from './types.js'
6
+ import mapboxgl from 'https://esm.run/mapbox-gl@3.0.1'
7
+ import * as GeoJSON from 'geojson'
8
+ import tinycolor from 'tinycolor2'
9
+ import { MapConfiguration } from './definition-schema.js'
10
+
11
+ type Dataseries = Exclude<MapConfiguration['dataseries'], undefined>[number]
12
+ type Point = Exclude<Dataseries['data'], undefined>[number]
10
13
 
11
14
  export class WidgetMapbox extends LitElement {
15
+ @property({ type: Object })
16
+ inputData?: MapConfiguration
17
+ @state()
18
+ private map: any | undefined = undefined
12
19
 
13
- @property({type: Object})
14
- inputData?: InputData = undefined
20
+ @state()
21
+ private dataSets: Dataseries[] = []
15
22
 
16
- @state()
17
- private map: any | undefined = undefined;
23
+ @state()
24
+ dataSources: any = new Map()
18
25
 
19
- @state()
20
- private dataSets: Dataseries[] = []
26
+ @state()
27
+ colors: any = new Map()
21
28
 
22
- @state()
23
- dataSources: any = new Map()
29
+ version: string = 'versionplaceholder'
24
30
 
25
- @state()
26
- colors: any = new Map()
31
+ resizeObserver: ResizeObserver
32
+ mapStyle?: string
33
+ constructor() {
34
+ super()
35
+ this.resizeObserver = new ResizeObserver(() => {
36
+ this.map?.resize()
37
+ this.fitBounds()
38
+ })
39
+ mapboxgl.accessToken =
40
+ 'pk.eyJ1IjoibWFya29wZSIsImEiOiJjazc1OWlsNjkwN2pyM2VxajV1eGRnYzgwIn0.3lVksk1nej_0KnWjCkBDAA'
41
+ }
27
42
 
28
- version: string = 'versionplaceholder'
43
+ update(changedProperties: Map<string, any>) {
44
+ if (changedProperties.has('inputData')) {
45
+ this.transformInputData()
46
+ }
47
+ super.update(changedProperties)
48
+ }
29
49
 
30
- resizeObserver: ResizeObserver
31
- mapStyle?: string
32
- constructor() {
33
- super()
34
- this.resizeObserver = new ResizeObserver(() => {
35
- this.map?.resize()
50
+ firstUpdated() {
51
+ this.transformInputData()
52
+ this.createMap()
53
+ this.resizeObserver.observe(this.map._container)
36
54
  this.fitBounds()
37
- })
38
- mapboxgl.accessToken = 'pk.eyJ1IjoibWFya29wZSIsImEiOiJjazc1OWlsNjkwN2pyM2VxajV1eGRnYzgwIn0.3lVksk1nej_0KnWjCkBDAA'
39
- }
55
+ }
40
56
 
41
- update(changedProperties: Map<string, any>) {
42
- changedProperties.forEach((propName: string) => {
43
- if (propName === 'inputData') {
44
- this.transformInputData()
45
- }
46
- })
47
- super.update(changedProperties)
48
- }
49
-
50
- firstUpdated() {
51
-
52
- this.transformInputData()
53
- this.createMap()
54
- this.resizeObserver.observe(this.map._container)
55
- this.fitBounds()
56
-
57
- }
58
-
59
- fitBounds() {
60
- const bounds = new mapboxgl.LngLatBounds()
61
- this.dataSources.forEach((col: GeoJSON.FeatureCollection) => {
62
- col.features.forEach(f => {
63
- // @ts-ignore
64
- if (f.geometry.coordinates?.length) bounds.extend(f.geometry.coordinates)
65
- })
66
- })
67
- if (!bounds.isEmpty()) {
68
- this.map.fitBounds(bounds, { maxZoom: 14, padding: 16, duration: 100, })
57
+ fitBounds() {
58
+ const bounds = new mapboxgl.LngLatBounds()
59
+ this.dataSources.forEach((col: GeoJSON.FeatureCollection) => {
60
+ col.features.forEach((f) => {
61
+ // @ts-ignore
62
+ if (f.geometry.coordinates?.length) bounds.extend(f.geometry.coordinates)
63
+ })
64
+ })
65
+ if (!bounds.isEmpty()) {
66
+ this.map.fitBounds(bounds, { maxZoom: 14, padding: 16, duration: 100 })
67
+ }
69
68
  }
70
- }
71
69
 
72
- transformInputData() {
73
- if(!this?.inputData?.settings || !this?.inputData?.dataseries?.length) return
70
+ transformInputData() {
71
+ if (!this?.inputData?.settings || !this?.inputData?.dataseries?.length) return
74
72
 
75
- if (this.map && this.inputData.settings.style !== this.mapStyle) {
76
- this.createMap()
77
- }
73
+ if (this.map && this.inputData.settings.style !== this.mapStyle) {
74
+ this.createMap()
75
+ }
78
76
 
79
- // choose random color if dataseries has none and store it for furure updates
80
- this.inputData.dataseries.forEach(ds => {
81
- if (!this.colors.has(ds.label)) {
82
- ds.color = ds.color ?? tinycolor.random().toString()
83
- this.colors.set(ds.label, ds.color)
84
- }
85
- })
86
- // console.log('The input data', this.inputData.dataseries[0], this.inputData.dataseries[1])
87
- // Pivot inputData if required
88
- this.dataSets = []
89
- this.inputData.dataseries.forEach(ds => {
90
- const color = this.colors.get(ds.label)
91
- const distincts = [...new Set(ds.data.map((d: Point) => d.pivot))]
92
- const derivedColors = tinycolor(color).monochromatic(distincts.length).map((c: any) => c.toHexString())
93
- if (distincts.length > 1) {
94
- distincts.forEach((piv, i) => {
95
- const pds: any = {
96
- label: `${ds.label} ${piv}`,
97
- order: ds.order,
98
- type: ds.type,
99
- latestValues: ds.latestValues,
100
- color: derivedColors[i],
101
- config: ds.config,
102
- data: ds.data.filter(d => d.pivot === piv)
103
- }
104
- this.dataSets.push(pds)
77
+ // choose random color if dataseries has none and store it for furure updates
78
+ this.inputData.dataseries.forEach((ds) => {
79
+ if (!this.colors.has(ds.label)) {
80
+ ds.color = ds.color ?? tinycolor.random().toString()
81
+ this.colors.set(ds.label, ds.color)
82
+ }
83
+ })
84
+ // console.log('The input data', this.inputData.dataseries[0], this.inputData.dataseries[1])
85
+ // Pivot inputData if required
86
+ this.dataSets = []
87
+ this.inputData.dataseries.forEach((ds) => {
88
+ const color = this.colors.get(ds.label)
89
+ const distincts = [...new Set(ds.data?.map((d: Point) => d.pivot))]
90
+ const derivedColors = tinycolor(color)
91
+ .monochromatic(distincts.length)
92
+ .map((c: any) => c.toHexString())
93
+ if (distincts.length > 1) {
94
+ distincts.forEach((piv, i) => {
95
+ const pds: any = {
96
+ label: `${ds.label} ${piv}`,
97
+ order: ds.order,
98
+ type: ds.type,
99
+ latestValues: ds.latestValues,
100
+ color: derivedColors[i],
101
+ config: ds.config,
102
+ data: ds.data?.filter((d) => d.pivot === piv)
103
+ }
104
+ this.dataSets.push(pds)
105
+ })
106
+ } else {
107
+ ds.color = ds.color ?? this.colors[ds.label]
108
+ this.dataSets.push(ds)
109
+ }
105
110
  })
106
- } else {
107
- ds.color = ds.color ?? this.colors[ds.label]
108
- this.dataSets.push(ds)
109
- }
110
- })
111
-
112
- this.inputData.dataseries = []
113
-
114
- // Filter for latest Values
115
- this.dataSets.forEach(ds => {
116
- if (ds.latestValues > 0) ds.data = ds.data.splice(-ds.latestValues)
117
- })
118
-
119
- // console.log('mapbox datasets', this.dataSets)
120
-
121
- // create geojson sources
122
- this.dataSets.sort((a, b) => b.order - a.order).forEach(ds => {
123
-
124
- this.dataSources.set('input:' + ds.label, {
125
- type: 'FeatureCollection',
126
- features: this.createGEOJson(ds)
127
- } as GeoJSON.FeatureCollection)
128
-
129
- if (this.map) this.syncDataLayers()
130
- // console.log('mapbox DataLayers', this.dataSources)
131
- })
132
- }
133
-
134
- createGEOJson(ds: Dataseries): GeoJSON.Feature[] {
135
- if (ds.type !== 'line') {
136
- const features: GeoJSON.Feature[] = ds.data
137
- .filter(p =>
138
- p.lon !== undefined
139
- && p.lat !== undefined
140
- && p.value !== undefined
141
- && typeof p.lon === 'number'
142
- && typeof p.lat === 'number'
143
- && typeof p.value === 'number')
144
- .map(p => {
145
- const point: GeoJSON.Feature = {
146
- type: 'Feature',
147
- geometry: {
148
- type: 'Point',
149
- coordinates: [p.lon, p.lat]
150
- },
151
- properties: {
152
- value: Math.round(p.value)
153
- }
111
+
112
+ this.inputData.dataseries = []
113
+
114
+ // Filter for latest Values
115
+ this.dataSets.forEach((ds) => {
116
+ if (ds?.latestValues && ds?.latestValues > 0) ds.data = ds.data?.splice(-ds.latestValues)
117
+ })
118
+
119
+ // console.log('mapbox datasets', this.dataSets)
120
+
121
+ // create geojson sources
122
+ this.dataSets
123
+ // .sort((a, b) => b.order - a.order)
124
+ .forEach((ds) => {
125
+ this.dataSources.set('input:' + ds.label, {
126
+ type: 'FeatureCollection',
127
+ features: this.createGEOJson(ds)
128
+ } as GeoJSON.FeatureCollection)
129
+
130
+ if (this.map) this.syncDataLayers()
131
+ // console.log('mapbox DataLayers', this.dataSources)
132
+ })
133
+ }
134
+
135
+ createGEOJson(ds: Dataseries): GeoJSON.Feature[] | undefined {
136
+ ds.data?.forEach((p) => {
137
+ p = {
138
+ lon: Number(p.lon),
139
+ lat: Number(p.lat),
140
+ alt: Number(p.alt),
141
+ value: Number(p.value),
142
+ pivot: p.pivot
154
143
  }
155
- return point
156
144
  })
157
- return features
158
- }
145
+ if (ds.type !== 'line') {
146
+ const features: GeoJSON.Feature[] | undefined = ds.data
147
+ ?.filter(
148
+ (p) =>
149
+ p.lon !== undefined &&
150
+ p.lat !== undefined &&
151
+ p.value !== undefined &&
152
+ typeof p.lon === 'number' &&
153
+ typeof p.lat === 'number' &&
154
+ typeof p.value === 'number'
155
+ )
156
+ .map((p) => {
157
+ const point: GeoJSON.Feature = {
158
+ type: 'Feature',
159
+ geometry: {
160
+ type: 'Point',
161
+ coordinates: [p.lon, p.lat]
162
+ },
163
+ properties: {
164
+ value: p.value ? Math.round(p.value) : undefined
165
+ }
166
+ }
167
+ return point
168
+ })
169
+ return features
170
+ }
159
171
 
160
- const line: number[][] = ds.data.reverse()
161
- .filter(p => p.lon !== undefined && p.lat !== undefined)
162
- .map(p => [p.lon, p.lat, p.alt])
172
+ const line: number[][] | undefined = ds?.data
173
+ ?.reverse()
174
+ ?.filter((p) => p.lon !== undefined && p.lat !== undefined)
175
+ ?.map((p) => [p.lon, p.lat, p?.alt]) as number[][]
163
176
 
164
- const feature: GeoJSON.Feature = {
177
+ const feature: GeoJSON.Feature = {
165
178
  type: 'Feature',
166
179
  geometry: {
167
- type: 'LineString',
168
- coordinates: line
180
+ type: 'LineString',
181
+ coordinates: line
169
182
  },
170
183
  properties: {}
171
- }
172
- return [feature]
173
- }
174
-
175
- addCircleLayer(dataSet: Dataseries) {
176
- if (!dataSet) return
177
- const layerConfig = {
178
- 'id': dataSet.label + ':circle',
179
- 'type': 'circle',
180
- 'source': 'input:' + dataSet.label,
181
- 'paint': {
182
- ...dataSet.config['circle'],
183
- "circle-radius": ['get', 'value'],
184
- "circle-radius-transition": {
185
- duration: 1000,
186
- delay: 0
187
- },
188
- "circle-color": dataSet.color
189
- },
190
- // Place polygons under labels, roads and buildings.
191
- // 'aeroway-polygon'
192
- }
193
-
194
- this.map?.addLayer(layerConfig)
195
-
196
- if (!dataSet.config['symbol']) return
197
- const layerConfig2 = {
198
- 'id': dataSet.label + ':symbol',
199
- 'type': 'symbol',
200
- 'source': 'input:' + dataSet.label,
201
- layout: {
202
- 'text-field': ['get', 'value'],
203
- 'text-size': dataSet.config['symbol']['text-size'],
204
- 'text-anchor': 'center',
205
- },
206
- paint: {
207
- 'text-color': dataSet.config['symbol']['text-color'],
208
184
  }
209
- // Place polygons under labels, roads and buildings.
210
- // 'aeroway-polygon'
211
- }
212
- this.map?.addLayer(layerConfig2)
185
+ return [feature]
213
186
  }
214
187
 
215
- addSymbolLayer(dataSet: Dataseries) {
216
- if (!dataSet) return
217
- const layerConfig = {
218
- 'id': dataSet.label + ':symbol',
219
- 'type': 'symbol',
220
- 'source': 'input:' + dataSet.label,
221
- layout: {
222
- 'text-field': ['get', 'value'],
223
- 'text-size': dataSet.config['symbol']['text-size'],
224
- 'text-anchor': 'center',
225
- 'icon-image': dataSet.config.symbol['icon-image'],
226
- 'icon-size': dataSet.config.symbol['icon-size']
227
- },
228
- paint: {
229
- 'text-color': dataSet.config['symbol']['text-color'],
230
- }
231
- // Place polygons under labels, roads and buildings.
232
- // 'aeroway-polygon'
233
- }
234
- this.map?.addLayer(layerConfig)
235
- }
236
-
237
- addHeatmapLayer(dataSet: Dataseries) {
238
- if (!dataSet) return
239
-
240
- const min = Math.min(...dataSet.data.map(p => p.value))
241
- const max = Math.max(...dataSet.data.map(p => p.value))
242
- const layerConfig = {
243
- 'id': dataSet.label + ':heatmap',
244
- 'type': 'heatmap',
245
- 'source': 'input:' + dataSet.label,
246
- paint: {
247
- ...dataSet.config.heatmap,
248
- 'heatmap-color': [
249
- "interpolate",
250
- ["linear"],
251
- ["heatmap-density"],
252
- 0,"rgba(0, 0, 255, 0)",
253
- 0.1, "royalblue",
254
- 0.3, "cyan",
255
- 0.5, "lime",
256
- 0.7, "yellow",
257
- 1, "tomato"
258
- ],
259
- // Increase the heatmap weight based on frequency and property magnitude
260
- 'heatmap-weight': [
261
- 'interpolate',
262
- ['linear'],
263
- ['get', 'value'],
264
- min, 0,
265
- max, 3
266
- ],
267
- // // Increase the heatmap color weight weight by zoom level
268
- // // heatmap-intensity is a multiplier on top of heatmap-weight
269
- // 'heatmap-intensity': [
270
- // 'interpolate',
271
- // ['linear'],
272
- // ['zoom'],
273
- // 0, 1,
274
- // 9, 3
275
- // ],
276
- // Adjust the heatmap radius by zoom level
277
- 'heatmap-radius': [
278
- 'interpolate',
279
- ['linear'],
280
- ['get', 'value'],
281
- min, 30,
282
- max, 30 + dataSet.config.heatmap['heatmap-radius']
283
- ],
284
- 'heatmap-radius-transition': {
285
- duration: 1000,
286
- delay: 0
188
+ addCircleLayer(dataSet: Dataseries) {
189
+ if (!dataSet) return
190
+ const layerConfig = {
191
+ id: dataSet.label + ':circle',
192
+ type: 'circle',
193
+ source: 'input:' + dataSet.label,
194
+ paint: {
195
+ ...dataSet.config?.['circle'],
196
+ 'circle-radius': ['get', 'value'],
197
+ 'circle-radius-transition': {
198
+ duration: 1000,
199
+ delay: 0
200
+ },
201
+ 'circle-color': dataSet.color
202
+ }
203
+ // Place polygons under labels, roads and buildings.
204
+ // 'aeroway-polygon'
287
205
  }
288
- // // Transition from heatmap to circle layer by zoom level
289
- // 'heatmap-opacity': [
290
- // 'interpolate',
291
- // ['linear'],
292
- // ['zoom'],
293
- // 7, 1,
294
- // 9, 0
295
- // ]
296
- }
297
- // Place polygons under labels, roads and buildings.
298
- // 'aeroway-polygon'
299
- }
300
- this.map?.addLayer(layerConfig)
301
- }
302
-
303
- addLineLayer(dataSet: Dataseries) {
304
- if (!dataSet) return
305
- const layerConfig = {
306
- 'id': dataSet.label + ':line',
307
- 'type': 'line',
308
- 'source': 'input:' + dataSet.label,
309
- layout: {
310
- 'line-cap': 'round',
311
- },
312
- paint: {
313
- ...dataSet.config.line,
314
- "line-color": dataSet.color,
315
- }
316
- // Place polygons under labels, roads and buildings.
317
- // 'aeroway-polygon'
318
- }
319
- this.map?.addLayer(layerConfig)
320
-
321
- if (!dataSet.config.symbol['icon-image']) return
322
-
323
- const layerConfig2 = {
324
- 'id': dataSet.label + ':symbol',
325
- 'type': 'symbol',
326
- 'source': 'input:' + dataSet.label,
327
- layout: {
328
- 'icon-image': dataSet.config.symbol['icon-image'],
329
- 'icon-size': dataSet.config.symbol['icon-size'],
330
- },
331
- paint: {
332
- 'icon-color': dataSet.color
333
- }
334
- // Place polygons under labels, roads and buildings.
335
- // 'aeroway-polygon'
206
+
207
+ this.map?.addLayer(layerConfig)
208
+
209
+ if (!dataSet.config?.['symbol']) return
210
+ const layerConfig2 = {
211
+ id: dataSet.label + ':symbol',
212
+ type: 'symbol',
213
+ source: 'input:' + dataSet.label,
214
+ layout: {
215
+ 'text-field': ['get', 'value'],
216
+ 'text-size': dataSet.config['symbol']['text-size'],
217
+ 'text-anchor': 'center'
218
+ },
219
+ paint: {
220
+ 'text-color': dataSet.config['symbol']['text-color']
221
+ }
222
+ // Place polygons under labels, roads and buildings.
223
+ // 'aeroway-polygon'
224
+ }
225
+ this.map?.addLayer(layerConfig2)
336
226
  }
337
- this.map?.addLayer(layerConfig2)
338
- }
339
-
340
- syncDataLayers() {
341
-
342
- // remove sources and all Layers that are not part of the inputData anymore
343
- const sources: any[] = this.map.getStyle().sources ?? []
344
- const mySources: [string, any][] = Object.entries(sources).filter(([l]) => l.startsWith('input:'))
345
-
346
- mySources.forEach(([label]) => {
347
- if (!this.dataSources.has(label)){
348
- // remove all layers using this source
349
- this.map.getStyle().layers.filter((la: any) => la.id === label + ':' + la.type).forEach((la: any) => {
350
- this.map.removeLayer(la.id)
351
- })
352
- this.map.removeSource(label)
353
- }
354
- })
355
-
356
- // add new layers or update the data of existing layers
357
- this.dataSets.forEach(ds => {
358
- const fc = this.dataSources.get('input:' + ds.label)
359
- const src = this.map.getSource('input:' + ds.label)
360
- if (src) {
361
- src.setData(fc || [])
362
- return
363
- }
364
- // console.log('adding source', ds.label, ds.type)
365
- this.map?.addSource('input:' + ds.label, {
366
- type: 'geojson',
367
- data: fc || []
368
- })
369
-
370
- switch(ds.type) {
371
- case 'circle':
372
- this.addCircleLayer(ds)
373
- return
374
- case 'symbol':
375
- this.addSymbolLayer(ds)
376
- return
377
- case 'heatmap':
378
- this.addHeatmapLayer(ds)
379
- return
380
- case 'line':
381
- this.addLineLayer(ds)
382
- }
383
- })
384
- if (this.inputData?.settings?.follow) this.fitBounds()
385
- }
386
-
387
- addBuildingLayer() {
388
- // Insert the layer beneath any symbol layer.
389
- const {layers} = this.map.getStyle()
390
-
391
- let labelLayerId: number = 0
392
- for (let i = 0; i < layers.length; i++) {
393
- if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
394
- labelLayerId = layers[i].id
395
- break
227
+
228
+ addSymbolLayer(dataSet: Dataseries) {
229
+ if (!dataSet) return
230
+ const layerConfig = {
231
+ id: dataSet.label + ':symbol',
232
+ type: 'symbol',
233
+ source: 'input:' + dataSet.label,
234
+ layout: {
235
+ 'text-field': ['get', 'value'],
236
+ 'text-size': dataSet.config?.['symbol']?.['text-size'],
237
+ 'text-anchor': 'center',
238
+ 'icon-image': dataSet.config?.symbol?.['icon-image'],
239
+ 'icon-size': dataSet.config?.symbol?.['icon-size']
240
+ },
241
+ paint: {
242
+ 'text-color': dataSet.config?.['symbol']?.['text-color']
243
+ }
244
+ // Place polygons under labels, roads and buildings.
245
+ // 'aeroway-polygon'
396
246
  }
247
+ this.map?.addLayer(layerConfig)
397
248
  }
398
249
 
399
- this.map.addLayer({
400
- 'id': '3d-buildings',
401
- 'source': 'composite',
402
- 'source-layer': 'building',
403
- 'filter': ['==', 'extrude', 'true'],
404
- 'type': 'fill-extrusion',
405
- 'minzoom': 14,
406
- 'paint': {
407
- 'fill-extrusion-color': '#aaa',
408
- // use an 'interpolate' expression to add a smooth transition effect to the
409
- // buildings as the user zooms in
410
- 'fill-extrusion-height': [
411
- "interpolate", ["linear"],
412
- ["zoom"],
413
- 14, 0,
414
- 14.05, ["get", "height"]
415
- ],
416
- 'fill-extrusion-base': [
417
- "interpolate", ["linear"],
418
- ["zoom"],
419
- 14, 0,
420
- 14.05, ["get", "min_height"]
421
- ],
422
- 'fill-extrusion-opacity': .6
250
+ addHeatmapLayer(dataSet: Dataseries) {
251
+ if (!dataSet) return
252
+ const values = (dataSet.data?.map((p) => p?.value).filter((v) => v !== undefined) as number[]) ?? []
253
+ const min = Math.min(...values)
254
+ const max = Math.max(...values)
255
+ const layerConfig = {
256
+ id: dataSet.label + ':heatmap',
257
+ type: 'heatmap',
258
+ source: 'input:' + dataSet.label,
259
+ paint: {
260
+ ...dataSet.config?.heatmap,
261
+ 'heatmap-color': [
262
+ 'interpolate',
263
+ ['linear'],
264
+ ['heatmap-density'],
265
+ 0,
266
+ 'rgba(0, 0, 255, 0)',
267
+ 0.1,
268
+ 'royalblue',
269
+ 0.3,
270
+ 'cyan',
271
+ 0.5,
272
+ 'lime',
273
+ 0.7,
274
+ 'yellow',
275
+ 1,
276
+ 'tomato'
277
+ ],
278
+ // Increase the heatmap weight based on frequency and property magnitude
279
+ 'heatmap-weight': ['interpolate', ['linear'], ['get', 'value'], min, 0, max, 3],
280
+ // // Increase the heatmap color weight weight by zoom level
281
+ // // heatmap-intensity is a multiplier on top of heatmap-weight
282
+ // 'heatmap-intensity': [
283
+ // 'interpolate',
284
+ // ['linear'],
285
+ // ['zoom'],
286
+ // 0, 1,
287
+ // 9, 3
288
+ // ],
289
+ // Adjust the heatmap radius by zoom level
290
+ 'heatmap-radius': [
291
+ 'interpolate',
292
+ ['linear'],
293
+ ['get', 'value'],
294
+ min,
295
+ 30,
296
+ max,
297
+ 30 + (dataSet.config?.heatmap?.['heatmap-radius'] ?? 0)
298
+ ],
299
+ 'heatmap-radius-transition': {
300
+ duration: 1000,
301
+ delay: 0
302
+ }
303
+ // // Transition from heatmap to circle layer by zoom level
304
+ // 'heatmap-opacity': [
305
+ // 'interpolate',
306
+ // ['linear'],
307
+ // ['zoom'],
308
+ // 7, 1,
309
+ // 9, 0
310
+ // ]
311
+ }
312
+ // Place polygons under labels, roads and buildings.
313
+ // 'aeroway-polygon'
423
314
  }
424
- }, labelLayerId)
425
- }
426
-
427
- createMap() {
428
- if (this.map) return
429
- this.mapStyle = this.inputData?.settings?.style
430
- this.map = new mapboxgl.Map({
431
- container: this.shadowRoot?.getElementById('map') as HTMLCanvasElement,
432
- style: `mapbox://styles/mapbox/${this.mapStyle ?? 'light-v11'}` ,
433
- center: [8.6841700, 50.1155200],
434
- zoom: 1.8,
435
- attributionControl: false
436
- })
437
-
438
- this.map.scrollZoom.disable();
439
-
440
- this.map.addControl(new mapboxgl.NavigationControl(), 'top-right')
441
-
442
- const scale = new mapboxgl.ScaleControl({
443
- maxWidth: 80,
444
- unit: 'metric'
445
- })
446
-
447
- this.map.addControl(scale, 'bottom-left')
448
-
449
- console.log('MAPBOX VERSION', mapboxgl.version)
450
-
451
- this.map.on('load', () => {
452
- this.addBuildingLayer()
453
- this.syncDataLayers()
454
- });
455
- }
456
-
457
- static styles = css`
458
- :host {
459
- display: block;
460
- color: var(--re-bar-text-color, #000);
461
- font-family: sans-serif;
462
- padding: 16px;
463
- box-sizing: border-box;
464
- margin: auto;
315
+ this.map?.addLayer(layerConfig)
465
316
  }
466
317
 
467
- .paging:not([active]) { display: none !important; }
318
+ addLineLayer(dataSet: Dataseries) {
319
+ if (!dataSet) return
320
+ const layerConfig = {
321
+ id: dataSet.label + ':line',
322
+ type: 'line',
323
+ source: 'input:' + dataSet.label,
324
+ layout: {
325
+ 'line-cap': 'round'
326
+ },
327
+ paint: {
328
+ ...dataSet.config?.line,
329
+ 'line-color': dataSet.color
330
+ }
331
+ // Place polygons under labels, roads and buildings.
332
+ // 'aeroway-polygon'
333
+ }
334
+ this.map?.addLayer(layerConfig)
468
335
 
469
- header {
470
- display: flex;
471
- margin: 0 0 16px 0;
472
- gap: 24px;
473
- justify-content: space-between;
474
- }
475
- h3 {
476
- margin: 0;
477
- max-width: 300px;
478
- overflow: hidden;
479
- text-overflow: ellipsis;
480
- white-space: nowrap;
481
- }
482
- p {
483
- margin: 10px 0 0 0;
484
- max-width: 300px;
485
- font-size: 14px;
486
- overflow: hidden;
487
- text-overflow: ellipsis;
488
- white-space: nowrap;
489
- line-height: 17px;
490
- }
336
+ if (!dataSet.config?.symbol?.['icon-image']) return
491
337
 
492
- .wrapper {
493
- display: flex;
494
- flex-direction: column;
495
- height: 100%;
496
- width: 100%;
497
- }
498
- #map {
499
- flex: 1;
338
+ const layerConfig2 = {
339
+ id: dataSet.label + ':symbol',
340
+ type: 'symbol',
341
+ source: 'input:' + dataSet.label,
342
+ layout: {
343
+ 'icon-image': dataSet.config.symbol['icon-image'],
344
+ 'icon-size': dataSet.config.symbol['icon-size']
345
+ },
346
+ paint: {
347
+ 'icon-color': dataSet.color
348
+ }
349
+ // Place polygons under labels, roads and buildings.
350
+ // 'aeroway-polygon'
351
+ }
352
+ this.map?.addLayer(layerConfig2)
500
353
  }
501
354
 
502
- .title {
503
- white-space: nowrap;
504
- }
505
-
506
- .legend {
507
- display: flex;
508
- flex-wrap: wrap;
509
- gap: 12px;
355
+ syncDataLayers() {
356
+ // remove sources and all Layers that are not part of the inputData anymore
357
+ const sources: any[] = this.map.getStyle().sources ?? []
358
+ const mySources: [string, any][] = Object.entries(sources).filter(([l]) => l.startsWith('input:'))
359
+
360
+ mySources.forEach(([label]) => {
361
+ if (!this.dataSources.has(label)) {
362
+ // remove all layers using this source
363
+ this.map
364
+ .getStyle()
365
+ .layers.filter((la: any) => la.id === label + ':' + la.type)
366
+ .forEach((la: any) => {
367
+ this.map.removeLayer(la.id)
368
+ })
369
+ this.map.removeSource(label)
370
+ }
371
+ })
372
+
373
+ // add new layers or update the data of existing layers
374
+ this.dataSets.forEach((ds) => {
375
+ const fc = this.dataSources.get('input:' + ds.label)
376
+ const src = this.map.getSource('input:' + ds.label)
377
+ if (src) {
378
+ src.setData(fc || [])
379
+ return
380
+ }
381
+ // console.log('adding source', ds.label, ds.type)
382
+ this.map?.addSource('input:' + ds.label, {
383
+ type: 'geojson',
384
+ data: fc || []
385
+ })
386
+
387
+ switch (ds.type) {
388
+ case 'circle':
389
+ this.addCircleLayer(ds)
390
+ return
391
+ case 'symbol':
392
+ this.addSymbolLayer(ds)
393
+ return
394
+ case 'heatmap':
395
+ this.addHeatmapLayer(ds)
396
+ return
397
+ case 'line':
398
+ this.addLineLayer(ds)
399
+ }
400
+ })
401
+ if (this.inputData?.settings?.follow) this.fitBounds()
510
402
  }
511
403
 
512
- .label {
513
- display: flex;
514
- align-items: center;
515
- font-size: 14px;
516
- gap: 8px
404
+ addBuildingLayer() {
405
+ // Insert the layer beneath any symbol layer.
406
+ const { layers } = this.map.getStyle()
407
+
408
+ let labelLayerId: number = 0
409
+ for (let i = 0; i < layers.length; i++) {
410
+ if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
411
+ labelLayerId = layers[i].id
412
+ break
413
+ }
414
+ }
415
+
416
+ this.map.addLayer(
417
+ {
418
+ id: '3d-buildings',
419
+ source: 'composite',
420
+ 'source-layer': 'building',
421
+ filter: ['==', 'extrude', 'true'],
422
+ type: 'fill-extrusion',
423
+ minzoom: 14,
424
+ paint: {
425
+ 'fill-extrusion-color': '#aaa',
426
+ // use an 'interpolate' expression to add a smooth transition effect to the
427
+ // buildings as the user zooms in
428
+ 'fill-extrusion-height': [
429
+ 'interpolate',
430
+ ['linear'],
431
+ ['zoom'],
432
+ 14,
433
+ 0,
434
+ 14.05,
435
+ ['get', 'height']
436
+ ],
437
+ 'fill-extrusion-base': [
438
+ 'interpolate',
439
+ ['linear'],
440
+ ['zoom'],
441
+ 14,
442
+ 0,
443
+ 14.05,
444
+ ['get', 'min_height']
445
+ ],
446
+ 'fill-extrusion-opacity': 0.6
447
+ }
448
+ },
449
+ labelLayerId
450
+ )
517
451
  }
518
452
 
519
- a.mapboxgl-ctrl-logo {
520
- display: none;
453
+ createMap() {
454
+ if (this.map) return
455
+ this.mapStyle = this.inputData?.settings?.style
456
+ this.map = new mapboxgl.Map({
457
+ container: this.shadowRoot?.getElementById('map') as HTMLCanvasElement,
458
+ style: `mapbox://styles/mapbox/${this.mapStyle ?? 'light-v11'}`,
459
+ center: [8.68417, 50.11552],
460
+ zoom: 1.8,
461
+ attributionControl: false
462
+ })
463
+
464
+ this.map.scrollZoom.disable()
465
+
466
+ this.map.addControl(new mapboxgl.NavigationControl(), 'top-right')
467
+
468
+ const scale = new mapboxgl.ScaleControl({
469
+ maxWidth: 80,
470
+ unit: 'metric'
471
+ })
472
+
473
+ this.map.addControl(scale, 'bottom-left')
474
+
475
+ console.log('MAPBOX VERSION', mapboxgl.version)
476
+
477
+ this.map.on('load', () => {
478
+ this.addBuildingLayer()
479
+ this.syncDataLayers()
480
+ })
521
481
  }
522
482
 
523
- `;
483
+ static styles = css`
484
+ :host {
485
+ display: block;
486
+ color: var(--re-bar-text-color, #000);
487
+ font-family: sans-serif;
488
+ padding: 16px;
489
+ box-sizing: border-box;
490
+ margin: auto;
491
+ }
492
+
493
+ .paging:not([active]) {
494
+ display: none !important;
495
+ }
524
496
 
525
- render() {
526
- return html`
527
- <link href="https://api.mapbox.com/mapbox-gl-js/v${mapboxgl.version}/mapbox-gl.css" rel="stylesheet">
528
- <div class="wrapper">
529
- <header>
530
- <div class="title">
531
- <h3 class="paging" ?active=${this.inputData?.settings?.title}>${this.inputData?.settings?.title}</h3>
532
- <p class="paging" ?active=${this.inputData?.settings?.subTitle}>${this.inputData?.settings?.subTitle}</p>
533
- </div>
534
- <div class="legend paging" ?active=${this?.inputData?.settings?.showLegend}>
535
- ${repeat(this.dataSets, ds => ds.label, ds => {
536
- return html`
537
- <div class="label">
538
- <div style="background: ${ds.color}; width: 24px; height: 12px;"></div>
539
- <div>${ds.label}</div>
540
- </div>
541
- `})}
497
+ header {
498
+ display: flex;
499
+ margin: 0 0 16px 0;
500
+ gap: 24px;
501
+ justify-content: space-between;
502
+ }
503
+ h3 {
504
+ margin: 0;
505
+ max-width: 300px;
506
+ overflow: hidden;
507
+ text-overflow: ellipsis;
508
+ white-space: nowrap;
509
+ }
510
+ p {
511
+ margin: 10px 0 0 0;
512
+ max-width: 300px;
513
+ font-size: 14px;
514
+ overflow: hidden;
515
+ text-overflow: ellipsis;
516
+ white-space: nowrap;
517
+ line-height: 17px;
518
+ }
519
+
520
+ .wrapper {
521
+ display: flex;
522
+ flex-direction: column;
523
+ height: 100%;
524
+ width: 100%;
525
+ }
526
+ #map {
527
+ flex: 1;
528
+ }
529
+
530
+ .title {
531
+ white-space: nowrap;
532
+ }
533
+
534
+ .legend {
535
+ display: flex;
536
+ flex-wrap: wrap;
537
+ gap: 12px;
538
+ }
539
+
540
+ .label {
541
+ display: flex;
542
+ align-items: center;
543
+ font-size: 14px;
544
+ gap: 8px;
545
+ }
546
+
547
+ a.mapboxgl-ctrl-logo {
548
+ display: none;
549
+ }
550
+ `
551
+
552
+ render() {
553
+ return html`
554
+ <link
555
+ href="https://api.mapbox.com/mapbox-gl-js/v${mapboxgl.version}/mapbox-gl.css"
556
+ rel="stylesheet"
557
+ />
558
+ <div class="wrapper">
559
+ <header>
560
+ <div class="title">
561
+ <h3 class="paging" ?active=${this.inputData?.settings?.title}>
562
+ ${this.inputData?.settings?.title}
563
+ </h3>
564
+ <p class="paging" ?active=${this.inputData?.settings?.subTitle}>
565
+ ${this.inputData?.settings?.subTitle}
566
+ </p>
567
+ </div>
568
+ <div class="legend paging" ?active=${this?.inputData?.settings?.showLegend}>
569
+ ${repeat(
570
+ this.dataSets,
571
+ (ds) => ds.label,
572
+ (ds) => {
573
+ return html`
574
+ <div class="label">
575
+ <div
576
+ style="background: ${ds.color}; width: 24px; height: 12px;"
577
+ ></div>
578
+ <div>${ds.label}</div>
579
+ </div>
580
+ `
581
+ }
582
+ )}
583
+ </div>
584
+ </header>
585
+ <div id="map"></div>
542
586
  </div>
543
- </header>
544
- <div id="map"></div>
545
- </div>
546
- `;
547
- }
587
+ `
588
+ }
548
589
  }
549
590
 
550
- window.customElements.define('widget-mapbox-versionplaceholder', WidgetMapbox);
591
+ window.customElements.define('widget-mapbox-versionplaceholder', WidgetMapbox)