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

24
package-lock.json generated
View File

@@ -13,9 +13,11 @@
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"axios": "^1.2.1", "axios": "^1.2.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-contextmenu": "^1.4.0",
"leaflet-extra-markers": "^2.0.1", "leaflet-extra-markers": "^2.0.1",
"leaflet-geosearch": "^4.2.2", "leaflet-geosearch": "^4.2.2",
"leaflet-routing-machine": "^3.2.12", "leaflet-routing-machine": "^3.2.12",
"openrouteservice-js": "^0.4.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
@@ -26,6 +28,7 @@
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@quasar/app-vite": "^2.1.0", "@quasar/app-vite": "^2.1.0",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/leaflet-contextmenu": "^1.4.4",
"@types/node": "^20.5.9", "@types/node": "^20.5.9",
"@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.4.0", "@vue/eslint-config-typescript": "^14.4.0",
@@ -2097,11 +2100,20 @@
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"devOptional": true, "devOptional": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/leaflet-contextmenu": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@types/leaflet-contextmenu/-/leaflet-contextmenu-1.4.4.tgz",
"integrity": "sha512-3BcUZceTEHDOu2kD6Is5cQB5z/DIVMPZoN/o5yXGrH0Y4CfWuSpP1Sm6ZHdBMQ+nVtEQXZnIV/GOE4ZEGX39HQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/leaflet": "^1.9"
}
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -5748,6 +5760,11 @@
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/leaflet-contextmenu": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/leaflet-contextmenu/-/leaflet-contextmenu-1.4.0.tgz",
"integrity": "sha512-BXASCmJ5bLkuJGDCpWmvGqhZi5AzeOY0IbQalfkgBcMAMfAOFSvD4y0gIQxF/XzEyLkjXaRiUpibVj4+Cf3tUA=="
},
"node_modules/leaflet-extra-markers": { "node_modules/leaflet-extra-markers": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/leaflet-extra-markers/-/leaflet-extra-markers-2.0.1.tgz", "resolved": "https://registry.npmjs.org/leaflet-extra-markers/-/leaflet-extra-markers-2.0.1.tgz",
@@ -6301,6 +6318,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/openrouteservice-js": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/openrouteservice-js/-/openrouteservice-js-0.4.1.tgz",
"integrity": "sha512-Oeb/KgzaYXEtafSHB40KfZvHFfTSPhtt0/oEf0jv5o5Ljw3//+C63CFxbknOqDBrOkYLLQMMCjJGa54rUOBtLg=="
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",

View File

@@ -19,9 +19,11 @@
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"axios": "^1.2.1", "axios": "^1.2.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-contextmenu": "^1.4.0",
"leaflet-extra-markers": "^2.0.1", "leaflet-extra-markers": "^2.0.1",
"leaflet-geosearch": "^4.2.2", "leaflet-geosearch": "^4.2.2",
"leaflet-routing-machine": "^3.2.12", "leaflet-routing-machine": "^3.2.12",
"openrouteservice-js": "^0.4.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
@@ -32,6 +34,7 @@
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@quasar/app-vite": "^2.1.0", "@quasar/app-vite": "^2.1.0",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/leaflet-contextmenu": "^1.4.4",
"@types/node": "^20.5.9", "@types/node": "^20.5.9",
"@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.4.0", "@vue/eslint-config-typescript": "^14.4.0",

View File

@@ -70,14 +70,30 @@ export default defineConfig((/* ctx */) => {
extendViteConf() { extendViteConf() {
return { return {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules/leaflet')) return 'vendor-leaflet-core';
if (id.includes('leaflet-routing-machine')) return 'vendor-leaflet-routing';
if (id.includes('leaflet-geosearch')) return 'vendor-leaflet-geosearch';
if (id.includes('leaflet-extra-markers')) return 'vendor-leaflet-markers';
if (id.includes('openrouteservice-js')) return 'vendor-openrouteservice';
if (id.includes('socket.io-client')) return 'vendor-socketio';
if (id.includes('node_modules/quasar')) return 'vendor-quasar';
if (id.includes('node_modules/vue') || id.includes('node_modules/pinia')) {
return 'vendor-vue-core';
}
return undefined;
},
},
},
},
server: { server: {
hmr: { hmr: {
// overlay: false, // overlay: false,
}, },
allowedHosts: [ allowedHosts: ['localhost', 'strixx.famor.org', 'simloc.strixx.intrepidnet.org'],
'localhost',
'strixx.famor.org'
],
}, },
}; };
}, },
@@ -102,7 +118,7 @@ export default defineConfig((/* ctx */) => {
devServer: { devServer: {
// https: true, // https: true,
open: false, // opens browser window automatically open: false, // opens browser window automatically
// public: 'http://strixx.famor.org:9000', // public: 'https://simloc.strixx.intrepidnet.org',
proxy: { proxy: {
// proxy all requests starting with /api to jsonplaceholder // proxy all requests starting with /api to jsonplaceholder
'/api': { '/api': {
@@ -118,9 +134,27 @@ export default defineConfig((/* ctx */) => {
changeOrigin: true, changeOrigin: true,
// rewrite: (path) => path.replace(/^\/socket.io/, ''), // rewrite: (path) => path.replace(/^\/socket.io/, ''),
}, },
'/osm': {
target: 'https://nominatim.openstreetmap.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/osm/, ''),
headers: {
Referer: 'https://nominatim.openstreetmap.org/',
'User-Agent': 'map-sim-location/0.0.1 (iam@williambr.uno)',
},
},
'/ors': {
// target: 'https://router.project-osrm.org',
target: 'http://localhost:8080',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/osrm/, ''),
// headers: {
// Referer: 'https://router.project-osrm.org/',
// 'User-Agent': 'map-sim-location/0.0.1 (iam@williambr.uno)',
// },
},
}, },
}, },
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
framework: { framework: {
config: { config: {

View File

@@ -5,16 +5,19 @@ declare module 'vue' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$axios: AxiosInstance; $axios: AxiosInstance;
$api: AxiosInstance; $api: AxiosInstance;
$osm: AxiosInstance;
} }
} }
const api = axios.create({ baseURL: '/api' }); const api = axios.create({ baseURL: '/api' });
const osm = axios.create({ baseURL: '/osm' });
export default defineBoot(({ app }) => { export default defineBoot(({ app }) => {
app.config.globalProperties.$axios = axios app.config.globalProperties.$axios = axios
app.config.globalProperties.$api = api app.config.globalProperties.$api = api
app.config.globalProperties.$osm = osm
}) })
export { axios, api }; export { axios, api, osm };

View File

@@ -18,7 +18,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
const props = defineProps({ defineProps({
name: { type: String, required: true }, name: { type: String, required: true },
}); });

View File

@@ -0,0 +1,278 @@
import * as L from 'leaflet';
import OpenrouteserviceModule from 'openrouteservice-js';
/**
* Minimal typings for openrouteservice-js (since official types are incomplete)
*/
interface ORSDirectionsOptions {
api_key: string;
}
interface ORSStep {
instruction: string;
distance: number;
duration: number;
way_points: [number, number];
}
interface ORSSegment {
distance: number;
duration: number;
steps: ORSStep[];
}
interface ORSRoute {
geometry: string;
segments: ORSSegment[];
way_points: [number, number];
}
interface ORSResponse {
routes: ORSRoute[];
}
interface ORSRequestOptions {
api_key?: string;
coordinates?: [number, number][];
[key: string]: unknown;
}
interface ORSDirectionsClient {
calculate(options: ORSRequestOptions): Promise<ORSResponse>;
}
interface OpenrouteserviceNamespace {
Directions: new (options: ORSDirectionsOptions) => ORSDirectionsClient;
}
const Openrouteservice = OpenrouteserviceModule as unknown as OpenrouteserviceNamespace;
/**
* Leaflet waypoint type
*/
interface Waypoint {
latLng: L.LatLng;
}
interface RouteSummary {
totalDistance: number;
totalTime: number;
}
interface Instruction {
text: string;
distance: number;
time: number;
index: number;
}
export interface AlternativeRoute {
name: string;
coordinates: L.LatLng[];
instructions: Instruction[];
summary: RouteSummary;
segments: RouteSummary[];
inputWaypoints: Waypoint[];
waypoints: L.LatLng[];
}
type RouteCallback = (
error: { status: string; message: string } | null,
routes?: AlternativeRoute[],
) => void;
export class OpenRouteServiceV2 extends L.Class {
private _apiKey: string;
private _orsOptions: ORSRequestOptions;
constructor(apiKey = '', orsOptions: ORSRequestOptions = {}, options?: Record<string, unknown>) {
super();
this._apiKey = apiKey;
this._orsOptions = orsOptions;
L.Util.setOptions(this, options);
}
route(waypoints: Waypoint[], callback: RouteCallback, context?: unknown): this {
const orsDirections = new Openrouteservice.Directions({
api_key: this._apiKey,
});
const coordinates: [number, number][] = waypoints.map((wp) => [wp.latLng.lng, wp.latLng.lat]);
this._orsOptions.coordinates = coordinates;
orsDirections
.calculate(this._orsOptions)
.then((json: ORSResponse) => {
this._routeDone(json, waypoints, callback, context);
})
.catch((err: unknown) => {
console.error(err);
callback(
{
status: 'REQUEST_FAILED',
message: String(err),
},
undefined,
);
});
return this;
}
private _routeDone(
response: ORSResponse,
inputWaypoints: Waypoint[],
callback: RouteCallback,
context?: unknown,
): void {
const alts: AlternativeRoute[] = [];
const ctx = context ?? callback;
if (!response.routes) {
callback.call(ctx, {
status: 'NO_ROUTES',
message: 'No routes found in response',
});
return;
}
response.routes.forEach((path, i) => {
const coordinates = this._decodePolyline(path.geometry);
const instructions: Instruction[] = [];
const waypoints: L.LatLng[] = [];
const segments: RouteSummary[] = [];
let totalTime = 0;
let totalDistance = 0;
path.segments.forEach((leg) => {
segments.push({
totalDistance: leg.distance,
totalTime: leg.duration,
});
totalDistance += leg.distance;
totalTime += leg.duration;
leg.steps.forEach((step) => {
instructions.push(this._convertInstructions(step));
const wpIndex = path.way_points[1];
if (coordinates[wpIndex]) {
waypoints.push(coordinates[wpIndex]);
}
});
});
alts.push({
name: `Route: ${i + 1}`,
coordinates,
instructions,
summary: {
totalDistance,
totalTime,
},
segments,
inputWaypoints,
waypoints,
});
});
callback.call(ctx, null, alts);
}
private _decodePolyline(encoded: string, includeElevation = false): L.LatLng[] {
const points: L.LatLng[] = [];
let index = 0;
let lat = 0;
let lng = 0;
while (index < encoded.length) {
let result = 0;
let shift = 0;
let b: number;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
lat += result & 1 ? ~(result >> 1) : result >> 1;
result = 0;
shift = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
lng += result & 1 ? ~(result >> 1) : result >> 1;
if (includeElevation) {
result = 0;
shift = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
// Parse and ignore elevation component when present.
result = result & 1 ? ~(result >> 1) : result >> 1;
}
points.push(L.latLng(lat / 1e5, lng / 1e5));
}
return points;
}
private _convertInstructions(step: ORSStep): Instruction {
return {
text: step.instruction,
distance: step.distance,
time: step.duration,
index: step.way_points[0],
};
}
}
/**
* Leaflet factory function (typed)
* Attach to L.Routing.openrouteserviceV2 so that it's callable as L.Routing.openrouteserviceV2
*/
export function openrouteserviceV2(
apiKeyOrOptions: string | ORSRequestOptions,
orsOptions?: ORSRequestOptions,
options?: Record<string, unknown>,
): OpenRouteServiceV2 {
if (typeof apiKeyOrOptions === 'string') {
return new OpenRouteServiceV2(apiKeyOrOptions, orsOptions ?? {}, options);
}
const resolvedApiKey = typeof apiKeyOrOptions.api_key === 'string' ? apiKeyOrOptions.api_key : '';
return new OpenRouteServiceV2(
resolvedApiKey,
apiKeyOrOptions,
orsOptions as Record<string, unknown>,
);
}
type LeafletWithRouting = typeof L & {
Routing?: {
openrouteserviceV2?: typeof openrouteserviceV2;
[key: string]: unknown;
};
};
const leafletWithRouting = L as LeafletWithRouting;
if (!leafletWithRouting.Routing) {
leafletWithRouting.Routing = {};
}
leafletWithRouting.Routing.openrouteserviceV2 = openrouteserviceV2;

View File

@@ -11,7 +11,6 @@ import {
onMounted, onMounted,
onBeforeUnmount, onBeforeUnmount,
useAttrs, useAttrs,
defineEmits,
} from 'vue'; } from 'vue';
import { routingControlProps, setupRoutingControl } from 'functions/routingControl'; import { routingControlProps, setupRoutingControl } from 'functions/routingControl';
@@ -21,11 +20,15 @@ import 'leaflet';
import 'leaflet-routing-machine'; import 'leaflet-routing-machine';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'; import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import type L from 'leaflet'; type RoutingControlInstance = {
on: (listeners: unknown) => void;
setWaypoints: (waypoints: unknown[]) => void;
remove: () => void;
};
// ---- Emits ---- // ---- Emits ----
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'ready', value: L.Routing.Control): void; (e: 'ready', value: RoutingControlInstance): void;
}>(); }>();
// ---- Props ---- // ---- Props ----
@@ -42,26 +45,33 @@ const useGlobalLeaflet = inject(UseGlobalLeafletInjection, false);
const registerControl = assertInject(RegisterControlInjection); const registerControl = assertInject(RegisterControlInjection);
// ---- State ---- // ---- State ----
const leafletObject = ref<L.Routing.Control | null>(null); const leafletObject = ref<RoutingControlInstance | null>(null);
// ---- Setup logic ---- // ---- Setup logic ----
const { options, methods } = setupRoutingControl(props); const { options, methods } = setupRoutingControl(props);
onMounted(async () => { onMounted(async () => {
const { routing } = useGlobalLeaflet const leafletModule = useGlobalLeaflet
? (WINDOW_OR_GLOBAL as any).L ? ((WINDOW_OR_GLOBAL as unknown as { L: { routing: { control: (options: unknown) => unknown } } }).L)
: await import('leaflet/dist/leaflet-src.esm'); : ((await import('leaflet/dist/leaflet-src.esm')) as unknown as {
routing: { control: (options: unknown) => unknown };
});
const routing = leafletModule.routing;
const { listeners } = remapEvents(attrs); const { listeners } = remapEvents(attrs);
leafletObject.value = markRaw(routing.control(options)); const control = markRaw(routing.control(options) as object) as RoutingControlInstance;
leafletObject.value.on(listeners); leafletObject.value = control;
control.on(listeners);
propsBinder(methods, leafletObject.value, props); propsBinder(methods, control, props);
registerControl({ leafletObject: leafletObject.value }); (registerControl as unknown as (payload: { leafletObject: unknown }) => void)({
leafletObject: control,
});
await nextTick(); await nextTick();
emit('ready', leafletObject.value); emit('ready', control);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="q-pa-md"> <div class="">
<q-layout <q-layout
view="hHh Lpr fFf" view="hHh Lpr fFf"
container container
style="height: 600px; width: 100vw" style="height: 600px; max-width: 800px; width: 100vw"
class="rounded-borders" class="rounded-borders"
> >
<q-footer :class="$q.dark.isActive ? 'bg-primary' : 'bg-black'" style="height: 48px"> <q-footer :class="$q.dark.isActive ? 'bg-primary' : 'bg-black'" style="height: 48px">
@@ -31,35 +31,53 @@
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </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-toolbar>
</q-footer> </q-footer>
<q-drawer <q-drawer
v-model="qLocDrawer" v-model="qLocDrawer"
show-if-above show-if-above
mini-to-overlay
overlay overlay
:width="300" :width="300"
side="left" side="left"
:breakpoint="500" :breakpoint="500"
:mini="miniState"
@mouseenter="miniState = false" @mouseenter="miniState = false"
@mouseleave="miniState = true" @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-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 /> <q-separator />
<LocationMark <LocationMark
v-for="key in locationQueueOrder" v-for="(key, index) in locationQueueOrder"
:key="key" :key="key"
:loc_id="key" :loc_id="key"
:active="locationQueueData[key].loc_id === currentLocation.loc_id" :active="
:start="locationQueueData[key].start" (locationQueueData as Record<string, any>)[key]?.loc_id === currentLocation?.loc_id
:address="locationQueueData[key].address" "
:latitude="locationQueueData[key].latitude" :isLast="index != locationQueueOrder.length - 1"
:longitude="locationQueueData[key].longitude" :start="(locationQueueData as Record<string, any>)[key]?.start ?? ''"
:end="locationQueueData[key].end" :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" @item-clicked="zoomToCoods"
/> />
</q-list> </q-list>
@@ -68,12 +86,13 @@
<q-page-container> <q-page-container>
<q-page> <q-page>
<div style="height: 560px; width: 90vw; color: #000000"> <div style="height: 550px; width: 100vw; max-width: 800px; color: #000000">
<L-Map <L-Map
id="map"
ref="mapRef" ref="mapRef"
:center="center" :center="safeCenter"
:zoom="zoom" :zoom="zoom"
style="height: 550px; width: 100%" style="height: 550px; width: 100vw; max-width: 800px"
@click="updateMarker" @click="updateMarker"
@ready="onMapReady" @ready="onMapReady"
> >
@@ -81,38 +100,60 @@
layer-type="base" layer-type="base"
name="OpenStreetMap" name="OpenStreetMap"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@click="updateMarker($event.latlng)"
></L-Tile-Layer> ></L-Tile-Layer>
<L-Layer-Group> <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-Marker>
</L-Layer-Group> </L-Layer-Group>
<L-Layer-Group v-if="locationQueueOrder"> <L-Layer-Group v-if="locationQueueOrder">
<L-Marker <L-Marker
v-for="locid in locationQueueOrder" v-for="locid in locationQueueOrder"
:key="locid" :key="locid"
:icon="getCustomIcon(locid)" :icon="getCustomIcon(locid) as any"
:lat-lng="[locationQueueData[locid].latitude, locationQueueData[locid].longitude]" :lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
> >
</L-Marker> </L-Marker>
</L-Layer-Group> </L-Layer-Group>
<L-Layer-Group v-if="routeLayer"> <L-Layer-Group v-if="routeSet.start && routeSet.end">
<LRoutingMachine <LRoutingMachine
v-bind="routingOptions" v-bind="routingOptions"
@routingstart="debugRoutingEvent" @routingstart="debugRoutingEvent"
@routesfound="debugRoutingEvent" @routesfound="handleRoutesFound"
@routingerror="debugRoutingEvent" @routingerror="debugRoutingEvent"
/> />
</L-Layer-Group> </L-Layer-Group>
<L-Layer-Group v-if="findMyUpdate"> <L-Layer-Group v-if="findMyUpdate">
<L-Marker <L-Marker
v-if="findMyUpdate" v-if="findMyUpdate"
:icon="fmIcon" :icon="fmIcon as any"
:lat-lng="[findMyUpdate.latitude, findMyUpdate.longitude]" :lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
></L-Marker> ></L-Marker>
<L-Circle <L-Circle
:fillOpacity="0.5" :fillOpacity="0.5"
:lat-lng="[findMyUpdate.latitude, findMyUpdate.longitude]" :lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
:radius="findMyUpdate.horizontalAccuracy" :radius="findMyUpdate.horizontalAccuracy"
color="firebrick" color="firebrick"
fillColor="indianred" fillColor="indianred"
@@ -129,40 +170,76 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
// Leaflet imports
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch'; import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'; import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers'; import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
import 'leaflet-geosearch/dist/geosearch.css'; import 'leaflet-geosearch/dist/geosearch.css';
import 'leaflet/dist/leaflet.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 LRoutingMachine from 'components/LRoutingMachine.vue';
import LocationMark from 'components/LocationMark.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'; import type { LeafletMouseEvent, Map } from 'leaflet';
// Stores
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useSocketioStore } from 'stores/socketio'; import { useSocketioStore } from 'stores/socketio';
import { useLeafletStore } from 'stores/leaflet'; 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 { favorites } from 'constants/favorites';
import { route } from 'quasar/wrappers';
const leafletStore = useLeafletStore(); const leafletStore = useLeafletStore();
const { zoom, center, markerLatLng, qLocDrawer } = storeToRefs(leafletStore); const { zoom, center, markerLatLng, qLocDrawer, routeSet, routeSegments } =
storeToRefs(leafletStore);
const socketStore = useSocketioStore(); const socketStore = useSocketioStore();
const { currentLocation, nextLocation, locationQueueData, locationQueueOrder, findMyUpdate } = const {
storeToRefs(socketStore); currentLocation,
nextLocation,
locationQueueData,
locationQueueOrder,
findMyUpdate,
simulationState,
testMode,
} = storeToRefs(socketStore);
const $q = useQuasar(); const $q = useQuasar();
const mapRef = ref(); const mapRef = ref();
const responseMessage = ref(''); const responseMessage = ref('');
const routeStart = ref(null);
const routeEnd = ref(null);
const routeLayer = ref(false); const routeLayer = ref(false);
const miniState = ref(true); 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 onMapReady = (map: Map) => {
const provider = new OpenStreetMapProvider(); const provider = new OpenStreetMapProvider();
@@ -172,7 +249,7 @@ const onMapReady = (map: Map) => {
autoClose: true, autoClose: true,
updateMap: true, updateMap: true,
showPopup: true, showPopup: true,
style: 'bar', style: 'button',
acceptAutoLoad: true, acceptAutoLoad: true,
autoComplete: true, autoComplete: true,
autoCompleteDelay: 250, autoCompleteDelay: 250,
@@ -195,6 +272,9 @@ const fmIcon = new Icon({
}); });
const getCustomIcon = (locid: string) => { const getCustomIcon = (locid: string) => {
const currentIndex = currentLocation.value
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: 0;
const locationIndex = locationQueueOrder.value.indexOf(locid); const locationIndex = locationQueueOrder.value.indexOf(locid);
if (currentLocation.value && currentLocation.value.loc_id === locid) { if (currentLocation.value && currentLocation.value.loc_id === locid) {
return new Icon({ return new Icon({
@@ -209,21 +289,26 @@ const getCustomIcon = (locid: string) => {
return new Icon({ return new Icon({
color: 'blue', color: 'blue',
accentColor: 'firebrick', accentColor: 'firebrick',
content: locationIndex.toString(), content: (locationIndex - currentIndex).toString(),
contentColor: 'white', contentColor: 'white',
scale: 1, scale: 1,
svg: PinCirclePanel, svg: PinCirclePanel,
}); });
}; };
const routingOptions = reactive({ const routingOptions = reactive<{
waypoints: [ waypoints: LeafLet.LatLng[];
[40.910773020811, -73.891069806448], router: IRouter;
[40.90930366920829, -73.87658695470259], 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); console.log(`${event.type} event: `, event);
}; };
@@ -232,31 +317,101 @@ function updateMarker(event: LeafletMouseEvent) {
center.value = [event.latlng.lat, event.latlng.lng]; center.value = [event.latlng.lat, event.latlng.lng];
} }
function handleMarkerClick(event: LeafletMouseEvent) { function routeToQueue() {
$q.dialog({ console.log('routeToQueue');
component: SetLocationDialog, if (routeSet.value.start && routeSet.value.end && routeSegments.value) {
componentProps: { console.log('routeToQueue: start: ', routeSet.value.start);
lat: event.latlng.lat, setLocation({ lat: routeSet.value.start.lat, lng: routeSet.value.start.lng }, 0);
lng: event.latlng.lng, routeSegments.value.forEach((segment: any, index: number) => {
}, console.log('routeToQueue: segment: ', segment);
}) setLocation(
.onOk((delay: number) => { { lat: segment.toCoordinates.lat, lng: segment.toCoordinates.lng },
void setLocation({ lat: event.latlng.lat, lng: event.latlng.lng }, delay); segment.timeSeconds,
console.log(
'Confirmed location add: latitude: ' +
event.latlng.lat +
', longitude: ' +
event.latlng.lng +
', delay: ' +
delay,
); );
})
.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) { function setLocation(coords: coords, delay: number) {
@@ -284,10 +439,11 @@ function setLocation(coords: coords, delay: number) {
} }
function zoomToCoods(arg: string) { function zoomToCoods(arg: string) {
leafletStore.setCenter( const item = locationQueueData.value[arg];
locationQueueData.value[arg].latitude, if (!item || item.latitude == null || item.longitude == null) {
locationQueueData.value[arg].longitude, return;
); }
leafletStore.setCenter(item.latitude, item.longitude);
leafletStore.setZoom(50); leafletStore.setZoom(50);
qLocDrawer.value = false; qLocDrawer.value = false;
} }
@@ -297,8 +453,9 @@ function zoomTo(loc: string) {
case 'fmLoc': case 'fmLoc':
if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) { if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) {
leafletStore.setCenter(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; break;
case 'simLoc': case 'simLoc':
if ( if (
@@ -307,14 +464,16 @@ function zoomTo(loc: string) {
currentLocation.value.longitude currentLocation.value.longitude
) { ) {
leafletStore.setCenter(currentLocation.value.latitude, 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; break;
case 'nextLoc': case 'nextLoc':
if (nextLocation.value && nextLocation.value.latitude && nextLocation.value.longitude) { if (nextLocation.value && nextLocation.value.latitude && nextLocation.value.longitude) {
leafletStore.setCenter(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; break;
default: default:
$q.notify({ type: 'negative', message: 'Invalid location' }); $q.notify({ type: 'negative', message: 'Invalid location' });
@@ -326,8 +485,10 @@ onMounted(() => {
if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) { if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) {
leafletStore.setCenter(findMyUpdate.value.latitude, findMyUpdate.value.longitude); leafletStore.setCenter(findMyUpdate.value.latitude, findMyUpdate.value.longitude);
} else { } else {
console.log('favorites: home: ', favorites.home.coords.lat, favorites.home.coords.lng);
leafletStore.setCenter(favorites.home.coords.lat, favorites.home.coords.lng); leafletStore.setCenter(favorites.home.coords.lat, favorites.home.coords.lng);
} }
socketStore.requestUpdate();
}); });
</script> </script>
@@ -339,4 +500,8 @@ onMounted(() => {
.q-item.q-router-link--active, .q-item--active .q-item.q-router-link--active, .q-item--active
background-color: $accent background-color: $accent
color: $primary color: $primary
.leafletDrawer
background-color: $dark
color: $dark
</style> </style>

View File

@@ -4,7 +4,7 @@ import { useSocketioStore } from 'stores/socketio';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
const socketStore = useSocketioStore(); const socketStore = useSocketioStore();
const { currentLocation, locationQueueOrder } = storeToRefs(socketStore); const { currentLocation, locationQueueOrder, simulationRunning } = storeToRefs(socketStore);
const props = defineProps({ const props = defineProps({
address: { address: {
@@ -43,6 +43,10 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
isLast: {
type: Boolean,
default: false,
},
}); });
// Define custom events that this component can emit // Define custom events that this component can emit
@@ -100,6 +104,16 @@ const calculateDeltaT = computed(() => {
return delta; return delta;
}); });
const secondsToTime = computed(() => {
const seconds = props.delay;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds
.toString()
.padStart(2, '0')}`;
});
const humanReadableDateTime = (iso: string) => { const humanReadableDateTime = (iso: string) => {
return new Date(iso).toLocaleDateString('en-US', { return new Date(iso).toLocaleDateString('en-US', {
// year: 'numeric', // year: 'numeric',
@@ -181,6 +195,24 @@ function formatAddress(input: string): string {
return `${streetNumber} ${streetName}, ${city}, ${state} ${zip}`; return `${streetNumber} ${streetName}, ${city}, ${state} ${zip}`;
} }
const markerIndex = computed(() => {
const currentIndex = currentLocation.value
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: 0;
const locationIndex = locationQueueOrder.value.indexOf(props.loc_id);
return props.active ? '*' : (locationIndex - currentIndex).toString();
});
const itemClass = computed(() => {
const currentIndex = currentLocation.value
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: 0;
const locationIndex = locationQueueOrder.value.indexOf(props.loc_id);
if (locationIndex - currentIndex > 0) return 'future';
else if (locationIndex - currentIndex < 0) return 'past';
else return 'active';
});
onMounted(() => { onMounted(() => {
const update = () => { const update = () => {
currentTime.value = new Date(); currentTime.value = new Date();
@@ -195,32 +227,35 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<q-item v-ripple clickable :active="active" @click="itemClicked"> <q-item v-ripple clickable :active="active" @click="itemClicked" :class="itemClass">
<q-item-section> <q-item-section>
<q-item-label <q-item-label
>{{ formatAddress(address) }} >{{ formatAddress(address) }}
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip> <q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
</q-item-label> </q-item-label>
<q-item-label caption lines="1" v-if="start"> <q-item-label caption lines="1" v-if="start && simulationRunning">
start: {{ humanReadableDateTime(start) }} start: {{ humanReadableDateTime(start) }}
</q-item-label> </q-item-label>
<q-item-label caption lines="1" v-if="end"> <q-item-label caption lines="1" v-if="end && simulationRunning">
end: {{ humanReadableDateTime(end) }} end: {{ humanReadableDateTime(end) }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section side top> <q-item-section side top>
<q-item-label caption lines="1"> <q-item-label caption lines="1" v-if="simulationRunning">
{{ calculateDeltaT }} {{ calculateDeltaT }}
</q-item-label> </q-item-label>
<q-item-label caption lines="1" v-else> delay: {{ secondsToTime }} seconds </q-item-label>
<q-item-section avatar class="q-pt-md"> <q-item-section avatar class="q-pt-md">
<q-btn dense color="primary" round icon="location_on" class="q-ml-md"> <q-btn dense color="primary" round icon="location_on" class="q-ml-md">
<q-badge color="accent" floating>{{ <q-badge color="accent" floating>{{ markerIndex }}</q-badge>
active ? '*' : locationQueueOrder.indexOf(loc_id)
}}</q-badge>
</q-btn> </q-btn>
</q-item-section> </q-item-section>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator spaced inset v-if="isLast" />
</template> </template>
<style scoped></style> <style lang="sass "scoped>
.past
color: gray
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useLeafletStore } from 'stores/leaflet'; import { useLeafletStore } from 'stores/leaflet';
import type { coords, CtrlAttrs } from 'components/models'; import type { coords } from 'components/models';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { socket } from 'boot/socketio'; import { socket } from 'boot/socketio';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue'; import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
@@ -10,6 +10,7 @@ import { useSocketioStore } from 'stores/socketio';
import { ref } from 'vue'; import { ref } from 'vue';
import { favorites } from 'constants/favorites'; import { favorites } from 'constants/favorites';
import { controls } from 'constants/controls'; import { controls } from 'constants/controls';
import type { DeviceCommands } from 'components/models';
const route = useRoute(); const route = useRoute();
const $q = useQuasar(); const $q = useQuasar();
@@ -18,16 +19,54 @@ const leafletStore = useLeafletStore();
const socketStore = useSocketioStore(); const socketStore = useSocketioStore();
const { center, markerLatLng } = storeToRefs(leafletStore); const { center, markerLatLng } = storeToRefs(leafletStore);
const { simulationRunning, simulationState, simulationQueueLength, icloudMonitor, testMode } = storeToRefs(socketStore); const { simulationRunning, simulationState, simulationQueueLength, icloudMonitor, testMode } =
storeToRefs(socketStore);
const menuOpen = ref(); const menuOpen = ref(false);
const favoritesMap = favorites as Record<string, unknown>;
type ControlAction = {
name: string;
cmd: string;
cmdClass: string;
icon: string;
cnfrm: boolean;
delay: number;
};
type FavoriteWithCoords = {
name: string;
icon: string;
coords: coords;
};
type FavoriteWithSubitems = {
name: string;
icon: string;
subitems: Record<string, FavoriteWithCoords>;
};
function hasCoords(item: unknown): item is FavoriteWithCoords {
return typeof item === 'object' && item !== null && 'coords' in item;
}
function hasSubitems(item: unknown): item is FavoriteWithSubitems {
return typeof item === 'object' && item !== null && 'subitems' in item;
}
function handleFavClick(coords: coords) { function handleFavClick(coords: coords) {
center.value = [coords.lat, coords.lng]; center.value = [coords.lat, coords.lng];
markerLatLng.value = [coords.lat, coords.lng]; markerLatLng.value = [coords.lat, coords.lng];
} }
function handleControlClick(cmdAttr) { function handleTestToggle() {
const response = socketStore.simulationControl('test-mode');
if (response.sts === 'error') {
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
}
}
function handleControlClick(cmdAttr: ControlAction) {
if (cmdAttr.cnfrm) { if (cmdAttr.cnfrm) {
$q.dialog({ $q.dialog({
component: ConfirmCommandDialog, component: ConfirmCommandDialog,
@@ -62,9 +101,13 @@ function handleControlClick(cmdAttr) {
} }
if (cmdAttr.cmdClass === 'dev_cntrl_class') { if (cmdAttr.cmdClass === 'dev_cntrl_class') {
socket.emit('device_control', { command: cmdAttr.cmd, delay: 0 }, (response) => { socket.emit(
console.log(response.status, response.command); 'device_control',
}); { command: cmdAttr.cmd as DeviceCommands, delay: 0 },
(response) => {
console.log(response.status, response.command);
},
);
} }
}) })
.onCancel(() => { .onCancel(() => {
@@ -124,9 +167,13 @@ function handleControlClick(cmdAttr) {
} }
if (cmdAttr.cmdClass === 'dev_cntrl_class') { if (cmdAttr.cmdClass === 'dev_cntrl_class') {
socket.emit('device_control', { command: cmdAttr.cmd, delay: 0 }, (response) => { socket.emit(
console.log(response.status, response.command); 'device_control',
}); { command: cmdAttr.cmd as DeviceCommands, delay: 0 },
(response) => {
console.log(response.status, response.command);
},
);
} }
} }
} }
@@ -134,7 +181,17 @@ function handleControlClick(cmdAttr) {
<template> <template>
<q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'"> <q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
<q-btn @click="$emit('drawer'); leafletStore.toggleQLocDrawer()" flat round dense icon="menu" class="q-mr-sm" /> <q-btn
@click="
$emit('drawer');
leafletStore.toggleQLocDrawer();
"
flat
round
dense
icon="menu"
class="q-mr-sm"
/>
<q-separator dark inset /> <q-separator dark inset />
<q-space /> <q-space />
<q-btn <q-btn
@@ -145,10 +202,12 @@ function handleControlClick(cmdAttr) {
v-if="route.name === 'Leaflet'" v-if="route.name === 'Leaflet'"
> >
<q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end"> <q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
<q-list> <q-list dense dark>
<template v-for="(favObj, favId) in favorites" :key="favId"> <template v-for="(favObj, favId) in favoritesMap" :key="favId">
<q-item <q-item
v-if="favObj.coords" dense
dark
v-if="hasCoords(favObj)"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@@ -161,7 +220,7 @@ function handleControlClick(cmdAttr) {
<q-item-label>{{ favObj.name }}</q-item-label> <q-item-label>{{ favObj.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-else clickable v-ripple> <q-item v-else-if="hasSubitems(favObj)" clickable v-ripple dense dark>
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="favObj.icon" color="primary" text-color="white" /> <q-avatar :icon="favObj.icon" color="primary" text-color="white" />
</q-item-section> </q-item-section>
@@ -172,8 +231,10 @@ function handleControlClick(cmdAttr) {
<q-icon name="keyboard_arrow_right" /> <q-icon name="keyboard_arrow_right" />
</q-item-section> </q-item-section>
<q-menu anchor="bottom start" self="bottom end"> <q-menu anchor="bottom start" self="bottom end">
<q-list> <q-list dense dark>
<q-item <q-item
dense
dark
v-for="(favSubObj, favSubId) in favObj.subitems" v-for="(favSubObj, favSubId) in favObj.subitems"
:key="favSubId" :key="favSubId"
clickable clickable
@@ -196,9 +257,26 @@ function handleControlClick(cmdAttr) {
</q-menu> </q-menu>
</q-btn> </q-btn>
<q-btn-dropdown stretch flat label="Controls"> <q-btn-dropdown stretch flat label="Controls">
<q-list> <q-list dense dark>
<q-item-label header>Simulation Controls</q-item-label> <q-item-label header>Simulation Controls</q-item-label>
<q-item dense dark tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
v-model="testMode"
size="sm"
color="yellow"
@update:model-value="handleTestToggle"
dark
dense
/>
</q-item-section>
<q-item-section>
<q-item-label>Test Mode</q-item-label>
</q-item-section>
</q-item>
<q-item <q-item
dense
dark
v-if="!simulationRunning" v-if="!simulationRunning"
clickable clickable
v-ripple v-ripple
@@ -206,13 +284,20 @@ function handleControlClick(cmdAttr) {
@click="handleControlClick(controls.simulation.start)" @click="handleControlClick(controls.simulation.start)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.simulation.start.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.start.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.simulation.start.name }} </q-item-label> <q-item-label> {{ controls.simulation.start.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark
v-if="simulationState === 'RUNNING' && simulationRunning" v-if="simulationState === 'RUNNING' && simulationRunning"
clickable clickable
v-ripple v-ripple
@@ -220,13 +305,20 @@ function handleControlClick(cmdAttr) {
@click="handleControlClick(controls.simulation.pause)" @click="handleControlClick(controls.simulation.pause)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.simulation.pause.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.pause.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.simulation.pause.name }} </q-item-label> <q-item-label> {{ controls.simulation.pause.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark
v-if="simulationState === 'PAUSED'" v-if="simulationState === 'PAUSED'"
clickable clickable
v-ripple v-ripple
@@ -234,13 +326,20 @@ function handleControlClick(cmdAttr) {
@click="handleControlClick(controls.simulation.resume)" @click="handleControlClick(controls.simulation.resume)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.simulation.resume.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.resume.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.simulation.resume.name }} </q-item-label> <q-item-label> {{ controls.simulation.resume.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark
v-if="simulationQueueLength && simulationQueueLength > 0" v-if="simulationQueueLength && simulationQueueLength > 0"
clickable clickable
v-ripple v-ripple
@@ -248,13 +347,20 @@ function handleControlClick(cmdAttr) {
@click="handleControlClick(controls.simulation.clear)" @click="handleControlClick(controls.simulation.clear)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.simulation.clear.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.clear.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.simulation.clear.name }} </q-item-label> <q-item-label> {{ controls.simulation.clear.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark
v-if="simulationRunning" v-if="simulationRunning"
clickable clickable
v-ripple v-ripple
@@ -262,7 +368,12 @@ function handleControlClick(cmdAttr) {
@click="handleControlClick(controls.simulation.end)" @click="handleControlClick(controls.simulation.end)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.simulation.end.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.end.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.simulation.end.name }} </q-item-label> <q-item-label> {{ controls.simulation.end.name }} </q-item-label>
@@ -271,6 +382,8 @@ function handleControlClick(cmdAttr) {
<q-separator spaced /> <q-separator spaced />
<q-item-label header>iCloud Monitor Controls</q-item-label> <q-item-label header>iCloud Monitor Controls</q-item-label>
<q-item <q-item
dense
dark
v-if="!icloudMonitor" v-if="!icloudMonitor"
clickable clickable
v-ripple v-ripple
@@ -279,10 +392,10 @@ function handleControlClick(cmdAttr) {
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar <q-avatar
v-if="icloudMonitor"
:icon="controls.icloudmonitor.start.icon" :icon="controls.icloudmonitor.start.icon"
color="primary" color="primary"
text-color="white" text-color="white"
size="sm"
/> />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
@@ -290,13 +403,22 @@ function handleControlClick(cmdAttr) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark
v-if="icloudMonitor"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.icloudmonitor.stop)" @click="handleControlClick(controls.icloudmonitor.stop)"
size="sm"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.icloudmonitor.stop.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.icloudmonitor.stop.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.icloudmonitor.stop.name }} </q-item-label> <q-item-label> {{ controls.icloudmonitor.stop.name }} </q-item-label>
@@ -305,26 +427,40 @@ function handleControlClick(cmdAttr) {
<q-separator spaced /> <q-separator spaced />
<q-item-label header>Device Controls</q-item-label> <q-item-label header>Device Controls</q-item-label>
<q-item <q-item
dense
dark
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.device.reboot)" @click="handleControlClick(controls.device.reboot)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.device.reboot.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.device.reboot.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.device.reboot.name }} </q-item-label> <q-item-label> {{ controls.device.reboot.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.device.shutdown)" @click="handleControlClick(controls.device.shutdown)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="controls.device.shutdown.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.device.shutdown.icon"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.device.shutdown.name }} </q-item-label> <q-item-label> {{ controls.device.shutdown.name }} </q-item-label>

View File

@@ -1,26 +1,30 @@
<template> <template>
<q-dialog ref="dlgRef" persistent> <q-dialog ref="dlgRef" persistent>
<q-card> <q-card class="bg-secondary text-grey-1 add-loc-card">
<q-card-section class="row items-center">
<q-avatar icon="add_location" color="primary" text-color="white" />
<span class="text-h6"> Add Simulated Location to Queue</span>
</q-card-section>
<q-card-section> <q-card-section>
<span class="q-ml-sm"> <q-avatar icon="add_location" color="dark" text-color="white" />
Are you sure you want to set location to {{ latitude }}, {{ longitude }} ? <span class="text-h6"> Add Location to Queue</span>
</span> </q-card-section>
<q-card-section class="q-ml-lg">
<div class="q-mb-sm">Are you sure you want to set location to:</div>
<div class="q-ml-lg">{{ formattedAddressLine1 }}</div>
<div class="q-ml-lg">{{ formattedAddressLine2 }}</div>
<q-input <q-input
dense style="max-width: 150px"
v-model="delay" v-model.number="delay"
autofocus filled
@keyup.enter="onOkClick" @keyup.enter="onOkClick"
label="Delay (seconds)" label="Delay "
type="number" type="number"
suffix="seconds"
color="grey-4"
/> />
</q-card-section> </q-card-section>
<q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="OK" color="primary" @click="onOkClick" /> <q-btn flat label="OK" @click="onOkClick" />
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" /> <q-btn flat label="Cancel" @click="onDialogCancel" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@@ -28,22 +32,51 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { ref } from 'vue'; import { computed, ref } from 'vue';
import type { PropType } from 'vue';
import type { NominatimAddress } from 'components/models';
const props = defineProps({ const props = defineProps({
lat: { type: Number, required: true }, lat: { type: Number, required: true },
lng: { type: Number, required: true }, lng: { type: Number, required: true },
address: { type: Object as PropType<NominatimAddress>, required: false },
}); });
const delay = ref(0); const delay = ref(0);
const latitude = props.lat;
const longitude = props.lng;
defineEmits([...useDialogPluginComponent.emits]); defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent(); const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
const formattedAddressLine1 = computed(() => {
if (props.address) {
const formAddress: string = props.address.house_number + ' ' + props.address.road;
return formAddress;
} else {
return '';
}
});
const formattedAddressLine2 = computed(() => {
if (props.address) {
const town: string = props.address.city
? props.address.city
: props.address.village
? props.address.village
: ' ';
const formAddress: string = town + ', ' + props.address.state + ' ' + props.address.postcode;
return formAddress;
} else {
return '';
}
});
function onOkClick() { function onOkClick() {
onDialogOK(delay.value); onDialogOK(delay.value);
} }
</script> </script>
<style lang="sass" scoped>
.add-loc-card
width: 100%
max-width: 450px
</style>

View File

@@ -1 +1 @@
import { socket } from 'boot/socketio'; export {};

View File

@@ -4,6 +4,7 @@ import { useQuasar } from 'quasar';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useSocketioStore } from 'stores/socketio'; import { useSocketioStore } from 'stores/socketio';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import type { ClientToServerEvents } from 'components/models';
const socketioStore = useSocketioStore(); const socketioStore = useSocketioStore();
const $q = useQuasar(); const $q = useQuasar();
@@ -30,18 +31,17 @@ function sendMessage() {
function handleEmit() { function handleEmit() {
const event = sockEvent.value; const event = sockEvent.value;
const jsonArgs = eventArgs.value; const jsonArgs = eventArgs.value;
socket.emit(event, jsonArgs, (resp) => { socket.emit(event as keyof ClientToServerEvents, jsonArgs, (resp: unknown) => {
console.log('Server Reponse: ' + resp); console.log('Server Reponse: ' + String(resp));
}); });
sockEvent.value = ''; sockEvent.value = '';
eventArgs.value = ''; eventArgs.value = '';
}; }
watch( watch(
messageList, messageList,
(newVal: string[], oldVal: string[]) => { (newVal: string[], oldVal: string[]) => {
let newMsg: string; const newMsg = newVal[newVal.length - 1] ?? '';
newMsg = newVal[newVal.length - 1];
console.log('New message received: ', newMsg); console.log('New message received: ', newMsg);
console.log('Past List', oldVal); console.log('Past List', oldVal);
$q.notify(newMsg); $q.notify(newMsg);

View File

@@ -9,8 +9,6 @@ const {
tunnelConnected, tunnelConnected,
simulationRunning, simulationRunning,
icloudMonitor, icloudMonitor,
currentLocation,
nextLocation,
} = storeToRefs(socketioStore); } = storeToRefs(socketioStore);
function statusDevColor(state: string | boolean): string { function statusDevColor(state: string | boolean): string {
if (typeof state === 'boolean') { if (typeof state === 'boolean') {
@@ -56,7 +54,7 @@ function statusDevColor(state: string | boolean): string {
</q-btn> </q-btn>
<q-btn <q-btn
size="sm" size="sm"
@click="socketioStore.toggleTunneld()" @click="socketioStore.requestUpdate()"
rounded rounded
icon="subway" icon="subway"
class="q-mr-sm" class="q-mr-sm"

View File

@@ -3,7 +3,7 @@ export interface CtrlAttrs {
} }
export type SimulationCommands = 'start' | 'pause' | 'resume' | 'clear' | 'end' | 'add'; export type SimulationCommands = 'start' | 'pause' | 'resume' | 'clear' | 'end' | 'add' | 'test-mode';
export type DeviceCommands = 'start_tunnel' | 'stop_tunnel' | 'shutdown' | 'reboot'; export type DeviceCommands = 'start_tunnel' | 'stop_tunnel' | 'shutdown' | 'reboot';
@@ -55,15 +55,14 @@ export interface ServerToClientEvents {
fmf_update: (d: FindMyUpdate) => void; fmf_update: (d: FindMyUpdate) => void;
} }
interface SimulationStatus { export interface SimulationStatus {
loc_id: string;
status: boolean; status: boolean;
data: { latitude: number;
latitude: number; longitude: number;
longitude: number; start: string;
start: string; end?: string;
end?: string; next_move?: number;
next_move?: number;
};
} }
export interface StatusUpdate { export interface StatusUpdate {
@@ -256,3 +255,36 @@ export interface NextLocation {
} }
export interface NominatimReverseResponse {
place_id: number
licence: string
osm_type: string
osm_id: number
lat: string
lon: string
class: string
type: string
place_rank: number
importance: number
addresstype: string
name: string
display_name: string
address: NominatimAddress
boundingbox: string[]
}
export interface NominatimAddress {
house_number: string;
road: string;
village?: string;
city? : string;
county: string;
state: string;
'ISO3166-2-lvl4': string;
postcode: string;
country: string;
country_code: string;
}

View File

@@ -0,0 +1,45 @@
import { ref, type Ref } from 'vue';
import * as LeafLet from 'leaflet';
import type { LeafletMouseEvent } from 'leaflet';
type RouteSet = {
start?: LeafLet.LatLng | null | undefined;
end?: LeafLet.LatLng | null | undefined;
};
type RouteSetLike = {
start?: unknown;
end?: unknown;
[key: string]: unknown;
};
export function useMarkerContextMenu(routeSet: Ref<RouteSetLike>, updateRoute: () => void) {
const clickedLatLng = ref<LeafLet.LatLng | null>(null);
const handleMarkerClick = (event: LeafletMouseEvent) => {
console.log('marker clicked', event);
clickedLatLng.value = event.latlng;
LeafLet.DomEvent.stopPropagation(event.originalEvent);
};
const setStartRoute = () => {
if (!clickedLatLng.value) return;
(routeSet.value as RouteSet).start = clickedLatLng.value;
console.log('setStartRoute: ', routeSet.value.start);
};
const setEndRoute = () => {
if (!clickedLatLng.value) return;
(routeSet.value as RouteSet).end = clickedLatLng.value;
console.log('setEndRoute: ', routeSet.value.end);
updateRoute();
console.log('updating Route');
};
return {
clickedLatLng,
handleMarkerClick,
setStartRoute,
setEndRoute,
};
}

View File

@@ -0,0 +1,54 @@
import { useLeafletStore } from 'stores/leaflet';
import { storeToRefs } from 'pinia';
const leafletStore = useLeafletStore();
const { routeSegments } = storeToRefs(leafletStore);
type RouteSummary = {
totalDistance: number;
totalTime: number;
};
type RouteWaypoint = {
latLng: {
lat: number;
lng: number;
};
};
type RouteResult = {
summary?: RouteSummary;
segments?: RouteSummary[];
inputWaypoints?: RouteWaypoint[];
};
export function useRoutingEvents() {
const handleRoutesFound = (event: { routes?: RouteResult[] }) => {
const route = event.routes?.[0];
console.log('routesfound event:', event);
if (!route) {
return;
}
if (route.summary) {
console.log('Route summary:', route.summary);
}
if (route.segments?.length) {
const segmentSummary = route.segments.map((segment, index) => ({
fromWaypoint: index,
toWaypoint: index + 1,
distanceMeters: segment.totalDistance,
timeSeconds: segment.totalTime,
toCoordinates: route.inputWaypoints?.[index + 1]?.latLng ?? null,
}));
routeSegments.value = segmentSummary;
console.log('Waypoint segment summary:', segmentSummary);
}
};
return {
handleRoutesFound,
};
}

View File

@@ -1,5 +1,3 @@
import type { CtrlAttrs } from 'components/models';
export const controls = { export const controls = {
simulation: { simulation: {
start: { start: {
@@ -59,7 +57,7 @@ export const controls = {
icon: 'restart_alt', icon: 'restart_alt',
cnfrm: true, cnfrm: true,
delay: 5, delay: 5,
} },
}, },
icloudmonitor: { icloudmonitor: {
start: { start: {
@@ -77,6 +75,6 @@ export const controls = {
icon: 'stop', icon: 'stop',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
} },
} },
}; };

View File

@@ -0,0 +1,47 @@
// services/nominatimService.ts
import { osm } from 'boot/axios';
// TypeScript Interface for Reverse Geocoding Response
export interface NominatimResponse {
place_id: number;
display_name: string;
address: {
road?: string;
city?: string;
country?: string;
[key: string]: unknown;
};
lat: string;
lon: string;
}
const API_URL = '/reverse';
// Simple debounce to respect 1s limit
let lastRequestTime = 0;
export const reverseGeocodeRateLimited = async (lat: number, lon: number): Promise<NominatimResponse> => {
const now = Date.now();
const timeSinceLast = now - lastRequestTime;
// Wait if less than 1000ms has passed
if (timeSinceLast < 1000) {
await new Promise((resolve) => setTimeout(resolve, 1000 - timeSinceLast));
}
const response = await osm.get<NominatimResponse>(API_URL, {
params: {
lat,
lon,
format: 'jsonv2',
addressdetails: 1,
},
headers: {
// It is mandatory to set a descriptive User-Agent
'User-Agent': 'Vue3App/1.0 (your-email@example.com)',
},
});
lastRequestTime = Date.now();
return response.data;
};

View File

@@ -1,48 +1,46 @@
import { Utilities } from '@vue-leaflet/vue-leaflet'; import { Utilities } from '@vue-leaflet/vue-leaflet';
import type L from 'leaflet'; import type { PropType } from 'vue';
import type { IRouter, IGeocoder, LineOptions } from 'leaflet-routing-machine'; import type { IRouter, LineOptions } from 'leaflet-routing-machine';
// ---- Props typing ----
export interface RoutingControlProps { export interface RoutingControlProps {
waypoints: L.Routing.Waypoint[]; waypoints: unknown[];
router?: IRouter; router?: IRouter | undefined;
plan?: L.Routing.Plan; plan?: unknown;
fitSelectedRoutes?: string | boolean; fitSelectedRoutes?: string | boolean;
lineOptions?: LineOptions; lineOptions?: LineOptions | undefined;
routeLine?: (route: any) => L.Layer; routeLine?: ((route: unknown) => unknown) | undefined;
autoRoute?: boolean; autoRoute?: boolean;
routeWhileDragging?: boolean; routeWhileDragging?: boolean;
routeDragInterval?: number; routeDragInterval?: number;
waypointMode?: string; waypointMode?: string;
useZoomParameter?: boolean; useZoomParameter?: boolean;
showAlternatives?: boolean; showAlternatives?: boolean;
altLineOptions?: LineOptions; altLineOptions?: LineOptions | undefined;
} }
// ---- Vue-compatible prop definition ----
export const routingControlProps = { export const routingControlProps = {
waypoints: { waypoints: {
type: Array as () => L.Routing.Waypoint[], type: Array as PropType<unknown[]>,
default: () => [], default: () => [],
}, },
router: { router: {
type: Object as () => IRouter | undefined, type: Object as PropType<IRouter | undefined>,
default: undefined, default: undefined,
}, },
plan: { plan: {
type: Object as () => L.Routing.Plan | undefined, type: Object as PropType<unknown>,
default: undefined, default: undefined,
}, },
fitSelectedRoutes: { fitSelectedRoutes: {
type: [String, Boolean] as unknown as () => string | boolean, type: [String, Boolean] as PropType<string | boolean>,
default: 'smart', default: 'smart',
}, },
lineOptions: { lineOptions: {
type: Object as () => LineOptions | undefined, type: Object as PropType<LineOptions | undefined>,
default: undefined, default: undefined,
}, },
routeLine: { routeLine: {
type: Function as unknown as () => ((route: any) => L.Layer) | undefined, type: Function as PropType<((route: unknown) => unknown) | undefined>,
default: undefined, default: undefined,
}, },
autoRoute: { autoRoute: {
@@ -70,17 +68,13 @@ export const routingControlProps = {
default: false, default: false,
}, },
altLineOptions: { altLineOptions: {
type: Object as () => LineOptions | undefined, type: Object as PropType<LineOptions | undefined>,
default: undefined, default: undefined,
}, },
}; };
// ---- Setup function ----
export const setupRoutingControl = (props: RoutingControlProps) => { export const setupRoutingControl = (props: RoutingControlProps) => {
const options = Utilities.propsToLeafletOptions( const options = Utilities.propsToLeafletOptions(props, routingControlProps);
props,
routingControlProps,
) as L.Routing.RoutingControlOptions;
return { return {
options, options,

View File

@@ -0,0 +1,11 @@
import { openrouteserviceV2 } from 'components/L.Routing.OpenRouteServiceV2';
export const customRouter = openrouteserviceV2({
profile: 'driving-car',
geometry_simplify: true,
host: '/ors',
});
//export const customRouter = L.Routing.osrmv1({
// serviceUrl: '/osrm/route/v1', // Replace with your URL
//});

View File

@@ -6,6 +6,7 @@
<q-footer> <q-footer>
<StatusBar /> <StatusBar />
</q-footer> </q-footer>
<!--
<q-drawer <q-drawer
v-model="drawer" v-model="drawer"
:width="200" :width="200"
@@ -34,6 +35,7 @@
</q-list> </q-list>
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
-->
<q-page-container> <q-page-container>
<router-view /> <router-view />
</q-page-container> </q-page-container>
@@ -51,6 +53,7 @@ import StatusBar from 'components/StatusBar.vue';
const socketioStore = useSocketioStore(); const socketioStore = useSocketioStore();
const drawer = ref(false); const drawer = ref(false);
/*
const route = useRoute(); const route = useRoute();
const menuList = [ const menuList = [
@@ -110,6 +113,7 @@ const menuList = [
route: 'ErrorNotFound', route: 'ErrorNotFound',
}, },
]; ];
*/
onMounted(() => { onMounted(() => {
socketioStore.bindEvents(); socketioStore.bindEvents();
socketioStore.connect(); socketioStore.connect();

View File

@@ -1,11 +1,42 @@
import { defineStore, acceptHMRUpdate } from 'pinia'; import { defineStore, acceptHMRUpdate } from 'pinia';
import { favorites } from 'constants/favorites' import { favorites } from 'constants/favorites'
interface RoutesSet {
[key: string]: RouteSet
}
interface LatLng {
lat: number | null | undefined;
lng: number | null | undefined;
}
interface routeSegments {
fromWaypoint: number;
toWaypoint: number;
distanceMeters: number;
timeSeconds: number;
toCoordinates: LatLng | null | undefined;
}
interface RouteSet {
start: [number, number] | [null, null] | [undefined, undefined] | null | undefined;
end: [number, number] | [null, null] | [undefined, undefined] | null | undefined;
wayPoints?: [number, number][] | [null, null] | [undefined, undefined] | null | undefined;
}
interface State { interface State {
zoom: number zoom: number;
center: [number, number] | [null, null] | null center: [number, number] | [null, null] | null;
markerLatLng: [number, number] | [null, null] | null markerLatLng: [number, number] | [null, null] | null;
qLocDrawer: boolean qLocDrawer: boolean;
routeSet: {
start: LatLng | null | undefined;
end: LatLng | null | undefined;
wayPoints?: LatLng[] | null | undefined;
};
routesSet: RoutesSet[] | null;
routeSegments?: routeSegments[] | null;
} }
export const useLeafletStore = defineStore('leaflet', { export const useLeafletStore = defineStore('leaflet', {
@@ -15,6 +46,15 @@ export const useLeafletStore = defineStore('leaflet', {
center: [favorites.home.coords.lat, favorites.home.coords.lng], center: [favorites.home.coords.lat, favorites.home.coords.lng],
markerLatLng: null, markerLatLng: null,
qLocDrawer: false, qLocDrawer: false,
routeSet:
{
start: { lat: null, lng: null },
end: { lat: null, lng: null},
wayPoints: null,
},
routesSet: null,
routeSegments: null,
} }
}, },
actions: { actions: {

View File

@@ -11,6 +11,7 @@ import type {
StatusUpdate, StatusUpdate,
FindMyUpdate, FindMyUpdate,
iCloudMonitorResponse, iCloudMonitorResponse,
SimulationStatus,
} from 'components/models'; } from 'components/models';
@@ -110,6 +111,20 @@ export const useSocketioStore = defineStore('socketio', {
this.findMyUpdate = data; this.findMyUpdate = data;
}); });
socket.on('simulation_status', (data: SimulationStatus): void => {
if (debugLog) {
console.log('event: simulation_status received: ', data);
}
console.log('updating currentLocation', data)
this.currentLocation = {
loc_id: data.loc_id,
latitude: data.latitude,
longitude: data.longitude,
next_move: data.next_move,
};
this.locationQueueData[data.loc_id]['start'] = data.start;
});
socket.on('icloud_2fa_request', (callback) => { socket.on('icloud_2fa_request', (callback) => {
if (debugLog) { if (debugLog) {
console.log('iCloud 2FA Request'); console.log('iCloud 2FA Request');
@@ -328,6 +343,24 @@ export const useSocketioStore = defineStore('socketio', {
}, },
); );
break; break;
case 'test-mode':
socket.emit(
'simulation_control',
{ command: 'test-mode' },
(response: SimulationControlResponse) => {
if (response.status === 'error') {
throw new Error(response.message);
} else {
this.simulationState = response.status;
if (debugLog) {
console.log(response.message, response);
}
return response.message;
}
},
);
break;
case 'pause': case 'pause':
if (this.simulationState !== 'RUNNING') { if (this.simulationState !== 'RUNNING') {
throw new Error('Simulation is not running'); throw new Error('Simulation is not running');
@@ -368,9 +401,6 @@ export const useSocketioStore = defineStore('socketio', {
if (this.simulationQueueLength == 0) { if (this.simulationQueueLength == 0) {
throw new Error('Simulation queue is empty'); throw new Error('Simulation queue is empty');
} }
if (this.simulationState == 'STOPPED ' || !this.simulationRunning) {
throw new Error('Simulation is not running');
}
socket.emit('simulation_control', { command: 'clear' }, (response) => { socket.emit('simulation_control', { command: 'clear' }, (response) => {
if (response.status == 'error') { if (response.status == 'error') {
throw new Error(response.message); throw new Error(response.message);
@@ -417,15 +447,6 @@ export const useSocketioStore = defineStore('socketio', {
if (debugLog) { if (debugLog) {
console.log('response from simulate_control_add: ', response); console.log('response from simulate_control_add: ', response);
} }
const locMrk = {
[response.loc_id]: {
loc_id: response.loc_id,
latitude: response.latitude,
longitude: response.longitude,
delay: response.delay,
start_time: response.start_time,
},
};
return response.message; return response.message;
} }
}, },

1
src/types/leaflet-esm.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'leaflet/dist/leaflet-src.esm';

View File

@@ -1,12 +1,12 @@
declare module "leaflet-routing-machine" { declare module 'leaflet-routing-machine' {
import * as L from "leaflet"; import type * as L from 'leaflet';
export interface IRouter {} export type IRouter = Record<string, unknown>;
export interface IGeocoder {} export type IGeocoder = Record<string, unknown>;
export interface LineOptions extends L.PolylineOptions {} export type LineOptions = L.PolylineOptions;
export namespace Routing { export namespace Routing {
function control(options: any): any; function control(options: Record<string, unknown>): Record<string, unknown>;
class Plan {} class Plan {}
} }

1
src/types/openrouteservice-js.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'openrouteservice-js';