leaflet routimg machine - lical osr

This commit is contained in:
2026-04-01 10:30:34 -04:00
parent 05e63a28f1
commit e63a8a6329
27 changed files with 1178 additions and 213 deletions

View File

@@ -1,9 +1,9 @@
<template>
<div class="q-pa-md">
<div class="">
<q-layout
view="hHh Lpr fFf"
container
style="height: 600px; width: 100vw"
style="height: 600px; max-width: 800px; width: 100vw"
class="rounded-borders"
>
<q-footer :class="$q.dark.isActive ? 'bg-primary' : 'bg-black'" style="height: 48px">
@@ -31,35 +31,53 @@
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn label="Routing" @click="routeLayer = !routeLayer" size="sm" stretch />
<q-btn-dropdown label="Routing" size="sm" stretch v-if="routeSet.start && routeSet.end">
<q-list>
<q-item v-close-popup v-ripple clickable @click="routeToQueue">
<q-item-section>
<q-item-label>Add Route to Sim Queue</q-item-label>
</q-item-section>
</q-item>
<q-item v-close-popup v-ripple clickable @click="clearRoute">
<q-item-section>
<q-item-label>clearRoute</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar>
</q-footer>
<q-drawer
v-model="qLocDrawer"
show-if-above
mini-to-overlay
overlay
:width="300"
side="left"
:breakpoint="500"
:mini="miniState"
@mouseenter="miniState = false"
@mouseleave="miniState = true"
class="leafletDrawer"
>
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: 0 }">
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '50' }">
<q-list padding>
<q-item-label header>Location Queue</q-item-label>
<q-item-label header
><span class="bold">Location Queue: </span> {{ simulationState }}</q-item-label
>
<q-separator />
<LocationMark
v-for="key in locationQueueOrder"
v-for="(key, index) in locationQueueOrder"
:key="key"
:loc_id="key"
:active="locationQueueData[key].loc_id === currentLocation.loc_id"
:start="locationQueueData[key].start"
:address="locationQueueData[key].address"
:latitude="locationQueueData[key].latitude"
:longitude="locationQueueData[key].longitude"
:end="locationQueueData[key].end"
:active="
(locationQueueData as Record<string, any>)[key]?.loc_id === currentLocation?.loc_id
"
:isLast="index != locationQueueOrder.length - 1"
:start="(locationQueueData as Record<string, any>)[key]?.start ?? ''"
:address="(locationQueueData as Record<string, any>)[key]?.address ?? ''"
:latitude="(locationQueueData as Record<string, any>)[key]?.latitude ?? 0"
:longitude="(locationQueueData as Record<string, any>)[key]?.longitude ?? 0"
:delay="(locationQueueData as Record<string, any>)[key]?.delay ?? 0"
:end="(locationQueueData as Record<string, any>)[key]?.end ?? undefined"
@item-clicked="zoomToCoods"
/>
</q-list>
@@ -68,12 +86,13 @@
<q-page-container>
<q-page>
<div style="height: 560px; width: 90vw; color: #000000">
<div style="height: 550px; width: 100vw; max-width: 800px; color: #000000">
<L-Map
id="map"
ref="mapRef"
:center="center"
:center="safeCenter"
:zoom="zoom"
style="height: 550px; width: 100%"
style="height: 550px; width: 100vw; max-width: 800px"
@click="updateMarker"
@ready="onMapReady"
>
@@ -81,38 +100,60 @@
layer-type="base"
name="OpenStreetMap"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@click="updateMarker($event.latlng)"
></L-Tile-Layer>
<L-Layer-Group>
<L-Marker v-if="markerLatLng" :lat-lng="markerLatLng" @click="handleMarkerClick">
<L-Marker
v-if="safeMarkerLatLng"
:lat-lng="safeMarkerLatLng"
@click="handleMarkerClick"
>
<L-Popup :options="{ closeOnClick: true }">
<q-list dense seperator style="min-width: 150px" class="bg-grey-10">
<q-item clickable v-close-popup @click="handleAddLocation">
<q-item-section avatar><q-icon name="add_location" /></q-item-section>
<q-item-section>Add Location</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="setStartRoute">
<q-item-section avatar><q-icon name="add_location" /></q-item-section>
<q-item-section>Set Route Start</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="setEndRoute">
<q-item-section avatar><q-icon name="add_location" /></q-item-section>
<q-item-section>Set Route End</q-item-section>
</q-item>
</q-list>
</L-Popup>
</L-Marker>
</L-Layer-Group>
<L-Layer-Group v-if="locationQueueOrder">
<L-Marker
v-for="locid in locationQueueOrder"
:key="locid"
:icon="getCustomIcon(locid)"
:lat-lng="[locationQueueData[locid].latitude, locationQueueData[locid].longitude]"
:icon="getCustomIcon(locid) as any"
:lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
>
</L-Marker>
</L-Layer-Group>
<L-Layer-Group v-if="routeLayer">
<L-Layer-Group v-if="routeSet.start && routeSet.end">
<LRoutingMachine
v-bind="routingOptions"
@routingstart="debugRoutingEvent"
@routesfound="debugRoutingEvent"
@routesfound="handleRoutesFound"
@routingerror="debugRoutingEvent"
/>
</L-Layer-Group>
<L-Layer-Group v-if="findMyUpdate">
<L-Marker
v-if="findMyUpdate"
:icon="fmIcon"
:lat-lng="[findMyUpdate.latitude, findMyUpdate.longitude]"
:icon="fmIcon as any"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
></L-Marker>
<L-Circle
:fillOpacity="0.5"
:lat-lng="[findMyUpdate.latitude, findMyUpdate.longitude]"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
:radius="findMyUpdate.horizontalAccuracy"
color="firebrick"
fillColor="indianred"
@@ -129,40 +170,76 @@
<script lang="ts" setup>
import { useQuasar } from 'quasar';
import { computed, onMounted, reactive, ref } from 'vue';
// Leaflet imports
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
import 'leaflet-geosearch/dist/geosearch.css';
import 'leaflet/dist/leaflet.css';
import { LCircle, LLayerGroup, LMap, LMarker, LPopup, LTileLayer } from '@vue-leaflet/vue-leaflet';
import * as LeafLet from 'leaflet';
// Custom Components
import LRoutingMachine from 'components/LRoutingMachine.vue';
import LocationMark from 'components/LocationMark.vue';
import SetLocationDialog from 'components/SetLocationDialog.vue';
import { customRouter } from 'functions/serviceURL';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocode';
import { useRoutingEvents } from '../composables/useRoutingEvents';
import { useMarkerContextMenu } from '../composables/useMarkerContextMenu';
import type { IRouter } from 'leaflet-routing-machine';
import type { coords, SearchControlProps } from 'src/types';
// Types
import type { coords, SearchControlProps, NominatimAddress } from 'components/models';
import type { LeafletMouseEvent, Map } from 'leaflet';
// Stores
import { storeToRefs } from 'pinia';
import { useSocketioStore } from 'stores/socketio';
import { useLeafletStore } from 'stores/leaflet';
import SetLocationDialog from 'components/SetLocationDialog.vue';
import { LCircle, LLayerGroup, LMap, LMarker, LTileLayer } from '@vue-leaflet/vue-leaflet';
import { favorites } from 'constants/favorites';
import { route } from 'quasar/wrappers';
const leafletStore = useLeafletStore();
const { zoom, center, markerLatLng, qLocDrawer } = storeToRefs(leafletStore);
const { zoom, center, markerLatLng, qLocDrawer, routeSet, routeSegments } =
storeToRefs(leafletStore);
const socketStore = useSocketioStore();
const { currentLocation, nextLocation, locationQueueData, locationQueueOrder, findMyUpdate } =
storeToRefs(socketStore);
const {
currentLocation,
nextLocation,
locationQueueData,
locationQueueOrder,
findMyUpdate,
simulationState,
testMode,
} = storeToRefs(socketStore);
const $q = useQuasar();
const mapRef = ref();
const responseMessage = ref('');
const routeStart = ref(null);
const routeEnd = ref(null);
const routeLayer = ref(false);
const miniState = ref(true);
const loading = ref(false);
const safeCenter = computed<[number, number]>(() => {
const lat = center.value?.[0];
const lng = center.value?.[1];
if (typeof lat === 'number' && typeof lng === 'number') {
return [lat, lng];
}
return [favorites.home.coords.lat, favorites.home.coords.lng];
});
const safeMarkerLatLng = computed<[number, number] | null>(() => {
const lat = markerLatLng.value?.[0];
const lng = markerLatLng.value?.[1];
if (typeof lat === 'number' && typeof lng === 'number') {
return [lat, lng];
}
return null;
});
const onMapReady = (map: Map) => {
const provider = new OpenStreetMapProvider();
@@ -172,7 +249,7 @@ const onMapReady = (map: Map) => {
autoClose: true,
updateMap: true,
showPopup: true,
style: 'bar',
style: 'button',
acceptAutoLoad: true,
autoComplete: true,
autoCompleteDelay: 250,
@@ -195,6 +272,9 @@ const fmIcon = new Icon({
});
const getCustomIcon = (locid: string) => {
const currentIndex = currentLocation.value
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: 0;
const locationIndex = locationQueueOrder.value.indexOf(locid);
if (currentLocation.value && currentLocation.value.loc_id === locid) {
return new Icon({
@@ -209,21 +289,26 @@ const getCustomIcon = (locid: string) => {
return new Icon({
color: 'blue',
accentColor: 'firebrick',
content: locationIndex.toString(),
content: (locationIndex - currentIndex).toString(),
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
};
const routingOptions = reactive({
waypoints: [
[40.910773020811, -73.891069806448],
[40.90930366920829, -73.87658695470259],
],
const routingOptions = reactive<{
waypoints: LeafLet.LatLng[];
router: IRouter;
routeWhileDragging: boolean;
}>({
waypoints: [],
router: customRouter as unknown as IRouter,
routeWhileDragging: true,
});
const debugRoutingEvent = (event) => {
const { handleRoutesFound } = useRoutingEvents();
const debugRoutingEvent = (event: Event) => {
console.log(`${event.type} event: `, event);
};
@@ -232,31 +317,101 @@ function updateMarker(event: LeafletMouseEvent) {
center.value = [event.latlng.lat, event.latlng.lng];
}
function handleMarkerClick(event: LeafletMouseEvent) {
$q.dialog({
component: SetLocationDialog,
componentProps: {
lat: event.latlng.lat,
lng: event.latlng.lng,
},
})
.onOk((delay: number) => {
void setLocation({ lat: event.latlng.lat, lng: event.latlng.lng }, delay);
console.log(
'Confirmed location add: latitude: ' +
event.latlng.lat +
', longitude: ' +
event.latlng.lng +
', delay: ' +
delay,
function routeToQueue() {
console.log('routeToQueue');
if (routeSet.value.start && routeSet.value.end && routeSegments.value) {
console.log('routeToQueue: start: ', routeSet.value.start);
setLocation({ lat: routeSet.value.start.lat, lng: routeSet.value.start.lng }, 0);
routeSegments.value.forEach((segment: any, index: number) => {
console.log('routeToQueue: segment: ', segment);
setLocation(
{ lat: segment.toCoordinates.lat, lng: segment.toCoordinates.lng },
segment.timeSeconds,
);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
console.log('routeToQueue: end: ', routeSet.value.end);
setLocation({ lat: routeSet.value.end.lat, lng: routeSet.value.end.lng }, 0);
}
}
function clearRoute() {
routeSegments.value = [];
routingOptions.waypoints = [];
routeSet.value.start = { lat: null, lng: null };
routeSet.value.end = { lat: null, lng: null };
$q.notify({ type: 'positive', message: 'Route cleared' });
}
const updateRoute = () => {
const waypoints: LeafLet.LatLng[] = [];
const start = routeSet.value.start;
const end = routeSet.value.end;
if (start && typeof start.lat === 'number' && typeof start.lng === 'number') {
waypoints.push(LeafLet.latLng(start.lat, start.lng));
}
if (end && typeof end.lat === 'number' && typeof end.lng === 'number') {
waypoints.push(LeafLet.latLng(end.lat, end.lng));
}
routingOptions.waypoints = waypoints;
};
const { clickedLatLng, handleMarkerClick, setStartRoute, setEndRoute } = useMarkerContextMenu(
routeSet,
updateRoute,
);
function handleAddLocation() {
if (clickedLatLng.value) {
const latlng = clickedLatLng.value;
$q.notify(`add location...${latlng.toString()}`);
reverseGeocode(latlng.lat, latlng.lng)
.then((data) => {
const NomAddress = data.address as unknown as NominatimAddress;
$q.dialog({
component: SetLocationDialog,
componentProps: {
lat: Number(latlng.lat),
lng: Number(latlng.lng),
address: NomAddress,
},
})
.onOk((delay: number) => {
void setLocation({ lat: Number(latlng.lat), lng: Number(latlng.lng) }, delay);
console.log(
'Confirmed location add: latitude: ' +
latlng.lat +
', longitude: ' +
latlng.lng +
', delay: ' +
delay,
);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
})
.catch((error) => {
console.error('Error fetching reverse geocode:', error);
});
}
}
async function reverseGeocode(lat: number, lng: number) {
loading.value = true;
try {
const response = await reverseGeocodeRateLimited(lat, lng);
console.log('reverse geocode response: ', response);
return response;
} catch (error) {
console.error('Error fetching reverse geocode:', error);
throw error;
} finally {
loading.value = false;
}
}
function setLocation(coords: coords, delay: number) {
@@ -284,10 +439,11 @@ function setLocation(coords: coords, delay: number) {
}
function zoomToCoods(arg: string) {
leafletStore.setCenter(
locationQueueData.value[arg].latitude,
locationQueueData.value[arg].longitude,
);
const item = locationQueueData.value[arg];
if (!item || item.latitude == null || item.longitude == null) {
return;
}
leafletStore.setCenter(item.latitude, item.longitude);
leafletStore.setZoom(50);
qLocDrawer.value = false;
}
@@ -297,8 +453,9 @@ function zoomTo(loc: string) {
case 'fmLoc':
if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) {
leafletStore.setCenter(findMyUpdate.value.latitude, findMyUpdate.value.longitude);
} else {
$q.notify({ type: 'negative', message: 'Find My Location not available' });
}
$q.notify({ type: 'negative', message: 'Find My Location not available' });
break;
case 'simLoc':
if (
@@ -307,14 +464,16 @@ function zoomTo(loc: string) {
currentLocation.value.longitude
) {
leafletStore.setCenter(currentLocation.value.latitude, currentLocation.value.longitude);
} else {
$q.notify({ type: 'negative', message: 'Simulation Location not available' });
}
$q.notify({ type: 'negative', message: 'Simulation Location not available' });
break;
case 'nextLoc':
if (nextLocation.value && nextLocation.value.latitude && nextLocation.value.longitude) {
leafletStore.setCenter(nextLocation.value.latitude, nextLocation.value.longitude);
} else {
$q.notify({ type: 'negative', message: 'Next Location not available' });
}
$q.notify({ type: 'negative', message: 'Next Location not available' });
break;
default:
$q.notify({ type: 'negative', message: 'Invalid location' });
@@ -326,8 +485,10 @@ onMounted(() => {
if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) {
leafletStore.setCenter(findMyUpdate.value.latitude, findMyUpdate.value.longitude);
} else {
console.log('favorites: home: ', favorites.home.coords.lat, favorites.home.coords.lng);
leafletStore.setCenter(favorites.home.coords.lat, favorites.home.coords.lng);
}
socketStore.requestUpdate();
});
</script>
@@ -339,4 +500,8 @@ onMounted(() => {
.q-item.q-router-link--active, .q-item--active
background-color: $accent
color: $primary
.leafletDrawer
background-color: $dark
color: $dark
</style>