@dative-gpi/foundation-shared-components 0.0.189 → 0.0.191

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.
@@ -28,7 +28,7 @@ export default defineComponent({
28
28
  },
29
29
  props: {
30
30
  modelValue: {
31
- type: Function as () => Address | null,
31
+ type: Object as () => Address | null,
32
32
  required: false,
33
33
  default: null
34
34
  },
@@ -1098,14 +1098,9 @@ export default defineComponent({
1098
1098
  emit("click:row", row.item);
1099
1099
  }
1100
1100
  },
1101
- mobile: (event: PointerEvent, item: any) => {
1101
+ mobile: (event: any, item: any) => {
1102
1102
  if (props.itemTo && router) {
1103
- if (event.metaKey || event.ctrlKey || event.button === 1) {
1104
- window.open(router.resolve(props.itemTo(item)).href, "_blank");
1105
- }
1106
- else {
1107
- router.push(props.itemTo(item));
1108
- }
1103
+ router.push(props.itemTo(item));
1109
1104
  }
1110
1105
  else {
1111
1106
  emit("click:row", item);
@@ -0,0 +1,467 @@
1
+ <template>
2
+ <FSCard
3
+ :width="$props.width"
4
+ v-bind="$attrs"
5
+ >
6
+ <FSCol
7
+ class="fs-map"
8
+ width="fill"
9
+ >
10
+ <FSRow
11
+ v-if="selectableLayers.length > 1"
12
+ class="fs-map-overlay-layer-choice"
13
+ gap="2px"
14
+ >
15
+ <FSChip
16
+ v-for="mapLayer in mapLayers.filter((layer) => selectableLayers.includes(layer.name))"
17
+ variant="full"
18
+ :color="innerSelectedLayer === mapLayer.name ? 'dark' : 'light'"
19
+ :label="mapLayer.label"
20
+ :key="mapLayer.name"
21
+ :editable="true"
22
+ @click="setMapBaseLayer(mapLayer.name)"
23
+ />
24
+ </FSRow>
25
+ <FSRow
26
+ v-if="$props.editable && !editingLocation && $props.selectedLocationId !== null"
27
+ class="fs-map-overlay-edit-button"
28
+ >
29
+ <FSButton
30
+ prepend-icon="mdi-pencil"
31
+ :label="$tr('ui.map.modify', 'Modify')"
32
+ @click="editingLocation = true"
33
+ />
34
+ </FSRow>
35
+ <FSCol
36
+ :style="style"
37
+ >
38
+ <div
39
+ class="fs-leaflet-container"
40
+ :id="mapId"
41
+ />
42
+ </FSCol>
43
+
44
+ <FSCol
45
+ class="fs-map-overlay-container"
46
+ align="center-center"
47
+ >
48
+ <FSCol
49
+ class="fs-map-zoom-overlay"
50
+ align="bottom-center"
51
+ width="hug"
52
+ >
53
+ <FSButton
54
+ v-if="$props.showMyLocation"
55
+ prependIcon="mdi-crosshairs-gps"
56
+ color="primary"
57
+ variant="full"
58
+ :elevation="true"
59
+ :border="false"
60
+ @click="locate"
61
+ />
62
+ <FSCol
63
+ v-if="$props.showZoomButtons"
64
+ gap="0"
65
+ >
66
+ <FSButton
67
+ class="fs-map-zoom-plus"
68
+ prependIcon="mdi-plus"
69
+ :elevation="true"
70
+ :border="false"
71
+ @click="zoomIn"
72
+ />
73
+ <FSButton
74
+ class="fs-map-zoom-minus"
75
+ prependIcon="mdi-minus"
76
+ :elevation="true"
77
+ :border="false"
78
+ @click="zoomOut"
79
+ />
80
+ </FSCol>
81
+ </FSCol>
82
+ <FSMapEditPointAddressOverlay
83
+ v-if="editingLocation"
84
+ :label="$tr('ui.map.address', 'Address')"
85
+ :modelValue="(innerModelValue.find((loc) => loc.id === $props.selectedLocationId))?.address"
86
+ @update:locationCoord="($event: Address) => onNewCoordEntered($event.latitude, $event.longitude)"
87
+ @update:modelValue="($event: Address) => onNewAddressEntered($event)"
88
+ @cancel="onCancel"
89
+ @submit="onSubmit"
90
+ />
91
+ </FSCol>
92
+ </FSCol>
93
+ </FSCard>
94
+ </template>
95
+
96
+ <script lang="ts">
97
+ import { computed, defineComponent, onMounted, type PropType, ref, watch } from "vue";
98
+ import { v4 as uuidv4 } from "uuid";
99
+
100
+ import { MarkerClusterGroup } from "leaflet.markercluster";
101
+ import * as L from "leaflet";
102
+
103
+ import { type Address, type SiteInfos } from '@dative-gpi/foundation-shared-domain/models';
104
+
105
+ import { ColorEnum, type FSLocation, type MapLayer } from "../../models";
106
+ import { useAddress } from "../../composables/useAddress";
107
+ import { useColors } from "../../composables";
108
+
109
+ import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
110
+ import FSButton from "../FSButton.vue";
111
+ import FSCard from "../FSCard.vue";
112
+ import FSChip from "../FSChip.vue";
113
+ import FSCol from "../FSCol.vue";
114
+ import FSRow from "../FSRow.vue";
115
+
116
+ export default defineComponent({
117
+ name: "FSMap",
118
+ components: {
119
+ FSMapEditPointAddressOverlay,
120
+ FSButton,
121
+ FSCard,
122
+ FSChip,
123
+ FSCol,
124
+ FSRow
125
+ },
126
+ props: {
127
+ height: {
128
+ type: [Array, String, Number] as PropType<string[] | number[] | string | number | null>,
129
+ required: false,
130
+ default: '400px'
131
+ },
132
+ width: {
133
+ type: [Array, String, Number] as PropType<string[] | number[] | string | number | null>,
134
+ required: false,
135
+ default: '100%'
136
+ },
137
+ sites: {
138
+ type: Array as PropType<SiteInfos[]>,
139
+ required: false,
140
+ default: () => [],
141
+ },
142
+ center: {
143
+ type: Array as PropType<number[]>,
144
+ required: false,
145
+ default: () => [45.71, 5.07]
146
+ },
147
+ selectedLayer: {
148
+ type: String,
149
+ required: false,
150
+ default: "osm"
151
+ },
152
+ selectableLayers: {
153
+ type: Array as PropType<string[]>,
154
+ required: false,
155
+ default: () => ["osm", "imagery"]
156
+ },
157
+ selectedLocationId: {
158
+ type: String as PropType<string | null>,
159
+ required: false,
160
+ default: null
161
+ },
162
+ selectedSiteId: {
163
+ type: String as PropType<string | null>,
164
+ required: false,
165
+ default: null
166
+ },
167
+ modelValue: {
168
+ type: Array as PropType<FSLocation[]>,
169
+ required: false,
170
+ default: () => [],
171
+ },
172
+ editable: {
173
+ type: Boolean,
174
+ required: false,
175
+ default: false
176
+ },
177
+ showMyLocation: {
178
+ type: Boolean,
179
+ required: false,
180
+ default: true
181
+ },
182
+ showZoomButtons: {
183
+ type: Boolean,
184
+ required: false,
185
+ default: true
186
+ }
187
+ },
188
+ emits: ["update:modelValue", "update:selectedLocationId", "update:selectedSiteId"],
189
+ setup(props, { emit }) {
190
+ const { reverseSearch } = useAddress();
191
+ const { getColors } = useColors();
192
+
193
+ const innerSelectedLayer = ref(props.selectedLayer);
194
+ const innerModelValue = ref(props.modelValue);
195
+ const editingLocation = ref(false);
196
+
197
+ const mapId = `map-${uuidv4()}`;
198
+ const defaultZoom = 15;
199
+ const markers: { [key: string]: L.Marker } = {};
200
+ const sites: { [key: string]: L.Polygon } = {};
201
+ const siteLayerGroup = new L.FeatureGroup();
202
+ const baseLayerGroup = new L.LayerGroup();
203
+ const myLocationLayerGroup = new L.LayerGroup();
204
+
205
+ let map: L.Map;
206
+ let markerLayerGroup: L.FeatureGroup | MarkerClusterGroup;
207
+
208
+ if (props.editable) {
209
+ markerLayerGroup = new L.FeatureGroup();
210
+ }
211
+ else {
212
+ markerLayerGroup = new MarkerClusterGroup({
213
+ spiderfyOnMaxZoom: false,
214
+ showCoverageOnHover: false,
215
+ disableClusteringAtZoom: 17,
216
+ iconCreateFunction: function(cluster: any) {
217
+ return L.divIcon({
218
+ html: `<div>
219
+ <span>${cluster.getChildCount()}</span>
220
+ </div>`,
221
+ className: 'fs-map-location fs-map-location-full',
222
+ iconSize: [36, 36],
223
+ iconAnchor: [18, 18],
224
+ });
225
+ }
226
+ });
227
+ }
228
+ const mapLayers: MapLayer[] = [
229
+ {
230
+ name: "osm",
231
+ label: "OpenStreetMap",
232
+ layer: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
233
+ maxZoom: 19,
234
+ attribution: '© OpenStreetMap'
235
+ })
236
+ },
237
+ {
238
+ name: "imagery",
239
+ label: "Imagery",
240
+ layer: L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.jpg', {
241
+ maxZoom: 20,
242
+ attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
243
+ }),
244
+ },
245
+ {
246
+ name: "light",
247
+ label: "Light",
248
+ layer: L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png', {
249
+ maxZoom: 20,
250
+ attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
251
+ })
252
+ }
253
+ ];
254
+
255
+ const style = computed((): { [key: string]: string | undefined } => {
256
+ return {
257
+ "--fs-map-location-pin-color": getColors(ColorEnum.Primary).base,
258
+ "--fs-map-mylocation-pin-color-alpha": getColors(ColorEnum.Primary).base + "50",
259
+ "--fs-map-leaflet-container-height": props.height as string,
260
+ };
261
+ });
262
+
263
+ const displayLocations = () => {
264
+ markerLayerGroup.clearLayers();
265
+ innerModelValue.value.forEach((location) => {
266
+ const iconHtml = `<div class="fs-map-location-pin"><i class="${location.icon} mdi v-icon notranslate v-theme--DefaultTheme fs-icon" aria-hidden="true" style="--fs-icon-font-size: 22px;" ></i></div>`;
267
+ const icon = L.divIcon({
268
+ html: iconHtml,
269
+ className: 'fs-map-location',
270
+ iconSize: [36, 36],
271
+ iconAnchor: [18, 18],
272
+ });
273
+ const marker = L.marker([location.address.latitude, location.address.longitude], { icon }).addTo(markerLayerGroup);
274
+ markers[location.id] = marker;
275
+ marker.on('click', () => emit('update:selectedLocationId', location.id));
276
+
277
+ });
278
+ };
279
+
280
+ const displaySites = () => {
281
+ siteLayerGroup.clearLayers();
282
+ props.sites.forEach((site) => {
283
+ const sitePolygon = L.polygon(site.coordinates.map((coord) => [coord.latitude, coord.longitude]), {
284
+ color: site.color,
285
+ fillColor: site.color + "50",
286
+ fillOpacity: 0.5,
287
+ className: 'fs-map-site',
288
+ }).addTo(siteLayerGroup);
289
+
290
+ sites[site.id] = sitePolygon;
291
+ sitePolygon.on('click', () => emit('update:selectedSiteId', site.id));
292
+ });
293
+ }
294
+
295
+ const modifyLocationAddress = (locationId: string, newAddress: Address) => {
296
+ const location = innerModelValue.value.find((loc) => loc.id === locationId);
297
+ if (!location) {
298
+ return;
299
+ }
300
+ const newLocation = {
301
+ ...location,
302
+ address: {
303
+ ...newAddress
304
+ },
305
+ };
306
+ innerModelValue.value = innerModelValue.value.map((loc) => loc.id === locationId ? newLocation : loc);
307
+ };
308
+
309
+ const initMap = () => {
310
+ const mapOptions = {
311
+ zoomControl: false,
312
+ scrollWheelZoom: false,
313
+ minZoom: 2,
314
+ maxBounds: L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180)),
315
+ maxBoundsViscosity: 1.0
316
+ };
317
+ map = L.map(mapId, mapOptions).setView([props.center[0], props.center[1]], defaultZoom);
318
+ map.attributionControl.remove();
319
+ L.control.attribution({ position: 'bottomleft' }).addTo(map);
320
+
321
+ baseLayerGroup.addTo(map);
322
+ siteLayerGroup.addTo(map);
323
+ myLocationLayerGroup.addTo(map);
324
+ setMapBaseLayer(props.selectedLayer);
325
+ displaySites();
326
+ displayLocations();
327
+ markerLayerGroup.addTo(map);
328
+
329
+ if (innerModelValue.value.length > 0) {
330
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
331
+ }
332
+
333
+ map.on('click', (e: L.LeafletMouseEvent) => {
334
+ if (editingLocation.value) {
335
+ onNewCoordEntered(+e.latlng.lat.toFixed(6), +e.latlng.lng.toFixed(6));
336
+ }
337
+ });
338
+ };
339
+
340
+ const setMapBaseLayer = (layerName: string) => {
341
+ innerSelectedLayer.value = layerName;
342
+ const layer = mapLayers.find((mapLayer) => mapLayer.name === layerName) ?? mapLayers[0];
343
+ baseLayerGroup.clearLayers();
344
+ layer.layer.addTo(baseLayerGroup);
345
+ };
346
+
347
+ const onNewAddressEntered = (address: Address) => {
348
+ if (!props.selectedLocationId) {
349
+ return;
350
+ }
351
+ modifyLocationAddress(props.selectedLocationId, address);
352
+ map?.flyTo([address.latitude, address.longitude], map?.getZoom() ?? defaultZoom);
353
+ };
354
+
355
+ const onNewCoordEntered = async (lat: number, lng: number) => {
356
+ const address = await reverseSearch(lat, lng);
357
+
358
+ onNewAddressEntered({
359
+ ...address,
360
+ latitude: lat,
361
+ longitude: lng,
362
+ });
363
+ };
364
+
365
+ const zoomIn = () => {
366
+ map?.zoomIn();
367
+ };
368
+
369
+ const zoomOut = () => {
370
+ map?.zoomOut();
371
+ };
372
+
373
+ const locate = () => {
374
+ map?.locate();
375
+ map?.on('locationfound', (e: L.LocationEvent) => {
376
+ map?.flyTo(e.latlng, map?.getZoom() ?? defaultZoom);
377
+ const iconHtml = `<div class="fs-map-mylocation-pin"></div>`;
378
+ const icon = L.divIcon({
379
+ html: iconHtml,
380
+ className: 'fs-map-mylocation',
381
+ iconSize: [16, 16],
382
+ iconAnchor: [8, 8],
383
+ });
384
+ myLocationLayerGroup.clearLayers();
385
+ L.marker(e.latlng, { icon }).addTo(myLocationLayerGroup);
386
+ });
387
+ };
388
+
389
+ const onCancel = () => {
390
+ editingLocation.value = false;
391
+ innerModelValue.value = props.modelValue;
392
+ displayLocations();
393
+ if (innerModelValue.value.length > 0) {
394
+ map?.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
395
+ }
396
+ else {
397
+ map?.flyTo([props.center[0], props.center[1]], map?.getZoom() ?? defaultZoom);
398
+ }
399
+ if (props.modelValue.length > 1) {
400
+ emit('update:selectedLocationId', null);
401
+ }
402
+ };
403
+
404
+ const onSubmit = () => {
405
+ emit('update:modelValue', innerModelValue.value);
406
+ editingLocation.value = false;
407
+ if (innerModelValue.value.length > 0) {
408
+ map?.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
409
+ }
410
+ else {
411
+ map?.flyTo([props.center[0], props.center[1]], map?.getZoom() ?? defaultZoom);
412
+ }
413
+ if (props.modelValue.length > 1) {
414
+ emit('update:selectedLocationId', null);
415
+ }
416
+ };
417
+
418
+ onMounted(() => {
419
+ initMap();
420
+ });
421
+
422
+ watch(() => innerModelValue.value, () => {
423
+ displayLocations();
424
+ });
425
+
426
+ watch(() => props.selectedLocationId, () => {
427
+ Object.values(markers).forEach((marker) => {
428
+ marker.getElement()?.classList.remove('fs-map-location-selected');
429
+ });
430
+
431
+ if (!props.selectedLocationId) {
432
+ return;
433
+ }
434
+ const marker = markers[props.selectedLocationId];
435
+ marker.getElement()?.classList.add('fs-map-location-selected');
436
+ map?.flyTo(marker.getLatLng(), 17);
437
+ })
438
+
439
+ watch(() => props.selectedSiteId, () => {
440
+ if (!props.selectedSiteId) {
441
+ return;
442
+ }
443
+ const site = sites[props.selectedSiteId];
444
+ if (site) {
445
+ map?.fitBounds(site.getBounds(), { maxZoom: 17 });
446
+ }
447
+ });
448
+
449
+ return {
450
+ innerSelectedLayer,
451
+ editingLocation,
452
+ innerModelValue,
453
+ mapLayers,
454
+ mapId,
455
+ style,
456
+ onNewAddressEntered,
457
+ onNewCoordEntered,
458
+ setMapBaseLayer,
459
+ onCancel,
460
+ onSubmit,
461
+ zoomOut,
462
+ locate,
463
+ zoomIn
464
+ };
465
+ },
466
+ });
467
+ </script>
@@ -0,0 +1,163 @@
1
+ <template>
2
+ <FSCard
3
+ padding="16px"
4
+ width="100%"
5
+ height="100%"
6
+ :elevation="true"
7
+ >
8
+ <FSCol
9
+ gap="24px"
10
+ >
11
+ <FSRow>
12
+ <FSText
13
+ font="text-h3"
14
+ >
15
+ {{ $tr('ui.map.modify-location', 'Modify location') }}
16
+ </FSText>
17
+ <v-spacer />
18
+ <FSButton
19
+ v-if="menuLocationCoord"
20
+ icon="mdi-arrow-collapse"
21
+ variant="icon"
22
+ @click="menuLocationCoord = !menuLocationCoord"
23
+ />
24
+ <FSButton
25
+ v-else
26
+ icon="mdi-arrow-expand"
27
+ variant="icon"
28
+ @click="menuLocationCoord = !menuLocationCoord"
29
+ />
30
+ </FSRow>
31
+ <FSCol
32
+ v-if="menuLocationCoord"
33
+ >
34
+ <FSAutoCompleteAddress
35
+ :modelValue="$props.modelValue"
36
+ @update:modelValue="onAddressFieldSubmit($event)"
37
+ />
38
+ <FSForm
39
+ variant="standard"
40
+ @submit="onCoordinateChange()"
41
+ >
42
+ <FSRow>
43
+ <FSNumberField
44
+ :label="$tr('ui.map.latitude', 'Latitude')"
45
+ v-model="latitude"
46
+ />
47
+ <FSNumberField
48
+ :label="$tr('ui.map.longitude', 'Longitude')"
49
+ v-model="longitude"
50
+ />
51
+ </FSRow>
52
+ <FSButton
53
+ :label="$tr('ui.map.save', 'Save')"
54
+ color="primary"
55
+ prepend-icon="mdi-content-save"
56
+ type="submit"
57
+ style="display: none;"
58
+ />
59
+ </FSForm>
60
+ </FSCol>
61
+ <FSRow
62
+ align="center-right"
63
+ >
64
+ <FSButton
65
+ :label="$tr('ui.map.cancel', 'Cancel')"
66
+ @click="onCancel"
67
+ />
68
+ <FSButton
69
+ :label="$tr('ui.map.save', 'Save')"
70
+ color="primary"
71
+ prepend-icon="mdi-content-save"
72
+ @click="onSubmit"
73
+ />
74
+ </FSRow>
75
+ </FSCol>
76
+ </FSCard>
77
+ </template>
78
+
79
+ <script lang="ts">
80
+ import { defineComponent, type PropType, ref, watch } from "vue";
81
+
82
+ import { Address } from "@dative-gpi/foundation-shared-domain/models";
83
+
84
+ import FSAutoCompleteAddress from "../autocompletes/FSAutoCompleteAddress.vue";
85
+ import FSNumberField from "../fields/FSNumberField.vue";
86
+ import FSButton from "../FSButton.vue";
87
+ import FSCard from "../FSCard.vue";
88
+ import FSForm from "../FSForm.vue";
89
+ import FSText from "../FSText.vue";
90
+ import FSCol from "../FSCol.vue";
91
+ import FSRow from "../FSRow.vue";
92
+
93
+ export default defineComponent({
94
+ name: "FSMapEditPointAddressOverlay.vue",
95
+ components: {
96
+ FSAutoCompleteAddress,
97
+ FSNumberField,
98
+ FSButton,
99
+ FSCard,
100
+ FSForm,
101
+ FSText,
102
+ FSCol,
103
+ FSRow
104
+ },
105
+ props: {
106
+ modelValue: {
107
+ type: Object as PropType<Address>,
108
+ default: null,
109
+ required: false,
110
+ }
111
+ },
112
+ emits: ["update:modelValue", "update:locationCoord", "submit", "cancel"],
113
+ setup(props, { emit }) {
114
+ const menuLocationCoord = ref(false);
115
+
116
+ const latitude = ref(props.modelValue.latitude);
117
+ const longitude = ref(props.modelValue.longitude);
118
+
119
+ const onCoordinateChange = () => {
120
+ const newModelValue = new Address({
121
+ country: "",
122
+ formattedAddress: "",
123
+ locality: "",
124
+ placeId: "",
125
+ placeLabel: "",
126
+ latitude: latitude.value,
127
+ longitude: longitude.value,
128
+ });
129
+ emit("update:locationCoord", newModelValue);
130
+ };
131
+
132
+ const onAddressFieldSubmit = (address: Address|null) => {
133
+ if(address === null) {
134
+ return;
135
+ }
136
+ emit('update:modelValue', address);
137
+ };
138
+
139
+ const onSubmit = () => {
140
+ emit('submit');
141
+ };
142
+
143
+ const onCancel = () => {
144
+ emit('cancel');
145
+ };
146
+
147
+ watch(() => props.modelValue, (value) => {
148
+ latitude.value = value.latitude;
149
+ longitude.value = value.longitude;
150
+ });
151
+
152
+ return {
153
+ menuLocationCoord,
154
+ longitude,
155
+ latitude,
156
+ onAddressFieldSubmit,
157
+ onCoordinateChange,
158
+ onSubmit,
159
+ onCancel
160
+ };
161
+ }
162
+ });
163
+ </script>
@@ -1,5 +1,4 @@
1
1
  export * from "./useAutocomplete";
2
- export * from "./useAuthTokens";
3
2
  export * from "./useBreakpoints";
4
3
  export * from "./useColors";
5
4
  export * from "./useDebounce";
@@ -8,6 +8,7 @@ export const useAddress = () => {
8
8
  let searchService: google.maps.places.AutocompleteService;
9
9
  let placeService: google.maps.places.PlacesService;
10
10
  let sessionId: google.maps.places.AutocompleteSessionToken;
11
+
11
12
 
12
13
  const init = async () => {
13
14
  await window.initMap;
@@ -19,7 +20,6 @@ export const useAddress = () => {
19
20
  initialized = true;
20
21
  }
21
22
 
22
-
23
23
  const search = async (search: string): Promise<Place[]> => {
24
24
  if(!initialized){
25
25
  await init();
@@ -54,6 +54,30 @@ export const useAddress = () => {
54
54
  throw new Error("missing informations");
55
55
  }
56
56
 
57
+ const reverseSearch = async (lat: number, lon: number): Promise<Address> => {
58
+ if(!initialized){
59
+ await init();
60
+ }
61
+
62
+ return _reverseSearch(lat, lon).then(result => {
63
+ if (result.length > 0) {
64
+ const response = result[0];
65
+ if (response.address_components && response.formatted_address && response.geometry) {
66
+ return new Address({
67
+ formattedAddress: response.formatted_address,
68
+ locality: _find(response.address_components, "locality"),
69
+ country: _find(response.address_components, "country"),
70
+ latitude: response.geometry.location?.lat() ?? 0,
71
+ longitude: response.geometry.location?.lng() ?? 0,
72
+ placeId: response.place_id,
73
+ placeLabel: response.formatted_address
74
+ });
75
+ }
76
+ }
77
+ throw new Error("missing informations");
78
+ });
79
+ }
80
+
57
81
  const _search = (search: string) => {
58
82
  if (!enabled) {
59
83
  throw new Error("offline mode, do not call this method");
@@ -77,6 +101,26 @@ export const useAddress = () => {
77
101
  );
78
102
  }
79
103
 
104
+ const _reverseSearch = (lat: number, lon: number) => {
105
+ if (!enabled) {
106
+ throw new Error("offline mode, do not call this method");
107
+ }
108
+ return new Promise<google.maps.GeocoderResult[]>((resolve, reject) => {
109
+ new google.maps.Geocoder().geocode(
110
+ {
111
+ location: { lat: lat, lng: lon }
112
+ },
113
+ (result, status) => {
114
+ if (status != google.maps.GeocoderStatus.OK || !result) {
115
+ reject(status);
116
+ } else {
117
+ resolve(result);
118
+ }
119
+ }
120
+ );
121
+ });
122
+ }
123
+
80
124
  const _get = (id: string) => {
81
125
  if (!enabled) {
82
126
  throw new Error("offline mode, do not call this method");
@@ -103,11 +147,12 @@ export const useAddress = () => {
103
147
  const found = _.find(components, c =>
104
148
  _.some(c.types, t => t === type)
105
149
  );
106
- return (found && found.long_name) || "";
150
+ return found?.long_name ?? "";
107
151
  }
108
152
 
109
153
  return {
110
154
  search,
111
- get
155
+ get,
156
+ reverseSearch
112
157
  }
113
158
  }
package/models/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./deviceStatuses";
6
6
  export * from "./errors";
7
7
  export * from "./grids";
8
8
  export * from "./images";
9
+ export * from "./map";
9
10
  export * from "./magicFields";
10
11
  export * from "./modelStatuses";
11
12
  export * from "./rules";
package/models/map.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type Layer } from "leaflet";
2
+
3
+ import { type Address } from "@/shared/foundation-shared-domain";
4
+
5
+ export interface MapLayer {
6
+ name : string;
7
+ label: string;
8
+ layer: Layer;
9
+ }
10
+
11
+ export interface FSLocation {
12
+ id: string;
13
+ label: string;
14
+ icon: string;
15
+ address: Address;
16
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dative-gpi/foundation-shared-components",
3
3
  "sideEffects": false,
4
- "version": "0.0.189",
4
+ "version": "0.0.191",
5
5
  "description": "",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -10,8 +10,10 @@
10
10
  "author": "",
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
- "@dative-gpi/foundation-shared-domain": "0.0.189",
14
- "@dative-gpi/foundation-shared-services": "0.0.189"
13
+ "@dative-gpi/foundation-shared-domain": "0.0.191",
14
+ "@dative-gpi/foundation-shared-services": "0.0.191",
15
+ "leaflet": "1.9.4",
16
+ "leaflet.markercluster": "1.5.3"
15
17
  },
16
18
  "peerDependencies": {
17
19
  "@dative-gpi/bones-ui": "^0.0.75",
@@ -26,15 +28,12 @@
26
28
  "@lexical/utils": "0.12.5",
27
29
  "@mdi/font": "^7.4.47",
28
30
  "blurhash": "2.0.5",
29
- "color": "^4.2.3",
30
- "lexical": "0.12.5",
31
- "vue": "^3.4.29"
31
+ "color": "^4.2.3"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/color": "3.0.6",
35
- "@types/google.maps": "^3.55.10",
36
35
  "sass": "1.71.1",
37
36
  "sass-loader": "13.3.2"
38
37
  },
39
- "gitHead": "ba53009e38ffce4d79563a89b19312c5544ef6fa"
38
+ "gitHead": "19372b25ed0b392e64c1d23933412b045f53a036"
40
39
  }
@@ -9,7 +9,7 @@ export const MapsPlugin: Plugin = {
9
9
  const maps = document.createElement("script");
10
10
  maps.setAttribute(
11
11
  "src",
12
- `https://maps.googleapis.com/maps/api/js?key=${key}&loading=async&libraries=geometry,places`
12
+ `https://maps.googleapis.com/maps/api/js?key=${key}&loading=async&libraries=geometry,places,geocoding`
13
13
  );
14
14
 
15
15
  let resolvePromise: (value: void) => void;
package/shims-plugin.d.ts CHANGED
@@ -6,6 +6,4 @@ declare module "vue" {
6
6
  interface ComponentCustomProperties {
7
7
  $color: (key: ColorBase) => string;
8
8
  }
9
- }
10
-
11
- declare module 'googlemaps';
9
+ }
@@ -0,0 +1,159 @@
1
+ @import 'leaflet/dist/leaflet.css';
2
+
3
+ .fs-map {
4
+ position: relative;
5
+
6
+ .fs-leaflet-container {
7
+ width: 100%;
8
+ height: var(--fs-map-leaflet-container-height);
9
+ }
10
+
11
+ .fs-map-overlay-layer-choice {
12
+ position: absolute;
13
+ top: 0;
14
+ left: 0;
15
+ z-index: 950;
16
+ margin: 8px;
17
+
18
+ >* {
19
+ opacity: 0.7;
20
+ transition: opacity 0.28s cubic-bezier(0.4, 0, 0.2, 1);
21
+ }
22
+
23
+ >*:hover {
24
+ opacity: 1;
25
+ }
26
+ }
27
+
28
+ .fs-map-overlay-edit-button {
29
+ position: absolute;
30
+ top: 0;
31
+ right: 0;
32
+ z-index: 960;
33
+ margin: 8px;
34
+ }
35
+
36
+ .fs-map-overlay-container {
37
+ position: absolute;
38
+ bottom: 0;
39
+ left: 0;
40
+ z-index: 1000;
41
+ margin: 4px 8px;
42
+ width: calc(100% - 16px);
43
+
44
+
45
+ .fs-map-zoom-overlay {
46
+ position: absolute;
47
+ bottom: 100%;
48
+ right: 0;
49
+ z-index: 1001;
50
+ margin-bottom: 8px;
51
+
52
+ button.fs-map-zoom-plus>* {
53
+ border-bottom-left-radius: 0 !important;
54
+ border-bottom-right-radius: 0 !important;
55
+ }
56
+
57
+ button.fs-map-zoom-minus>* {
58
+ margin-top: 1px;
59
+ border-top-left-radius: 0 !important;
60
+ border-top-right-radius: 0 !important;
61
+
62
+ border-top: solid 1px var(--fs-card-border-color) !important;
63
+
64
+
65
+ }
66
+ }
67
+ }
68
+
69
+ .fs-map-mylocation {
70
+ background-color: var(--fs-map-location-pin-color);
71
+ border: 3px solid white;
72
+ border-radius: 100%;
73
+ animation: fs-map-shadow 1.4s linear infinite;
74
+
75
+ @keyframes fs-map-shadow {
76
+ 0% {
77
+ box-shadow: 0 0 0px 0px var(--fs-map-mylocation-pin-color-alpha);
78
+ }
79
+
80
+ 50% {
81
+ box-shadow: 0 0 0px 7px var(--fs-map-mylocation-pin-color-alpha);
82
+ }
83
+
84
+ 100% {
85
+ box-shadow: 0 0 0px 20px transparent;
86
+ }
87
+ }
88
+ }
89
+
90
+ .fs-map-location {
91
+ display: flex;
92
+ color: var(--fs-map-location-pin-color);
93
+ border-radius: 50%;
94
+ background-color: white;
95
+ filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.4));
96
+ align-items: center;
97
+ justify-content: center;
98
+
99
+ &.fs-map-location-full {
100
+ background-color: var(--fs-map-location-pin-color);
101
+ color: white;
102
+ }
103
+
104
+ >* {
105
+ transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
106
+ }
107
+
108
+ .mdi-loading {
109
+ animation: spin 1s linear infinite;
110
+ }
111
+
112
+ &:hover {
113
+ filter: brightness(0.92) drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.4));
114
+
115
+ >* {
116
+
117
+ transform: scale(1.15);
118
+ }
119
+ }
120
+
121
+ @keyframes spin {
122
+ 0% {
123
+ transform: rotate(0deg);
124
+ }
125
+
126
+ 100% {
127
+ transform: rotate(360deg);
128
+ }
129
+ }
130
+
131
+ &.fs-map-location-selected {
132
+ animation: fs-map-shadow 1.4s linear infinite;
133
+
134
+ @keyframes fs-map-shadow {
135
+ 0% {
136
+ box-shadow: 0 0 0px 0px var(--fs-map-mylocation-pin-color-alpha);
137
+ }
138
+
139
+ 50% {
140
+ box-shadow: 0 0 0px 7px var(--fs-map-mylocation-pin-color-alpha);
141
+ }
142
+
143
+ 100% {
144
+ box-shadow: 0 0 0px 20px transparent;
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ .fs-map-site {
151
+ opacity: 0.6;
152
+ transition: opacity 0.28s cubic-bezier(0.4, 0, 0.2, 1);
153
+
154
+ &:hover {
155
+ opacity: 1;
156
+ }
157
+ }
158
+
159
+ }
@@ -36,6 +36,7 @@
36
36
  @import "fs_load_tile.scss";
37
37
  @import "fs_loader.scss";
38
38
  @import "fs_magic_config_field.scss";
39
+ @import "fs_map.scss";
39
40
  @import "fs_meta_field.scss";
40
41
  @import "fs_option_group.scss";
41
42
  @import "fs_pagination.scss";
@@ -1,15 +0,0 @@
1
- import { ComposableFactory, ServiceFactory } from "@dative-gpi/bones-ui/core";
2
- import type { AuthTokenDetailsDTO, AuthTokenFilters, AuthTokenInfosDTO, CreateAuthTokenDTO } from "@dative-gpi/foundation-shared-domain/models";
3
- import { AuthTokenDetails, AuthTokenInfos } from "@dative-gpi/foundation-shared-domain/models";
4
- import { AUTH_TOKENS_URL, AUTH_TOKEN_URL } from "../../foundation-shared-services/config/urls";
5
-
6
- const AuthTokenServiceFactory = new ServiceFactory<AuthTokenDetailsDTO, AuthTokenDetails>("authToken", AuthTokenDetails).create(factory => factory.build(
7
- factory.addGetMany<AuthTokenInfosDTO, AuthTokenInfos, AuthTokenFilters>(AUTH_TOKENS_URL, AuthTokenInfos),
8
- factory.addCreate<CreateAuthTokenDTO>(AUTH_TOKENS_URL),
9
- factory.addRemove(AUTH_TOKEN_URL),
10
- factory.addNotify()
11
- ));
12
-
13
- export const useAuthTokens = ComposableFactory.getMany(AuthTokenServiceFactory);
14
- export const useCreateAuthToken = ComposableFactory.create(AuthTokenServiceFactory);
15
- export const useRemoveAuthToken = ComposableFactory.remove(AuthTokenServiceFactory);