Compare commits
13 Commits
9022ac1b94
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ddaf682d16 | |||
| 52f05550e0 | |||
| d72550b3c1 | |||
| 32c4f2a835 | |||
| 27a2904bab | |||
| 9b8b9ec664 | |||
| a6264fad67 | |||
| e63a8a6329 | |||
| 05e63a28f1 | |||
| 3f3a5136eb | |||
| 8214f0543a | |||
| d1cb31b2c8 | |||
| e45a9eb20c |
@@ -39,6 +39,7 @@ export default defineConfigWithVueTs(
|
||||
files: ['**/*.ts', '**/*.vue'],
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
||||
'vue/no-v-text-v-html-on-component': 'off'
|
||||
},
|
||||
},
|
||||
// https://github.com/vuejs/eslint-config-typescript
|
||||
|
||||
3706
package-lock.json
generated
15
package.json
@@ -15,21 +15,30 @@
|
||||
"postinstall": "quasar prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.4",
|
||||
"@lucide/vue": "^1.7.0",
|
||||
"@quasar/extras": "^1.18.0",
|
||||
"@sentry/tracing": "^7.120.4",
|
||||
"@sentry/vue": "^10.47.0",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"axios": "^1.2.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-contextmenu": "^1.4.0",
|
||||
"leaflet-extra-markers": "^2.0.1",
|
||||
"leaflet-geosearch": "^4.2.2",
|
||||
"leaflet-routing-machine": "^3.2.12",
|
||||
"openrouteservice-js": "^0.4.1",
|
||||
"pinia": "^3.0.4",
|
||||
"quasar": "^2.16.0",
|
||||
"quasar": "^2.19.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-draggable-plus": "^0.6.1",
|
||||
"vue-router": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@quasar/app-vite": "^2.1.0",
|
||||
"@quasar/app-vite": "^2.6.0",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/leaflet-contextmenu": "^1.4.4",
|
||||
"@types/node": "^20.5.9",
|
||||
"@vue/eslint-config-prettier": "^10.1.0",
|
||||
"@vue/eslint-config-typescript": "^14.4.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 759 B |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -2,6 +2,7 @@
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file
|
||||
|
||||
import { defineConfig } from '#q-app/wrappers';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export default defineConfig((/* ctx */) => {
|
||||
return {
|
||||
@@ -11,10 +12,7 @@ export default defineConfig((/* ctx */) => {
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: [
|
||||
'axios',
|
||||
'socket',
|
||||
],
|
||||
boot: ['axios', 'socketio'],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
||||
css: ['app.scss'],
|
||||
@@ -22,19 +20,24 @@ export default defineConfig((/* ctx */) => {
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
// 'mdi-v7',
|
||||
'mdi-v7',
|
||||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'line-awesome',
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
|
||||
'roboto-font', // optional, you are not bound to it
|
||||
'material-icons', // optional, you are not bound to it
|
||||
// 'roboto-font', // optional, you are not bound to it
|
||||
// 'material-icons', // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||
build: {
|
||||
alias: {
|
||||
constants: fileURLToPath(new URL('./src/constants', import.meta.url)),
|
||||
functions: fileURLToPath(new URL('./src/functions', import.meta.url)),
|
||||
types: fileURLToPath(new URL('./src/types', import.meta.url)),
|
||||
},
|
||||
target: {
|
||||
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||
node: 'node20',
|
||||
@@ -46,7 +49,7 @@ export default defineConfig((/* ctx */) => {
|
||||
// extendTsConfig (tsConfig) {}
|
||||
},
|
||||
|
||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||
// vueRouterBase,
|
||||
// vueDevtools,
|
||||
// vueOptionsAPI: false,
|
||||
@@ -65,13 +68,32 @@ export default defineConfig((/* ctx */) => {
|
||||
// extendViteConf (viteConf) {},
|
||||
// viteVuePluginOptions: {},
|
||||
|
||||
extendViteConf () {
|
||||
extendViteConf() {
|
||||
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: {
|
||||
allowedHosts: [
|
||||
'localhost',
|
||||
'strixx.famor.org',
|
||||
],
|
||||
hmr: {
|
||||
// overlay: false,
|
||||
},
|
||||
allowedHosts: ['localhost', 'strixx.famor.org', 'simloc.strixx.intrepidnet.org'],
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -85,6 +107,7 @@ export default defineConfig((/* ctx */) => {
|
||||
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{ts,js,mjs,cjs,vue}"',
|
||||
useFlatConfig: true,
|
||||
},
|
||||
overlay: false,
|
||||
},
|
||||
{ server: false },
|
||||
],
|
||||
@@ -95,30 +118,41 @@ export default defineConfig((/* ctx */) => {
|
||||
devServer: {
|
||||
// https: true,
|
||||
open: false, // opens browser window automatically
|
||||
public: 'http://strixx.famor.org:9000',
|
||||
// public: 'https://simloc.strixx.intrepidnet.org',
|
||||
proxy: {
|
||||
// proxy all requests starting with /api to jsonplaceholder
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
target: 'http://localhost:49151',
|
||||
changeOrigin: true,
|
||||
},
|
||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'http://localhost:8000', // Your backend WebSocket server
|
||||
target: 'http://localhost:49151', // Your backend WebSocket server
|
||||
secure: false, // Set to true if using wss://
|
||||
ws: true, // Enable WebSocket proxying
|
||||
rewriteWsOrigin: true,
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/socket.io/, ''),
|
||||
}
|
||||
// rewrite: (path) => path.replace(/^\/socket.io/, ''),
|
||||
},
|
||||
'/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
|
||||
framework: {
|
||||
config: {
|
||||
dark: true,
|
||||
},
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
iconSet: 'mdi-v7',
|
||||
// lang: 'en-US', // Quasar language pack
|
||||
|
||||
// For special cases outside of where the auto-import strategy can have an impact
|
||||
@@ -129,14 +163,11 @@ export default defineConfig((/* ctx */) => {
|
||||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [
|
||||
'Dialog',
|
||||
'Notify',
|
||||
],
|
||||
plugins: ['Dialog', 'Notify'],
|
||||
},
|
||||
// animations: 'all', // --- includes all animations
|
||||
// https://v2.quasar.dev/options/animations
|
||||
animations: [],
|
||||
animations: ['slideInLeft', 'slideOutLeft', 'slideOutRight'],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
|
||||
// sourceFiles: {
|
||||
|
||||
194
src/]
@@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div style="height:600px; width:800px">
|
||||
<q-toolbar class="bg-primary text-white q-my-md shadow-2">
|
||||
<q-btn flat round dense icon="menu" class="q-mr-sm" />
|
||||
<q-separator dark vertical inset />
|
||||
<q-btn stretch flat :icon="home.icon" @click="handleFavClick(home.coords)" />
|
||||
|
||||
<q-space />
|
||||
|
||||
<q-btn-dropdown stretch flat label="Favorites">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="fav in favorites"
|
||||
:key="fav.id"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleFavClick(fav.coords)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar :icon="fav.icon" color="primary" text-color="white" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ fav.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</q-toolbar>
|
||||
<l-map @ready="onMapReady" ref="mapRef" :zoom="zoom" :center="center" style="height: 500px; width= 100%;" @click="updateMarker">
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
@click="updateMarker($event.latlng)"
|
||||
></l-tile-layer>
|
||||
<l-marker :lat-lng="markerLatLng" @click="handleMarkerClick"></l-marker>
|
||||
</l-map>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from "quasar";
|
||||
import { Ref, ref } from "vue";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { LMap, LTileLayer, LMarker } from "@vue-leaflet/vue-leaflet";
|
||||
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
|
||||
import "leaflet-geosearch/dist/geosearch.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import type { Map, LeafletMouseEvent } from 'leaflet';
|
||||
import SetLocationnDialog from "components/SetLocationDialog.vue";
|
||||
import { api } from "src/boot/axios";
|
||||
|
||||
const $q = useQuasar();
|
||||
const mapRef = ref(null);
|
||||
const zoom = ref(10);
|
||||
const center: Ref<[number, number]> = ref([40.71278, -74.00594]);
|
||||
const markerLatLng: Ref<[number, number]> = ref([40.71278, -74.00594]);
|
||||
const responseMessage = ref("");
|
||||
|
||||
interface coords {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
interface SearchControlProps {
|
||||
provider: OpenStreetMapProvider;
|
||||
showMarker: boolean;
|
||||
autoClose: boolean;
|
||||
updateMap: boolean;
|
||||
showPopup: boolean;
|
||||
style: 'button' | 'bar';
|
||||
acceptAutoLoad: boolean;
|
||||
autoComplete: boolean;
|
||||
autoCompleteDelay: number;
|
||||
retainZoomLevel: boolean;
|
||||
animateZoom: boolean;
|
||||
keepResult: boolean;
|
||||
}
|
||||
const onMapReady = (map: Map) => {
|
||||
const provider = new OpenStreetMapProvider();
|
||||
const searchOptions: SearchControlProps = {
|
||||
provider: provider,
|
||||
showMarker: false,
|
||||
autoClose: true,
|
||||
updateMap: true,
|
||||
showPopup: true,
|
||||
style: 'bar',
|
||||
acceptAutoLoad: true,
|
||||
autoComplete: true,
|
||||
autoCompleteDelay: 250,
|
||||
retainZoomLevel: false,
|
||||
animateZoom: true,
|
||||
keepResult: true
|
||||
};
|
||||
|
||||
const searchControl = new GeoSearchControl(searchOptions);
|
||||
map.addControl(searchControl);
|
||||
};
|
||||
|
||||
function updateMarker(event: LeafletMouseEvent) {
|
||||
markerLatLng.value = [event.latlng.lat, event.latlng.lng];
|
||||
center.value = [event.latlng.lat, event.latlng.lng];
|
||||
}
|
||||
|
||||
function handleMarkerClick(event: LeafletMouseEvent) {
|
||||
$q.dialog({
|
||||
component: SetLocationnDialog,
|
||||
componentProps: {
|
||||
lat: event.latlng.lat,
|
||||
lng: event.latlng.lng
|
||||
}
|
||||
}).onOk(() => {
|
||||
console.log("Dialog confirmed");
|
||||
setLocation({ lat: event.latlng.lat, lng: event.latlng.lng });
|
||||
}).onCancel(() => {
|
||||
console.log("Dialog cancelled");
|
||||
}).onDismiss(() => {
|
||||
console.log("Dialog dismissed");
|
||||
});
|
||||
}
|
||||
|
||||
function handleFavClick(coords: coords) {
|
||||
center.value = [coords.lat, coords.lng];
|
||||
markerLatLng.value = [coords.lat, coords.lng];
|
||||
}
|
||||
|
||||
async function setLocation(coords: coords) {
|
||||
try {
|
||||
const payLoadData = {
|
||||
lat: coords.lat,
|
||||
lng: coords.lng
|
||||
};
|
||||
const { data } = await api({
|
||||
method: "post",
|
||||
url: "/set",
|
||||
data: payLoadData
|
||||
});
|
||||
console.log("Location set successfully:", data.data);
|
||||
responseMessage.value = `Location successfully set! New location: ${data.lat}, ${data.lng}`;
|
||||
$q.notify({ type: 'positive', message: responseMessage.value });
|
||||
} catch (error) {
|
||||
console.error("Error setting location:", error);
|
||||
responseMessage.value = `Failed to set location: ${error}`;
|
||||
$q.notify({ type: 'negative', message: responseMessage.value });
|
||||
}
|
||||
}
|
||||
|
||||
const home = {
|
||||
name: "Home",
|
||||
coords: {
|
||||
lat: 40.71278,
|
||||
lng: -74.00594
|
||||
},
|
||||
icon: "home"
|
||||
};
|
||||
|
||||
const favorites = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Central Park",
|
||||
coords: {
|
||||
lat: 40.785091,
|
||||
lng: -73.968285
|
||||
},
|
||||
icon: "park"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Times Square",
|
||||
coords: {
|
||||
lat: 40.758896,
|
||||
lng: -73.985130,
|
||||
},
|
||||
icon: "star"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Empire State Building",
|
||||
coords: {
|
||||
lat: 40.748817,
|
||||
lng: -73.985428
|
||||
},
|
||||
icon: "building"
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.l-map {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
15
src/assets/mdi-v7-icons.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
"mdi-dark",
|
||||
"mdi-flip-h",
|
||||
"mdi-flip-v",
|
||||
"mdi-inactive",
|
||||
"mdi-light",
|
||||
"mdi-rotate-135",
|
||||
"mdi-rotate-180",
|
||||
"mdi-rotate-225",
|
||||
"mdi-rotate-270",
|
||||
"mdi-rotate-315",
|
||||
"mdi-rotate-45",
|
||||
"mdi-rotate-90",
|
||||
"mdi-spin"
|
||||
]
|
||||
BIN
src/assets/simloc_logo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
1978
src/assets/simloc_logo.svg
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
src/assets/simloc_logo2.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
31
src/axios.ts
@@ -1,31 +0,0 @@
|
||||
import { defineBoot } from '#q-app/wrappers';
|
||||
import axios, { type AxiosInstance } from 'axios';
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$axios: AxiosInstance;
|
||||
$api: AxiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
// due to creating a Singleton instance here;
|
||||
// If any client changes this (global) instance, it might be a
|
||||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
const api = axios.create({ baseURL: 'http://localhost:5000/api' });
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
});
|
||||
|
||||
export { api };
|
||||
@@ -5,16 +5,19 @@ declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$axios: AxiosInstance;
|
||||
$api: AxiosInstance;
|
||||
$osm: AxiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
const api = axios.create({ baseURL: '/api' });
|
||||
const osm = axios.create({ baseURL: '/osm' });
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
app.config.globalProperties.$axios = axios
|
||||
app.config.globalProperties.$api = api
|
||||
app.config.globalProperties.$osm = osm
|
||||
})
|
||||
|
||||
export { axios, api };
|
||||
export { axios, api, osm };
|
||||
|
||||
|
||||
|
||||
18
src/boot/sentry.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineBoot } from '#q-app/wrappers'
|
||||
import * as Sentry from '@sentry/vue';
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: "https://c117cc7eee3a88df9d289edc77d57c60@o4511152447553536.ingest.us.sentry.io/4511152493559808",
|
||||
sendDefaultPii: true,
|
||||
enableLogs: true,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration()
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineBoot } from '#q-app/wrappers';
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import { io } from 'socket.io-client';
|
||||
import type { StatusUpdate } from 'src/types';
|
||||
|
||||
interface ServerToClientEvents {
|
||||
noArg: () => void;
|
||||
basicEmit: (a: number, b: string, c: Buffer) => void;
|
||||
withAck: (d: string, callback: (e: number) => void) => void;
|
||||
status_update: (d: StatusUpdate) => void;
|
||||
}
|
||||
|
||||
interface ClientToServerEvents {
|
||||
message: (data: string) => void;
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
set_location: (latitude: number, longitude: number, callback: (response: { success: boolean; data: { latitude: number; longitude: number }; message?: string }) => void) => void;
|
||||
request_update: (callback: (response: { statusUpdate: StatusUpdate }) => void) => void;
|
||||
command: (command: string, callback: (response: { success: boolean; message?: string }) => void ) => void;
|
||||
shutdown: (delay: number, callback: (response: { success: boolean; message?: string }) => void) => void;
|
||||
// ... other events
|
||||
}
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
app.config.globalProperties.$socket = socket;
|
||||
});
|
||||
|
||||
export { socket };
|
||||
|
||||
16
src/boot/socketio.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineBoot } from '#q-app/wrappers';
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import { io } from 'socket.io-client';
|
||||
import type { ServerToClientEvents, ClientToServerEvents } from 'components/models';
|
||||
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('/', {
|
||||
autoConnect: true,
|
||||
transports: ['websocket', 'webtransport', 'polling'],
|
||||
});
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
app.config.globalProperties.$socket = socket;
|
||||
});
|
||||
|
||||
export { socket };
|
||||
35
src/components/ConfirmCommandDiaglog.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<q-dialog ref="dlgRef" persistent>
|
||||
<q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm">
|
||||
<q-toolbar>
|
||||
<q-avatar :icon="icon" color="secondary" text-color="black" />
|
||||
<q-toolbar-title>Command Control</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
<q-card-section class="q-ml-lg">
|
||||
<div class="q-mb-sm">Are you sure you want to {{ name }} ? </div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="OK" @click="onOkClick" />
|
||||
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialogPluginComponent } from 'quasar';
|
||||
|
||||
defineProps({
|
||||
name: { type: String, required: true },
|
||||
icon: { type: String, required: true },
|
||||
});
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||
|
||||
function onOkClick() {
|
||||
onDialogOK();
|
||||
}
|
||||
</script>
|
||||
218
src/components/EditFavoriteDialog.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<q-dialog ref="dlgRef" persistent>
|
||||
<q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm" v-if="operation == 'clear'">
|
||||
<q-toolbar>
|
||||
<q-avatar icon="mdi-star" color="secondary" text-color="yellow" />
|
||||
<q-toolbar-title>Clear Favorite</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
<q-card-section class="q-ml-lg">
|
||||
Are you sure you want to clear favorite {{ formData.name }}?
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="OK" @click="onOkClick" />
|
||||
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
<q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm" v-else>
|
||||
<q-toolbar>
|
||||
<q-avatar icon="mdi-star" color="secondary" text-color="yellow" />
|
||||
<q-toolbar-title v-if="!isFavorite && operation == 'set'"
|
||||
>Save Location as Favorite</q-toolbar-title
|
||||
>
|
||||
<q-toolbar-title v-else-if="!isFavorite && operation == 'edit'"
|
||||
>Edit Favorite</q-toolbar-title
|
||||
>
|
||||
</q-toolbar>
|
||||
<q-card-section class="q-ml-lg">
|
||||
<q-input
|
||||
color="secondary"
|
||||
v-model="formData.name"
|
||||
label="Favorite Name"
|
||||
autofocus
|
||||
@keyup.enter="onOkClick"
|
||||
style="max-width: 250px"
|
||||
/>
|
||||
<div class="flex col q-col-gutter-md">
|
||||
<q-select
|
||||
color="accent"
|
||||
v-model="formData.category"
|
||||
:options="categories"
|
||||
label="Category"
|
||||
use-input
|
||||
new-value-mode="add-unique"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey"> No results or loading... </q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<q-select
|
||||
color="accent"
|
||||
v-model="formData.icon"
|
||||
:options="filteredIcons"
|
||||
option-label="name"
|
||||
option-value="icon"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
clearable
|
||||
label="Select Icon"
|
||||
@filter="iconFilterFcn"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
:name="formData.icon ? formData.icon : 'mdi-library'"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="scope.opt.icon" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
<div class="flex col q-col-gutter-md">
|
||||
<q-input
|
||||
v-model="formData.longitude"
|
||||
label="Longitude"
|
||||
style="max-width: 150px"
|
||||
color="secondary"
|
||||
readonly
|
||||
/>
|
||||
<q-input
|
||||
v-model="formData.latitude"
|
||||
label="Latitude"
|
||||
style="max-width: 150px"
|
||||
color="secondary"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="OK" @click="onOkClick" />
|
||||
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFavoriteStore } from 'stores/favorite';
|
||||
import { useDialogPluginComponent } from 'quasar';
|
||||
import type { Favorite } from 'components/models';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
|
||||
import iconList from '@quasar/extras/mdi-v7/icons.json';
|
||||
|
||||
interface Props {
|
||||
lat: number;
|
||||
lng: number;
|
||||
isFavorite: boolean;
|
||||
operation: string;
|
||||
}
|
||||
|
||||
const favoriteStore = useFavoriteStore();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||
|
||||
const iconsList = ref(iconList);
|
||||
|
||||
interface FormattedIcon {
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
const iconsListFormatted: FormattedIcon[] = iconsList.value.map((str) => {
|
||||
const name = str
|
||||
.replace(/^[a-z]+/, '')
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.trim();
|
||||
const icon = str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
return { name, icon };
|
||||
});
|
||||
|
||||
const filteredIcons = ref(iconsListFormatted.slice(0, 100));
|
||||
const icon = ref();
|
||||
const name = ref();
|
||||
const address = ref();
|
||||
const category = ref();
|
||||
const categories = computed({
|
||||
get: () => favoriteStore.categories,
|
||||
set: (value) => {
|
||||
favoriteStore.categories = value;
|
||||
},
|
||||
});
|
||||
const formData: Favorite = reactive({
|
||||
name: name.value,
|
||||
longitude: props.lng,
|
||||
latitude: props.lat,
|
||||
category: category.value,
|
||||
icon: icon.value,
|
||||
}) as Favorite;
|
||||
const loading = ref(true);
|
||||
|
||||
function iconFilterFcn(val: string, update: (callback: () => void) => void) {
|
||||
update(() => {
|
||||
if (val === '') {
|
||||
filteredIcons.value = iconsListFormatted.slice(0, 100);
|
||||
} else {
|
||||
const needle = val.toLowerCase();
|
||||
filteredIcons.value = iconsListFormatted
|
||||
.filter(v =>
|
||||
v.name.toLowerCase().includes(needle) ||
|
||||
v.icon.toLowerCase().includes(needle)
|
||||
)
|
||||
.slice(0, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onOkClick() {
|
||||
if (props.isFavorite && props.operation === 'clear') {
|
||||
favoriteStore.favoriteControl({ command: 'delete', favorite: formData });
|
||||
}
|
||||
favoriteStore.favoriteControl({ command: 'set', favorite: formData });
|
||||
onDialogOK({ operation: props.operation, data: formData });
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await reverseGeocodeRateLimited(props.lat, props.lng);
|
||||
console.log('reverse geocode response: ', response);
|
||||
address.value = response.address;
|
||||
if (response.favorite) {
|
||||
formData.name = response.favorite.name;
|
||||
formData.category = response.favorite.category;
|
||||
formData.icon = response.favorite.icon;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching reverse geocode:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await sleep(500);
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="sass" scoped>
|
||||
.add-loc-card
|
||||
width: 100%
|
||||
max-width: 450px
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>{{ title }}</p>
|
||||
<ul>
|
||||
<li v-for="todo in todos" :key="todo.id" @click="increment">
|
||||
{{ todo.id }} - {{ todo.content }}
|
||||
</li>
|
||||
</ul>
|
||||
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
|
||||
<p>Active: {{ active ? 'yes' : 'no' }}</p>
|
||||
<p>Clicks on todos: {{ clickCount }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Todo, Meta } from './models';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
todos?: Todo[];
|
||||
meta: Meta;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
todos: () => [],
|
||||
});
|
||||
|
||||
const clickCount = ref(0);
|
||||
function increment() {
|
||||
clickCount.value += 1;
|
||||
return clickCount.value;
|
||||
}
|
||||
|
||||
const todoCount = computed(() => props.todos.length);
|
||||
</script>
|
||||
93
src/components/FormattedAddress.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="white-space: pre-line">
|
||||
{{ formattedAddressLine1 }}
|
||||
</div>
|
||||
<div>
|
||||
{{ formattedAddressLine2 }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { NominatimResponse } from 'components/models';
|
||||
|
||||
const props = defineProps<{
|
||||
address?: NominatimResponse | undefined;
|
||||
}>();
|
||||
|
||||
const formattedAddressLine1 = computed(() => {
|
||||
if (props.address) {
|
||||
const place = [props.address.leisure, props.address.shop].filter(Boolean);
|
||||
const addy = [props.address.house_number, props.address.road].filter(Boolean).join(' ');
|
||||
return [place, addy].filter(Boolean).join('\n');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const formattedAddressLine2 = computed(() => {
|
||||
if (props.address) {
|
||||
const town = props.address.city ?? props.address.village ?? props.address.town;
|
||||
const stateAbbr = stateAbbrevMap[props.address.state] ?? props.address.state ?? ' ';
|
||||
const cityState = [town, stateAbbr].filter(Boolean).join(', ');
|
||||
return [cityState, props.address.postcode].filter(Boolean).join(' ');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const stateAbbrevMap: Record<string, string> = {
|
||||
Alabama: 'AL',
|
||||
Alaska: 'AK',
|
||||
Arizona: 'AZ',
|
||||
Arkansas: 'AR',
|
||||
California: 'CA',
|
||||
Colorado: 'CO',
|
||||
Connecticut: 'CT',
|
||||
Delaware: 'DE',
|
||||
Florida: 'FL',
|
||||
Georgia: 'GA',
|
||||
Hawaii: 'HI',
|
||||
Idaho: 'ID',
|
||||
Illinois: 'IL',
|
||||
Indiana: 'IN',
|
||||
Iowa: 'IA',
|
||||
Kansas: 'KS',
|
||||
Kentucky: 'KY',
|
||||
Louisiana: 'LA',
|
||||
Maine: 'ME',
|
||||
Maryland: 'MD',
|
||||
Massachusetts: 'MA',
|
||||
Michigan: 'MI',
|
||||
Minnesota: 'MN',
|
||||
Mississippi: 'MS',
|
||||
Missouri: 'MO',
|
||||
Montana: 'MT',
|
||||
Nebraska: 'NE',
|
||||
Nevada: 'NV',
|
||||
'New Hampshire': 'NH',
|
||||
'New Jersey': 'NJ',
|
||||
'New Mexico': 'NM',
|
||||
'New York': 'NY',
|
||||
'North Carolina': 'NC',
|
||||
'North Dakota': 'ND',
|
||||
Ohio: 'OH',
|
||||
Oklahoma: 'OK',
|
||||
Oregon: 'OR',
|
||||
Pennsylvania: 'PA',
|
||||
'Rhode Island': 'RI',
|
||||
'South Carolina': 'SC',
|
||||
'South Dakota': 'SD',
|
||||
Tennessee: 'TN',
|
||||
Texas: 'TX',
|
||||
Utah: 'UT',
|
||||
Vermont: 'VT',
|
||||
Virginia: 'VA',
|
||||
Washington: 'WA',
|
||||
'West Virginia': 'WV',
|
||||
Wisconsin: 'WI',
|
||||
Wyoming: 'WY',
|
||||
};
|
||||
</script>
|
||||
267
src/components/L.Routing.OpenRouteServiceV2.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
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)
|
||||
*/
|
||||
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>,
|
||||
);
|
||||
}
|
||||
|
||||
// Do not mutate the ESM namespace import (`import * as L`) because it is non-extensible
|
||||
// in production bundles. Consumers should import and use `openrouteserviceV2` directly.
|
||||
83
src/components/LRoutingMachine.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div style="display: none"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref,
|
||||
markRaw,
|
||||
inject,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
useAttrs,
|
||||
} from 'vue';
|
||||
|
||||
import { routingControlProps, setupRoutingControl } from 'functions/routingControl';
|
||||
import { Utilities, InjectionKeys } from '@vue-leaflet/vue-leaflet';
|
||||
|
||||
import 'leaflet';
|
||||
import 'leaflet-routing-machine';
|
||||
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
|
||||
|
||||
type RoutingControlInstance = {
|
||||
on: (listeners: unknown) => void;
|
||||
setWaypoints: (waypoints: unknown[]) => void;
|
||||
remove: () => void;
|
||||
};
|
||||
|
||||
// ---- Emits ----
|
||||
const emit = defineEmits<{
|
||||
(e: 'ready', value: RoutingControlInstance): void;
|
||||
}>();
|
||||
|
||||
// ---- Props ----
|
||||
const props = defineProps(routingControlProps);
|
||||
|
||||
// ---- Attrs (for events) ----
|
||||
const attrs = useAttrs();
|
||||
|
||||
// ---- Injections ----
|
||||
const { UseGlobalLeafletInjection, RegisterControlInjection } = InjectionKeys;
|
||||
const { WINDOW_OR_GLOBAL, assertInject, propsBinder, remapEvents } = Utilities;
|
||||
|
||||
const useGlobalLeaflet = inject(UseGlobalLeafletInjection, false);
|
||||
const registerControl = assertInject(RegisterControlInjection);
|
||||
|
||||
// ---- State ----
|
||||
const leafletObject = ref<RoutingControlInstance | null>(null);
|
||||
|
||||
// ---- Setup logic ----
|
||||
|
||||
const { options, methods } = setupRoutingControl(props);
|
||||
|
||||
onMounted(async () => {
|
||||
const leafletModule = useGlobalLeaflet
|
||||
? ((WINDOW_OR_GLOBAL as unknown as { L: { routing: { control: (options: unknown) => unknown } } }).L)
|
||||
: ((await import('leaflet/dist/leaflet-src.esm')) as unknown as {
|
||||
routing: { control: (options: unknown) => unknown };
|
||||
});
|
||||
const routing = leafletModule.routing;
|
||||
|
||||
const { listeners } = remapEvents(attrs);
|
||||
|
||||
const control = markRaw(routing.control(options) as object) as RoutingControlInstance;
|
||||
leafletObject.value = control;
|
||||
control.on(listeners);
|
||||
|
||||
propsBinder(methods, control, props);
|
||||
(registerControl as unknown as (payload: { leafletObject: unknown }) => void)({
|
||||
leafletObject: control,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
emit('ready', control);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (leafletObject.value) {
|
||||
leafletObject.value.setWaypoints([]);
|
||||
leafletObject.value.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
412
src/components/LocationItem.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useSimulationStore } from 'stores/simulation';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import type { NominatimResponse } from 'components/models';
|
||||
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
|
||||
|
||||
import FormattedAddress from 'components/FormattedAddress.vue';
|
||||
//import NestedKnob from 'components/NestedKnob.vue';
|
||||
|
||||
const simulationStore = useSimulationStore();
|
||||
const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } =
|
||||
storeToRefs(simulationStore);
|
||||
|
||||
const props = defineProps({
|
||||
address: {
|
||||
type: Object as () => NominatimResponse,
|
||||
required: false,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'mdi-map-marker',
|
||||
},
|
||||
start: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
end: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCurrentDelay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loc_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLast: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: 'queued',
|
||||
},
|
||||
});
|
||||
|
||||
// Define custom events that this component can emit
|
||||
const emit = defineEmits(['item-clicked']);
|
||||
|
||||
function itemClicked() {
|
||||
const param1 = props.loc_id;
|
||||
emit('item-clicked', param1);
|
||||
}
|
||||
|
||||
const currentTime = ref(new Date());
|
||||
let timerId: number;
|
||||
|
||||
function formatInAgo(seconds: number) {
|
||||
let delta;
|
||||
if (seconds === 0) {
|
||||
delta = 'now';
|
||||
}
|
||||
if (seconds > 0) {
|
||||
const minutes: number = Math.floor(seconds / 60);
|
||||
const hours: number = Math.floor(minutes / 60);
|
||||
const days: number = Math.floor(hours / 24);
|
||||
if (days > 0) {
|
||||
delta = `in ${days} day${days > 1 ? 's' : ''}`;
|
||||
} else if (hours > 0) {
|
||||
delta = `in ${hours} hour${hours > 1 ? 's' : ''}`;
|
||||
} else if (minutes > 0) {
|
||||
delta = `in ${minutes} minute${minutes > 1 ? 's' : ''}`;
|
||||
} else {
|
||||
delta = `in ${seconds} second${seconds > 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
const calculateDeltaTime = computed(() => {
|
||||
let delta;
|
||||
if (props.active) {
|
||||
delta = 'now';
|
||||
} else if (props.end && props.end !== '') {
|
||||
const pastTime: Date = new Date(props.end);
|
||||
const nowMs: number = currentTime.value.getTime();
|
||||
const pastMs: number = pastTime.getTime();
|
||||
const diffMs: number = Math.abs(nowMs - pastMs);
|
||||
const seconds: number = Math.floor(diffMs / 1000);
|
||||
const minutes: number = Math.floor(seconds / 60);
|
||||
const hours: number = Math.floor(minutes / 60);
|
||||
const days: number = Math.floor(hours / 24);
|
||||
if (days > 0) {
|
||||
delta = `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
} else if (hours > 0) {
|
||||
delta = `${hours} hr${hours > 1 ? 's' : ''} ago`;
|
||||
} else if (minutes > 0) {
|
||||
delta = `${minutes} min${minutes > 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
delta = `${seconds} sec${seconds > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
} else {
|
||||
let startSlice;
|
||||
if (currentIndex.value > 0) {
|
||||
startSlice = currentIndex.value;
|
||||
} else {
|
||||
startSlice = 0;
|
||||
}
|
||||
const waitingFor = locationQueueOrder.value.slice(startSlice, myIndex.value + 1);
|
||||
const willWait = waitingFor.reduce((sum, loc_id) => {
|
||||
return sum + (locationQueueData.value[loc_id]?.delay || 0);
|
||||
}, 0);
|
||||
delta = formatInAgo(willWait);
|
||||
}
|
||||
return delta;
|
||||
});
|
||||
|
||||
const secondsToHhMmSs = (seconds: number) => {
|
||||
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 secondsToTimeShort = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (hours > 2) {
|
||||
return hours.toString();
|
||||
}
|
||||
if (minutes > 5) {
|
||||
return minutes.toString();
|
||||
} else return seconds.toString();
|
||||
};
|
||||
const timeToSeconds = (timeIn: string) => {
|
||||
const a = timeIn.split(':');
|
||||
const seconds = +a[0] * 60 * 60 + +a[1] * 60 + +a[2];
|
||||
return seconds;
|
||||
};
|
||||
*/
|
||||
const humanReadableDateTime = (iso: string) => {
|
||||
return new Date(iso).toLocaleDateString('en-US', {
|
||||
// year: 'numeric',
|
||||
// month: 'long',
|
||||
// day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const currentIndex = computed(() => {
|
||||
return currentLocation.value ? locationQueueOrder.value.indexOf(currentLocation.value.loc_id) : 0;
|
||||
});
|
||||
|
||||
const myIndex = computed(() => {
|
||||
return locationQueueOrder.value.indexOf(props.loc_id);
|
||||
});
|
||||
|
||||
const myUpdatedIndex = computed(() => {
|
||||
return myIndex.value - currentIndex.value;
|
||||
});
|
||||
|
||||
const itemClass = computed(() => {
|
||||
if (myUpdatedIndex.value > 0) return 'future';
|
||||
else if (myUpdatedIndex.value < 0) return 'past';
|
||||
else return 'active';
|
||||
});
|
||||
|
||||
const customIcon = computed(() => {
|
||||
if (myUpdatedIndex.value > 0) {
|
||||
return new Icon({
|
||||
color: 'blue',
|
||||
accentColor: 'firebrick',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
});
|
||||
} else if (myUpdatedIndex.value < 0) {
|
||||
return new Icon({
|
||||
color: 'black',
|
||||
accentColor: 'grey',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
});
|
||||
} else {
|
||||
return new Icon({
|
||||
color: 'pink',
|
||||
accentColor: 'black',
|
||||
content: '*',
|
||||
contentColor: 'black',
|
||||
scale: 1,
|
||||
svg: PinStarPanel,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const iconElement = computed(() => {
|
||||
return customIcon.value.createIcon();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const update = () => {
|
||||
currentTime.value = new Date();
|
||||
timerId = requestAnimationFrame(update);
|
||||
};
|
||||
timerId = requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
const delayConverted = computed({
|
||||
get: () => {
|
||||
if (props.delay <= 260) {
|
||||
return props.delay;
|
||||
} else if (props.delay <= 3600) {
|
||||
return Math.floor(props.delay / 60);
|
||||
} else {
|
||||
return Math.floor(props.delay / 3600);
|
||||
}
|
||||
},
|
||||
set: (val) => {
|
||||
if (getDelayAttrs.value.units == 's') {
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', val);
|
||||
} else if (getDelayAttrs.value.units == 'm') {
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', val * 60);
|
||||
} else if (getDelayAttrs.value.units == 'h') {
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', val * 3600);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const delayHhMmSs = computed({
|
||||
get: () => secondsToHhMmSs(props.delay),
|
||||
set: (val) => {
|
||||
const totalSeconds = val.split(':').reduce((acc, time) => 60 * acc + +time, 0);
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', totalSeconds);
|
||||
},
|
||||
});
|
||||
|
||||
const getDelayAttrs = computed(() => {
|
||||
if (props.delay <= 260) {
|
||||
return {
|
||||
units: 's',
|
||||
display: Math.floor((props.delay % 3600) / 60),
|
||||
step: 10,
|
||||
max: 80,
|
||||
} as const;
|
||||
} else if (props.delay <= 3600) {
|
||||
return {
|
||||
units: 'm',
|
||||
display: Math.floor((props.delay % 3600) / 60),
|
||||
step: 1,
|
||||
max: 80,
|
||||
} as const;
|
||||
} else {
|
||||
return {
|
||||
units: 'h',
|
||||
display: Math.floor(props.delay / 3600),
|
||||
step: 1,
|
||||
max: 12,
|
||||
} as const;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
const delayMask = computed(() => {
|
||||
if (props.delay < 60) {
|
||||
return 'ss';
|
||||
}
|
||||
if (props.delay < 3600) {
|
||||
return 'mm:ss';
|
||||
} else return 'HH:mm:ss';
|
||||
});
|
||||
|
||||
const delayInputMask = computed(() => {
|
||||
if (props.delay < 60) {
|
||||
return '##';
|
||||
}
|
||||
if (props.delay < 3600) {
|
||||
return '##:##';
|
||||
} else return '##:##:##';
|
||||
});
|
||||
*/
|
||||
|
||||
const displayDelay = computed(() => {
|
||||
return props.index > currentIndex.value;
|
||||
});
|
||||
|
||||
const delayActive = computed(() => {
|
||||
return props.index === currentIndex.value + 1;
|
||||
});
|
||||
|
||||
const canDrag = computed(() => {
|
||||
return props.index > currentIndex.value + 1;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(timerId);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<q-item
|
||||
v-if="displayDelay"
|
||||
:active="delayActive"
|
||||
:class="delayActive ? 'text-orange bg-dark' : ''"
|
||||
active-class="text-orange bg-dark"
|
||||
>
|
||||
<q-item-section class="flex flex-center">
|
||||
<q-input
|
||||
class="q-pt-md"
|
||||
filled
|
||||
v-model="delayHhMmSs"
|
||||
mask="fulltime"
|
||||
stack-label
|
||||
label="Delay"
|
||||
dense
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:append> </template>
|
||||
</q-input>
|
||||
<q-slider
|
||||
v-model="delayConverted"
|
||||
:min="0"
|
||||
:step="getDelayAttrs.step"
|
||||
:max="getDelayAttrs.max"
|
||||
label
|
||||
switch-label-side
|
||||
:label-value="delayConverted + ' ' + getDelayAttrs.units"
|
||||
markers
|
||||
snap
|
||||
color="primary"
|
||||
style="max-width: 75%"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator inset spaced v-if="displayDelay" />
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
:active="active"
|
||||
@click="itemClicked"
|
||||
:class="itemClass"
|
||||
active-class="text-orange bg-dark"
|
||||
>
|
||||
<q-item-section style="width: 70%">
|
||||
<q-item-label v-if="address">
|
||||
<FormattedAddress :address="props.address" />
|
||||
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
|
||||
</q-item-label>
|
||||
<q-item-label v-else> {{ latitude }}, {{ longitude }} </q-item-label>
|
||||
<q-item-label caption lines="1" v-if="start && simulationRunning">
|
||||
start: {{ humanReadableDateTime(start) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="1" v-if="end && simulationRunning">
|
||||
end: {{ humanReadableDateTime(end) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="1" v-if="!active && !end">
|
||||
delay: {{ delay }} seconds
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
side
|
||||
style="
|
||||
padding: 30px 0 0 10px;
|
||||
height: 100%;
|
||||
width: 35%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<q-icon :class="{ 'drag-handle': canDrag }" v-html="iconElement.outerHTML" />
|
||||
<q-item-label caption lines="1" v-if="simulationRunning">
|
||||
{{ calculateDeltaTime }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator spaced inset v-if="!isLast" />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="sass" scoped>
|
||||
.past
|
||||
color: gray
|
||||
</style>
|
||||
821
src/components/MenuBar.vue
Normal file
@@ -0,0 +1,821 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
|
||||
|
||||
import { favorites } from 'constants/favorites';
|
||||
import { controls } from 'constants/controls';
|
||||
|
||||
import type { coords, DeviceCommands, TunneldCommands } from 'components/models';
|
||||
|
||||
import { useDeviceStore } from 'stores/device';
|
||||
import { useLeafletStore } from 'stores/leaflet';
|
||||
import { useSimulationStore } from 'stores/simulation';
|
||||
import { useIcloudStore } from 'stores/icloud';
|
||||
import { useTunneldStore } from 'stores/tunneld';
|
||||
|
||||
const route = useRoute();
|
||||
const $q = useQuasar();
|
||||
|
||||
const deviceStore = useDeviceStore();
|
||||
const leafletStore = useLeafletStore();
|
||||
const simulationStore = useSimulationStore();
|
||||
const icloudStore = useIcloudStore();
|
||||
const tunneldStore = useTunneldStore();
|
||||
const { center, markerLatLng, zoom } = storeToRefs(leafletStore);
|
||||
|
||||
const {
|
||||
simulationRunning,
|
||||
simulationState,
|
||||
simulationQueueLength,
|
||||
locationQueueOrder,
|
||||
testMode,
|
||||
gpsNoise,
|
||||
currentLocation,
|
||||
} = storeToRefs(simulationStore);
|
||||
|
||||
const { icloudMonitor } = storeToRefs(icloudStore);
|
||||
|
||||
const menuOpen = ref(false);
|
||||
const favoritesMap = favorites as Record<string, unknown>;
|
||||
|
||||
const isLast = computed(() => {
|
||||
if (locationQueueOrder.value && locationQueueOrder.value.length > 0 && currentLocation.value) {
|
||||
const currentIndex = locationQueueOrder.value.indexOf(currentLocation.value.loc_id);
|
||||
return locationQueueOrder.value.length - 1 === currentIndex;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
center.value = [coords.lat, coords.lng];
|
||||
markerLatLng.value = [coords.lat, coords.lng];
|
||||
zoom.value = 15;
|
||||
}
|
||||
|
||||
function handleTestToggle() {
|
||||
const response = simulationStore.simulationControl({
|
||||
command: 'test-mode',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
loc_id: null,
|
||||
delay: 0,
|
||||
address: null,
|
||||
});
|
||||
if (response.sts === 'error') {
|
||||
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
|
||||
}
|
||||
}
|
||||
|
||||
function handleGpsNoiseToggle() {
|
||||
const response = simulationStore.simulationControl({
|
||||
command: 'gps-noise',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
loc_id: null,
|
||||
delay: 0,
|
||||
address: null,
|
||||
});
|
||||
if (response.sts === 'error') {
|
||||
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
|
||||
}
|
||||
}
|
||||
|
||||
function handleControlClick(cmdAttr: ControlAction) {
|
||||
if (cmdAttr.cnfrm) {
|
||||
$q.dialog({
|
||||
component: ConfirmCommandDialog,
|
||||
componentProps: {
|
||||
name: cmdAttr.name,
|
||||
icon: cmdAttr.icon,
|
||||
},
|
||||
})
|
||||
.onOk(() => {
|
||||
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = simulationStore.simulationControl({
|
||||
command: cmdAttr['cmd'],
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
loc_id: null,
|
||||
delay: cmdAttr.delay,
|
||||
address: null,
|
||||
});
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Simulation Command ERROR: ', error.message);
|
||||
notMsg = `Simulation Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Simulation Command Error: ', error);
|
||||
notMsg = 'Simulation Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Device Command ERROR: ', error.message);
|
||||
notMsg = `Device Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Device Command Error: ', error);
|
||||
notMsg = 'Device Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Tunneld Command ERROR: ', error.message);
|
||||
notMsg = `Tunneld Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Tunneld Command Error: ', error);
|
||||
notMsg = 'Tunneld Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
})
|
||||
.onCancel(() => {
|
||||
console.log('Dialog cancelled');
|
||||
})
|
||||
.onDismiss(() => {
|
||||
console.log('Dialog dismissed');
|
||||
});
|
||||
} else {
|
||||
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = simulationStore.simulationControl({
|
||||
command: cmdAttr.cmd,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
loc_id: null,
|
||||
delay: cmdAttr.delay,
|
||||
address: null,
|
||||
});
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Simulation Command ERROR: ', error.message);
|
||||
notMsg = `Simulation Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Simulation Command Error: ', error);
|
||||
notMsg = 'Simulation Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
if (cmdAttr.cmdClass === 'icloud-monitor_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = icloudStore.icloudMonitorControl(cmdAttr.cmd);
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Simulation Command ERROR: ', error.message);
|
||||
notMsg = `Simulation Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Simulation Command Error: ', error);
|
||||
notMsg = 'Simulation Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Device Command ERROR: ', error.message);
|
||||
notMsg = `Device Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Device Command Error: ', error);
|
||||
notMsg = 'Device Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
|
||||
let notType: string = 'positive';
|
||||
let notMsg: string = '';
|
||||
try {
|
||||
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
|
||||
if (ack.sts === 'error') {
|
||||
notType = 'negative';
|
||||
}
|
||||
if (ack.msg) {
|
||||
notMsg = ack.msg;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
notType = 'negative';
|
||||
if (error instanceof Error) {
|
||||
console.error('Tunneld Command ERROR: ', error.message);
|
||||
notMsg = `Tunneld Command Error: ${error.message}`;
|
||||
} else {
|
||||
console.error('Tunneld Command Error: ', error);
|
||||
notMsg = 'Tunneld Command Error: Unknow error';
|
||||
}
|
||||
} finally {
|
||||
$q.notify({ type: notType, message: notMsg });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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-avatar>
|
||||
<q-img src="~assets/simloc_logo2.png" class="logo" />
|
||||
</q-avatar>
|
||||
<q-separator dark inset />
|
||||
<q-space />
|
||||
<q-btn
|
||||
:icon-right="menuOpen ? 'mdi-menu-up' : 'mdi-menu-down'"
|
||||
stretch
|
||||
flat
|
||||
label="Favorites"
|
||||
v-if="route.name === 'Leaflet'"
|
||||
class="text-weight-bold"
|
||||
>
|
||||
<q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
|
||||
<q-list dark>
|
||||
<template v-for="(favObj, favId) in favoritesMap" :key="favId">
|
||||
<q-item
|
||||
dark
|
||||
v-if="hasCoords(favObj)"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleFavClick(favObj.coords)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar :icon="favObj.icon" color="secondary" size="sm" text-color="black" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ favObj.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-else-if="hasSubitems(favObj)" clickable v-ripple dark>
|
||||
<q-item-section avatar>
|
||||
<q-avatar :icon="favObj.icon" color="secondary" size="sm" text-color="black" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ favObj.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="mdi-chevron-right" />
|
||||
</q-item-section>
|
||||
<q-menu anchor="bottom start" self="bottom end">
|
||||
<q-list dark>
|
||||
<q-item
|
||||
dark
|
||||
v-for="(favSubObj, favSubId) in favObj.subitems"
|
||||
:key="favSubId"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleFavClick(favSubObj.coords)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar
|
||||
:icon="favSubObj.icon"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
text-color="black"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ favSubObj.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<q-btn-dropdown class="text-weight-bold" stretch flat label="Controls">
|
||||
<q-list dark>
|
||||
<q-expansion-item
|
||||
group="controls"
|
||||
dense-toggle
|
||||
default-opened
|
||||
icon="mdi-map-marker"
|
||||
label="Simulation Controls"
|
||||
color="accent"
|
||||
>
|
||||
<q-item dark tag="label" v-ripple>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-toggle
|
||||
v-model="testMode"
|
||||
size="sm"
|
||||
color="yellow"
|
||||
@update:model-value="handleTestToggle"
|
||||
dark
|
||||
dense
|
||||
:disabled="simulationRunning"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Test Mode</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dark tag="label" v-ripple>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-toggle
|
||||
v-model="gpsNoise"
|
||||
size="sm"
|
||||
color="accent"
|
||||
@update:model-value="handleGpsNoiseToggle"
|
||||
dark
|
||||
dense
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>GPS Noise</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="!simulationRunning"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.simulation.start)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.simulation.start.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.simulation.start.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="simulationState === 'RUNNING' && simulationRunning"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.simulation.pause)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.simulation.pause.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.simulation.pause.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="simulationState === 'PAUSED'"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.simulation.resume)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.simulation.resume.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.simulation.resume.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="simulationQueueLength && simulationQueueLength > 0 && !isLast"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.simulation.clear)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.simulation.clear.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.simulation.clear.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="simulationQueueLength && simulationQueueLength > 0"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.simulation.reset)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.simulation.reset.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.simulation.reset.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="simulationRunning"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.simulation.end)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.simulation.end.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.simulation.end.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="controls"
|
||||
icon="mdi-cloud"
|
||||
dense-toggle
|
||||
label="iCloud Monitor Controls"
|
||||
color="accent"
|
||||
>
|
||||
<q-item
|
||||
dark
|
||||
v-if="!icloudMonitor"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.icloudmonitor.start)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.icloudmonitor.start.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.icloudmonitor.start.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
v-if="icloudMonitor"
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.icloudmonitor.stop)"
|
||||
size="sm"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.icloudmonitor.stop.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.icloudmonitor.stop.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="controls" dense-toggle icon="mdi-subway" label="Tunneld Controls">
|
||||
<q-item
|
||||
v-if="!tunneldStore.tunnelConnected"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.start)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.start.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.start.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="!tunneldStore.tunneldWatcher"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.start_watcher)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.start_watcher.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.start_watcher.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="tunneldStore.tunnelConnected"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.restart)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.restart.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.restart.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="tunneldStore.tunnelConnected"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.clear)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.clear.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.clear.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="tunneldStore.tunnelConnected"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.cancel)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.cancel.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.cancel.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="tunneldStore.tunneldWatcher"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.end_watcher)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.end_watcher.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.end_watcher.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
v-if="tunneldStore.tunnelConnected"
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.tunneld.shutdown)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.tunneld.shutdown.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.tunneld.shutdown.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
dense-toggle
|
||||
icon="mdi-raspberry-pi"
|
||||
label="Device Controls"
|
||||
group="controls"
|
||||
>
|
||||
<q-item
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.device.reboot)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.device.reboot.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.device.reboot.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item
|
||||
dark
|
||||
clickable
|
||||
v-ripple
|
||||
v-close-popup
|
||||
@click="handleControlClick(controls.device.shutdown)"
|
||||
>
|
||||
<q-item-section avatar class="q-pl-lg">
|
||||
<q-avatar
|
||||
:icon="controls.device.shutdown.icon"
|
||||
color="secondary"
|
||||
text-color="black"
|
||||
size="sm"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label> {{ controls.device.shutdown.name }} </q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</q-toolbar>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
.logo
|
||||
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||
display: inline-block
|
||||
cursor: pointer
|
||||
|
||||
|
||||
.logo:hover
|
||||
transform: scale(1.3) translateY(-5px)
|
||||
filter: drop-shadow(0 10px 15px rgba(0, 0, 0, 0.3))
|
||||
</style>
|
||||
110
src/components/NestedKnob.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="q-pa-md flex flex-center">
|
||||
<!-- Outer Container for positioning -->
|
||||
<div class="relative-position flex flex-center">
|
||||
<!-- 1. Outer Knob (e.g., Value A) -->
|
||||
<q-knob
|
||||
v-model="delayHours"
|
||||
:min="0"
|
||||
:max="24"
|
||||
size="55px"
|
||||
color="accent"
|
||||
track-color="primary"
|
||||
class="absolute"
|
||||
:thickness="0.2"
|
||||
/>
|
||||
|
||||
<!-- 2. Middle Knob (e.g., Value B) -->
|
||||
<q-knob
|
||||
v-model="delayMinutes"
|
||||
:min="0"
|
||||
:max="60"
|
||||
size="40px"
|
||||
color="accent"
|
||||
track-color="primary"
|
||||
class="absolute"
|
||||
:thickness="0.2"
|
||||
/>
|
||||
|
||||
<!-- 3. Inner Knob (e.g., Value C) -->
|
||||
<q-knob
|
||||
v-model="delaySeconds"
|
||||
:min="0"
|
||||
:max="60"
|
||||
size="25px"
|
||||
color="accent"
|
||||
track-color="secondary"
|
||||
class="absolute"
|
||||
:thickness="0.2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{ hhMmSs }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useSimulationStore } from 'stores/simulation';
|
||||
|
||||
const simulationStore = useSimulationStore();
|
||||
|
||||
const props = defineProps({
|
||||
loc_id: { type: String, required: true },
|
||||
delay: { type: Number, required: true },
|
||||
});
|
||||
|
||||
const delayHours = computed({
|
||||
get: () => Math.floor(props.delay / 3600),
|
||||
set: (val) => {
|
||||
const hrSeconds: number = val * 3600;
|
||||
const minSeconds: number = delayMinutes.value * 60;
|
||||
const secSeconds: number = delaySeconds.value;
|
||||
const totalSeconds: number = hrSeconds + minSeconds + secSeconds;
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', totalSeconds);
|
||||
},
|
||||
});
|
||||
|
||||
const delayMinutes = computed({
|
||||
get: () => Math.floor((props.delay % 3600) / 60),
|
||||
set: (val) => {
|
||||
const hrSeconds: number = delayHours.value * 3600;
|
||||
const minSeconds: number = val * 60;
|
||||
const secSeconds: number = delaySeconds.value;
|
||||
const totalSeconds: number = hrSeconds + minSeconds + secSeconds;
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', totalSeconds);
|
||||
},
|
||||
});
|
||||
|
||||
const delaySeconds = computed({
|
||||
get: () => Math.floor(props.delay % 60),
|
||||
set: (val) => {
|
||||
const hrSeconds: number = delayHours.value * 3600;
|
||||
const minSeconds: number = delayMinutes.value * 60;
|
||||
const secSeconds: number = val;
|
||||
const totalSeconds: number = hrSeconds + minSeconds + secSeconds;
|
||||
simulationStore.updateLocationMark(props.loc_id, 'delay', totalSeconds);
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
const secondsToHhMmSs = (seconds: number) => {
|
||||
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 hhMmSs = computed(() => {
|
||||
if (delayHours.value > 0)
|
||||
return `${delayHours.value.toString().padStart(2, '0')}:${delayMinutes.value.toString().padStart(2, '0')}:${delaySeconds.value.toString().padStart(2, '0')}`;
|
||||
if (delayMinutes.value > 0)
|
||||
return `${delayMinutes.value.toString().padStart(2, '0')}:${delaySeconds.value.toString().padStart(2, '0')}`;
|
||||
else return `${delaySeconds.value.toString().padStart(2, '0')}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass"></style>
|
||||
@@ -1,13 +1,60 @@
|
||||
<template>
|
||||
<q-dialog ref="dlgRef" persistent>
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-avatar icon="add_location" color="primary" text-color="white" />
|
||||
<span class="q-ml-sm"> Are you sure you want to set location to {{ lat }}, {{ lng }} ?</span>
|
||||
<q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm">
|
||||
<q-toolbar>
|
||||
<q-avatar icon="mdi-map-marker" color="primary" text-color="white" />
|
||||
<q-toolbar-title>Add Location to Queue</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
<q-card-section class="q-ml-lg">
|
||||
<div>Are you sure you want to set location to:</div>
|
||||
<div class="q-ml-md">
|
||||
<div class="relative-position">
|
||||
<div>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeIn"
|
||||
leave-active-class="animated fadeOut"
|
||||
>
|
||||
<formatted-address :address="address" v-if="!loading" />
|
||||
</transition>
|
||||
</div>
|
||||
<q-inner-loading :showing="loading" />
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
class="q-mt-md"
|
||||
style="max-width: 150px"
|
||||
v-model.number="delay"
|
||||
filled
|
||||
@keyup.enter="onOkClick"
|
||||
label="Delay "
|
||||
type="number"
|
||||
suffix="seconds"
|
||||
color="grey-4"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="OK" color="primary" @click="onOkClick" />
|
||||
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" />
|
||||
<q-separator />
|
||||
<q-card-actions align="between">
|
||||
<div class="text-yellow">
|
||||
<q-checkbox
|
||||
class="cursor-pointer"
|
||||
v-model="isFavorite"
|
||||
checked-icon="mdi-star"
|
||||
unchecked-icon="mdi-star-outline"
|
||||
indeterminate-icon="mdi-help"
|
||||
color="yellow"
|
||||
@click="handleFavoriteClick"
|
||||
:label="favoriteName"
|
||||
>
|
||||
<q-tooltip>Favorite</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<q-btn flat label="OK" @click="onOkClick" />
|
||||
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
||||
</div>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -15,21 +62,88 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialogPluginComponent } from 'quasar';
|
||||
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import FormattedAddress from 'components/FormattedAddress.vue';
|
||||
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
|
||||
import type { NominatimResponse } from 'components/models';
|
||||
|
||||
const props = defineProps({
|
||||
lat: { type: Number, required: true },
|
||||
lng: { type: Number, required: true }
|
||||
lng: { type: Number, required: true },
|
||||
});
|
||||
|
||||
defineEmits([
|
||||
...useDialogPluginComponent.emits
|
||||
])
|
||||
const $q = useQuasar();
|
||||
const loading = ref(true);
|
||||
const delay = ref(300);
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||
|
||||
function onOkClick () {
|
||||
onDialogOK()
|
||||
const address = ref<NominatimResponse>();
|
||||
const isFavorite = ref(false);
|
||||
const favorite = ref();
|
||||
const favoriteName = computed(() => favorite.value?.name ?? '');
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await reverseGeocodeRateLimited(props.lat, props.lng);
|
||||
console.log('reverse geocode response: ', response);
|
||||
address.value = response.address;
|
||||
if (response.favorite) {
|
||||
favorite.value = response.favorite;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching reverse geocode:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await sleep(500);
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
function handleFavoriteClick() {
|
||||
if (!favorite.value) {
|
||||
favorite.value = {
|
||||
name: '',
|
||||
icon: '',
|
||||
category: '',
|
||||
latitude: props.lat,
|
||||
longitude: props.lng,
|
||||
};
|
||||
}
|
||||
$q.dialog({
|
||||
component: EditFavoriteDialog,
|
||||
componentProps: {
|
||||
lat: props.lat,
|
||||
lng: props.lng,
|
||||
fav: favorite.value,
|
||||
},
|
||||
})
|
||||
.onOk(({ data }) => {
|
||||
favorite.value = data;
|
||||
})
|
||||
.onCancel(() => {
|
||||
console.log('Dialog cancelled');
|
||||
})
|
||||
.onDismiss(() => {
|
||||
console.log('Dialog dismissed');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onOkClick() {
|
||||
onDialogOK({ delay: delay.value, address: address.value });
|
||||
}
|
||||
</script>
|
||||
<style lang="sass" scoped>
|
||||
.add-loc-card
|
||||
width: 100%
|
||||
max-width: 450px
|
||||
</style>
|
||||
|
||||
1
src/components/SimulationCommands.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -1,23 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { socket } from 'boot/socket';
|
||||
import { useStatusStore } from 'stores/status';
|
||||
import { computed } from 'vue';
|
||||
import { socket } from 'boot/socketio';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ClientToServerEvents } from 'components/models';
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
const socketStore = useSocketStore();
|
||||
|
||||
const socketStatus = computed(() => statusStore.socketConnected ? 'green' : 'red');
|
||||
const sockEvent = ref('');
|
||||
const eventArgs = ref('');
|
||||
|
||||
const { sockConnected } = storeToRefs(socketStore);
|
||||
|
||||
const sockStatColor = computed(() => {
|
||||
return sockConnected.value ? 'green' : 'red';
|
||||
});
|
||||
|
||||
function handleEmit() {
|
||||
const event = sockEvent.value;
|
||||
const jsonArgs = eventArgs.value;
|
||||
socket.emit(event as keyof ClientToServerEvents, jsonArgs, (resp: unknown) => {
|
||||
console.log('Server Response: %s', resp);
|
||||
});
|
||||
sockEvent.value = '';
|
||||
eventArgs.value = '';
|
||||
}
|
||||
const timeWithSeconds = ref('00:00:45');
|
||||
|
||||
socket.off();
|
||||
statusStore.socketConnect();
|
||||
statusStore.bindEvents();
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
||||
<q-btn round icon="webhook">
|
||||
<q-badge floating :color="socketStatus" rounded />
|
||||
</q-btn>
|
||||
<q-btn label="Connect" @click="statusStore.socketConnect" />
|
||||
<q-btn label="Disconnect" @click="statusStore.socketDisconnect" />
|
||||
<q-btn label="Emit Hello" @click="socket.emit('message', 'Hello from Vue!')" />
|
||||
<q-btn icom="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
|
||||
<q-btn label="Connect" @click="socketStore.connect()" />
|
||||
<q-btn label="Disconnect" @click="socketStore.disconnect()" />
|
||||
</div>
|
||||
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
||||
<div class="text-h3">SocketIO Functions</div>
|
||||
<q-input v-model="sockEvent" filled label="Event" />
|
||||
<q-input v-model="eventArgs" filled label="Args" />
|
||||
<div>
|
||||
<q-btn label="Emit" @click="handleEmit" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<q-time v-model="timeWithSeconds" with-seconds format24h default-view="seconds" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,38 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
import { useSimulationStore } from 'stores/simulation';
|
||||
import { useIcloudStore } from 'stores/icloud';
|
||||
import { useDeviceStore } from 'stores/device';
|
||||
import { useTunneldStore } from 'stores/tunneld';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { api } from 'boot/axios';
|
||||
const socketStore = useSocketStore();
|
||||
const simulationStore = useSimulationStore();
|
||||
const icloudStore = useIcloudStore();
|
||||
const deviceStore = useDeviceStore();
|
||||
const tunneldStore = useTunneldStore();
|
||||
|
||||
const $q = useQuasar();
|
||||
const { sockConnected } = storeToRefs(socketStore);
|
||||
const { deviceConnected } = storeToRefs(deviceStore);
|
||||
const { tunnelConnected } = storeToRefs(tunneldStore);
|
||||
const { simulationState, testMode } = storeToRefs(simulationStore);
|
||||
const { icloudMonitor } = storeToRefs(icloudStore);
|
||||
|
||||
const statusDev = ref({ color: 'red', deviceName: 'No connected Device' });
|
||||
|
||||
async function getStatus() {
|
||||
try {
|
||||
const { data } = await api({
|
||||
method: 'get',
|
||||
url: '/status',
|
||||
});
|
||||
console.log('API Call: usbmux/status returned:', data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error setting location:', error.message);
|
||||
$q.notify({ type: 'negative', message: error.message });
|
||||
} else {
|
||||
console.error('Error setting location:', error);
|
||||
function statusDevColor(state: string | boolean | null | undefined): string {
|
||||
if (state === null || state === undefined) {
|
||||
return 'grey';
|
||||
}
|
||||
if (typeof state === 'boolean') {
|
||||
return state ? 'green' : 'red';
|
||||
} else {
|
||||
switch (state.toLowerCase()) {
|
||||
case 'paused':
|
||||
return 'yellow';
|
||||
case 'running':
|
||||
return 'green';
|
||||
case 'stopped':
|
||||
return 'red';
|
||||
default:
|
||||
return 'grey';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-toolbar class="bg-primary text-white q-my-md shadow-2">
|
||||
<q-badge :color="statusDev.color" rounded class="q-mr-sm" />{{ statusDev.deviceName }}
|
||||
<q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
|
||||
<div class="flex col">
|
||||
<q-space />
|
||||
<!--
|
||||
<div style="width: 80vw" class="flex justify-end">
|
||||
-->
|
||||
<div class="q-gutter-x-sm">
|
||||
<q-btn dense rounded push size="sm" icon="mdi-cog" @click="socketStore.toggleSock()">
|
||||
<q-badge :color="statusDevColor(sockConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
push
|
||||
size="sm"
|
||||
icon="mdi-cellphone"
|
||||
@click="socketStore.requestUpdate()"
|
||||
>
|
||||
<q-badge :color="statusDevColor(deviceConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn dense rounded push size="sm" icon="mdi-subway" @click="socketStore.requestUpdate()">
|
||||
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
push
|
||||
size="sm"
|
||||
icon="mdi-map-marker"
|
||||
@click="socketStore.requestUpdate()"
|
||||
>
|
||||
<q-badge :color="statusDevColor(simulationState)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
push
|
||||
size="sm"
|
||||
icon="mdi-cloud"
|
||||
@click="icloudStore.icloudMonitorControl('refresh')"
|
||||
>
|
||||
<q-badge :color="statusDevColor(icloudMonitor)" rounded floating />
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-toolbar>
|
||||
</template>
|
||||
|
||||
|
||||
41
src/components/iCloudCodeDialog.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<q-dialog ref="dlgRef" persistent>
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-avatar icon="password_2" color="primary" text-color="white" />
|
||||
<span class="q-ml-sm"> iCloud Account requires Two-factor authentication.</span>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
dense
|
||||
v-model="code"
|
||||
mask="######"
|
||||
autofocus
|
||||
@keyup.enter="onOkClick"
|
||||
label="Delay (seconds)"
|
||||
type="number"
|
||||
:rules="[(val) => val.length === 6 || 'Must be 6 digits']"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="OK" color="primary" @click="onOkClick" />
|
||||
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialogPluginComponent } from 'quasar';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const code = ref('');
|
||||
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||
|
||||
function onOkClick() {
|
||||
onDialogOK(code.value);
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +1,492 @@
|
||||
export interface Todo {
|
||||
id: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/*
|
||||
export interface Meta {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface CtrlAttrs {
|
||||
[key: string]: CtrlAttr;
|
||||
}
|
||||
|
||||
export interface CtrlAttr {
|
||||
name: string;
|
||||
cmd: string;
|
||||
cmdClass: string;
|
||||
icon: string;
|
||||
cnfrm: boolean;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export interface DevCtrlAttr {
|
||||
name: string;
|
||||
cmd: DeviceCommands;
|
||||
cmdClass: 'device_control';
|
||||
icon: string;
|
||||
cnfrm: boolean;
|
||||
delay: number;
|
||||
}
|
||||
*/
|
||||
|
||||
export type FavoriteCommands = 'set' | 'delete' | 'get';
|
||||
|
||||
export type SimulationCommands =
|
||||
'restart'
|
||||
| 'start'
|
||||
| 'pause'
|
||||
| 'resume'
|
||||
| 'reset'
|
||||
| 'clear'
|
||||
| 'end'
|
||||
| 'add'
|
||||
| 'test-mode'
|
||||
| 'delete'
|
||||
| 'next'
|
||||
| 'gps-noise';
|
||||
|
||||
export type DeviceCommands = 'shutdown' | 'reboot';
|
||||
|
||||
export type TunneldCommands =
|
||||
| 'start'
|
||||
| 'start-watcher'
|
||||
| 'end-watcher'
|
||||
| 'shutdown'
|
||||
| 'restart'
|
||||
| 'clear'
|
||||
| 'cancel';
|
||||
|
||||
export interface LocationQueue {
|
||||
[key: string]: LocationMark;
|
||||
}
|
||||
|
||||
export interface LocationMarkUpdateResponse {
|
||||
command_status: string;
|
||||
command: string;
|
||||
command_class: string;
|
||||
message: string;
|
||||
data: LocationMark;
|
||||
}
|
||||
|
||||
export interface QueueOrderUpdateResponse {
|
||||
command_status: string;
|
||||
command: string;
|
||||
command_class: string;
|
||||
message: string;
|
||||
data: string[];
|
||||
}
|
||||
|
||||
export interface LocationMark {
|
||||
loc_id: string;
|
||||
latitude: number | undefined | null;
|
||||
longitude: number | undefined | null;
|
||||
address?: string | undefined | null;
|
||||
delay?: number | undefined | null;
|
||||
start: string | undefined | null;
|
||||
status: string | undefined | null;
|
||||
end?: string | undefined | null;
|
||||
}
|
||||
|
||||
// SERVER TO CLIENT
|
||||
|
||||
export interface ServerToClientEvents {
|
||||
// built-in
|
||||
noArg: () => void;
|
||||
withAck: (a: string, callback: (b: number) => void) => void;
|
||||
message: (e: string) => void;
|
||||
error: (data: ErrorFull) => void;
|
||||
// testing
|
||||
test_prompt: (callback: (s: string) => void) => void;
|
||||
// deprecated
|
||||
simulation_status: (c: SimulationStatus) => void;
|
||||
// future
|
||||
device_status: (d: DeviceStatus) => void;
|
||||
// in-use
|
||||
status: (d: StatusUpdate) => void;
|
||||
app_error: (data: AppError) => void;
|
||||
icloud_2fa_request: (callback: (e: number) => void) => void;
|
||||
fmf_update: (d: FindMyUpdate) => void;
|
||||
queue_data_update: (d: QueueData) => void;
|
||||
location_item_update: (d: LocationItemUpdate) => void;
|
||||
}
|
||||
|
||||
export interface AppError {
|
||||
type: string;
|
||||
message: string;
|
||||
error: string;
|
||||
data: {
|
||||
udids?: string | null | undefined;
|
||||
udid?: string | null | undefined;
|
||||
disconnected_udids?: string | null | undefined;
|
||||
tunnel_timeout_seconds?: string | null | undefined;
|
||||
dvt_timeout_seconds?: string | null | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocationItemUpdate {
|
||||
loc_id: string;
|
||||
data: LocationMark;
|
||||
}
|
||||
|
||||
export interface SimulationStatus {
|
||||
loc_id: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface icloudData {
|
||||
consumer_queue: string | number | boolean;
|
||||
consumer_task: string | boolean;
|
||||
monitor_enabled: boolean;
|
||||
monitor_task: string | boolean;
|
||||
monitor_running: boolean;
|
||||
}
|
||||
|
||||
export interface QueueData {
|
||||
active: boolean;
|
||||
data: LocationQueue;
|
||||
order: string[];
|
||||
deleted_items: string[];
|
||||
state: string | undefined | null;
|
||||
worker_task: string | undefined | null;
|
||||
gps_noise: boolean;
|
||||
test_mode: boolean;
|
||||
}
|
||||
|
||||
export interface StatusUpdate {
|
||||
connected_clients: { [key: string]: string };
|
||||
current_location: {
|
||||
loc_id: string;
|
||||
latitude: number | undefined | null;
|
||||
longitude: number | undefined | null;
|
||||
start?: string | undefined | null;
|
||||
};
|
||||
device_name: string | undefined | null;
|
||||
fmf_location: FindMyUpdate | undefined | null;
|
||||
icloud: icloudData;
|
||||
next_move?: number | undefined | null;
|
||||
set_location_enabled: boolean;
|
||||
simulation_queue: QueueData
|
||||
tunnel: string | undefined | null;
|
||||
tunnel_watcher_running: boolean;
|
||||
device_count?: number | undefined | null;
|
||||
udid?: string | null;
|
||||
product_version?: string | null | undefined;
|
||||
phone_number?: string | null | undefined;
|
||||
developer_mode_enabled?: boolean | undefined | null;
|
||||
ddi_mounted?: boolean;
|
||||
rsd_address?: string | null | undefined;
|
||||
rsd_port?: number | undefined | null;
|
||||
lockdown_trusted_port?: number | undefined | null;
|
||||
lockdown_untrusted_port?: number | undefined | null;
|
||||
lockdown_trusted_reachable?: boolean | undefined | null;
|
||||
lockdown_untrusted_reachable?: boolean | undefined | null;
|
||||
dtservicehub_reachable?: boolean | undefined | null;
|
||||
}
|
||||
|
||||
interface DeviceStatus {
|
||||
device_connected: boolean;
|
||||
device_count: number;
|
||||
udid?: string | null;
|
||||
device_name?: string | null;
|
||||
product_version?: string | null;
|
||||
phone_number?: string | null;
|
||||
developer_mode_enabled?: boolean;
|
||||
ddi_mounted?: boolean;
|
||||
rsd_address?: string | null;
|
||||
rsd_port?: number;
|
||||
lockdown_trusted_port?: number;
|
||||
lockdown_untrusted_port?: number;
|
||||
lockdown_trusted_reachable?: boolean;
|
||||
lockdown_untrusted_reachable?: boolean;
|
||||
dtservicehub_reachable?: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorFull {
|
||||
type: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface FindMyUpdate {
|
||||
altitude: number;
|
||||
batteryLevel: number;
|
||||
deviceDisplayName: string;
|
||||
deviceStatus: number;
|
||||
horizontalAccuracy: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
timeStamp: number;
|
||||
verticalAccuracy: number;
|
||||
}
|
||||
|
||||
// END SERVER TO CLIENT
|
||||
|
||||
// CLIENT TO SERVER
|
||||
|
||||
export interface ClientToServerEvents {
|
||||
// built-in
|
||||
message: (e: string, callback: (b: boolean, r: string) => void) => void;
|
||||
// testing
|
||||
send_test_prompt: (p: string, callback: (response: string) => void) => void;
|
||||
// status
|
||||
request_update: (callback: (response: StatusUpdate) => void) => void;
|
||||
// control
|
||||
tunneld_control: (
|
||||
args: {
|
||||
command: TunneldCommands;
|
||||
udid?: string;
|
||||
},
|
||||
callback?: (response: TunneldControlResponse) => void,
|
||||
) => void;
|
||||
favorite_control: (
|
||||
args: {
|
||||
command: FavoriteCommands;
|
||||
favorite?: Favorite | undefined | null;
|
||||
},
|
||||
callback: (response: FavoriteControlResponse) => void,
|
||||
) => void;
|
||||
simulation_control: (
|
||||
args: {
|
||||
command: SimulationCommands;
|
||||
latitude?: number | null | undefined;
|
||||
longitude?: number | null | undefined;
|
||||
delay?: number | null | undefined;
|
||||
loc_id?: string | null | undefined;
|
||||
address?: NominatimResponse | string | null | undefined;
|
||||
},
|
||||
callback: (response: SimulationControlResponse) => void,
|
||||
) => void;
|
||||
device_control: (
|
||||
args: {
|
||||
command: DeviceCommands;
|
||||
delay?: number;
|
||||
},
|
||||
callback?: (response: DeviceControlResponse) => void,
|
||||
) => void;
|
||||
icloud_monitor_control: (
|
||||
args: {
|
||||
command: string;
|
||||
},
|
||||
callback?: (response: iCloudMonitorResponse) => void,
|
||||
) => void;
|
||||
// data updates
|
||||
location_item_update: (
|
||||
args: {
|
||||
loc_id: string;
|
||||
key: string;
|
||||
value: string | number;
|
||||
},
|
||||
callback?: (response: LocationMarkUpdateResponse) => void,
|
||||
) => void;
|
||||
queue_order_update: (
|
||||
args: {
|
||||
newOrder: string[];
|
||||
},
|
||||
callback?: (response: QueueOrderUpdateResponse) => void,
|
||||
) => void;
|
||||
// data requests
|
||||
reverse_geocode: (
|
||||
args: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
},
|
||||
callback?: (response: GeoCacheResponse) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface FavoriteControlResponse {
|
||||
command_status: string;
|
||||
command: FavoriteCommands;
|
||||
command_class: string;
|
||||
data?: {
|
||||
favorites: Favorite[] | undefined | null;
|
||||
};
|
||||
message?: string | undefined;
|
||||
}
|
||||
|
||||
export interface SimulationControlResponse {
|
||||
command_status: string;
|
||||
command: SimulationCommands;
|
||||
command_class: string;
|
||||
data?: SimulationControlResponseData | undefined | null;
|
||||
message?: string | undefined;
|
||||
}
|
||||
|
||||
interface SimulationControlResponseData {
|
||||
simulation_queue?: QueueData;
|
||||
location_item?: LocationMark;
|
||||
}
|
||||
|
||||
interface DeviceControlResponse {
|
||||
command_status: string;
|
||||
command_class: string;
|
||||
command: DeviceCommands;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
|
||||
interface TunneldControlResponse {
|
||||
command_status: string;
|
||||
command_class: string;
|
||||
command: TunneldCommands;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface iCloudMonitorResponse {
|
||||
command_status: string;
|
||||
command: string;
|
||||
command_class: string;
|
||||
icloud_monitor_enabled?: boolean | undefined | null;
|
||||
icloud_monitor_running?: boolean | undefined | null;
|
||||
message?: string | undefined | null;
|
||||
error?: string | undefined | null;
|
||||
}
|
||||
|
||||
// END CLIENT TO SERVER
|
||||
|
||||
export interface coords {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
|
||||
export interface SearchControlProps {
|
||||
provider: OpenStreetMapProvider;
|
||||
showMarker: boolean;
|
||||
autoClose: boolean;
|
||||
updateMap: boolean;
|
||||
showPopup: boolean;
|
||||
style: 'button' | 'bar';
|
||||
acceptAutoLoad: boolean;
|
||||
autoComplete: boolean;
|
||||
autoCompleteDelay: number;
|
||||
retainZoomLevel: boolean;
|
||||
animateZoom: boolean;
|
||||
keepResult: boolean;
|
||||
}
|
||||
|
||||
export interface CurrentLocation {
|
||||
loc_id: string;
|
||||
latitude: number | null | undefined;
|
||||
longitude: number | null | undefined;
|
||||
// start_time?: string | null | undefined;
|
||||
// end_time?: string | null | undefined
|
||||
next_move?: number | null | undefined;
|
||||
}
|
||||
|
||||
export interface NextLocation {
|
||||
loc_id: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
time_at_location?: number | null;
|
||||
}
|
||||
|
||||
/*
|
||||
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;
|
||||
}
|
||||
*/
|
||||
|
||||
export interface routeSegments {
|
||||
fromWaypoint: number;
|
||||
toWaypoint: number;
|
||||
distanceMeters: number;
|
||||
timeSeconds: number;
|
||||
toCoordinates: LatLng;
|
||||
}
|
||||
|
||||
export interface RoutesSet {
|
||||
[key: string]: RouteSet;
|
||||
}
|
||||
|
||||
export interface LatLng {
|
||||
lat: number | null | undefined;
|
||||
lng: number | null | undefined;
|
||||
}
|
||||
|
||||
export interface routeDirections {
|
||||
dirIndex?: number | undefined;
|
||||
coordinateIndex: number;
|
||||
text?: string | undefined;
|
||||
distance?: number | undefined;
|
||||
time?: number | undefined;
|
||||
coordinates: LatLng | null | undefined;
|
||||
}
|
||||
|
||||
export interface routeCoordinates {
|
||||
coordinateIndex: number;
|
||||
lat: number;
|
||||
lng: number;
|
||||
distanceFromPrev: number;
|
||||
timeFromPrev: number;
|
||||
instruction: string;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface Favorite {
|
||||
name: string | null | undefined;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
icon: string | null;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface GeoCacheResponse {
|
||||
address: NominatimResponse;
|
||||
favorite?: Favorite;
|
||||
}
|
||||
|
||||
// TypeScript Interface for Reverse Geocoding Response
|
||||
export interface NominatimResponse {
|
||||
shop?: string | null | undefined;
|
||||
leisure?: string | null | undefined;
|
||||
house_number?: string | null | undefined;
|
||||
road: string;
|
||||
neighbourhood?: string | null | undefined;
|
||||
suburb?: string | null | undefined;
|
||||
county: string;
|
||||
town? : string | null | undefined;
|
||||
city?: string | null | undefined;
|
||||
village?: string | null | undefined;
|
||||
municipality?: string | null | undefined;
|
||||
state: string;
|
||||
'ISO3166-2-lvl4': string;
|
||||
postcode: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
}
|
||||
|
||||
export interface NominatimRequest {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
57
src/composables/useMarkerContextMenu.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ref, type Ref } from 'vue';
|
||||
import * as LeafLet from 'leaflet';
|
||||
import type { LeafletMouseEvent } from 'leaflet';
|
||||
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
|
||||
|
||||
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,
|
||||
closeAllPopups: () => void,
|
||||
) {
|
||||
const clickedLatLng = ref<LeafLet.LatLng | null>(null);
|
||||
const isFavorite = ref(false);
|
||||
|
||||
const handleMarkerClick = async (event: LeafletMouseEvent) => {
|
||||
console.log('marker clicked', event);
|
||||
closeAllPopups();
|
||||
clickedLatLng.value = event.latlng;
|
||||
const resp = await reverseGeocodeRateLimited(Number(event.latlng.lat), Number(event.latlng.lng));
|
||||
isFavorite.value = !!resp.favorite;
|
||||
LeafLet.DomEvent.stopPropagation(event.originalEvent);
|
||||
};
|
||||
|
||||
const setStartRoute = () => {
|
||||
if (!clickedLatLng.value) return;
|
||||
closeAllPopups();
|
||||
(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;
|
||||
closeAllPopups();
|
||||
console.log('setEndRoute: ', routeSet.value.end);
|
||||
updateRoute();
|
||||
console.log('updating Route');
|
||||
};
|
||||
|
||||
return {
|
||||
isFavorite,
|
||||
clickedLatLng,
|
||||
handleMarkerClick,
|
||||
setStartRoute,
|
||||
setEndRoute,
|
||||
};
|
||||
}
|
||||
173
src/composables/useRoutingEvents.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useLeafletStore } from 'stores/leaflet';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { routeCoordinates as StoreRouteCoordinates } from 'components/models';
|
||||
|
||||
const leafletStore = useLeafletStore();
|
||||
const { routeSegments, routeDirections, routeCoordinates } = storeToRefs(leafletStore);
|
||||
|
||||
type RouteSummary = {
|
||||
totalDistance: number;
|
||||
totalTime: number;
|
||||
};
|
||||
|
||||
type RouteWaypoint = {
|
||||
latLng: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
};
|
||||
|
||||
type RouteCoordinates = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
|
||||
type RouteInstructions = {
|
||||
text: string;
|
||||
distance: number;
|
||||
time: number;
|
||||
index: number;
|
||||
};
|
||||
|
||||
type RouteResult = {
|
||||
summary?: RouteSummary;
|
||||
segments?: RouteSummary[];
|
||||
inputWaypoints?: RouteWaypoint[];
|
||||
instructions?: RouteInstructions[];
|
||||
waypoints?: RouteWaypoint[];
|
||||
coordinates?: RouteCoordinates[];
|
||||
};
|
||||
|
||||
function getDistance(a: RouteCoordinates, b: RouteCoordinates) {
|
||||
const R = 6371000; // meters
|
||||
const toRad = (x: number) => (x * Math.PI) / 180;
|
||||
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLng = toRad(b.lng - a.lng);
|
||||
|
||||
const lat1 = toRad(a.lat);
|
||||
const lat2 = toRad(b.lat);
|
||||
|
||||
const aVal = Math.sin(dLat / 2) ** 2 + Math.sin(dLng / 2) ** 2 * Math.cos(lat1) * Math.cos(lat2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(aVal), Math.sqrt(1 - aVal));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
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 ??
|
||||
route.waypoints?.[index + 1]?.latLng ?? { lat: 0, lng: 0 },
|
||||
}));
|
||||
if (routeSegments) {
|
||||
routeSegments.value = segmentSummary;
|
||||
}
|
||||
console.log('Waypoint segment summary:', segmentSummary);
|
||||
}
|
||||
|
||||
if (route.instructions?.length) {
|
||||
const directionsSummary = route.instructions.map((direction, inx) => ({
|
||||
dirIndex: inx,
|
||||
coordinateIndex: direction.index,
|
||||
text: direction.text,
|
||||
distance: direction.distance,
|
||||
time: direction.time,
|
||||
coordinates: {
|
||||
lat: route.coordinates?.[direction.index]?.lat,
|
||||
lng: route.coordinates?.[direction.index]?.lng,
|
||||
},
|
||||
}));
|
||||
console.log('Direction waypoint segment summary:', directionsSummary);
|
||||
if (routeDirections) {
|
||||
routeDirections.value = directionsSummary;
|
||||
}
|
||||
}
|
||||
|
||||
if (route.coordinates?.length) {
|
||||
const coordinatesAll: StoreRouteCoordinates[] = [];
|
||||
|
||||
const instructions = route.instructions || [];
|
||||
const coords = route.coordinates;
|
||||
|
||||
for (let i = 0; i < instructions.length; i++) {
|
||||
const current = instructions[i];
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = instructions[i + 1];
|
||||
|
||||
const startIndex = current.index;
|
||||
const endIndex = Math.min(next ? next.index : coords.length - 1, coords.length - 1);
|
||||
if (startIndex < 0 || startIndex > endIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// slice this segment
|
||||
const segmentCoords = coords.slice(startIndex, endIndex + 1);
|
||||
|
||||
// calculate distances between each pair
|
||||
let totalSegmentDistance = 0;
|
||||
|
||||
for (let j = 0; j < segmentCoords.length - 1; j++) {
|
||||
const from = segmentCoords[j];
|
||||
const to = segmentCoords[j + 1];
|
||||
if (from && to) {
|
||||
totalSegmentDistance += getDistance(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
// distribute time proportionally
|
||||
for (let j = 0; j < segmentCoords.length; j++) {
|
||||
const coord = segmentCoords[j];
|
||||
if (!coord) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const coordIndex = startIndex + j;
|
||||
const previousCoord = j === 0 ? undefined : segmentCoords[j - 1];
|
||||
const segmentDistance = previousCoord ? getDistance(previousCoord, coord) : 0;
|
||||
|
||||
const timePortion =
|
||||
totalSegmentDistance > 0 ? (segmentDistance / totalSegmentDistance) * current.time : 0;
|
||||
|
||||
coordinatesAll[coordIndex] = {
|
||||
coordinateIndex: coordIndex,
|
||||
lat: coord.lat,
|
||||
lng: coord.lng,
|
||||
distanceFromPrev: segmentDistance,
|
||||
timeFromPrev: timePortion,
|
||||
instruction: j === 0 ? current.text : '',
|
||||
};
|
||||
}
|
||||
}
|
||||
console.log('Coordinates:', coordinatesAll);
|
||||
if (routeCoordinates) {
|
||||
routeCoordinates.value = coordinatesAll;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return {
|
||||
handleRoutesFound,
|
||||
};
|
||||
}
|
||||
146
src/constants/controls.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
export const controls = {
|
||||
simulation: {
|
||||
start: {
|
||||
name: 'Start Location Sim',
|
||||
cmd: 'start',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
pause: {
|
||||
name: 'Pause Location Sim',
|
||||
cmd: 'pause',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-pause',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
resume: {
|
||||
name: 'Resume Location Simulation',
|
||||
cmd: 'resume',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-play-pause',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
clear: {
|
||||
name: 'Clear Future Items',
|
||||
cmd: 'clear',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'msi-map-marker-remove',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
reset: {
|
||||
name: 'Reset Location Queue',
|
||||
cmd: 'reset',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: true,
|
||||
delay: 0,
|
||||
},
|
||||
end: {
|
||||
name: 'End Location Sim',
|
||||
cmd: 'end',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: true,
|
||||
delay: 0,
|
||||
},
|
||||
},
|
||||
device: {
|
||||
shutdown: {
|
||||
name: 'Shutdown',
|
||||
cmd: 'shutdown',
|
||||
cmdClass: 'dev_cntrl_class',
|
||||
icon: 'mdi-power',
|
||||
cnfrm: true,
|
||||
delay: 5,
|
||||
},
|
||||
reboot: {
|
||||
name: 'Reboot',
|
||||
cmd: 'reboot',
|
||||
cmdClass: 'dev_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: true,
|
||||
delay: 5,
|
||||
},
|
||||
},
|
||||
icloudmonitor: {
|
||||
start: {
|
||||
name: 'Start iCloud Monitor',
|
||||
cmd: 'start',
|
||||
cmdClass: 'icloud-monitor_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
stop: {
|
||||
name: 'Stop iCloud Monitor',
|
||||
cmd: 'stop',
|
||||
cmdClass: 'icloud-monitor_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
},
|
||||
tunneld: {
|
||||
start: {
|
||||
name: 'Start Tunneld',
|
||||
cmd: 'start',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
start_watcher: {
|
||||
name: 'Start TunneldWatcher',
|
||||
cmd: 'start-watcher',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
end_watcher: {
|
||||
name: 'End TunneldWatcher',
|
||||
cmd: 'end-watcher',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
clear: {
|
||||
name: 'Clear Tunneld',
|
||||
cmd: 'clear',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-backspace',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
cancel: {
|
||||
name: 'Cancel Tunneld',
|
||||
cmd: 'cancel',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-cancel',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
restart: {
|
||||
name: 'Restart Tunneld',
|
||||
cmd: 'restart',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
shutdown: {
|
||||
name: 'Shutdown Tunneld',
|
||||
cmd: 'shutdown',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
79
src/constants/favorites.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export const favorites = {
|
||||
home: {
|
||||
name: 'Home',
|
||||
icon: 'mdi-home',
|
||||
coords: {
|
||||
lat: 40.910773020811,
|
||||
lng: -73.891069806448,
|
||||
},
|
||||
},
|
||||
work_places: {
|
||||
name: 'Work Places',
|
||||
icon: 'mdi-briefcase',
|
||||
subitems: {
|
||||
jeong: {
|
||||
name: 'Jeong',
|
||||
icon: 'mdi-doctor',
|
||||
coords: {
|
||||
lat: 40.76624975651346,
|
||||
lng: -73.81444335286128,
|
||||
},
|
||||
address: '35-02 150th Pl, Flushing, NY 11354',
|
||||
},
|
||||
santos: {
|
||||
name: 'Santos',
|
||||
icon: 'mdi-doctor',
|
||||
coords: {
|
||||
lat: 40.74504671877868,
|
||||
lng: -73.8880099638491,
|
||||
},
|
||||
address: '77-08 Broadway, Elmhurst, NY 11373',
|
||||
},
|
||||
natalya_qns: {
|
||||
name: 'Natalya (Qns)',
|
||||
icon: 'mdi-doctor',
|
||||
coords: {
|
||||
lat: 40.69644966409178,
|
||||
lng: -73.837453217826,
|
||||
},
|
||||
address: '110-14 Jamaica Ave, Richmond Hill, NY 11418',
|
||||
},
|
||||
natalya_bx: {
|
||||
name: 'Natalya (Bronx)',
|
||||
icon: 'mdi-doctor',
|
||||
coords: {
|
||||
lat: 40.85384419116598,
|
||||
lng: -73.86314767911834,
|
||||
},
|
||||
address: '2109 Matthews Ave, Bronx, NY 10462',
|
||||
},
|
||||
office: {
|
||||
name: 'Linwood Plaza',
|
||||
icon: 'mdi-allergy',
|
||||
coords: {
|
||||
lat: 40.86141832913106,
|
||||
lng: -73.96997583196286,
|
||||
},
|
||||
address: '158 Linwood Plaza, Fort Lee, NJ 07024',
|
||||
},
|
||||
},
|
||||
},
|
||||
strg: {
|
||||
name: 'Man Mini Storage',
|
||||
icon: 'mdi-dolly',
|
||||
coords: {
|
||||
lat: 40.75158955085288,
|
||||
lng: -73.9328988710467,
|
||||
},
|
||||
address: '31-08 Northern Blvd, Long Island City, NY 11101',
|
||||
},
|
||||
acme: {
|
||||
name: 'Acme',
|
||||
icon: 'mdi-cart',
|
||||
coords: {
|
||||
lat: 40.90930366920829,
|
||||
lng: -73.87658695470259,
|
||||
},
|
||||
address: '31-08 Northern Blvd, Long Island City, NY 11101',
|
||||
},
|
||||
};
|
||||
@@ -12,14 +12,14 @@
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary: #1976d2;
|
||||
$secondary: #26a69a;
|
||||
$accent: #9c27b0;
|
||||
$primary: #161B36;
|
||||
$secondary: #8FA9BF;
|
||||
$accent: #5a728a;
|
||||
|
||||
$dark: #1d1d1d;
|
||||
$dark-page: #121212;
|
||||
$dark-page: #0B1026;
|
||||
|
||||
$positive: #21ba45;
|
||||
$negative: #c10015;
|
||||
$info: #31ccec;
|
||||
$warning: #f2c037;
|
||||
$positive: #4CAF50;
|
||||
$negative: #EF5350;
|
||||
$info: #42A5F5;
|
||||
$warning: #FFCA28;
|
||||
|
||||
25
src/functions/reverseGeocode.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// services/nominatimService.ts
|
||||
import { api } from 'boot/axios';
|
||||
import type { NominatimResponse, NominatimRequest } from 'components/models';
|
||||
|
||||
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 api.post<NominatimResponse>('/rev_geocode', {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
} as NominatimRequest);
|
||||
|
||||
lastRequestTime = Date.now();
|
||||
return response.data;
|
||||
};
|
||||
36
src/functions/reverseGeocodeSocket.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// services/nominatimService.ts
|
||||
import { socket } from 'boot/socketio';
|
||||
import type { NominatimRequest, GeoCacheResponse } from 'components/models';
|
||||
|
||||
|
||||
|
||||
let lastRequestTime = 0;
|
||||
|
||||
function revGeoCode(nomRequest: NominatimRequest): Promise<GeoCacheResponse> {
|
||||
return new Promise((resolve) => {
|
||||
socket.emit(
|
||||
'reverse_geocode',
|
||||
{ latitude: nomRequest.latitude, longitude: nomRequest.longitude },
|
||||
(response) => {
|
||||
resolve(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const reverseGeocodeRateLimited = async (lat: number, lon: number): Promise<GeoCacheResponse> => {
|
||||
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: GeoCacheResponse = await revGeoCode({
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
} as NominatimRequest);
|
||||
|
||||
lastRequestTime = Date.now();
|
||||
return response;
|
||||
};
|
||||
93
src/functions/routingControl.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Utilities } from '@vue-leaflet/vue-leaflet';
|
||||
import type { PropType } from 'vue';
|
||||
import type { IRouter, LineOptions } from 'leaflet-routing-machine';
|
||||
import type * as L from 'leaflet';
|
||||
|
||||
export interface RoutingControlProps {
|
||||
waypoints: unknown[];
|
||||
router?: IRouter | undefined;
|
||||
plan?: unknown;
|
||||
fitSelectedRoutes?: string | boolean;
|
||||
lineOptions?: LineOptions | undefined;
|
||||
routeLine?: ((route: unknown) => unknown) | undefined;
|
||||
createMarker?:
|
||||
| ((i: number, waypoint: { latLng: L.LatLng }, n: number) => L.Marker | false)
|
||||
| undefined;
|
||||
autoRoute?: boolean;
|
||||
routeWhileDragging?: boolean;
|
||||
routeDragInterval?: number;
|
||||
waypointMode?: string;
|
||||
useZoomParameter?: boolean;
|
||||
showAlternatives?: boolean;
|
||||
altLineOptions?: LineOptions | undefined;
|
||||
}
|
||||
|
||||
export const routingControlProps = {
|
||||
waypoints: {
|
||||
type: Array as PropType<unknown[]>,
|
||||
default: () => [],
|
||||
},
|
||||
router: {
|
||||
type: Object as PropType<IRouter | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
plan: {
|
||||
type: Object as PropType<unknown>,
|
||||
default: undefined,
|
||||
},
|
||||
fitSelectedRoutes: {
|
||||
type: [String, Boolean] as PropType<string | boolean>,
|
||||
default: 'smart',
|
||||
},
|
||||
lineOptions: {
|
||||
type: Object as PropType<LineOptions | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
routeLine: {
|
||||
type: Function as PropType<((route: unknown) => unknown) | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
createMarker: {
|
||||
type: Function as PropType<
|
||||
((i: number, waypoint: { latLng: L.LatLng }, n: number) => L.Marker | false) | undefined
|
||||
>,
|
||||
default: undefined,
|
||||
},
|
||||
autoRoute: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
routeWhileDragging: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
routeDragInterval: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
waypointMode: {
|
||||
type: String,
|
||||
default: 'connect',
|
||||
},
|
||||
useZoomParameter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showAlternatives: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
altLineOptions: {
|
||||
type: Object as PropType<LineOptions | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const setupRoutingControl = (props: RoutingControlProps) => {
|
||||
const options = Utilities.propsToLeafletOptions(props, routingControlProps);
|
||||
|
||||
return {
|
||||
options,
|
||||
methods: {} as Record<string, never>,
|
||||
};
|
||||
};
|
||||
12
src/functions/serviceURL.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { openrouteserviceV2 } from 'components/L.Routing.OpenRouteServiceV2';
|
||||
|
||||
export const customRouter = openrouteserviceV2({
|
||||
api_key: '',
|
||||
profile: 'driving-car',
|
||||
geometry_simplify: true,
|
||||
host: '/api/ors/proxy/ors',
|
||||
});
|
||||
|
||||
//export const customRouter = L.Routing.osrmv1({
|
||||
// serviceUrl: '/osrm/route/v1', // Replace with your URL
|
||||
//});
|
||||
@@ -1,15 +1,41 @@
|
||||
<template>
|
||||
<q-layout view="lHh lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat dense round icon="menu" aria-label="Menu"/>
|
||||
|
||||
<q-toolbar-title> Quasar App </q-toolbar-title>
|
||||
|
||||
<div>Quasar v{{ $q.version }}</div>
|
||||
</q-toolbar>
|
||||
<q-layout view="hhh lpr fff">
|
||||
<q-header>
|
||||
<MenuBar @drawer="drawer = !drawer" />
|
||||
</q-header>
|
||||
|
||||
<q-footer>
|
||||
<StatusBar />
|
||||
</q-footer>
|
||||
<!--
|
||||
<q-drawer
|
||||
v-model="drawer"
|
||||
:width="200"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
:class="$q.dark.isActive ? 'bg-grey-8' : 'bg-grey-3'"
|
||||
>
|
||||
<q-scroll-area class="fit">
|
||||
<q-list>
|
||||
<template v-for="(menuItem, index) in menuList" :key="index">
|
||||
<q-item
|
||||
clickable
|
||||
:active="menuItem.route === route.name"
|
||||
v-ripple
|
||||
@click="$router.push({ name: menuItem.route })"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="menuItem.icon" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ menuItem.label }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator :key="'sep' + index" v-if="menuItem.separator" />
|
||||
</template>
|
||||
</q-list>
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
-->
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
@@ -17,4 +43,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useSimulationStore } from 'stores/simulation';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
import { useIcloudStore } from 'stores/icloud';
|
||||
import { useFavoriteStore } from 'stores/favorite';
|
||||
|
||||
import MenuBar from 'components/MenuBar.vue';
|
||||
import StatusBar from 'components/StatusBar.vue';
|
||||
|
||||
const simulationStore = useSimulationStore();
|
||||
const socketStore = useSocketStore();
|
||||
const icloudStore = useIcloudStore();
|
||||
const favoriteStore = useFavoriteStore();
|
||||
|
||||
const drawer = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
socketStore.bindEvents();
|
||||
simulationStore.bindEvents();
|
||||
icloudStore.bindEvents();
|
||||
socketStore.connect();
|
||||
favoriteStore.initialize();
|
||||
});
|
||||
</script>
|
||||
|
||||
59
src/pages/DeviceInfo.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { api, axios } from 'boot/axios';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const deviceInfo = ref();
|
||||
|
||||
async function fetchDeviceInfo(): Promise<void> {
|
||||
try {
|
||||
const res = await api.get('/device_info');
|
||||
console.log('DeviceInfo fetched successfully:', res.data);
|
||||
deviceInfo.value = res.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching device info:', error);
|
||||
if (axios.isCancel(error)) {
|
||||
console.log('Request canceled', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
await fetchDeviceInfo();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="row items-center justify-evenly">
|
||||
<div style="width: 70vw; white-space: normal">
|
||||
<h3>Device Info:</h3>
|
||||
<ul>
|
||||
<li v-for="(value, key) in deviceInfo" :key="key">
|
||||
<strong>{{ key }}:</strong>
|
||||
<span v-if="typeof value === 'object' && value !== null">
|
||||
<ul>
|
||||
<li v-for="(subValue, subKey) in value" :key="subKey">
|
||||
<strong>{{ subKey }}:</strong>
|
||||
<span v-if="typeof subValue === 'object' && subValue !== null">
|
||||
<ul>
|
||||
<li v-for="(subSubValue, subSubKey) in subValue" :key="subSubKey">
|
||||
<strong>{{ subSubKey }}:</strong>
|
||||
<span v-if="typeof subSubValue === 'object' && subSubValue !== null">
|
||||
<ul>
|
||||
<li v-for="(subSubSubValue, subSubSubKey) in subSubValue" :key="subSubSubKey">
|
||||
<strong>{{ subSubSubKey }}:</strong> {{ subSubSubValue }}
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<span v-else>{{ subSubValue }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<span v-else>{{ subValue }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,31 +1,32 @@
|
||||
<template>
|
||||
<q-page class="row items-start justify-evenly">
|
||||
<SocketTest />
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="q-pa-md">
|
||||
<q-btn color="purple" @click="showNotif" label="Show Notification" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" style="border: 5px pink dashed;" >
|
||||
<q-icon :name="darkStatus" size="100px" @click="toggleDarkLight" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" style="border: 5px pink dashed">
|
||||
<q-icon :name="darkStatus" size="100px" @click="toggleDarkLight" />
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuasar } from 'quasar'
|
||||
import { computed } from 'vue'
|
||||
import SocketTest from 'components/SocketTest.vue'
|
||||
const $q = useQuasar()
|
||||
import { useQuasar } from 'quasar';
|
||||
import { computed } from 'vue';
|
||||
import SocketTest from 'components/SocketTest.vue';
|
||||
const $q = useQuasar();
|
||||
|
||||
const darkStatus = computed(() => $q.dark.isActive ? 'dark_mode' : 'light_mode')
|
||||
const darkStatus = computed(() => ($q.dark.isActive ? 'dark_mode' : 'light_mode'));
|
||||
|
||||
function toggleDarkLight() {
|
||||
$q.dark.toggle()
|
||||
$q.dark.toggle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
function showNotif() {
|
||||
$q.notify({
|
||||
message: 'This is a notification',
|
||||
@@ -33,9 +34,16 @@ function showNotif() {
|
||||
position: 'top-right',
|
||||
timeout: 3000,
|
||||
actions: [
|
||||
{ label: 'Dismiss', color: 'white', handler: () => { /* Dismiss action */ } }
|
||||
]
|
||||
})
|
||||
{
|
||||
label: 'Dismiss',
|
||||
color: 'white',
|
||||
handler: () => {
|
||||
/* Dismiss action */
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
</style>
|
||||
|
||||
@@ -5,8 +5,28 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
component: () => import('layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('pages/IndexPage.vue') },
|
||||
{ path: 'test', component: () => import('pages/TestPage.vue') },
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
redirect: {
|
||||
name: 'Leaflet',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'leaflet',
|
||||
name: 'Leaflet',
|
||||
component: () => import('pages/IndexPage.vue'),
|
||||
},
|
||||
{
|
||||
path: 'test',
|
||||
name: 'Test',
|
||||
component: () => import('pages/TestPage.vue'),
|
||||
},
|
||||
{
|
||||
path: 'device-info',
|
||||
name: 'DeviceInfo',
|
||||
component: () => import('pages/DeviceInfo.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -14,6 +34,7 @@ const routes: RouteRecordRaw[] = [
|
||||
// but you can also remove it
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
name: 'ErrorNotFound',
|
||||
component: () => import('pages/ErrorNotFound.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
47
src/stores/device.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { socket } from 'boot/socketio';
|
||||
import type { DeviceCommands } from 'components/models';
|
||||
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
|
||||
|
||||
|
||||
export const useDeviceStore = defineStore('deviceStore', {
|
||||
state: () => {
|
||||
return {
|
||||
deviceConnected: false as boolean,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
digestUpdate(data: boolean) {
|
||||
this.deviceConnected = data;
|
||||
},
|
||||
deviceControl(command: DeviceCommands, delay: number = 0) {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
||||
if (debugLog) {
|
||||
console.log('deviceStore: got command: ', command, ' with delay: ', delay, ' seconds');
|
||||
}
|
||||
socket.emit(
|
||||
'device_control',
|
||||
{ command: command, delay: delay },
|
||||
(response) => {
|
||||
console.log(response.command_status, response.command);
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { sts: 'error', msg: response.message?.toString() };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
}
|
||||
},
|
||||
);
|
||||
return fnctRtn;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useDeviceStore, import.meta.hot));
|
||||
}
|
||||
124
src/stores/favorite.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { socket } from 'boot/socketio';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
import type { Favorite } from 'components/models';
|
||||
|
||||
export const useFavoriteStore = defineStore('favoriteStore', {
|
||||
state: () => {
|
||||
return {
|
||||
favorites: [] as Favorite[],
|
||||
categories: [] as string[],
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
favoriteControl({ command, favorite }: { command: string; favorite?: Favorite }) {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
||||
switch (command) {
|
||||
case 'set':
|
||||
if (!favorite) {
|
||||
console.log('Favorite not provided, favdata: %s', favorite);
|
||||
fnctRtn = { sts: 'error', msg: 'Favorite not provided.' };
|
||||
throw new Error('Favorite not provided.');
|
||||
}
|
||||
socket.emit('favorite_control', { command: 'set', favorite: favorite }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.favorites) {
|
||||
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
console.log('Favorite added, favdata: %s', favorite);
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.favorites = response.data.favorites;
|
||||
response.data.favorites.forEach((fav) => {
|
||||
if (!this.categories.includes(fav.category)) {
|
||||
this.categories.push(fav.category);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from favorite_control_add: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
socket.emit('favorite_control', { command: 'delete', favorite: favorite }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { sts: 'error', msg: response.message };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.favorites) {
|
||||
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.favorites = response.data.favorites;
|
||||
response.data.favorites.forEach((fav) => {
|
||||
if (!this.categories.includes(fav.category)) {
|
||||
this.categories.push(fav.category);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from favorite_control_delete: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'get':
|
||||
socket.emit('favorite_control', { command: 'get', favorite: favorite }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { sts: 'error', msg: response.message };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.favorites) {
|
||||
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.favorites = response.data.favorites;
|
||||
response.data.favorites.forEach((fav) => {
|
||||
if(!this.categories.includes(fav.category)) {
|
||||
this.categories.push(fav.category);
|
||||
}
|
||||
})
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from favorite_control_get: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
fnctRtn = { sts: 'error', msg: 'Invalid command' + command };
|
||||
throw new Error('Invalid command' + command);
|
||||
}
|
||||
return fnctRtn;
|
||||
},
|
||||
initialize(): void {
|
||||
this.favoriteControl({ command: 'get' });
|
||||
},
|
||||
getCategories(): string[] {
|
||||
this.favoriteControl({ command: 'get' });
|
||||
const categories: string[] = [];
|
||||
this.favorites.forEach((favorite) => {
|
||||
if (!categories.includes(favorite.category)) {
|
||||
categories.push(favorite.category);
|
||||
}
|
||||
});
|
||||
return categories;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useFavoriteStore, import.meta.hot));
|
||||
}
|
||||
219
src/stores/icloud.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { socket } from 'boot/socketio';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
import iCloudCodeDialog from 'components/iCloudCodeDialog.vue';
|
||||
|
||||
import type { FindMyUpdate, icloudData, iCloudMonitorResponse } from 'components/models';
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
|
||||
export const useIcloudStore = defineStore('icloudStore', {
|
||||
state: () => {
|
||||
return {
|
||||
icloudMonitor: false as boolean,
|
||||
findMyUpdate: null as FindMyUpdate | null | undefined,
|
||||
consumerQueue: 0 as number | string | boolean,
|
||||
consumerTask: false as boolean | string,
|
||||
monitorTask: false as boolean | string,
|
||||
monitorEnabled: false as boolean,
|
||||
monitorRunning: false as boolean,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
bindEvents() {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
// fmf_update
|
||||
socket.on('fmf_update', (data: FindMyUpdate): void => {
|
||||
if (debugLog) {
|
||||
console.log('event: fmf_update received: ', data);
|
||||
}
|
||||
this.findMyUpdate = data;
|
||||
});
|
||||
|
||||
// icloud_2fa_request
|
||||
socket.on('icloud_2fa_request', (callback) => {
|
||||
if (debugLog) {
|
||||
console.log('iCloud 2FA Request');
|
||||
}
|
||||
$q.dialog({
|
||||
component: iCloudCodeDialog,
|
||||
})
|
||||
.onOk((code: number) => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(code);
|
||||
}
|
||||
})
|
||||
.onCancel(() => {
|
||||
if (debugLog) {
|
||||
console.log('Dialog cancelled');
|
||||
}
|
||||
})
|
||||
.onDismiss(() => {
|
||||
if (debugLog) {
|
||||
console.log('Dialog dismissed');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
icloudMonitorControl(command: string) {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
||||
switch (command) {
|
||||
case 'start':
|
||||
if (debugLog) {
|
||||
console.log('socketStore: got command: icloudMonitor start');
|
||||
}
|
||||
if (this.icloudMonitor) {
|
||||
fnctRtn = { sts: 'error', msg: 'iCloud Monitor is already running' };
|
||||
throw new Error('iCloud Monitor is already running');
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('Emitting icloud_monitor_control: start');
|
||||
}
|
||||
socket.emit(
|
||||
'icloud_monitor_control',
|
||||
{ command: 'start' },
|
||||
(response: iCloudMonitorResponse) => {
|
||||
fnctRtn.sts = response.command_status;
|
||||
if (response.command_status == 'ERROR') {
|
||||
if (response.message) {
|
||||
if (debugLog) {
|
||||
console.log(response.message);
|
||||
}
|
||||
fnctRtn.msg = response.message;
|
||||
} else {
|
||||
fnctRtn.msg = 'Error';
|
||||
}
|
||||
throw new Error(fnctRtn.msg);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: 'iCloud Monitor: ' + response.command_status };
|
||||
this.icloudMonitor = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'stop':
|
||||
if (debugLog) {
|
||||
console.log('socketStore: got command: icloudMonitor stop');
|
||||
}
|
||||
if (!this.icloudMonitor) {
|
||||
fnctRtn = { sts: 'error', msg: 'iCloud Monitor is not running' };
|
||||
throw new Error('iCloud Monitor is not running');
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('Emitting icloud_monitor_control: stop');
|
||||
}
|
||||
socket.emit(
|
||||
'icloud_monitor_control',
|
||||
{ command: 'stop' },
|
||||
(response: iCloudMonitorResponse) => {
|
||||
fnctRtn.sts = response.command_status;
|
||||
if (response.command_status == 'ERROR') {
|
||||
if (response.message) {
|
||||
if (debugLog) {
|
||||
console.log(response.message);
|
||||
}
|
||||
fnctRtn.msg = response.message;
|
||||
} else {
|
||||
fnctRtn.msg = 'Error';
|
||||
}
|
||||
throw new Error(fnctRtn.msg);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: 'iCloud Monitor: ' + response.command_status };
|
||||
this.icloudMonitor = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'status':
|
||||
if (debugLog) {
|
||||
console.log('socketStore: got command: icloudMonitor status');
|
||||
}
|
||||
socket.emit(
|
||||
'icloud_monitor_control',
|
||||
{ command: 'get' },
|
||||
(response: iCloudMonitorResponse) => {
|
||||
fnctRtn.sts = response.command_status;
|
||||
if (response.command_status == 'error') {
|
||||
if (response.message) {
|
||||
if (debugLog) {
|
||||
console.log(response.message);
|
||||
}
|
||||
fnctRtn.msg = response.message;
|
||||
} else {
|
||||
fnctRtn.msg = 'Error';
|
||||
}
|
||||
throw new Error(fnctRtn.msg);
|
||||
} else {
|
||||
fnctRtn = {
|
||||
sts: 'OK',
|
||||
msg: 'iCloud Location Requested',
|
||||
};
|
||||
}
|
||||
this.icloudMonitor = !!(
|
||||
response.icloud_monitor_enabled && response.icloud_monitor_running
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'refresh':
|
||||
if (debugLog) {
|
||||
console.log('socketStore: got command: icloudMonitor refresh');
|
||||
}
|
||||
socket.emit(
|
||||
'icloud_monitor_control',
|
||||
{ command: 'status' },
|
||||
(response: iCloudMonitorResponse) => {
|
||||
fnctRtn.sts = response.command_status;
|
||||
if (response.command_status == 'ERROR') {
|
||||
if (response.message) {
|
||||
if (debugLog) {
|
||||
console.log(response.message);
|
||||
}
|
||||
fnctRtn.msg = response.message;
|
||||
} else {
|
||||
fnctRtn.msg = 'Error';
|
||||
}
|
||||
throw new Error(fnctRtn.msg);
|
||||
} else {
|
||||
fnctRtn = {
|
||||
sts: 'OK',
|
||||
msg:
|
||||
'iCloud Monitor Enabled: ' +
|
||||
response.icloud_monitor_enabled +
|
||||
'iCloud Monitor Running: ' +
|
||||
response.icloud_monitor_running,
|
||||
};
|
||||
}
|
||||
this.icloudMonitor = !!(
|
||||
response.icloud_monitor_enabled && response.icloud_monitor_running
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
fnctRtn = { sts: 'error', msg: 'Invalid command' };
|
||||
throw new Error('Invalid command');
|
||||
}
|
||||
return fnctRtn;
|
||||
},
|
||||
digestUpdate(data: icloudData) {
|
||||
this.icloudMonitor = data.monitor_running;
|
||||
this.consumerQueue= data.consumer_queue;
|
||||
this.consumerTask = data.consumer_task;
|
||||
this.monitorEnabled = data.monitor_enabled;
|
||||
this.monitorTask = data.monitor_task;
|
||||
this.monitorRunning = data.monitor_running;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useIcloudStore, import.meta.hot));
|
||||
}
|
||||
69
src/stores/leaflet.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { favorites } from 'constants/favorites'
|
||||
import type {
|
||||
RoutesSet,
|
||||
LatLng,
|
||||
routeSegments,
|
||||
routeDirections,
|
||||
routeCoordinates,
|
||||
} from 'components/models';
|
||||
|
||||
interface State {
|
||||
zoom: number;
|
||||
center: [number, number] | [null, null] | null;
|
||||
markerLatLng: [number, number] | [null, null] | null;
|
||||
qLocDrawer: boolean;
|
||||
routeSet: {
|
||||
start: LatLng;
|
||||
end: LatLng;
|
||||
wayPoints?: LatLng[] | null | undefined;
|
||||
};
|
||||
routesSet: RoutesSet[] | null;
|
||||
routeSegments: routeSegments[] | null;
|
||||
routeDirections: routeDirections[] | null;
|
||||
routeCoordinates: routeCoordinates[] | null;
|
||||
}
|
||||
|
||||
export const useLeafletStore = defineStore('leaflet', {
|
||||
state: (): State => {
|
||||
return {
|
||||
zoom: 10,
|
||||
center: [favorites.home.coords.lat, favorites.home.coords.lng],
|
||||
markerLatLng: null,
|
||||
qLocDrawer: false,
|
||||
routeSet: {
|
||||
start: { lat: null, lng: null },
|
||||
end: { lat: null, lng: null },
|
||||
wayPoints: null,
|
||||
},
|
||||
routesSet: [],
|
||||
routeSegments: [],
|
||||
routeDirections: [],
|
||||
routeCoordinates: [],
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
clearRouteSegments() {
|
||||
this.routeSegments = [];
|
||||
},
|
||||
setCenter(lat: number, lng: number) {
|
||||
this.center = [lat, lng];
|
||||
},
|
||||
setZoom(zoom: number) {
|
||||
this.zoom = zoom;
|
||||
},
|
||||
setMarkerLatLng(lat: number, lng: number) {
|
||||
this.markerLatLng = [lat, lng];
|
||||
},
|
||||
toggleQLocDrawer() {
|
||||
this.qLocDrawer = !this.qLocDrawer
|
||||
},
|
||||
setRouteDirs(data: routeDirections[]) {
|
||||
this.routeDirections = data;
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useLeafletStore, import.meta.hot));
|
||||
}
|
||||
443
src/stores/simulation.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { socket } from 'boot/socketio';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
|
||||
import type {
|
||||
CurrentLocation,
|
||||
LocationItemUpdate,
|
||||
LocationMarkUpdateResponse,
|
||||
LocationQueue,
|
||||
NominatimResponse,
|
||||
QueueData,
|
||||
SimulationControlResponse,
|
||||
SimulationStatus,
|
||||
} from 'components/models';
|
||||
|
||||
|
||||
|
||||
export const useSimulationStore = defineStore('simulationStore', {
|
||||
state: () => {
|
||||
return {
|
||||
currentLocation: null as CurrentLocation | null | undefined,
|
||||
gpsNoise: null as boolean | undefined | null,
|
||||
locationQueueData: {} as LocationQueue,
|
||||
locationQueueDeletedItems: [] as string[],
|
||||
locationQueueOrder: [] as string[],
|
||||
simulationQueueLength: 0 as number | null | undefined,
|
||||
simulationRunning: false as boolean | undefined | null,
|
||||
simulationState: null as string | null | undefined,
|
||||
testMode: null as boolean | undefined | null,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
bindEvents() {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
// simulation_status
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
// queue_data_update
|
||||
socket.on('queue_data_update', (inData: QueueData): void => {
|
||||
if (debugLog) {
|
||||
console.log('QueueUpdate received: ', inData);
|
||||
}
|
||||
this.digestQueueUpdate(inData);
|
||||
});
|
||||
// location_item_update
|
||||
socket.on('location_item_update', (inData: LocationItemUpdate): void => {
|
||||
console.log('Location item update received, data: ', inData);
|
||||
this.locationQueueData[inData.loc_id] = inData.data;
|
||||
});
|
||||
},
|
||||
simulationControl({
|
||||
command,
|
||||
latitude,
|
||||
longitude,
|
||||
loc_id,
|
||||
delay,
|
||||
address,
|
||||
}: {
|
||||
command: string;
|
||||
latitude?: number | null | undefined;
|
||||
longitude?: number | null | undefined;
|
||||
loc_id?: string | null | undefined;
|
||||
delay?: number | null | undefined;
|
||||
address?: NominatimResponse | string | null | undefined;
|
||||
}) {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
||||
switch (command) {
|
||||
case 'start':
|
||||
if (debugLog) {
|
||||
console.log('socketStore: got command: start');
|
||||
}
|
||||
if (
|
||||
this.simulationRunning ||
|
||||
this.simulationState == 'RUNNING' ||
|
||||
this.simulationState == 'PAUSED'
|
||||
) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation is already running' };
|
||||
throw new Error('Simulation is already running' + this.simulationState);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('Emitting simulation_control: start');
|
||||
}
|
||||
socket.emit(
|
||||
'simulation_control',
|
||||
{ command: 'start' },
|
||||
(response: SimulationControlResponse) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { sts: 'error', msg: response.message?.toString() };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_start: ', response);
|
||||
}
|
||||
// return response.message;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'test-mode':
|
||||
socket.emit(
|
||||
'simulation_control',
|
||||
{ command: 'test-mode' },
|
||||
(response: SimulationControlResponse) => {
|
||||
if (response.command_status === 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_test-mode: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'gps-noise':
|
||||
socket.emit(
|
||||
'simulation_control',
|
||||
{ command: 'gps-noise' },
|
||||
(response: SimulationControlResponse) => {
|
||||
if (response.command_status === 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_gps-noise: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'pause':
|
||||
if (this.simulationState !== 'RUNNING') {
|
||||
throw new Error('Simulation is not running');
|
||||
}
|
||||
socket.emit(
|
||||
'simulation_control',
|
||||
{ command: 'pause' },
|
||||
(response: SimulationControlResponse) => {
|
||||
if (response.command_status === 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_pause: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'resume':
|
||||
if (this.simulationState !== 'PAUSED') {
|
||||
throw new Error('Simulation is not paused');
|
||||
}
|
||||
socket.emit('simulation_control', { command: 'resume' }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_resume: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'clear':
|
||||
if (this.simulationQueueLength == 0) {
|
||||
throw new Error('Simulation queue is empty');
|
||||
}
|
||||
socket.emit('simulation_control', { command: 'clear' }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_clear: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'reset':
|
||||
if (this.simulationQueueLength == 0) {
|
||||
throw new Error('Simulation queue is empty');
|
||||
}
|
||||
socket.emit('simulation_control', { command: 'reset' }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_reset: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'end':
|
||||
if (this.simulationState == 'ENDED' || !this.simulationRunning) {
|
||||
throw new Error('Simulation has already ended');
|
||||
}
|
||||
socket.emit('simulation_control', { command: 'end' }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_end: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'add':
|
||||
if (!latitude || !longitude) {
|
||||
throw new Error('latitude or longitude not set');
|
||||
}
|
||||
if (!address) {
|
||||
address = '';
|
||||
}
|
||||
socket.emit(
|
||||
'simulation_control',
|
||||
{
|
||||
command: 'add',
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
delay: delay,
|
||||
address: address,
|
||||
},
|
||||
(response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_add: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
if (!loc_id) {
|
||||
throw new Error('loc_id not set.');
|
||||
}
|
||||
socket.emit('simulation_control', { command: 'delete', loc_id: loc_id }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
if (!response.data?.simulation_queue) {
|
||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
this.digestQueueUpdate(response.data.simulation_queue);
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_delete: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
this.updateLocationMark(loc_id, 'status', 'deleted');
|
||||
break;
|
||||
case 'next':
|
||||
socket.emit('simulation_control', { command: 'next' }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
this.simulationState = response.data?.simulation_queue?.state;
|
||||
if (debugLog) {
|
||||
console.log('response from simulate_control_next: ', response);
|
||||
}
|
||||
return response.message;
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
fnctRtn = { sts: 'error', msg: 'Invalid command' };
|
||||
throw new Error('Invalid command');
|
||||
}
|
||||
return fnctRtn;
|
||||
},
|
||||
updateLocationMark(loc_id: string, key: string, value: string | number) {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { command_status: string; message: string };
|
||||
if (debugLog) {
|
||||
console.log(
|
||||
'socketStore: Update LocationMark request, loc_id: %s, key: %s, new value: %s',
|
||||
loc_id,
|
||||
key,
|
||||
value,
|
||||
);
|
||||
}
|
||||
if (!this.locationQueueData[loc_id]) {
|
||||
fnctRtn = {
|
||||
command_status: 'error',
|
||||
message: 'Location Id: ' + loc_id + ' is not in the simulation queue',
|
||||
};
|
||||
throw new Error('loc_id ' + loc_id + 'not found');
|
||||
}
|
||||
if (debugLog) {
|
||||
console.log('Emitting LocationItem Update');
|
||||
}
|
||||
socket.emit(
|
||||
'location_item_update',
|
||||
{ loc_id: loc_id, key: key, value: value },
|
||||
(response: LocationMarkUpdateResponse) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { command_status: 'error', message: response.message?.toString() };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { command_status: 'OK', message: response.message?.toString() };
|
||||
if (debugLog) {
|
||||
console.log('response from backend: ', response);
|
||||
}
|
||||
this.locationQueueData[loc_id] = response.data;
|
||||
}
|
||||
return fnctRtn;
|
||||
},
|
||||
);
|
||||
},
|
||||
updateLocationQueueOrder(newOrder: string[]) {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { command_status: string; message: string };
|
||||
if (debugLog) {
|
||||
console.log('socketStore: Update Location Queue Order, new order: %s', newOrder);
|
||||
}
|
||||
socket.emit('queue_order_update', { newOrder: newOrder }, (response) => {
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { command_status: 'error', message: response.message?.toString() };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { command_status: 'OK', message: response.message?.toString() };
|
||||
if (debugLog) {
|
||||
console.log('response from queue_order_update: ', response);
|
||||
}
|
||||
this.locationQueueOrder = response.data;
|
||||
}
|
||||
return fnctRtn;
|
||||
});
|
||||
},
|
||||
digestQueueUpdate(data: QueueData): void {
|
||||
console.log('digesting QueueUpdate: ', data);
|
||||
this.simulationRunning = data.active;
|
||||
console.log('Setting SimulationState to %s', data.state);
|
||||
this.simulationState = data.state;
|
||||
this.simulationQueueLength = data.order.length;
|
||||
this.locationQueueData = data.data;
|
||||
this.locationQueueOrder = data.order;
|
||||
this.locationQueueDeletedItems = data.deleted_items;
|
||||
this.testMode = data.test_mode;
|
||||
this.gpsNoise = data.gps_noise;
|
||||
},
|
||||
digestCurrentLocation(data: CurrentLocation): void {
|
||||
this.currentLocation = {
|
||||
loc_id: data.loc_id,
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
next_move: data.next_move,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useSimulationStore, import.meta.hot));
|
||||
}
|
||||
129
src/stores/socket.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import { socket } from 'boot/socketio';
|
||||
|
||||
import { useDeviceStore } from 'stores/device';
|
||||
import { useIcloudStore } from 'stores/icloud';
|
||||
import { useSimulationStore } from 'stores/simulation';
|
||||
import { useTunneldStore } from 'stores/tunneld';
|
||||
|
||||
import type { AppError, ErrorFull, StatusUpdate } from 'components/models';
|
||||
|
||||
|
||||
|
||||
export const useSocketStore = defineStore('socketStore', {
|
||||
state: () => {
|
||||
return {
|
||||
sockConnected: false as boolean,
|
||||
socketID: null as string | null | undefined,
|
||||
errorList: [] as ErrorFull[],
|
||||
debugLog: true,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
setSockStatus() {
|
||||
this.sockConnected = socket.connected;
|
||||
this.socketID = socket.id;
|
||||
},
|
||||
connect() {
|
||||
if (this.debugLog) {
|
||||
console.log('Connecting to server...');
|
||||
}
|
||||
socket.connect();
|
||||
this.setSockStatus();
|
||||
},
|
||||
disconnect() {
|
||||
socket.disconnect();
|
||||
this.setSockStatus();
|
||||
},
|
||||
toggleSock() {
|
||||
this.setSockStatus();
|
||||
if (this.sockConnected) {
|
||||
socket.disconnect();
|
||||
} else {
|
||||
socket.connect();
|
||||
}
|
||||
this.setSockStatus();
|
||||
},
|
||||
bindEvents() {
|
||||
this.setSockStatus();
|
||||
|
||||
// connect
|
||||
socket.on('connect', () => {
|
||||
this.setSockStatus();
|
||||
socket.emit('message', 'Hello from client', (e: boolean) => {
|
||||
if (this.debugLog) {
|
||||
console.log('Message delivered: ' + e);
|
||||
}
|
||||
});
|
||||
if (this.debugLog) {
|
||||
console.log('Connected to server');
|
||||
}
|
||||
});
|
||||
|
||||
// disconnect
|
||||
socket.on('disconnect', () => {
|
||||
this.setSockStatus();
|
||||
console.log('Disconnected from server');
|
||||
});
|
||||
|
||||
// error
|
||||
socket.on('error', (data: ErrorFull) => {
|
||||
this.setSockStatus();
|
||||
const errorFull = { type: data.type, error: data.error };
|
||||
this.errorList.push(errorFull);
|
||||
console.error('Error Received: ', data);
|
||||
});
|
||||
|
||||
// app_error
|
||||
socket.on('app_error', (data: AppError) => {
|
||||
this.setSockStatus();
|
||||
const errorFull = {
|
||||
type: data.type,
|
||||
error: data.error,
|
||||
message: data.message,
|
||||
data: data.data,
|
||||
};
|
||||
this.errorList.push(errorFull);
|
||||
console.error('Error Received: ', data);
|
||||
});
|
||||
|
||||
// status
|
||||
socket.on('status', (data: StatusUpdate): void => {
|
||||
if (this.debugLog) {
|
||||
console.log('StatusUpdate received: ', data);
|
||||
}
|
||||
this.digestUpdate(data);
|
||||
});
|
||||
},
|
||||
requestUpdate(): void {
|
||||
socket.emit('request_update', (response: StatusUpdate) => {
|
||||
this.digestUpdate(response);
|
||||
});
|
||||
},
|
||||
digestUpdate(data: StatusUpdate): void {
|
||||
const deviceStore = useDeviceStore();
|
||||
const icloudStore = useIcloudStore();
|
||||
const simulationStore = useSimulationStore();
|
||||
const tunneldStore = useTunneldStore();
|
||||
if (data.simulation_queue) {
|
||||
simulationStore.digestQueueUpdate(data.simulation_queue);
|
||||
}
|
||||
deviceStore.deviceConnected = !!(data.udid && data.tunnel);
|
||||
if (data.current_location) {
|
||||
simulationStore.digestCurrentLocation(data.current_location);
|
||||
}
|
||||
tunneldStore.tunnelConnected = !!data.tunnel;
|
||||
tunneldStore.tunneldWatcher = data.tunnel_watcher_running;
|
||||
if (data.icloud) {
|
||||
icloudStore.digestUpdate(data.icloud);
|
||||
}
|
||||
icloudStore.findMyUpdate = data.fmf_location;
|
||||
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useSocketStore, import.meta.hot));
|
||||
}
|
||||
63
src/stores/socketio.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const useSocketioStore = defineStore('socketio', {
|
||||
state: () => {
|
||||
return {
|
||||
|
||||
|
||||
|
||||
// nextLocation: null as NextLocation | null,
|
||||
// messageList: [''] as string[],
|
||||
|
||||
leafletZoom: 10 as number,
|
||||
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
/* sockState: (state) => state.sockConnected,
|
||||
deviceState: (state) => state.deviceConnected,
|
||||
lMarkerLatLng: (state): [number, number] => {
|
||||
if (
|
||||
state.currentLocation == null ||
|
||||
!state.currentLocation.latitude ||
|
||||
!state.currentLocation.longitude
|
||||
) {
|
||||
return [0, 0];
|
||||
}
|
||||
return [state.currentLocation.latitude, state.currentLocation.longitude];
|
||||
},
|
||||
lCenter(): [number, number] {
|
||||
return this.lMarkerLatLng;
|
||||
},
|
||||
lZoom: (state): number => state.leafletZoom,
|
||||
*/
|
||||
|
||||
},
|
||||
actions: {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useSocketioStore, import.meta.hot));
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia';
|
||||
import { socket } from 'boot/socket';
|
||||
|
||||
export const useStatusStore = defineStore('status', {
|
||||
state: () => ({
|
||||
device: {},
|
||||
socketConnected: false,
|
||||
}),
|
||||
actions: {
|
||||
bindEvents() {
|
||||
socket.on('connect', () => {
|
||||
this.socketConnected = true;
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
this.socketConnected = false;
|
||||
});
|
||||
socket.on('status_update', (data) => {
|
||||
this.$patch((state) => {
|
||||
Object.assign(state.device, data);
|
||||
});
|
||||
});
|
||||
},
|
||||
socketConnect() {
|
||||
socket.connect();
|
||||
},
|
||||
socketDisconnect() {
|
||||
socket.disconnect();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useStatusStore, import.meta.hot));
|
||||
}
|
||||
48
src/stores/tunneld.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia';
|
||||
import type { TunneldCommands } from 'components/models';
|
||||
import { useSocketStore } from 'stores/socket';
|
||||
import { socket } from 'boot/socketio';
|
||||
|
||||
export const useTunneldStore = defineStore('tunneldStore', {
|
||||
state: () => {
|
||||
return {
|
||||
tunnelConnected: false as boolean,
|
||||
tunneldWatcher: false as boolean,
|
||||
};
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
digestUpdate(data: boolean) {
|
||||
this.tunnelConnected = data;
|
||||
},
|
||||
tunneldControl(command: TunneldCommands, udid: string = '') {
|
||||
const socketStore = useSocketStore();
|
||||
const debugLog = socketStore.debugLog;
|
||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
||||
if (debugLog) {
|
||||
console.log('tunneldStore: got command: ', command);
|
||||
}
|
||||
let args: {command: TunneldCommands, udid?: string} = {command: command};
|
||||
if (udid != '') {
|
||||
args = { command: command, udid: udid };
|
||||
}
|
||||
socket.emit(
|
||||
'tunneld_control', args, (response) => {
|
||||
console.log(response.command_status, response.command);
|
||||
if (response.command_status == 'ERROR') {
|
||||
fnctRtn = { sts: 'error', msg: response.message?.toString() };
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
||||
}
|
||||
},
|
||||
);
|
||||
return fnctRtn;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useTunneldStore, import.meta.hot));
|
||||
}
|
||||
23
src/types.ts
@@ -1,23 +0,0 @@
|
||||
export interface DeviceShort {
|
||||
udid: string | null;
|
||||
connection_type: string | null;
|
||||
}
|
||||
|
||||
export interface StatusUpdate {
|
||||
device_connected: boolean;
|
||||
device_count: number;
|
||||
// devices?: Array<DeviceShort>;
|
||||
udid?: string | null;
|
||||
device_name?: string | null;
|
||||
product_version?: string | null;
|
||||
phone_number?: string | null;
|
||||
developer_mode_enabled?: boolean;
|
||||
ddi_mounted?: boolean;
|
||||
rsd_address?: string | null;
|
||||
rsd_port?: number;
|
||||
lockdown_trusted_port?: number;
|
||||
lockdown_untrusted_port?: number;
|
||||
lockdown_trusted_reachable?: boolean;
|
||||
lockdown_untrusted_reachable?: boolean;
|
||||
dtservicehub_reachable?: boolean;
|
||||
}
|
||||
1
src/types/leaflet-esm.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'leaflet/dist/leaflet-src.esm';
|
||||
22
src/types/leaflet-routing-machine.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
declare module 'leaflet-routing-machine' {
|
||||
import type * as L from 'leaflet';
|
||||
|
||||
export interface IRouter {
|
||||
route(
|
||||
waypoints: Array<{ latLng: L.LatLng }>,
|
||||
callback: (error: unknown, routes?: unknown[]) => void,
|
||||
context?: unknown,
|
||||
options?: unknown,
|
||||
): unknown;
|
||||
}
|
||||
export type IGeocoder = Record<string, unknown>;
|
||||
export type LineOptions = L.PolylineOptions;
|
||||
|
||||
export namespace Routing {
|
||||
function control(options: Record<string, unknown>): Record<string, unknown>;
|
||||
class Plan {}
|
||||
}
|
||||
|
||||
const Routing: typeof Routing;
|
||||
export default Routing;
|
||||
}
|
||||
1
src/types/openrouteservice-js.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'openrouteservice-js';
|
||||