@dative-gpi/foundation-shared-components 1.0.33 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/components/FSClickable.vue +9 -0
  2. package/components/FSDialogFormBody.vue +4 -4
  3. package/components/FSDialogMultiFormBody.vue +3 -3
  4. package/components/FSDialogSubmit.vue +3 -3
  5. package/components/FSFadeOut.vue +10 -3
  6. package/components/fields/FSAutocompleteField.vue +3 -3
  7. package/components/fields/FSSelectField.vue +3 -3
  8. package/components/fields/FSTreeViewField.vue +3 -3
  9. package/components/fields/periodicField/FSPeriodicDailyField.vue +17 -10
  10. package/components/fields/periodicField/FSPeriodicMonthlyField.vue +29 -15
  11. package/components/fields/periodicField/FSPeriodicWeeklyField.vue +13 -7
  12. package/components/fields/periodicField/FSPeriodicYearlyField.vue +19 -10
  13. package/components/lists/FSFilterButton.vue +19 -20
  14. package/components/lists/FSHiddenButton.vue +10 -12
  15. package/components/map/FSMap.vue +240 -399
  16. package/components/map/FSMapFeatureGroup.vue +51 -0
  17. package/components/map/FSMapLayerButton.vue +2 -2
  18. package/components/map/FSMapMarker.vue +116 -0
  19. package/components/map/FSMapMarkerClusterGroup.vue +67 -0
  20. package/components/map/FSMapOverlay.vue +69 -83
  21. package/components/map/FSMapPolygon.vue +81 -0
  22. package/components/map/FSMapTileLayer.vue +50 -0
  23. package/components/map/keys.ts +4 -0
  24. package/package.json +4 -4
  25. package/styles/components/fs_card.scss +0 -1
  26. package/styles/components/fs_clickable.scss +1 -1
  27. package/styles/components/fs_fade_out.scss +2 -1
  28. package/styles/components/fs_map.scss +36 -30
  29. package/styles/components/fs_tabs.scss +4 -0
  30. package/styles/components/index.scss +0 -1
  31. package/utils/leafletMarkers.ts +8 -2
  32. package/components/map/FSMapEditPointAddressOverlay.vue +0 -164
  33. package/styles/components/fs_map_overlay.scss +0 -38
@@ -1,162 +1,158 @@
1
1
  <template>
2
2
  <FSCard
3
+ class="fs-map"
3
4
  :width="$props.width"
5
+ :height="$props.height"
4
6
  :style="style"
5
7
  v-bind="$attrs"
6
8
  >
7
- <FSCol
8
- v-if="L"
9
- width="fill"
10
- :class="['fs-map', { 'fs-map-fullscreen': fullScreen }]"
9
+ <div
10
+ ref="leafletContainer"
11
+ class="fs-leaflet-container"
11
12
  >
12
- <FSMapOverlay
13
- v-if="$slots['leftoverlay-header'] || $slots['leftoverlay-body']"
14
- :mode="$props.overlayMode"
15
- :height="$props.height"
16
- :mapId="mapId"
17
- @update:mode="$emit('update:overlayMode', $event)"
13
+ <template
14
+ v-if="map"
18
15
  >
19
- <template
20
- v-slot:leftoverlay-header
16
+ <FSMapTileLayer
17
+ :layer="actualLayer"
18
+ />
19
+ <FSMapMarker
20
+ v-if="gpsPosition"
21
+ variant="gps"
22
+ :color="ColorEnum.Primary"
23
+ :latlng="gpsPosition"
24
+ />
25
+
26
+ <FSMapFeatureGroup
27
+ v-if="$props.areas"
28
+ :expected-layers="$props.areas.length"
29
+ @update:bounds="(bounds) => areaGroupBounds = bounds"
21
30
  >
22
- <slot
23
- name="leftoverlay-header"
31
+ <FSMapPolygon
32
+ v-for="area in areas"
33
+ :key="area.id"
34
+ :color="area.color"
35
+ :latlngs="area.coordinates.map((coord) => ({lat: coord.latitude, lng: coord.longitude}))"
36
+ @click="$emit('update:selectedAreaId', area.id)"
24
37
  />
25
- </template>
26
- <template
27
- v-slot:leftoverlay-body
38
+ </FSMapFeatureGroup>
39
+
40
+ <FSMapMarkerClusterGroup
41
+ v-if="$props.locations"
42
+ :expected-layers="$props.locations.length"
43
+ @update:bounds="(bounds) => locationGroupBounds = bounds"
28
44
  >
29
- <slot
30
- name="leftoverlay-body"
45
+ <FSMapMarker
46
+ v-for="location in $props.locations"
47
+ :selected="location.id === $props.selectedLocationId"
48
+ :key="location.id"
49
+ :color="location.color"
50
+ :icon="location.icon"
51
+ :latlng="{lat: location.address.latitude, lng: location.address.longitude}"
52
+ @click="$emit('update:selectedLocationId', location.id)"
31
53
  />
32
- </template>
33
- </FSMapOverlay>
34
- <FSRow
35
- v-if="$props.editable && !editingLocation && $props.selectedLocationId !== null"
36
- class="fs-map-overlay-edit-button"
37
- >
38
- <FSButton
39
- prependIcon="mdi-pencil-outline"
40
- :label="$tr('ui.map.modify', 'Modify')"
41
- @click="editingLocation = true"
42
- />
43
- </FSRow>
44
- <FSCol>
45
- <div
46
- class="fs-leaflet-container"
47
- :id="mapId"
48
- />
49
- </FSCol>
50
- <FSCol
51
- class="fs-map-overlay-right-top"
52
- align="center-center"
54
+ </FSMapMarkerClusterGroup>
55
+ </template>
56
+ </div>
57
+
58
+ <FSMapLayerButton
59
+ v-if="$props.allowedLayers?.length && $props.allowedLayers.length > 1"
60
+ :layers="mapLayers.filter((layer) => $props.allowedLayers?.includes(layer.name) ?? true)"
61
+ :modelValue="$props.currentLayer"
62
+ @update:model-value="$emit('update:currentLayer', $event)"
63
+ />
64
+
65
+ <FSCol
66
+ v-if="map"
67
+ class="fs-map-control-buttons"
68
+ >
69
+ <FSButton
70
+ v-if="$props.showMyLocation"
71
+ icon="mdi-crosshairs-gps"
72
+ color="primary"
73
+ variant="full"
74
+ :elevation="true"
75
+ @click="() => map!.locate()"
76
+ />
77
+ <FSCard
78
+ v-if="$props.showZoomButtons"
79
+ :elevation="true"
53
80
  >
54
- <slot
55
- name="toprightoverlay"
81
+ <FSCol
82
+ gap="0"
56
83
  >
57
- <FSRow
58
- gap="2px"
59
- >
60
- <FSMapLayerButton
61
- v-if="$props.selectableLayers?.length && $props.selectableLayers.length > 1"
62
- :layers="mapLayers.filter((layer) => $props.selectableLayers?.includes(layer.name) ?? true)"
63
- v-model="innerSelectedLayer"
64
- />
65
- <FSButton
66
- v-if="$props.showFullScreen"
67
- prependIcon="mdi-fullscreen"
68
- padding="0 7px"
69
- :elevation="true"
70
- @click="fullScreen = !fullScreen"
71
- />
72
- </FSRow>
73
- </slot>
74
- </FSCol>
75
- <FSCol
76
- class="fs-map-overlay-right-bottom"
77
- align="center-center"
84
+ <FSButton
85
+ class="fs-map-zoom-plus-button"
86
+ icon="mdi-plus"
87
+ @click="() => map!.zoomIn()"
88
+ :border="false"
89
+ />
90
+ <FSButton
91
+ class="fs-map-zoom-minus-button"
92
+ icon="mdi-minus"
93
+ @click="() => map!.zoomOut()"
94
+ :border="false"
95
+ />
96
+ </FSCol>
97
+ </FSCard>
98
+ </FSCol>
99
+
100
+ <FSMapOverlay
101
+ v-if="$slots['overlay']"
102
+ :mode="$props.overlayMode"
103
+ @update:mode="$emit('update:overlayMode', $event)"
104
+ @update:height="(height) => overlayHeight = height"
105
+ @update:width="(width) => overlayWidth = width"
106
+ >
107
+ <template
108
+ #body
78
109
  >
79
110
  <slot
80
- name="bottomrightoverlay"
81
- >
82
- <FSCol
83
- class="fs-map-zoom-overlay"
84
- align="bottom-center"
85
- width="hug"
86
- >
87
- <FSButton
88
- v-if="$props.showMyLocation"
89
- prependIcon="mdi-crosshairs-gps"
90
- padding="0 7px"
91
- color="primary"
92
- variant="full"
93
- :elevation="true"
94
- @click="locate"
95
- />
96
- <FSCol
97
- v-if="$props.showZoomButtons"
98
- gap="0"
99
- >
100
-
101
- <FSButton
102
- prependIcon="mdi-plus"
103
- padding="0 7px"
104
- :elevation="true"
105
- @click="zoomIn"
106
- />
107
- <FSButton
108
- prependIcon="mdi-minus"
109
- padding="0 7px"
110
- :elevation="true"
111
- @click="zoomOut"
112
- />
113
- </FSCol>
114
- </FSCol>
115
- </slot>
116
- <FSMapEditPointAddressOverlay
117
- v-if="editingLocation"
118
- :label="$tr('ui.map.address', 'Address')"
119
- :modelValue="(innerModelValue.find((loc) => loc.id === $props.selectedLocationId))?.address"
120
- @update:locationCoordinates="($event: Address) => onNewCoordEntered($event.latitude, $event.longitude)"
121
- @update:modelValue="($event: Address) => onNewAddressEntered($event)"
122
- @cancel="onCancel"
123
- @submit="onSubmit"
111
+ name="overlay"
124
112
  />
125
- </FSCol>
126
- </FSCol>
113
+ </template>
114
+ </FSMapOverlay>
127
115
  </FSCard>
128
116
  </template>
129
117
 
130
118
  <script lang="ts">
131
- import { computed, defineComponent, onMounted, onUnmounted, type PropType, ref, type StyleValue, watch } from "vue";
132
- import "leaflet.markercluster";
133
- import * as L from "leaflet";
119
+ import { computed, defineComponent, onMounted, type Ref, provide, type PropType, ref, type StyleValue, watch } from "vue";
120
+
121
+ import type {} from "leaflet.markercluster";
122
+ import { map as createMap, control, tileLayer, latLngBounds, latLng, type LatLng, LatLngBounds, type FitBoundsOptions } from "leaflet";
134
123
 
135
124
  import { useTranslations as useTranslationsProvider } from "@dative-gpi/bones-ui/composables";
136
- import { type Address, type FSArea } from '@dative-gpi/foundation-shared-domain/models';
125
+ import { type FSArea } from '@dative-gpi/foundation-shared-domain/models';
137
126
 
138
- import { clusterMarkerHtml, locationMarkerHtml, myLocationMarkerHtml } from "../../utils";
139
- import { useColors, useAddress, useBreakpoints } from "../../composables";
127
+ import { useBreakpoints, useColors } from "../../composables";
140
128
  import { ColorEnum, type FSLocation, type MapLayer } from "../../models";
141
129
 
142
- import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
143
130
  import FSMapLayerButton from "./FSMapLayerButton.vue";
144
131
  import FSMapOverlay from "./FSMapOverlay.vue";
145
132
  import FSButton from "../FSButton.vue";
146
133
  import FSCard from "../FSCard.vue";
147
134
  import FSCol from "../FSCol.vue";
148
- import FSRow from "../FSRow.vue";
135
+
136
+ import FSMapMarker from "./FSMapMarker.vue";
137
+ import FSMapTileLayer from "./FSMapTileLayer.vue";
138
+ import FSMapFeatureGroup from "./FSMapFeatureGroup.vue";
139
+ import FSMapMarkerClusterGroup from "./FSMapMarkerClusterGroup.vue";
140
+ import FSMapPolygon from "./FSMapPolygon.vue";
149
141
 
150
142
  export default defineComponent({
151
143
  name: "FSMap",
152
144
  components: {
153
- FSMapEditPointAddressOverlay,
145
+ FSMapMarker,
146
+ FSMapTileLayer,
147
+ FSMapFeatureGroup,
148
+ FSMapMarkerClusterGroup,
149
+ FSMapPolygon,
150
+
154
151
  FSMapLayerButton,
155
152
  FSMapOverlay,
156
153
  FSButton,
157
154
  FSCard,
158
155
  FSCol,
159
- FSRow
160
156
  },
161
157
  props: {
162
158
  height: {
@@ -174,11 +170,6 @@ export default defineComponent({
174
170
  required: false,
175
171
  default: false
176
172
  },
177
- editable: {
178
- type: Boolean,
179
- required: false,
180
- default: false
181
- },
182
173
  overlayMode: {
183
174
  type: String as PropType<'collapse' | 'half' | 'expand'>,
184
175
  required: false,
@@ -194,11 +185,6 @@ export default defineComponent({
194
185
  required: false,
195
186
  default: true
196
187
  },
197
- showFullScreen: {
198
- type: Boolean,
199
- required: false,
200
- default: false
201
- },
202
188
  enableScrollWheelZoom: {
203
189
  type: Boolean,
204
190
  required: false,
@@ -209,7 +195,7 @@ export default defineComponent({
209
195
  required: false,
210
196
  default: () => [45.71, 5.07]
211
197
  },
212
- modelValue: {
198
+ locations: {
213
199
  type: Array as PropType<FSLocation[]>,
214
200
  required: false,
215
201
  default: () => [],
@@ -219,12 +205,12 @@ export default defineComponent({
219
205
  required: false,
220
206
  default: () => [],
221
207
  },
222
- selectedLayer: {
208
+ currentLayer: {
223
209
  type: String as PropType<"map" | "imagery">,
224
210
  required: false,
225
211
  default: "map"
226
212
  },
227
- selectableLayers: {
213
+ allowedLayers: {
228
214
  type: Array as PropType<string[]>,
229
215
  required: false,
230
216
  default: () => ["map", "imagery"]
@@ -240,40 +226,30 @@ export default defineComponent({
240
226
  default: null
241
227
  }
242
228
  },
243
- emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId", 'update:overlayMode'],
229
+ emits: ["update:modelValue", "update:selectedLocationId", "update:selectedAreaId", 'update:overlayMode', 'update:currentLayer', "click:latlng"],
244
230
  setup(props, { emit }) {
245
231
  const { $tr } = useTranslationsProvider();
246
- const { reverseSearch } = useAddress();
247
232
  const { getColors } = useColors();
248
233
  const { isExtraSmall } = useBreakpoints();
249
234
 
250
- const LL = window.L;
235
+ const leafletContainer = ref<HTMLElement>();
236
+ const locationGroupBounds = ref<LatLngBounds>();
237
+ const areaGroupBounds = ref<LatLngBounds>();
238
+ const gpsPosition : Ref<LatLng | null> = ref(null);
239
+ const map: Ref<L.Map | null> = ref(null);
240
+ const overlayHeight = ref<number>();
241
+ const overlayWidth = ref<number>();
251
242
 
252
- const innerSelectedLayer = ref(props.selectedLayer);
253
- const innerModelValue = ref(props.modelValue);
254
- const editingLocation = ref(false);
255
- const fullScreen = ref(false);
256
- const leftOverlayHeight = ref<number>();
257
- const leftOverlayWidth = ref<number>();
258
- const resizeObserver = ref<ResizeObserver | null>(null);
259
-
260
- const mapId = `map-${Math.random().toString(36).substring(7)}`;
261
243
  const defaultZoom = 15;
262
- const markers: { [key: string]: L.Marker } = {};
263
- const areas: { [key: string]: L.Polygon } = {};
264
- const areaLayerGroup = new LL.FeatureGroup();
265
- const baseLayerGroup = new LL.LayerGroup();
266
- const myLocationLayerGroup = new LL.LayerGroup();
267
-
268
- let map: L.Map;
269
- let markerLayerGroup: L.FeatureGroup | any;
244
+
245
+ provide('map', map);
270
246
 
271
247
  const mapLayers: MapLayer[] = [
272
248
  {
273
249
  name: "map",
274
250
  label: $tr("ui.map.layer.map", "Map"),
275
251
  image: new URL("../../assets/images/map/map.png", import.meta.url).href,
276
- layer: LL.tileLayer(`http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
252
+ layer: tileLayer(`http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
277
253
  maxZoom: 22,
278
254
  subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
279
255
  attribution: '© Google Map Data'
@@ -283,7 +259,7 @@ export default defineComponent({
283
259
  name: "imagery",
284
260
  label: $tr("ui.map.layer.imagery", "Imagery"),
285
261
  image: new URL("../../assets/images/map/imagery.png", import.meta.url).href,
286
- layer: LL.tileLayer(`http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
262
+ layer: tileLayer(`http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ""}`, {
287
263
  maxZoom: 22,
288
264
  subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
289
265
  attribution: '© Google Map Data'
@@ -291,303 +267,168 @@ export default defineComponent({
291
267
  }
292
268
  ];
293
269
 
294
- const bottomMargin = computed(() => {
295
- let margin = 0;
296
- if (props.overlayMode !== 'expand' && leftOverlayHeight.value && isExtraSmall.value) {
297
- margin += leftOverlayHeight.value;
270
+ const bottomOffset = computed(() => {
271
+ if (props.overlayMode !== 'expand' && overlayHeight.value && isExtraSmall.value) {
272
+ return overlayHeight.value;
298
273
  }
299
- return margin;
274
+ return 0;
300
275
  });
301
276
 
302
- const leftMargin = computed(() => {
303
- let margin = 0;
304
- if (leftOverlayWidth.value && !isExtraSmall.value) {
305
- margin += leftOverlayWidth.value;
277
+ const leftOffset = computed(() => {
278
+ if (overlayWidth.value && !isExtraSmall.value) {
279
+ return overlayWidth.value;
306
280
  }
307
- return margin;
281
+ return 0;
308
282
  });
309
283
 
310
284
  const style = computed((): StyleValue => ({
311
285
  "--fs-map-location-pin-color": getColors(ColorEnum.Primary).base,
312
286
  "--fs-map-mylocation-pin-color": getColors(ColorEnum.Primary).base,
313
287
  "--fs-map-mylocation-pin-color-alpha": getColors(ColorEnum.Primary).base + "50",
314
- "--fs-map-leaflet-container-height": props.height as string,
315
- "--fs-map-leaflet-bottom-overlay-margin": `${bottomMargin.value}px`,
316
- "--fs-map-container-grayscale": props.grayscale ? '0.9' : '0'
288
+ "--fs-map-container-grayscale": props.grayscale ? '0.9' : '0',
289
+ "--fs-map-control-buttons-margin-bottom": `${bottomOffset.value}px`,
317
290
  }));
318
291
 
319
- const displayLocations = () => {
320
- markerLayerGroup.clearLayers();
321
- innerModelValue.value.forEach((location) => {
322
- const size = 36;
323
- const icon = L.divIcon({
324
- html: locationMarkerHtml(location.icon, getColors(location.color).base),
325
- iconSize: [size, size],
326
- className: 'fs-map-location',
327
- iconAnchor: [size / 2, size / 2],
328
- });
329
- const marker = LL.marker([location.address.latitude, location.address.longitude], { icon }).addTo(markerLayerGroup);
330
- markers[location.id] = marker;
331
- marker.on('click', () => emit('update:selectedLocationId', location.id));
332
- });
333
- };
292
+ const actualLayer = computed(() => {
293
+ return mapLayers.find((layer) => layer.name === props.currentLayer)?.layer ?? mapLayers[0].layer;
294
+ });
334
295
 
335
- const displayAreas = () => {
336
- areaLayerGroup.clearLayers();
337
- props.areas.forEach((area) => {
338
- const areaPolygon = LL.polygon(area.coordinates.map((coord) => [coord.latitude, coord.longitude]), {
339
- color: area.color,
340
- fillColor: area.color + "50",
341
- fillOpacity: 0.5,
342
- className: 'fs-map-area',
343
- }).addTo(areaLayerGroup);
344
-
345
- areas[area.id] = areaPolygon;
346
- areaPolygon.on('click', () => emit('update:selectedAreaId', area.id));
347
- });
348
- };
296
+ const bounds = computed<LatLngBounds | null>(() => {
297
+ if(!locationGroupBounds.value && !areaGroupBounds.value) {
298
+ return null;
299
+ }
300
+ let bounds = locationGroupBounds.value;
301
+ if(bounds && areaGroupBounds.value) {
302
+ bounds.extend(areaGroupBounds.value);
303
+ } else if(areaGroupBounds.value) {
304
+ bounds = areaGroupBounds.value;
305
+ }
306
+ return bounds as LatLngBounds;
307
+ });
308
+
309
+ const calculateTargetPosition = (target: L.LatLng) => {
310
+ if(!map.value) {
311
+ return target;
312
+ }
313
+ const zoom = map.value.getZoom();
314
+ const targetPoint = map.value.project(target, zoom).subtract([leftOffset.value / 2, -bottomOffset.value / 2]);
315
+ return map.value.unproject(targetPoint, zoom);
316
+ }
349
317
 
350
- const modifyLocationAddress = (locationId: string, newAddress: Address) => {
351
- const location = innerModelValue.value.find((loc) => loc.id === locationId);
352
- if (!location) {
318
+ const panTo = (lat: number, lng: number) => {
319
+ if(!map.value) {
353
320
  return;
354
321
  }
355
- const newLocation = {
356
- ...location,
357
- address: {
358
- ...newAddress
359
- },
360
- };
361
- innerModelValue.value = innerModelValue.value.map((loc) => loc.id === locationId ? newLocation : loc);
362
- };
322
+ map.value.panTo(calculateTargetPosition(latLng(lat, lng)));
323
+ }
363
324
 
364
- const initMap = () => {
365
- if (props.editable) {
366
- markerLayerGroup = new LL.FeatureGroup();
325
+ const setView = (lat: number, lng: number, zoom: number) => {
326
+ if(!map.value) {
327
+ return;
367
328
  }
368
- else {
369
- markerLayerGroup = new LL.MarkerClusterGroup({
370
- spiderfyOnMaxZoom: false,
371
- showCoverageOnHover: false,
372
- disableClusteringAtZoom: 17,
373
- iconCreateFunction: function (cluster: any) {
374
- const size = 36;
375
- return L.divIcon({
376
- html: clusterMarkerHtml(cluster.getChildCount()),
377
- className: 'fs-map-location fs-map-location-full',
378
- iconSize: [size, size],
379
- iconAnchor: [size / 2, size / 2],
380
- });
381
- }
382
- });
329
+ map.value.setView(calculateTargetPosition(latLng(lat, lng)), zoom);
330
+ }
331
+
332
+ const fitBounds = (bounds: LatLngBounds, options?: FitBoundsOptions) => {
333
+ if(!map.value) {
334
+ return;
335
+ }
336
+ const calculatedBounds = new LatLngBounds(
337
+ calculateTargetPosition(bounds.getSouthWest()),
338
+ calculateTargetPosition(bounds.getNorthEast())
339
+ );
340
+ map.value.fitBounds(calculatedBounds, options);
341
+ }
342
+
343
+ onMounted(() => {
344
+ if(!leafletContainer.value) {
345
+ return;
383
346
  }
347
+
384
348
  const mapOptions = {
385
349
  zoomControl: false,
386
350
  scrollWheelZoom: props.enableScrollWheelZoom,
387
351
  minZoom: 2,
388
- maxBounds: LL.latLngBounds(LL.latLng(-90, -180), LL.latLng(90, 180)),
352
+ maxZoom: 22,
353
+ maxBounds: latLngBounds(latLng(-90, -180), latLng(90, 180)),
389
354
  maxBoundsViscosity: 1.0
390
355
  };
391
- map = LL.map(mapId, mapOptions).setView([props.center[0], props.center[1]], defaultZoom);
392
- map.attributionControl.remove();
393
- LL.control.attribution({ position: 'bottomleft' }).addTo(map);
394
-
395
- baseLayerGroup.addTo(map);
396
- areaLayerGroup.addTo(map);
397
- myLocationLayerGroup.addTo(map);
398
- setMapBaseLayer(innerSelectedLayer.value);
399
- displayAreas();
400
- displayLocations();
401
- markerLayerGroup.addTo(map);
402
-
403
- if (innerModelValue.value.length > 0) {
404
- map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
405
- }
406
356
 
407
- map.on('click', (e: L.LeafletMouseEvent) => {
408
- if (editingLocation.value) {
409
- onNewCoordEntered(+e.latlng.lat.toFixed(6), +e.latlng.lng.toFixed(6));
410
- }
357
+ map.value = createMap(leafletContainer.value, mapOptions);
358
+ setView(props.center[0], props.center[1], defaultZoom);
359
+
360
+ map.value.on('click', (e: L.LeafletMouseEvent) => {
361
+ emit('click:latlng', e.latlng);
411
362
  });
412
- };
413
363
 
414
- const setMapBaseLayer = (layerName: 'map' | 'imagery') => {
415
- const layer = mapLayers.find((mapLayer) => mapLayer.name === layerName) ?? mapLayers[0];
416
- baseLayerGroup.clearLayers();
417
- layer.layer.addTo(baseLayerGroup);
418
- };
364
+ map.value.attributionControl.remove();
365
+ // to display google attribution in bottom left corner
366
+ control.attribution({ position: 'bottomleft' }).addTo(map.value);
419
367
 
420
- const onNewAddressEntered = (address: Address) => {
421
- if (!props.selectedLocationId || !map) {
422
- return;
423
- }
424
- modifyLocationAddress(props.selectedLocationId, address);
425
- map.panTo(calculateTargetPosition(new L.LatLng(address.latitude, address.longitude)));
426
- };
368
+ map.value.on('locationfound', (e: L.LocationEvent) => {
369
+ if(!e.latlng) {
370
+ return;
371
+ }
427
372
 
428
- const onNewCoordEntered = async (lat: number, lng: number) => {
429
- const address = await reverseSearch(lat, lng);
373
+ gpsPosition.value = e.latlng;
430
374
 
431
- onNewAddressEntered({
432
- ...address,
433
- latitude: lat,
434
- longitude: lng,
375
+ if(!map.value) {
376
+ return;
377
+ }
378
+
379
+ panTo(e.latlng.lat, e.latlng.lng);
435
380
  });
436
- };
381
+ });
437
382
 
438
- const zoomIn = () => {
439
- if (!map) {
383
+ watch (() => props.center, (center) => {
384
+ if(!map.value) {
440
385
  return;
441
386
  }
442
- map.zoomIn();
443
- };
387
+ setView(center[0], center[1], defaultZoom);
388
+ });
444
389
 
445
- const zoomOut = () => {
446
- if (!map) {
390
+ watch (() => props.selectedLocationId, (selectedLocationId) => {
391
+ if(!map.value) {
447
392
  return;
448
393
  }
449
- map.zoomOut();
450
- };
451
-
452
- const locate = () => {
453
- if (!map) {
394
+ const selectedLocation = props.locations.find((location) => location.id === selectedLocationId);
395
+ if(!selectedLocation) {
454
396
  return;
455
397
  }
456
- map.locate();
457
- map.on('locationfound', (e: L.LocationEvent) => {
458
- map.panTo(calculateTargetPosition(e.latlng));
459
- const size= 16;
460
- const icon = L.divIcon({
461
- html: myLocationMarkerHtml(L),
462
- className: 'fs-map-mylocation',
463
- iconSize: [size, size],
464
- iconAnchor: [size / 2, size / 2],
465
- });
466
- myLocationLayerGroup.clearLayers();
467
- LL.marker(e.latlng, { icon }).addTo(myLocationLayerGroup);
468
- });
469
- };
398
+ panTo(selectedLocation?.address.latitude, selectedLocation?.address.longitude);
399
+ }, { immediate: true });
470
400
 
471
- const calculateTargetPosition = (target: L.LatLng, zoom: number = map.getZoom()) => {
472
- const targetPoint = map.project(target, zoom).subtract([leftMargin.value / 2, -bottomMargin.value / 2]);
473
- return map.unproject(targetPoint, zoom);
474
- }
475
-
476
- const onCancel = () => {
477
- editingLocation.value = false;
478
- innerModelValue.value = props.modelValue;
479
- if (!map) {
401
+ watch(() => props.selectedAreaId, (selectedAreaId) => {
402
+ if(!map.value) {
480
403
  return;
481
404
  }
482
- displayLocations();
483
- if (innerModelValue.value.length > 0) {
484
- map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
485
- }
486
- else {
487
- map.panTo(calculateTargetPosition(new L.LatLng(props.center[0], props.center[1])), { animate: false });
488
- }
489
- if (props.modelValue.length > 1) {
490
- emit('update:selectedLocationId', null);
491
- }
492
- };
493
-
494
- const onSubmit = () => {
495
- emit('update:modelValue', innerModelValue.value);
496
- if (!map) {
405
+ const selectedArea = props.areas.find((area) => area.id === selectedAreaId);
406
+ if(!selectedArea) {
497
407
  return;
498
408
  }
499
- editingLocation.value = false;
500
- if (innerModelValue.value.length > 0) {
501
- map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
502
- }
503
- else {
504
- map.panTo(calculateTargetPosition(new L.LatLng(props.center[0], props.center[1])), { animate: false });
505
- }
506
- if (props.modelValue.length > 1) {
507
- emit('update:selectedLocationId', null);
508
- }
509
- };
510
-
511
- onMounted(() => {
512
- initMap();
513
- if (props.selectedLocationId && props.modelValue.length === 1) {
514
- editingLocation.value = true;
515
- }
516
-
517
- resizeObserver.value = new ResizeObserver(entries => {
518
- entries.forEach((entry) => {
519
- if (entry.target.id === `left-overlay-${mapId}`) {
520
- leftOverlayWidth.value = entry.contentRect.width;
521
- }
522
- if (entry.target.id === `left-overlay-mobile-${mapId}`) {
523
- leftOverlayHeight.value = entry.contentRect.height;
524
- }
525
- });
526
- });
527
- if (document.querySelector(`#left-overlay-mobile-${mapId}`)) {
528
- resizeObserver.value.observe(document.querySelector(`#left-overlay-mobile-${mapId}`)!);
529
- }
530
- if (document.querySelector(`#left-overlay-${mapId}`)) {
531
- resizeObserver.value.observe(document.querySelector(`#left-overlay-${mapId}`)!);
532
- }
533
- });
534
-
535
- onUnmounted((): void => {
536
- if (resizeObserver.value) {
537
- resizeObserver.value.disconnect();
538
- }
539
- });
540
-
541
- watch(() => innerModelValue.value, () => {
542
- displayLocations();
543
- });
409
+ const bounds = latLngBounds(selectedArea.coordinates.map((coord) => latLng(coord.latitude, coord.longitude)));
410
+ fitBounds(bounds);
411
+ }, { immediate: true });
544
412
 
545
- watch(() => props.selectedLocationId, () => {
546
- if (!props.selectedLocationId || !map) {
413
+ watch( () => bounds.value, (bounds) => {
414
+ if(!map.value || !bounds) {
547
415
  return;
548
416
  }
549
-
550
- Object.values(markers).forEach((marker) => {
551
- marker.getElement()?.classList.remove('fs-map-location-selected');
552
- });
553
-
554
- const marker = markers[props.selectedLocationId];
555
- map.flyTo(calculateTargetPosition(marker.getLatLng(), 17), 17, { animate: false });
556
- marker.getElement()?.classList.add('fs-map-location-selected');
557
- })
558
-
559
- watch(() => props.selectedAreaId, () => {
560
- if (!props.selectedAreaId || !map) {
561
- return;
562
- }
563
- const area = areas[props.selectedAreaId];
564
- if (area) {
565
- map.fitBounds(area.getBounds(), { maxZoom: 17 });
566
- }
567
- });
568
-
569
- watch(innerSelectedLayer, () => {
570
- setMapBaseLayer(innerSelectedLayer.value);
417
+ fitBounds(bounds, { maxZoom: defaultZoom });
571
418
  });
572
419
 
573
420
  return {
574
- bottomMargin,
575
- editingLocation,
576
- fullScreen,
577
- innerModelValue,
578
- innerSelectedLayer,
421
+ ColorEnum,
422
+ leafletContainer,
423
+ locationGroupBounds,
424
+ overlayHeight,
425
+ overlayWidth,
426
+ areaGroupBounds,
427
+ map,
428
+ actualLayer,
579
429
  mapLayers,
580
- mapId,
581
- style,
582
- L,
583
- onNewAddressEntered,
584
- onNewCoordEntered,
585
- setMapBaseLayer,
586
- onCancel,
587
- onSubmit,
588
- zoomOut,
589
- locate,
590
- zoomIn
430
+ gpsPosition,
431
+ style
591
432
  };
592
433
  }
593
434
  });