leaflet routimg machine - lical osr
This commit is contained in:
24
package-lock.json
generated
24
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
278
src/components/L.Routing.OpenRouteServiceV2.ts
Normal file
278
src/components/L.Routing.OpenRouteServiceV2.ts
Normal 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;
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
import { socket } from 'boot/socketio';
|
export {};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
45
src/composables/useMarkerContextMenu.ts
Normal file
45
src/composables/useMarkerContextMenu.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
54
src/composables/useRoutingEvents.ts
Normal file
54
src/composables/useRoutingEvents.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
47
src/functions/reverseGeocode.ts
Normal file
47
src/functions/reverseGeocode.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
11
src/functions/serviceURL.ts
Normal file
11
src/functions/serviceURL.ts
Normal 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
|
||||||
|
//});
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
1
src/types/leaflet-esm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module 'leaflet/dist/leaflet-src.esm';
|
||||||
12
src/types/leaflet-routing-machine.d.ts
vendored
12
src/types/leaflet-routing-machine.d.ts
vendored
@@ -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
1
src/types/openrouteservice-js.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module 'openrouteservice-js';
|
||||||
Reference in New Issue
Block a user