@dative-gpi/foundation-shared-components 0.0.188 → 0.0.190

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
  },
@@ -86,10 +86,10 @@ export default defineComponent({
86
86
  if (props.deviceAlerts < 1) {
87
87
  return null;
88
88
  }
89
- if (props.deviceAlerts > 8) {
89
+ if (props.deviceAlerts > 9) {
90
90
  return "9+";
91
91
  }
92
- return (props.deviceAlerts + 1).toString();
92
+ return (props.deviceAlerts).toString();
93
93
  });
94
94
 
95
95
  return {
@@ -0,0 +1,468 @@
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 { useAddress } from "../../composables/useAddress";
104
+
105
+ import FSMapEditPointAddressOverlay from "./FSMapEditPointAddressOverlay.vue";
106
+ import FSButton from '../FSButton.vue';
107
+ import FSCard from '../FSCard.vue';
108
+ import FSChip from '../FSChip.vue';
109
+ import FSCol from '../FSCol.vue';
110
+ import FSRow from '../FSRow.vue';
111
+
112
+
113
+ import { type Address, type SiteInfos } from '@dative-gpi/foundation-shared-domain/models';
114
+ import { type LocationInfos } from '@dative-gpi/foundation-shared-domain/models';
115
+
116
+ import { ColorEnum, type MapLayer } from "../../models";
117
+ import { useColors } from "../../composables";
118
+
119
+ export default defineComponent({
120
+ name: "FSMap",
121
+ components: {
122
+ FSMapEditPointAddressOverlay,
123
+ FSButton,
124
+ FSCard,
125
+ FSChip,
126
+ FSCol,
127
+ FSRow
128
+ },
129
+ props: {
130
+ height: {
131
+ type: [Array, String, Number] as PropType<string[] | number[] | string | number | null>,
132
+ required: false,
133
+ default: '400px'
134
+ },
135
+ width: {
136
+ type: [Array, String, Number] as PropType<string[] | number[] | string | number | null>,
137
+ required: false,
138
+ default: '100%'
139
+ },
140
+ sites: {
141
+ type: Array as PropType<SiteInfos[]>,
142
+ required: false,
143
+ default: () => [],
144
+ },
145
+ center: {
146
+ type: Array as PropType<number[]>,
147
+ required: false,
148
+ default: () => [45.71, 5.07]
149
+ },
150
+ selectedLayer: {
151
+ type: String,
152
+ required: false,
153
+ default: "osm"
154
+ },
155
+ selectableLayers: {
156
+ type: Array as PropType<string[]>,
157
+ required: false,
158
+ default: () => ["osm", "imagery"]
159
+ },
160
+ selectedLocationId: {
161
+ type: String as PropType<string | null>,
162
+ required: false,
163
+ default: null
164
+ },
165
+ selectedSiteId: {
166
+ type: String as PropType<string | null>,
167
+ required: false,
168
+ default: null
169
+ },
170
+ modelValue: {
171
+ type: Array as PropType<LocationInfos[]>,
172
+ required: false,
173
+ default: () => [],
174
+ },
175
+ editable: {
176
+ type: Boolean,
177
+ required: false,
178
+ default: false
179
+ },
180
+ showMyLocation: {
181
+ type: Boolean,
182
+ required: false,
183
+ default: true
184
+ },
185
+ showZoomButtons: {
186
+ type: Boolean,
187
+ required: false,
188
+ default: true
189
+ }
190
+ },
191
+ emits: ['update:modelValue', 'update:selectedLocationId', 'update:selectedSiteId'],
192
+ setup(props, { emit }) {
193
+ const { reverseSearch } = useAddress();
194
+ const { getColors } = useColors();
195
+
196
+ const innerSelectedLayer = ref(props.selectedLayer);
197
+ const innerModelValue = ref(props.modelValue);
198
+ const editingLocation = ref(false);
199
+
200
+ const mapId = `map-${uuidv4()}`;
201
+ const defaultZoom = 15;
202
+ const markers: { [key: string]: L.Marker } = {};
203
+ const sites: { [key: string]: L.Polygon } = {};
204
+ const siteLayerGroup = new L.FeatureGroup();
205
+ const baseLayerGroup = new L.LayerGroup();
206
+ const myLocationLayerGroup = new L.LayerGroup();
207
+
208
+ let map: L.Map;
209
+ let markerLayerGroup: L.FeatureGroup | MarkerClusterGroup;
210
+
211
+ if (props.editable) {
212
+ markerLayerGroup = new L.FeatureGroup();
213
+ }
214
+ else {
215
+ markerLayerGroup = new MarkerClusterGroup({
216
+ spiderfyOnMaxZoom: false,
217
+ showCoverageOnHover: false,
218
+ disableClusteringAtZoom: 17,
219
+ iconCreateFunction: function(cluster: any) {
220
+ return L.divIcon({
221
+ html: `<div>
222
+ <span>${cluster.getChildCount()}</span>
223
+ </div>`,
224
+ className: 'fs-map-location fs-map-location-full',
225
+ iconSize: [36, 36],
226
+ iconAnchor: [18, 18],
227
+ });
228
+ }
229
+ });
230
+ }
231
+ const mapLayers: MapLayer[] = [
232
+ {
233
+ name: "osm",
234
+ label: "OpenStreetMap",
235
+ layer: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
236
+ maxZoom: 19,
237
+ attribution: '© OpenStreetMap'
238
+ })
239
+ },
240
+ {
241
+ name: "imagery",
242
+ label: "Imagery",
243
+ layer: L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.jpg', {
244
+ maxZoom: 20,
245
+ attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
246
+ }),
247
+ },
248
+ {
249
+ name: "light",
250
+ label: "Light",
251
+ layer: L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png', {
252
+ maxZoom: 20,
253
+ 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',
254
+ })
255
+ }
256
+ ];
257
+
258
+ const style = computed((): { [key: string]: string | undefined } => {
259
+ return {
260
+ "--fs-map-location-pin-color": getColors(ColorEnum.Primary).base,
261
+ "--fs-map-mylocation-pin-color-alpha": getColors(ColorEnum.Primary).base + "50",
262
+ "--fs-map-leaflet-container-height": props.height as string,
263
+ };
264
+ });
265
+
266
+ const displayLocations = () => {
267
+ markerLayerGroup.clearLayers();
268
+ innerModelValue.value.forEach((location) => {
269
+ 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>`;
270
+ const icon = L.divIcon({
271
+ html: iconHtml,
272
+ className: 'fs-map-location',
273
+ iconSize: [36, 36],
274
+ iconAnchor: [18, 18],
275
+ });
276
+ const marker = L.marker([location.address.latitude, location.address.longitude], { icon }).addTo(markerLayerGroup);
277
+ markers[location.id] = marker;
278
+ marker.on('click', () => emit('update:selectedLocationId', location.id));
279
+
280
+ });
281
+ };
282
+
283
+ const displaySites = () => {
284
+ siteLayerGroup.clearLayers();
285
+ props.sites.forEach((site) => {
286
+ const sitePolygon = L.polygon(site.coordinates.map((coord) => [coord.latitude, coord.longitude]), {
287
+ color: site.color,
288
+ fillColor: site.color + "50",
289
+ fillOpacity: 0.5,
290
+ className: 'fs-map-site',
291
+ }).addTo(siteLayerGroup);
292
+
293
+ sites[site.id] = sitePolygon;
294
+ sitePolygon.on('click', () => emit('update:selectedSiteId', site.id));
295
+ });
296
+ }
297
+
298
+ const modifyLocationAddress = (locationId: string, newAddress: Address) => {
299
+ const location = innerModelValue.value.find((loc) => loc.id === locationId);
300
+ if (!location) {
301
+ return;
302
+ }
303
+ const newLocation = {
304
+ ...location,
305
+ address: {
306
+ ...newAddress
307
+ },
308
+ };
309
+ innerModelValue.value = innerModelValue.value.map((loc) => loc.id === locationId ? newLocation : loc);
310
+ };
311
+
312
+ const initMap = () => {
313
+ const mapOptions = {
314
+ zoomControl: false,
315
+ scrollWheelZoom: false,
316
+ minZoom: 2,
317
+ maxBounds: L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180)),
318
+ maxBoundsViscosity: 1.0
319
+ };
320
+ map = L.map(mapId, mapOptions).setView([props.center[0], props.center[1]], defaultZoom);
321
+ map.attributionControl.remove();
322
+ L.control.attribution({ position: 'bottomleft' }).addTo(map);
323
+
324
+ baseLayerGroup.addTo(map);
325
+ siteLayerGroup.addTo(map);
326
+ myLocationLayerGroup.addTo(map);
327
+ setMapBaseLayer(props.selectedLayer);
328
+ displaySites();
329
+ displayLocations();
330
+ markerLayerGroup.addTo(map);
331
+
332
+ if (innerModelValue.value.length > 0) {
333
+ map.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
334
+ }
335
+
336
+ map.on('click', (e: L.LeafletMouseEvent) => {
337
+ if (editingLocation.value) {
338
+ onNewCoordEntered(+e.latlng.lat.toFixed(6), +e.latlng.lng.toFixed(6));
339
+ }
340
+ });
341
+ };
342
+
343
+ const setMapBaseLayer = (layerName: string) => {
344
+ innerSelectedLayer.value = layerName;
345
+ const layer = mapLayers.find((mapLayer) => mapLayer.name === layerName) ?? mapLayers[0];
346
+ baseLayerGroup.clearLayers();
347
+ layer.layer.addTo(baseLayerGroup);
348
+ };
349
+
350
+ const onNewAddressEntered = (address: Address) => {
351
+ if (!props.selectedLocationId) { return; }
352
+ modifyLocationAddress(props.selectedLocationId, address);
353
+ map?.flyTo([address.latitude, address.longitude], map?.getZoom() ?? defaultZoom);
354
+ };
355
+
356
+ const onNewCoordEntered = async (lat: number, lng: number) => {
357
+ const address = await reverseSearch(lat, lng);
358
+
359
+ onNewAddressEntered({
360
+ ...address,
361
+ latitude: lat,
362
+ longitude: lng,
363
+ });
364
+ };
365
+
366
+ const zoomIn = () => {
367
+ map?.zoomIn();
368
+ };
369
+
370
+ const zoomOut = () => {
371
+ map?.zoomOut();
372
+ };
373
+
374
+ const locate = () => {
375
+ map?.locate();
376
+ map?.on('locationfound', (e: L.LocationEvent) => {
377
+ map?.flyTo(e.latlng, map?.getZoom() ?? defaultZoom);
378
+ const iconHtml = `<div class="fs-map-mylocation-pin"></div>`;
379
+ const icon = L.divIcon({
380
+ html: iconHtml,
381
+ className: 'fs-map-mylocation',
382
+ iconSize: [16, 16],
383
+ iconAnchor: [8, 8],
384
+ });
385
+ myLocationLayerGroup.clearLayers();
386
+ L.marker(e.latlng, { icon }).addTo(myLocationLayerGroup);
387
+ });
388
+ };
389
+
390
+ const onCancel = () => {
391
+ editingLocation.value = false;
392
+ innerModelValue.value = props.modelValue;
393
+ displayLocations();
394
+ if (innerModelValue.value.length > 0) {
395
+ map?.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
396
+ }
397
+ else {
398
+ map?.flyTo([props.center[0], props.center[1]], map?.getZoom() ?? defaultZoom);
399
+ }
400
+ if (props.modelValue.length > 1) {
401
+ emit('update:selectedLocationId', null);
402
+ }
403
+ };
404
+
405
+ const onSubmit = () => {
406
+ emit('update:modelValue', innerModelValue.value);
407
+ editingLocation.value = false;
408
+ if (innerModelValue.value.length > 0) {
409
+ map?.fitBounds(markerLayerGroup.getBounds(), { maxZoom: defaultZoom });
410
+ }
411
+ else {
412
+ map?.flyTo([props.center[0], props.center[1]], map?.getZoom() ?? defaultZoom);
413
+ }
414
+ if (props.modelValue.length > 1) {
415
+ emit('update:selectedLocationId', null);
416
+ }
417
+ };
418
+
419
+ onMounted(() => {
420
+ initMap();
421
+ });
422
+
423
+ watch(() => innerModelValue.value, () => {
424
+ displayLocations();
425
+ });
426
+
427
+ watch(() => props.selectedLocationId, () => {
428
+ Object.values(markers).forEach((marker) => {
429
+ marker.getElement()?.classList.remove('fs-map-location-selected');
430
+ });
431
+
432
+ if (!props.selectedLocationId) {
433
+ return;
434
+ }
435
+ const marker = markers[props.selectedLocationId];
436
+ marker.getElement()?.classList.add('fs-map-location-selected');
437
+ map?.flyTo(marker.getLatLng(), 17);
438
+ })
439
+
440
+ watch(() => props.selectedSiteId, () => {
441
+ if (!props.selectedSiteId) {
442
+ return;
443
+ }
444
+ const site = sites[props.selectedSiteId];
445
+ if (site) {
446
+ map?.fitBounds(site.getBounds(), { maxZoom: 17 });
447
+ }
448
+ });
449
+
450
+ return {
451
+ innerSelectedLayer,
452
+ editingLocation,
453
+ innerModelValue,
454
+ mapLayers,
455
+ mapId,
456
+ style,
457
+ onNewAddressEntered,
458
+ onNewCoordEntered,
459
+ setMapBaseLayer,
460
+ onCancel,
461
+ onSubmit,
462
+ zoomOut,
463
+ locate,
464
+ zoomIn
465
+ };
466
+ },
467
+ });
468
+ </script>
@@ -0,0 +1,165 @@
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 type { PropType } from "vue";
81
+ import { defineComponent, ref, watch } from "vue";
82
+
83
+ import FSCard from '../FSCard.vue'
84
+ import FSCol from '../FSCol.vue'
85
+ import FSRow from '../FSRow.vue'
86
+ import FSText from '../FSText.vue'
87
+ import FSButton from '../FSButton.vue'
88
+ import FSNumberField from '../fields/FSNumberField.vue'
89
+ import FSForm from '../FSForm.vue'
90
+ import FSAutoCompleteAddress from '../autocompletes/FSAutoCompleteAddress.vue'
91
+
92
+ import { Address } from "@dative-gpi/foundation-shared-domain/models/locations/address";
93
+
94
+
95
+ export default defineComponent({
96
+ name: "FSMapEditPointAddressOverlay.vue",
97
+ components: {
98
+ FSCard,
99
+ FSCol,
100
+ FSRow,
101
+ FSText,
102
+ FSButton,
103
+ FSNumberField,
104
+ FSAutoCompleteAddress,
105
+ FSForm
106
+ },
107
+ props: {
108
+ modelValue: {
109
+ type: Object as PropType<Address>,
110
+ default: null,
111
+ required: false,
112
+ },
113
+ },
114
+ emits: ['update:modelValue', 'update:locationCoord', 'submit', 'cancel'],
115
+ setup(props, { emit }) {
116
+ const menuLocationCoord = ref(false);
117
+
118
+ const latitude = ref(props.modelValue.latitude);
119
+ const longitude = ref(props.modelValue.longitude);
120
+
121
+ const onCoordinateChange = () => {
122
+ const newModelValue = new Address({
123
+ country: "",
124
+ formattedAddress: "",
125
+ locality: "",
126
+ placeId: "",
127
+ placeLabel: "",
128
+ latitude: latitude.value,
129
+ longitude: longitude.value,
130
+ });
131
+ emit('update:locationCoord', newModelValue);
132
+ };
133
+
134
+ const onAddressFieldSubmit = (address: Address|null) => {
135
+ if(address === null) {
136
+ return;
137
+ }
138
+ emit('update:modelValue', address);
139
+ };
140
+
141
+ const onSubmit = () => {
142
+ emit('submit');
143
+ };
144
+
145
+ const onCancel = () => {
146
+ emit('cancel');
147
+ };
148
+
149
+ watch(() => props.modelValue, (value) => {
150
+ latitude.value = value.latitude;
151
+ longitude.value = value.longitude;
152
+ });
153
+
154
+ return {
155
+ onAddressFieldSubmit,
156
+ onCoordinateChange,
157
+ onSubmit,
158
+ onCancel,
159
+ latitude,
160
+ longitude,
161
+ menuLocationCoord,
162
+ };
163
+ },
164
+ });
165
+ </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,7 @@
1
+ import type { Layer } from "leaflet";
2
+
3
+ export interface MapLayer {
4
+ name:string;
5
+ label:string;
6
+ layer: Layer;
7
+ }
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.188",
4
+ "version": "0.0.190",
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.188",
14
- "@dative-gpi/foundation-shared-services": "0.0.188"
13
+ "@dative-gpi/foundation-shared-domain": "0.0.190",
14
+ "@dative-gpi/foundation-shared-services": "0.0.190",
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": "da23d7f7a5dc7d6dced0f7651bc359c61bcb8eb1"
38
+ "gitHead": "226125292c2f5ed27334b3bdee88b1877a9d9d6a"
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);