@dative-gpi/foundation-shared-components 1.0.154 → 1.0.156-fix-alert-tools
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.
- package/components/map/FSMap.vue +86 -82
- package/components/map/FSMapFeatureGroup.vue +7 -1
- package/components/map/FSMapLayerButton.vue +1 -0
- package/components/map/FSMapMarker.vue +65 -47
- package/components/map/FSMapMarkerClusterGroup.vue +31 -6
- package/components/map/FSMapPolygon.vue +14 -2
- package/components/map/FSMapTileLayer.vue +12 -1
- package/components/map/layers.ts +0 -0
- package/composables/index.ts +1 -0
- package/composables/useMapLayers.ts +62 -0
- package/package.json +4 -4
- package/tools/alertsTools.ts +42 -10
- package/utils/operations.ts +42 -3
package/components/map/FSMap.vue
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
<FSMapLayerButton
|
|
30
30
|
v-if="$props.allowedLayers?.length && $props.allowedLayers.length > 1"
|
|
31
|
-
:
|
|
31
|
+
:disabled="$props.disabled"
|
|
32
|
+
:layers="layers.filter((layer) => $props.allowedLayers?.includes(layer.name) ?? true)"
|
|
32
33
|
:modelValue="$props.currentLayer"
|
|
33
34
|
@update:model-value="$emit('update:currentLayer', $event)"
|
|
34
35
|
/>
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
>
|
|
40
41
|
<FSButton
|
|
41
42
|
v-if="$props.showMyLocation"
|
|
43
|
+
:disabled="$props.disabled"
|
|
42
44
|
icon="mdi-crosshairs-gps"
|
|
43
45
|
color="primary"
|
|
44
46
|
variant="full"
|
|
@@ -53,12 +55,14 @@
|
|
|
53
55
|
gap="0"
|
|
54
56
|
>
|
|
55
57
|
<FSButton
|
|
58
|
+
:disabled="$props.disabled"
|
|
56
59
|
class="fs-map-zoom-plus-button"
|
|
57
60
|
icon="mdi-plus"
|
|
58
61
|
@click="() => map!.zoomIn()"
|
|
59
62
|
:border="false"
|
|
60
63
|
/>
|
|
61
64
|
<FSButton
|
|
65
|
+
:disabled="$props.disabled"
|
|
62
66
|
class="fs-map-zoom-minus-button"
|
|
63
67
|
icon="mdi-minus"
|
|
64
68
|
@click="() => map!.zoomOut()"
|
|
@@ -92,12 +96,10 @@
|
|
|
92
96
|
import { computed, defineComponent, onMounted, type Ref, provide, type PropType, ref, type StyleValue, watch, onUnmounted, markRaw } from "vue";
|
|
93
97
|
|
|
94
98
|
import type {} from "leaflet.markercluster";
|
|
95
|
-
import { map as createMap, control,
|
|
99
|
+
import { map as createMap, control, latLngBounds, latLng, type LatLng, type FitBoundsOptions, type ZoomPanOptions, type LatLngBounds } from "leaflet";
|
|
96
100
|
|
|
97
|
-
import {
|
|
98
|
-
|
|
99
|
-
import { useBreakpoints, useColors, useSlots } from "../../composables";
|
|
100
|
-
import { ColorEnum, MapLayers, MapOverlayPositions, type MapLayer } from "../../models";
|
|
101
|
+
import { useBreakpoints, useColors, useMapLayers, useSlots } from "../../composables";
|
|
102
|
+
import { ColorEnum, MapLayers, MapOverlayPositions } from "../../models";
|
|
101
103
|
|
|
102
104
|
import FSMapLayerButton from "./FSMapLayerButton.vue";
|
|
103
105
|
import FSMapOverlay from "./FSMapOverlay.vue";
|
|
@@ -130,6 +132,11 @@ export default defineComponent({
|
|
|
130
132
|
required: false,
|
|
131
133
|
default: '100%'
|
|
132
134
|
},
|
|
135
|
+
disabled: {
|
|
136
|
+
type: Boolean,
|
|
137
|
+
required: false,
|
|
138
|
+
default: false
|
|
139
|
+
},
|
|
133
140
|
grayscale: {
|
|
134
141
|
type: Boolean,
|
|
135
142
|
required: false,
|
|
@@ -175,22 +182,20 @@ export default defineComponent({
|
|
|
175
182
|
required: false,
|
|
176
183
|
default: () => [MapLayers.Map, MapLayers.Imagery]
|
|
177
184
|
},
|
|
178
|
-
|
|
185
|
+
zoom: {
|
|
179
186
|
type: Number,
|
|
180
187
|
required: false,
|
|
181
188
|
default: 16
|
|
182
189
|
}
|
|
183
190
|
},
|
|
184
|
-
emits: [
|
|
191
|
+
emits: ['update:overlayMode', 'update:currentLayer', "click:latlng", "update:zoom", "update:center"],
|
|
185
192
|
setup(props, { emit }) {
|
|
186
|
-
const {
|
|
193
|
+
const { layers } = useMapLayers();
|
|
187
194
|
const { isExtraSmall } = useBreakpoints();
|
|
188
195
|
const { getColors } = useColors();
|
|
189
196
|
const { slots } = useSlots();
|
|
190
197
|
|
|
191
198
|
const leafletContainer = ref<HTMLElement>();
|
|
192
|
-
const locationGroupBounds = ref<LatLngBounds>();
|
|
193
|
-
const areaGroupBounds = ref<LatLngBounds>();
|
|
194
199
|
const gpsPosition : Ref<LatLng | null> = ref(null);
|
|
195
200
|
const map: Ref<L.Map | null> = ref(null);
|
|
196
201
|
const overlayHeight = ref<number>();
|
|
@@ -198,7 +203,6 @@ export default defineComponent({
|
|
|
198
203
|
|
|
199
204
|
provide('map', map);
|
|
200
205
|
|
|
201
|
-
const defaultZoom = ref(props.dirtyZoom);
|
|
202
206
|
const mapResizeObserver = new ResizeObserver(() => {
|
|
203
207
|
if(!map.value) {
|
|
204
208
|
return;
|
|
@@ -206,53 +210,6 @@ export default defineComponent({
|
|
|
206
210
|
map.value.invalidateSize();
|
|
207
211
|
});
|
|
208
212
|
|
|
209
|
-
const mapLayers: MapLayer[] = [
|
|
210
|
-
{
|
|
211
|
-
name: MapLayers.Map,
|
|
212
|
-
label: $tr("ui.map-layer.map", "Map"),
|
|
213
|
-
image: new URL("../../assets/images/map/map.png", import.meta.url).href,
|
|
214
|
-
layers: [
|
|
215
|
-
tileLayer(`https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
|
|
216
|
-
maxZoom: 22,
|
|
217
|
-
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
218
|
-
attribution: '© Google Map Data',
|
|
219
|
-
className: 'fs-map-tile-base-layer'
|
|
220
|
-
})
|
|
221
|
-
]
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
name: MapLayers.Imagery,
|
|
225
|
-
label: $tr("ui.map-layer.imagery", "Imagery"),
|
|
226
|
-
image: new URL("../../assets/images/map/imagery.png", import.meta.url).href,
|
|
227
|
-
layers: [
|
|
228
|
-
tileLayer(`https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
|
|
229
|
-
maxZoom: 22,
|
|
230
|
-
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
231
|
-
attribution: '© Google Map Data',
|
|
232
|
-
className: 'fs-map-tile-base-layer'
|
|
233
|
-
})
|
|
234
|
-
]
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
name: MapLayers.Snow,
|
|
238
|
-
label: $tr("ui.map-layer.snow", "Snow ski map"),
|
|
239
|
-
image: new URL("../../assets/images/map/snow.png", import.meta.url).href,
|
|
240
|
-
layers: [
|
|
241
|
-
tileLayer(`https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
|
|
242
|
-
maxZoom: 22,
|
|
243
|
-
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
244
|
-
attribution: '© Google Map Data',
|
|
245
|
-
className: 'fs-map-tile-base-layer fs-map-tile-grayscale-layer'
|
|
246
|
-
}),
|
|
247
|
-
tileLayer(`https://tiles.opensnowmap.org/pistes/{z}/{x}/{y}.png`, {
|
|
248
|
-
maxZoom: 18,
|
|
249
|
-
attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors & ODbL, © <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
|
250
|
-
className: 'fs-map-tile-base-layer'
|
|
251
|
-
})
|
|
252
|
-
]
|
|
253
|
-
}
|
|
254
|
-
];
|
|
255
|
-
|
|
256
213
|
const bottomOffset = computed(() => {
|
|
257
214
|
if (props.overlayMode !== MapOverlayPositions.Expand && overlayHeight.value && isExtraSmall.value) {
|
|
258
215
|
return overlayHeight.value;
|
|
@@ -276,7 +233,7 @@ export default defineComponent({
|
|
|
276
233
|
}));
|
|
277
234
|
|
|
278
235
|
const actualLayer = computed(() => {
|
|
279
|
-
return
|
|
236
|
+
return layers.find((mapLayer) => mapLayer.name === props.currentLayer)?.layers ?? layers[0].layers;
|
|
280
237
|
});
|
|
281
238
|
|
|
282
239
|
const overlaySlots = computed(() => {
|
|
@@ -295,7 +252,7 @@ export default defineComponent({
|
|
|
295
252
|
return map.value.unproject(targetPoint, zoom);
|
|
296
253
|
}
|
|
297
254
|
|
|
298
|
-
const flyTo = (lat: number, lng: number, zoom: number
|
|
255
|
+
const flyTo = (lat: number, lng: number, zoom: number, options?: ZoomPanOptions) => {
|
|
299
256
|
if(!map.value) {
|
|
300
257
|
return;
|
|
301
258
|
}
|
|
@@ -316,7 +273,6 @@ export default defineComponent({
|
|
|
316
273
|
} else {
|
|
317
274
|
map.value.flyTo(calculateTargetPosition(latLng(lat, lng), zoom), zoom, options);
|
|
318
275
|
}
|
|
319
|
-
|
|
320
276
|
}
|
|
321
277
|
|
|
322
278
|
const setView = (lat: number, lng: number, zoom: number) => {
|
|
@@ -329,13 +285,13 @@ export default defineComponent({
|
|
|
329
285
|
const fitBounds = (bounds: LatLngBounds, options?: FitBoundsOptions) => {
|
|
330
286
|
if (!map.value) {return;}
|
|
331
287
|
const paddingTopLeft: [number, number] = [
|
|
332
|
-
leftOffset.value,
|
|
333
|
-
|
|
288
|
+
leftOffset.value + 24,
|
|
289
|
+
24
|
|
334
290
|
];
|
|
335
291
|
|
|
336
292
|
const paddingBottomRight: [number, number] = [
|
|
337
|
-
|
|
338
|
-
bottomOffset.value
|
|
293
|
+
24,
|
|
294
|
+
bottomOffset.value + 24
|
|
339
295
|
];
|
|
340
296
|
const paddingOptions = {
|
|
341
297
|
paddingTopLeft,
|
|
@@ -353,14 +309,16 @@ export default defineComponent({
|
|
|
353
309
|
|
|
354
310
|
const mapOptions = {
|
|
355
311
|
zoomControl: false,
|
|
356
|
-
scrollWheelZoom: props.enableScrollWheelZoom,
|
|
312
|
+
scrollWheelZoom: props.enableScrollWheelZoom && !props.disabled,
|
|
313
|
+
dragging: !props.disabled,
|
|
314
|
+
doubleClickZoom: false,
|
|
357
315
|
minZoom: 2,
|
|
358
316
|
maxZoom: 22,
|
|
359
317
|
maxBounds: latLngBounds(latLng(-90, -180), latLng(90, 180)),
|
|
360
318
|
maxBoundsViscosity: 1.0,
|
|
361
|
-
zoom:
|
|
319
|
+
zoom: props.zoom,
|
|
362
320
|
center: props.center ? latLng(props.center[0], props.center[1]) : latLng(48.85782, 2.29521)
|
|
363
|
-
};
|
|
321
|
+
} satisfies L.MapOptions;
|
|
364
322
|
|
|
365
323
|
map.value = markRaw(createMap(leafletContainer.value, mapOptions));
|
|
366
324
|
|
|
@@ -368,6 +326,21 @@ export default defineComponent({
|
|
|
368
326
|
emit('click:latlng', e.latlng);
|
|
369
327
|
});
|
|
370
328
|
|
|
329
|
+
map.value.on('zoomend', () => {
|
|
330
|
+
if(!map.value) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
emit('update:zoom', map.value.getZoom());
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
map.value.on('moveend', () => {
|
|
337
|
+
if(!map.value) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const center = map.value.getCenter();
|
|
341
|
+
emit('update:center', [center.lat, center.lng]);
|
|
342
|
+
});
|
|
343
|
+
|
|
371
344
|
map.value.attributionControl.remove();
|
|
372
345
|
control.attribution({ position: 'bottomleft' }).addTo(map.value);
|
|
373
346
|
|
|
@@ -382,7 +355,7 @@ export default defineComponent({
|
|
|
382
355
|
return;
|
|
383
356
|
}
|
|
384
357
|
|
|
385
|
-
flyTo(e.latlng.lat, e.latlng.lng);
|
|
358
|
+
flyTo(e.latlng.lat, e.latlng.lng, 14);
|
|
386
359
|
});
|
|
387
360
|
|
|
388
361
|
mapResizeObserver.observe(leafletContainer.value);
|
|
@@ -392,38 +365,69 @@ export default defineComponent({
|
|
|
392
365
|
mapResizeObserver.disconnect();
|
|
393
366
|
});
|
|
394
367
|
|
|
395
|
-
watch ([() => props.center, () =>
|
|
396
|
-
if(!map.value || !props.center) {
|
|
368
|
+
watch ([() => props.center, () => props.zoom], ([newCenter, newZoom], [oldCenter, oldZoom]) => {
|
|
369
|
+
if(!map.value || !props.center || !newCenter) {
|
|
397
370
|
return;
|
|
398
371
|
}
|
|
399
|
-
|
|
372
|
+
|
|
373
|
+
if(map.value.getZoom() === newZoom && map.value.getCenter().equals(latLng(newCenter[0], newCenter[1]))) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if((newCenter[0] !== oldCenter?.[0] || newCenter[1] !== oldCenter?.[1]) && newZoom !== oldZoom) {
|
|
378
|
+
setView(newCenter[0], newCenter[1], newZoom);
|
|
379
|
+
}
|
|
380
|
+
else if ((newCenter[0] !== oldCenter?.[0] || newCenter[1] !== oldCenter?.[1])) {
|
|
381
|
+
setView(newCenter[0], newCenter[1], map.value.getZoom());
|
|
382
|
+
}
|
|
383
|
+
else if(newZoom !== oldZoom) {
|
|
384
|
+
map.value.setZoom(newZoom);
|
|
385
|
+
}
|
|
400
386
|
}, { immediate: true });
|
|
401
387
|
|
|
402
388
|
watch([() => props.bounds, () => map.value], () => {
|
|
403
389
|
if(!map.value || !props.bounds) {
|
|
404
390
|
return;
|
|
405
391
|
}
|
|
406
|
-
|
|
392
|
+
|
|
393
|
+
//console.log("Bounds changed", props.bounds);
|
|
394
|
+
fitBounds(props.bounds, { maxZoom: 14 });
|
|
407
395
|
});
|
|
408
396
|
|
|
409
|
-
watch(() => props.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
397
|
+
watch(() => props.enableScrollWheelZoom, (newValue) => {
|
|
398
|
+
if(!map.value) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if(newValue) {
|
|
402
|
+
map.value.scrollWheelZoom.enable();
|
|
403
|
+
} else {
|
|
404
|
+
map.value.scrollWheelZoom.disable();
|
|
405
|
+
}
|
|
406
|
+
}, { immediate: true });
|
|
407
|
+
|
|
408
|
+
watch(() => props.disabled, (newValue) => {
|
|
409
|
+
if(!map.value) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if(newValue) {
|
|
413
|
+
map.value.dragging.disable();
|
|
414
|
+
map.value.scrollWheelZoom.disable();
|
|
415
|
+
} else {
|
|
416
|
+
map.value.dragging.enable();
|
|
417
|
+
if(props.enableScrollWheelZoom) {
|
|
418
|
+
map.value.scrollWheelZoom.enable();
|
|
419
|
+
}
|
|
413
420
|
}
|
|
414
421
|
}, { immediate: true });
|
|
415
422
|
|
|
416
423
|
return {
|
|
417
424
|
ColorEnum,
|
|
418
|
-
defaultZoom,
|
|
419
425
|
leafletContainer,
|
|
420
|
-
locationGroupBounds,
|
|
421
426
|
overlayHeight,
|
|
422
427
|
overlayWidth,
|
|
423
|
-
areaGroupBounds,
|
|
424
428
|
map,
|
|
425
429
|
actualLayer,
|
|
426
|
-
|
|
430
|
+
layers,
|
|
427
431
|
gpsPosition,
|
|
428
432
|
style,
|
|
429
433
|
overlaySlots
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { inject, provide, ref, type Ref } from 'vue';
|
|
6
|
+
import { inject, provide, ref, type Ref, onUnmounted } from 'vue';
|
|
7
7
|
|
|
8
8
|
import { type Map, FeatureGroup } from 'leaflet';
|
|
9
9
|
import { MAP } from './keys';
|
|
@@ -46,6 +46,12 @@ export default {
|
|
|
46
46
|
emit("update:bounds", featureGroup.value.getBounds());
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
onUnmounted(() => {
|
|
51
|
+
if (map.value && map.value.hasLayer(featureGroup.value as unknown as FeatureGroup)) {
|
|
52
|
+
map.value.removeLayer(featureGroup.value as unknown as FeatureGroup);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
49
55
|
}
|
|
50
56
|
};
|
|
51
57
|
</script>
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { inject, type PropType,
|
|
6
|
+
import { inject, type PropType, type Ref, watch, ref, onUnmounted, onMounted } from 'vue';
|
|
7
7
|
|
|
8
|
-
import { type Map,
|
|
8
|
+
import { type Map, divIcon, type LatLng, marker, type Marker, type MarkerClusterGroup, type LeafletMouseEvent } from 'leaflet';
|
|
9
9
|
|
|
10
10
|
import { useColors } from "../../composables";
|
|
11
11
|
|
|
@@ -49,72 +49,90 @@ export default {
|
|
|
49
49
|
const markerClusterGroup = inject<Ref<MarkerClusterGroup | null>>(MARKERCLUSTERGROUP, ref(null));
|
|
50
50
|
|
|
51
51
|
const { getColors } = useColors();
|
|
52
|
-
|
|
53
|
-
const lastMarker = ref<Marker | null>(null);
|
|
54
|
-
|
|
55
|
-
if(!map) {
|
|
56
|
-
throw new Error('FSMapTileLayer must be used inside a FSMap component');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if(!map.value) {
|
|
60
|
-
throw new Error('FSMapTileLayer must be used inside a FSMap component with a map');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const updateMarker = () => {
|
|
64
|
-
if(!map.value || !props.latlng) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if(lastMarker.value) {
|
|
69
|
-
if(markerClusterGroup && markerClusterGroup.value) {
|
|
70
|
-
markerClusterGroup.value.removeLayer(lastMarker.value as Marker);
|
|
71
|
-
} else {
|
|
72
|
-
map.value.removeLayer(lastMarker.value as Marker);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let icon: DivIcon | null = null;
|
|
52
|
+
const getMarkerIcon = () => {
|
|
77
53
|
if(props.variant === 'gps') {
|
|
78
54
|
const size = 16;
|
|
79
|
-
|
|
55
|
+
return divIcon({
|
|
80
56
|
html: gpsMarkerHtml(),
|
|
81
57
|
className: 'fs-map-mylocation',
|
|
82
58
|
iconSize: [size, size],
|
|
83
59
|
iconAnchor: [size / 2, size / 2],
|
|
84
60
|
});
|
|
85
|
-
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if(props.variant === 'location') {
|
|
86
64
|
const size = 36;
|
|
87
|
-
|
|
65
|
+
return divIcon({
|
|
88
66
|
html: locationMarkerHtml(props.icon ?? "mdi-map-marker", getColors(props.color).base, props.label),
|
|
89
67
|
iconSize: [size, size],
|
|
90
68
|
className: props.selected ? 'fs-map-marker fs-map-location fs-map-location-selected' : 'fs-map-marker fs-map-location',
|
|
91
69
|
iconAnchor: [size / 2, size / 2],
|
|
92
70
|
});
|
|
93
|
-
} else {
|
|
94
|
-
const size = 16;
|
|
95
|
-
icon = divIcon({
|
|
96
|
-
html: pinMarkerHtml(getColors(props.color).base, props.label),
|
|
97
|
-
iconSize: [size, size],
|
|
98
|
-
className: props.selected ? 'fs-map-marker fs-map-pin fs-map-pin-selected' : 'fs-map-marker fs-map-pin',
|
|
99
|
-
iconAnchor: [size / 2, size / 2],
|
|
100
|
-
});
|
|
101
71
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
72
|
+
|
|
73
|
+
const size = 16;
|
|
74
|
+
return divIcon({
|
|
75
|
+
html: pinMarkerHtml(getColors(props.color).base, props.label),
|
|
76
|
+
iconSize: [size, size],
|
|
77
|
+
className: props.selected ? 'fs-map-marker fs-map-pin fs-map-pin-selected' : 'fs-map-marker fs-map-pin',
|
|
78
|
+
iconAnchor: [size / 2, size / 2],
|
|
106
79
|
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const actualMarker = ref(marker(props.latlng ?? [0, 0], { icon: getMarkerIcon() }));
|
|
83
|
+
|
|
84
|
+
if(!map) {
|
|
85
|
+
throw new Error('FSMapTileLayer must be used inside a FSMap component');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if(!map.value) {
|
|
89
|
+
throw new Error('FSMapTileLayer must be used inside a FSMap component with a map');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
watch(map, () => {
|
|
93
|
+
if(!map.value) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
107
96
|
|
|
108
97
|
if(markerClusterGroup && markerClusterGroup.value) {
|
|
109
|
-
|
|
98
|
+
actualMarker.value.addTo(markerClusterGroup.value);
|
|
110
99
|
} else {
|
|
111
|
-
|
|
100
|
+
actualMarker.value.addTo(map.value);
|
|
112
101
|
}
|
|
113
|
-
};
|
|
102
|
+
}, { immediate: true });
|
|
114
103
|
|
|
115
|
-
|
|
104
|
+
watch([() => props.variant, () => props.color, () => props.selected], () => {
|
|
105
|
+
if(!actualMarker.value || !map.value) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const icon = getMarkerIcon();
|
|
110
|
+
actualMarker.value?.setIcon(icon);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
watch([() => props.latlng?.lat, () => props.latlng?.lng], () => {
|
|
114
|
+
if(!actualMarker.value || !map.value || !props.latlng) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
actualMarker.value.setLatLng(props.latlng);
|
|
119
|
+
});
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
onMounted(() => {
|
|
122
|
+
actualMarker.value.on('click', (event: LeafletMouseEvent) => {
|
|
123
|
+
emit('click', event);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
onUnmounted(() => {
|
|
128
|
+
if(actualMarker.value && map.value) {
|
|
129
|
+
if(markerClusterGroup && markerClusterGroup.value) {
|
|
130
|
+
markerClusterGroup.value.removeLayer(actualMarker.value as Marker);
|
|
131
|
+
} else {
|
|
132
|
+
map.value.removeLayer(actualMarker.value as Marker);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
118
136
|
}
|
|
119
137
|
};
|
|
120
138
|
</script>
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { inject, provide, ref, type Ref } from 'vue';
|
|
6
|
+
import { inject, provide, ref, type Ref, onUnmounted } from 'vue';
|
|
7
7
|
|
|
8
|
-
import { type Map, MarkerClusterGroup, divIcon } from 'leaflet';
|
|
8
|
+
import { LatLngBounds, type Map, MarkerClusterGroup, divIcon } from 'leaflet';
|
|
9
9
|
|
|
10
10
|
import { clusterMarkerHtml } from '../../utils/leafletMarkers';
|
|
11
11
|
import { MAP } from './keys';
|
|
@@ -54,17 +54,42 @@ export default {
|
|
|
54
54
|
|
|
55
55
|
provide('markerClusterGroup', markerClusterGroup);
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
if(!map.value) {
|
|
57
|
+
const handleLayerChange = () => {
|
|
58
|
+
if (!map.value) {
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const layers = markerClusterGroup.value.getLayers();
|
|
63
63
|
|
|
64
|
-
if(layers.length ===
|
|
64
|
+
if (layers.length === 0 && added) {
|
|
65
|
+
map.value.removeLayer(markerClusterGroup.value as unknown as L.Layer);
|
|
66
|
+
added = false;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (layers.length === props.expectedLayers && !added) {
|
|
65
70
|
markerClusterGroup.value.addTo(map.value);
|
|
66
71
|
added = true;
|
|
67
|
-
|
|
72
|
+
}
|
|
73
|
+
if (layers.length === props.expectedLayers) {
|
|
74
|
+
const bounds = new LatLngBounds([]);
|
|
75
|
+
for (const layer of layers as any[]) {
|
|
76
|
+
if (layer.getBounds) {
|
|
77
|
+
bounds.extend(layer.getBounds());
|
|
78
|
+
} else if (layer.getLatLng) {
|
|
79
|
+
bounds.extend(layer.getLatLng());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
emit("update:bounds", layers.length > 0 ? bounds : null);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
markerClusterGroup.value.on("layeradd", handleLayerChange);
|
|
88
|
+
markerClusterGroup.value.on("layerremove", handleLayerChange);
|
|
89
|
+
|
|
90
|
+
onUnmounted(() => {
|
|
91
|
+
if (map.value && map.value.hasLayer(markerClusterGroup.value as unknown as L.Layer)) {
|
|
92
|
+
map.value.removeLayer(markerClusterGroup.value as unknown as L.Layer);
|
|
68
93
|
}
|
|
69
94
|
});
|
|
70
95
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { inject, type PropType, onMounted, type Ref, watch, ref } from 'vue';
|
|
6
|
+
import { inject, type PropType, onMounted, type Ref, watch, ref, onUnmounted } from 'vue';
|
|
7
7
|
|
|
8
8
|
import { type Map, type LatLng, type Polygon, type FeatureGroup, polygon } from 'leaflet';
|
|
9
9
|
|
|
@@ -75,7 +75,19 @@ export default {
|
|
|
75
75
|
|
|
76
76
|
onMounted(updatePolygon);
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
onUnmounted(() => {
|
|
79
|
+
if (lastPolygon.value) {
|
|
80
|
+
if (featureGroup?.value) {
|
|
81
|
+
featureGroup.value.removeLayer(lastPolygon.value);
|
|
82
|
+
} else if (map.value?.hasLayer(lastPolygon.value)) {
|
|
83
|
+
map.value.removeLayer(lastPolygon.value);
|
|
84
|
+
}
|
|
85
|
+
lastPolygon.value = null;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
watch([() => props.color, () => props.latlngs], updatePolygon);
|
|
79
91
|
}
|
|
80
92
|
};
|
|
81
93
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</template>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { inject, type PropType, onMounted, type Ref, watch } from 'vue';
|
|
6
|
+
import { inject, type PropType, onMounted, type Ref, watch, onUnmounted } from 'vue';
|
|
7
7
|
|
|
8
8
|
import type { Map, Layer } from 'leaflet';
|
|
9
9
|
|
|
@@ -50,6 +50,17 @@ export default {
|
|
|
50
50
|
|
|
51
51
|
onMounted(updateLayer);
|
|
52
52
|
|
|
53
|
+
onUnmounted(() => {
|
|
54
|
+
if (lastLayers && map.value) {
|
|
55
|
+
lastLayers.forEach(layer => {
|
|
56
|
+
if (map.value.hasLayer(layer)) {
|
|
57
|
+
map.value.removeLayer(layer);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
lastLayers = [];
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
53
64
|
watch(() => props.layers, updateLayer);
|
|
54
65
|
}
|
|
55
66
|
};
|
|
File without changes
|
package/composables/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { tileLayer } from 'leaflet';
|
|
2
|
+
|
|
3
|
+
import { useTranslations as useTranslationsProvider } from "@dative-gpi/bones-ui/composables";
|
|
4
|
+
|
|
5
|
+
import { MapLayers } from '@dative-gpi/foundation-shared-components/models';
|
|
6
|
+
|
|
7
|
+
export const useMapLayers = () => {
|
|
8
|
+
const { $tr } = useTranslationsProvider();
|
|
9
|
+
|
|
10
|
+
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? "";
|
|
11
|
+
|
|
12
|
+
const layers = [
|
|
13
|
+
{
|
|
14
|
+
name: MapLayers.Map,
|
|
15
|
+
label: $tr("ui.map-layer.map", "Map"),
|
|
16
|
+
image: new URL("../../assets/images/map/map.png", import.meta.url).href,
|
|
17
|
+
layers: [
|
|
18
|
+
tileLayer(`https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${apiKey}`, {
|
|
19
|
+
maxZoom: 22,
|
|
20
|
+
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
21
|
+
attribution: '© Google Map Data',
|
|
22
|
+
className: 'fs-map-tile-base-layer'
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: MapLayers.Imagery,
|
|
28
|
+
label: $tr("ui.map-layer.imagery", "Imagery"),
|
|
29
|
+
image: new URL("../../assets/images/map/imagery.png", import.meta.url).href,
|
|
30
|
+
layers: [
|
|
31
|
+
tileLayer(`https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&key=${apiKey}`, {
|
|
32
|
+
maxZoom: 22,
|
|
33
|
+
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
34
|
+
attribution: '© Google Map Data',
|
|
35
|
+
className: 'fs-map-tile-base-layer'
|
|
36
|
+
})
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: MapLayers.Snow,
|
|
41
|
+
label: $tr("ui.map-layer.snow", "Snow ski map"),
|
|
42
|
+
image: new URL("../../assets/images/map/snow.png", import.meta.url).href,
|
|
43
|
+
layers: [
|
|
44
|
+
tileLayer(`https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${apiKey}`, {
|
|
45
|
+
maxZoom: 22,
|
|
46
|
+
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
47
|
+
attribution: '© Google Map Data',
|
|
48
|
+
className: 'fs-map-tile-base-layer fs-map-tile-grayscale-layer'
|
|
49
|
+
}),
|
|
50
|
+
tileLayer(`https://tiles.opensnowmap.org/pistes/{z}/{x}/{y}.png`, {
|
|
51
|
+
maxZoom: 18,
|
|
52
|
+
attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors & ODbL, © <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
|
53
|
+
className: 'fs-map-tile-base-layer'
|
|
54
|
+
})
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
layers
|
|
61
|
+
};
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dative-gpi/foundation-shared-components",
|
|
3
3
|
"sideEffects": false,
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.156-fix-alert-tools",
|
|
5
5
|
"description": "",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"author": "",
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@dative-gpi/foundation-shared-domain": "1.0.
|
|
14
|
-
"@dative-gpi/foundation-shared-services": "1.0.
|
|
13
|
+
"@dative-gpi/foundation-shared-domain": "1.0.156-fix-alert-tools",
|
|
14
|
+
"@dative-gpi/foundation-shared-services": "1.0.156-fix-alert-tools"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@dative-gpi/bones-ui": "^1.0.0",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"sass": "1.71.1",
|
|
36
36
|
"sass-loader": "13.3.2"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "e142d2893cf730a0b78d2c42eee5f9ec06239155"
|
|
39
39
|
}
|
package/tools/alertsTools.ts
CHANGED
|
@@ -21,19 +21,19 @@ export const AlertTools = {
|
|
|
21
21
|
statusLabel(value: AlertStatus): string {
|
|
22
22
|
switch (value) {
|
|
23
23
|
case AlertStatus.Pending:
|
|
24
|
-
return $tr('ui.alert-status.pending','Pending');
|
|
24
|
+
return $tr('ui.alert-status.pending', 'Pending');
|
|
25
25
|
case AlertStatus.Untriggered:
|
|
26
|
-
return $tr('ui.alert-status.untriggered','Untriggered');
|
|
26
|
+
return $tr('ui.alert-status.untriggered', 'Untriggered');
|
|
27
27
|
case AlertStatus.Unresolved:
|
|
28
|
-
return $tr('ui.alert-status.unresolved','Unresolved');
|
|
28
|
+
return $tr('ui.alert-status.unresolved', 'Unresolved');
|
|
29
29
|
case AlertStatus.Resolved:
|
|
30
|
-
return $tr('ui.alert-status.resolved','Resolved');
|
|
30
|
+
return $tr('ui.alert-status.resolved', 'Resolved');
|
|
31
31
|
case AlertStatus.Expired:
|
|
32
|
-
return $tr('ui.alert-status.expired','Expired');
|
|
32
|
+
return $tr('ui.alert-status.expired', 'Expired');
|
|
33
33
|
case AlertStatus.Triggered:
|
|
34
|
-
return $tr('ui.alert-status.triggered','Triggered');
|
|
34
|
+
return $tr('ui.alert-status.triggered', 'Triggered');
|
|
35
35
|
case AlertStatus.Abandoned:
|
|
36
|
-
return $tr('ui.alert-status.abandoned','Abandoned');
|
|
36
|
+
return $tr('ui.alert-status.abandoned', 'Abandoned');
|
|
37
37
|
default: return "";
|
|
38
38
|
}
|
|
39
39
|
},
|
|
@@ -74,11 +74,43 @@ export const AlertTools = {
|
|
|
74
74
|
},
|
|
75
75
|
criticityLabel(value: Criticity): string {
|
|
76
76
|
switch (value) {
|
|
77
|
-
case Criticity.Warning: return $tr('ui.common.warning','Warning');
|
|
78
|
-
case Criticity.Error: return $tr('ui.common.error','Error');
|
|
79
|
-
default: return $tr('ui.common.information','Information');
|
|
77
|
+
case Criticity.Warning: return $tr('ui.common.warning', 'Warning');
|
|
78
|
+
case Criticity.Error: return $tr('ui.common.error', 'Error');
|
|
79
|
+
default: return $tr('ui.common.information', 'Information');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
statusColor(status: AlertStatus): ColorEnum {
|
|
83
|
+
switch (status) {
|
|
84
|
+
case AlertStatus.None:
|
|
85
|
+
case AlertStatus.Pending:
|
|
86
|
+
case AlertStatus.Expired:
|
|
87
|
+
return ColorEnum.Warning;
|
|
88
|
+
case AlertStatus.Unresolved:
|
|
89
|
+
case AlertStatus.Triggered:
|
|
90
|
+
return ColorEnum.Error;
|
|
91
|
+
case AlertStatus.Resolved:
|
|
92
|
+
case AlertStatus.Untriggered:
|
|
93
|
+
return ColorEnum.Success;
|
|
94
|
+
default:
|
|
95
|
+
return ColorEnum.Warning;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
formatValue(value: string) {
|
|
100
|
+
const n = parseFloat(value);
|
|
101
|
+
|
|
102
|
+
if (isNaN(n)) {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Number.isInteger(n)) {
|
|
107
|
+
return n;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return n.toFixed(2);
|
|
80
111
|
}
|
|
81
112
|
}
|
|
113
|
+
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
export const prettyDuration = (n: number | undefined) => {
|
package/utils/operations.ts
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
const MinusOperator = "-";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Splits an expression by operators while keeping unary minus signs.
|
|
5
|
+
*/
|
|
6
|
+
const splitByOperators = (expression: string): string[] => {
|
|
7
|
+
const tokens: string[] = [];
|
|
8
|
+
let current = '';
|
|
9
|
+
let expectOperand = true;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < expression.length; i++) {
|
|
12
|
+
const char = expression[i];
|
|
13
|
+
if ('+-*/'.includes(char)) {
|
|
14
|
+
const isUnaryMinus = char === '-' && expectOperand;
|
|
15
|
+
if (isUnaryMinus) {
|
|
16
|
+
current += char;
|
|
17
|
+
expectOperand = true;
|
|
18
|
+
} else {
|
|
19
|
+
if (expectOperand) {
|
|
20
|
+
// 2 consecutive operators or operator at the start
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
if (current !== '') {
|
|
24
|
+
tokens.push(current);
|
|
25
|
+
current = '';
|
|
26
|
+
}
|
|
27
|
+
tokens.push(char);
|
|
28
|
+
expectOperand = true;
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
current += char;
|
|
32
|
+
expectOperand = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (current !== '') {
|
|
36
|
+
tokens.push(current);
|
|
37
|
+
}
|
|
38
|
+
return tokens;
|
|
39
|
+
};
|
|
5
40
|
|
|
6
41
|
// Matches a nested block of parenthesis
|
|
7
42
|
const parenthesisRegex = new RegExp(/\([^)(]+\)/gm);
|
|
@@ -14,7 +49,11 @@ const validateBlock = (block: string, operands: string[] = [], variables: string
|
|
|
14
49
|
block = block.replaceAll("(", "").replaceAll(")", "");
|
|
15
50
|
|
|
16
51
|
// Split block on operators (Leave negative signs)
|
|
17
|
-
const
|
|
52
|
+
const tokens = splitByOperators(block);
|
|
53
|
+
if (tokens.length === 0) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const components = tokens.filter(token => !'+-*/'.includes(token));
|
|
18
57
|
|
|
19
58
|
// Check if each bit is a valid operand
|
|
20
59
|
for (let i = 0; i < components.length; i++) {
|