diff --git a/src/components/ControlFab.vue b/src/components/ControlFab.vue index 690e3ad..ce8ea7f 100644 --- a/src/components/ControlFab.vue +++ b/src/components/ControlFab.vue @@ -37,12 +37,16 @@ type CommandAck = { message: string; }; -function setOpenedAppFab(fab: ControlCategoryKey) { +function setOpenedAppFab(fab: ControlCategoryKey | null) { leafletStore.fabs.appControl.openedFab = fab; } function handleOpenedAppFabUpdate(val: boolean, key: ControlCategoryKey) { - if (val) setOpenedAppFab(key); + if (val) { + setOpenedAppFab(key); + } else { + setOpenedAppFab(null); + } } function formatUnknownError(commandLabel: string, error: unknown): NotificationResult { @@ -138,6 +142,23 @@ async function runRoutingCommand(cmdAttr: ControlAction): Promise -import { ref, computed, onMounted } from 'vue'; +import { computed, onMounted } from 'vue'; import { useLeafletStore } from 'stores/leaflet'; -import { LocateControl } from 'leaflet.locatecontrol'; +import { storeToRefs } from 'pinia'; const leafletStore = useLeafletStore(); - -const locateControl = new LocateControl({ - position: 'topleft', - flyTo: true, - keepCurrentZoomLevel: true, - returnToPrevBounds: true, - layer: leafletStore.layerGroups.locateControl ?? undefined, -}); - -const locationControlInit = ref(false); -const locateControlActive = ref(false); -const locateControlFollowing = ref(false); -const locationFound = ref(false); +const { locateControl } = storeToRefs(leafletStore); const iconColor = computed(() => - locateControlActive.value - ? locationFound.value - ? locateControlFollowing.value + locateControl.value.status.active + ? locationControl.value.status.Found + ? locateControl.value.status.Following ? '#FC8428FF' : '#2074B6FF' : '#FFFFFF' @@ -29,7 +17,11 @@ const iconColor = computed(() => ); const iconClass = computed(() => { - if (locateControlActive.value && !locationFound.value && !locateControlFollowing.value) { + if ( + locateControl.value.status.active && + !locateControl.value.status.found && + !locateControl.value.status.following + ) { return 'locate-control-icon-spinner'; } else { return ''; @@ -38,53 +30,18 @@ const iconClass = computed(() => { const handleClick = () => { initialize(); - if (locateControlActive.value) { + if (locateControl.value.status.active) { console.log('Stopping locate control'); - locateControl.stop(); + leafletStore.stopLocate(); } else { console.log('Starting locate control'); - locateControl.start(); + leafletStore.startLocate(); } }; function initialize() { - if (!locationControlInit.value) { - if (leafletStore.mapObject) { - locationControlInit.value = true; - leafletStore.mapObject.addControl(locateControl); - - leafletStore.mapObject?.on('locateactivate', function () { - console.log('Locate control activated'); - locateControlActive.value = true; - }); - - leafletStore.mapObject?.on('locatedeactivate', function () { - console.log('Locate control deactivated'); - locateControlActive.value = false; - leafletStore.layerGroups.locateControl?.clearLayers(); - }); - - leafletStore.mapObject?.on('locationfound', function (e) { - console.log('Location Found: ', e); - console.log('Location found:', e.latlng); - console.log('Accuracy:', e.accuracy, 'meters'); - locateControlFollowing.value = true; - locationFound.value = true; - }); - - leafletStore.mapObject?.on('moveend', function () { - if (locateControlFollowing.value) { - locateControlFollowing.value = false; - } - }); - - leafletStore.mapObject?.on('locationtimeout', function (e) { - const timeoutEvent = e as typeof e & { count?: number }; - console.log('Location timeout count:', timeoutEvent.count); - locationFound.value = false; - leafletStore.layerGroups.locateControl?.clearLayers(); - }); - } + if (!locationControl.value.status.init) { + leafletStore.initLocateControl(); } } diff --git a/src/components/leaflet/LocationDrawer.vue b/src/components/leaflet/LocationDrawer.vue index 4796162..3d3b6dd 100644 --- a/src/components/leaflet/LocationDrawer.vue +++ b/src/components/leaflet/LocationDrawer.vue @@ -269,6 +269,7 @@ function zoomToCoords(arg: string) { " @item-clicked="zoomToCoords" @contextmenu.prevent="onDrawerContextMenu($event, key)" + @touchstart.prevent="onDrawerContextMenu($event, key)" active-class="text-orange" /> diff --git a/src/composables/useAddLocation.ts b/src/composables/useAddLocation.ts index 833c19d..2b6e631 100644 --- a/src/composables/useAddLocation.ts +++ b/src/composables/useAddLocation.ts @@ -52,6 +52,7 @@ export async function useAddLocation( const simulationStore = useSimulationStore(); const locationStore = useLocationStore(); try { + console.log ('address: %s, getRevGeocode: %s', address, getRevGeocode); const revGeo = !address && getRevGeocode /* ? await reverseGeocodeRateLimited(coords.lat, coords.lng) */ diff --git a/src/constants/controls.ts b/src/constants/controls.ts index 8a02a54..36b8cd4 100644 --- a/src/constants/controls.ts +++ b/src/constants/controls.ts @@ -306,6 +306,16 @@ const baseControls = { iconColor: 'grey-10', isVisible: true, }, + set_route_start_to_current_location: { + name: 'Set route start to current location', + cmd: 'set_route_start_to_current_location', + cmdClass: 'routing_cntrl_class', + icon: 'mdi-navigation', + cnfrm: false, + disabled: false, + iconColor: 'grey-10', + isVisible: true, + }, }, }, } satisfies Record; @@ -349,7 +359,8 @@ export function useControls() { return true; }), routing: computed(() => { - return Boolean(leafletStore.routeSet.start.lng && leafletStore.routeSet.end.lng); + return true; +// return Boolean(leafletStore.routeSet.start.lng && leafletStore.routeSet.end.lng); }), } satisfies Record>; diff --git a/src/layouts/FullMapLayout.vue b/src/layouts/FullMapLayout.vue index ee4ad5f..3ac3256 100644 --- a/src/layouts/FullMapLayout.vue +++ b/src/layouts/FullMapLayout.vue @@ -23,7 +23,13 @@ useMeta({ function layoutClickHandler(event: LeafletMouseEvent) { // if (!leafletStore.locationDrawerSettings.opened && !leafletStore.controlDrawerSettings.opened && !leafletStore.statusBarSettings.opened) { - if (!leafletStore.locationDrawerSettings.opened && !leafletStore.fabs.statusBar) { + if ( + !leafletStore.locationDrawerSettings.opened && + !leafletStore.fabs.statusBar.opened && + !leafletStore.fabs.layerControl.opened && + !leafletStore.fabs.favorites.opened && + !leafletStore.fabs.appControl.opened + ) { return; } if ( @@ -33,9 +39,19 @@ function layoutClickHandler(event: LeafletMouseEvent) { ) { leafletStore.locationsOpenedHandler(false); } - if (!event.target.closest('#status-bar') && leafletStore.fabs.statusBar) { + if (!event.target.closest('#status-bar') && leafletStore.fabs.statusBar.opened) { leafletStore.fabs.statusBar.opened = false; } + if (!event.target.closest('#app-ctrl') && leafletStore.fabs.appControl.opened) { + leafletStore.fabs.appControl.opened = false; + } + if (!event.target.closest('#favorite-ctrl') && leafletStore.fabs.favorites.opened) { + leafletStore.fabs.favorites.opened = false; + } + if (!event.target.closest('#layer-ctrl') && leafletStore.fabs.layerControl.opened) { + leafletStore.fabs.layerControl.opened = false; + } + /* if (!event.target.closest('#control-drawer') && !leafletStore.controlDrawerSettings.pinned) { leafletStore.locationsOpenedHandler(false); diff --git a/src/stores/leaflet.ts b/src/stores/leaflet.ts index 6316525..e762192 100644 --- a/src/stores/leaflet.ts +++ b/src/stores/leaflet.ts @@ -6,6 +6,7 @@ import * as L from 'leaflet'; import type { Map, LatLngExpression, LayerGroup, FeatureGroup, Marker } from 'leaflet'; import type { SearchResult } from 'components/models'; import type { IRouter } from 'leaflet-routing-machine'; +import { LocateControl } from 'leaflet.locatecontrol'; import { customRouter } from 'functions/serviceURL'; import { startIcon, intermediateIcon, endIcon } from 'constants/icons'; import type { @@ -66,7 +67,20 @@ interface RoutingOptions { createMarker: (i: number, waypoint: RoutingWaypoint, n: number) => L.Marker | false; } -interface State { +interface LcStatus { + active: boolean; + found: boolean; + following: boolean; + init: boolean; +} + +interface LocateControlInstance { + lc: ReturnType>; + status: LcStatus; + latLng: L.LatLng | null; +} + +interface StoreState { zoom: number; center: [number, number] | [null, null] | null; mapObject: ReturnType>; @@ -85,6 +99,7 @@ interface State { }; layerControl: LayerControl; layerGroups: LayerGroups; + locateControl: LocateControlInstance; routeSet: { start: LatLng; end: LatLng; @@ -105,7 +120,7 @@ interface State { } export const useLeafletStore = defineStore('leafletStore', { - state: (): State => { + state: (): StoreState => { return { zoom: 10, center: [favorites.home.coords.lat, favorites.home.coords.lng], @@ -128,6 +143,16 @@ export const useLeafletStore = defineStore('leafletStore', { findMy: shallowRef(null), locateControl: shallowRef(null), }, + locateControl: { + lc: shallowRef(null), + status: { + active: false, + following: false, + init: false, + found: false, + }, + latLng: null as L.LatLng | null, + }, leafletDrawerOpen: false, locationDrawerSettings: { opened: false, @@ -171,45 +196,48 @@ export const useLeafletStore = defineStore('leafletStore', { value: 1, label: 'Open Street Map', layer: shallowRef( - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - { - maxZoom: 19, - attribution: '© OpenStreetMap', - minZoom: 2, - noWrap: true, - } - )) + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap', + minZoom: 2, + noWrap: true, + }), + ), }, - { - value: 2, - label: 'Topography', - layer: shallowRef(L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { - minZoom: 2, - maxZoom: 17, - attribution: '© OpenTopoMap', - noWrap: true, - })) - }, - { - value: 3, - label: 'Satellite', - layer: shallowRef(L.tileLayer( - 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { - minZoom: 2, - maxZoom: 19, - attribution: '© ArcGIS', - noWrap: true, - })) - } - ], + value: 2, + label: 'Topography', + layer: shallowRef( + L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + minZoom: 2, + maxZoom: 17, + attribution: '© OpenTopoMap', + noWrap: true, + }), + ), + }, + { + value: 3, + label: 'Satellite', + layer: shallowRef( + L.tileLayer( + 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', + { + minZoom: 2, + maxZoom: 19, + attribution: '© ArcGIS', + noWrap: true, + }, + ), + ), + }, + ], }, baseLayerWMS: { selected: false, label: 'OpenStreetMap Transparent', - layer: shallowRef(L.tileLayer.wms( - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - { + layer: shallowRef( + L.tileLayer.wms('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { minZoom: 2, maxZoom: 19, attribution: '© OpenStreetMap', @@ -217,8 +245,8 @@ export const useLeafletStore = defineStore('leafletStore', { transparent: true, format: 'image/png', opacity: 0.5, - } - )), + }), + ), }, routingOptions: { waypoints: [], @@ -259,6 +287,61 @@ export const useLeafletStore = defineStore('leafletStore', { setMap(map: Map) { this.mapObject = map; }, + initLocateControl() { + const lc = new LocateControl({ + position: 'topleft', + flyTo: true, + keepCurrentZoomLevel: true, + returnToPrevBounds: true, + layer: this.layerGroups.locateControl ?? undefined, + }); + this.locateControl.lc = lc; + this.locateControl.status.init = true; + if (this.mapObject) { + this.mapObject.addControl(lc); + this.mapObject.on('locationerror', () => { + this.locateControl.latLng = null; + }); + this.mapObject.on('locateactivate', () => { + console.log('Locate control activated'); + this.locateControl.status.active = true; + }); + this.mapObject.on('locatedeactivate', () => { + console.log('Locate control deactivated'); + this.locateControl.status.active = false; + this.layerGroups.locateControl?.clearLayers(); + }); + this.mapObject.on('locationfound', (e) => { + console.log('Location Found: ', e); + console.log('Location found:', e.latlng); + console.log('Accuracy:', e.accuracy, 'meters'); + this.locateControl.latLng = e.latlng; + this.locateControl.status.following = true; + this.locateControl.status.found = true; + }); + this.mapObject.on('moveend', () => { + if (this.locateControl.status.following) { + this.locateControl.status.following = false; + } + }); + this.mapObject.on('locationtimeout', (e) => { + const timeoutEvent = e as typeof e & { count?: number }; + console.log('Location timeout count:', timeoutEvent.count); + this.locateControl.status.found = false; + this.layerGroups.locateControl?.clearLayers(); + }); + } + }, + startLocate() { + if (this.locateControl.lc) { + this.locateControl.lc.start(); + } + }, + stopLocate() { + if (this.locateControl.lc) { + this.locateControl.lc.stop(); + } + }, /* setLayerGroup(layerGroup: LayerGroup, name: string) this.layerGroups[name] = layerGroup; @@ -293,23 +376,23 @@ export const useLeafletStore = defineStore('leafletStore', { this.zoom = zoom; }, zoomIn() { - this.mapObject?.zoomIn() + this.mapObject?.zoomIn(); }, zoomOut() { - this.mapObject?.zoomOut() + this.mapObject?.zoomOut(); }, setBaseLayer(newVal?: number, oldVal?: number) { if (newVal === undefined && this.baseLayer.selected === undefined) return; if (oldVal && this.mapObject) { - const oldLayer = this.baseLayer.options.filter(u => u.value === oldVal)[0]; + const oldLayer = this.baseLayer.options.filter((u) => u.value === oldVal)[0]; if (oldLayer) { - console.log("OldLayer name: ", oldLayer.label); + console.log('OldLayer name: ', oldLayer.label); oldLayer.layer!.remove(); } } - const bLayer = this.baseLayer.options.filter(u => u.value === this.baseLayer.selected)[0]; - if(this.mapObject && bLayer) { - console.log("NewLayer name: ", bLayer.label); + const bLayer = this.baseLayer.options.filter((u) => u.value === this.baseLayer.selected)[0]; + if (this.mapObject && bLayer) { + console.log('NewLayer name: ', bLayer.label); bLayer.layer!.addTo(this.mapObject); } }, @@ -422,12 +505,28 @@ export const useLeafletStore = defineStore('leafletStore', { } return { type: result.notificationType, message: result.responseMessage }; }, + setRouteStart(lat: number, lng: number) { + this.layerControl.routing = true; + this.routeSet.start = { lat: lat, lng: lng }; + return { type: 'positive', message: 'Route start set.' }; + }, + getGeoLocationAsync() { + return new Promise((resolve, reject) => { + this.locateControl?.lc?.start(); + this.mapObject?.once('locationfound', function (e) { + resolve(e.latlng); + }); + this.mapObject?.once('locationerror', function (e) { + reject(new Error(e.message)); + }); + }); + }, clearRoute() { this.layerControl.routing = false; this.clearRouteSegments(); - this.routeSet.start = {lat: null, lng: null}; - this.routeSet.end = {lat: null, lng: null}; - return {type: 'positive', message: 'Route cleared successfully'}; + this.routeSet.start = { lat: null, lng: null }; + this.routeSet.end = { lat: null, lng: null }; + return { type: 'positive', message: 'Route cleared successfully' }; }, moveToLocation(location: SearchResult) { if (!location) return; @@ -443,9 +542,9 @@ export const useLeafletStore = defineStore('leafletStore', { // Otherwise, pan to center this.mapObject?.setView([lat, lng], 14); } - let searchMarker: Marker = new L.Marker([0,0]); - // Update/create a marker at the selected location - if (searchMarker) { + let searchMarker: Marker = new L.Marker([0, 0]); + // Update/create a marker at the selected location + if (searchMarker) { searchMarker.setLatLng([lat, lng]); } else { searchMarker = L.marker([lat, lng]).addTo(this.mapObject!);