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'],
|
files: ['**/*.ts', '**/*.vue'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
'@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
|
// https://github.com/vuejs/eslint-config-typescript
|
||||||
|
|||||||
3702
package-lock.json
generated
15
package.json
@@ -15,21 +15,30 @@
|
|||||||
"postinstall": "quasar prepare"
|
"postinstall": "quasar prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet-contextmenu": "^1.4.0",
|
||||||
|
"leaflet-extra-markers": "^2.0.1",
|
||||||
"leaflet-geosearch": "^4.2.2",
|
"leaflet-geosearch": "^4.2.2",
|
||||||
|
"leaflet-routing-machine": "^3.2.12",
|
||||||
|
"openrouteservice-js": "^0.4.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"quasar": "^2.16.0",
|
"quasar": "^2.19.3",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
"vue-draggable-plus": "^0.6.1",
|
||||||
"vue-router": "^5.0.0"
|
"vue-router": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@quasar/app-vite": "^2.1.0",
|
"@quasar/app-vite": "^2.6.0",
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/leaflet-contextmenu": "^1.4.4",
|
||||||
"@types/node": "^20.5.9",
|
"@types/node": "^20.5.9",
|
||||||
"@vue/eslint-config-prettier": "^10.1.0",
|
"@vue/eslint-config-prettier": "^10.1.0",
|
||||||
"@vue/eslint-config-typescript": "^14.4.0",
|
"@vue/eslint-config-typescript": "^14.4.0",
|
||||||
|
|||||||
|
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
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file
|
||||||
|
|
||||||
import { defineConfig } from '#q-app/wrappers';
|
import { defineConfig } from '#q-app/wrappers';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
export default defineConfig((/* ctx */) => {
|
export default defineConfig((/* ctx */) => {
|
||||||
return {
|
return {
|
||||||
@@ -11,10 +12,7 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
boot: [
|
boot: ['axios', 'socketio'],
|
||||||
'axios',
|
|
||||||
'socket',
|
|
||||||
],
|
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
||||||
css: ['app.scss'],
|
css: ['app.scss'],
|
||||||
@@ -22,19 +20,24 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
extras: [
|
extras: [
|
||||||
// 'ionicons-v4',
|
// 'ionicons-v4',
|
||||||
// 'mdi-v7',
|
'mdi-v7',
|
||||||
// 'fontawesome-v6',
|
// 'fontawesome-v6',
|
||||||
// 'eva-icons',
|
// 'eva-icons',
|
||||||
// 'themify',
|
// 'themify',
|
||||||
// 'line-awesome',
|
// 'line-awesome',
|
||||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||||
|
|
||||||
'roboto-font', // 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
|
// 'material-icons', // optional, you are not bound to it
|
||||||
],
|
],
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||||
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: {
|
target: {
|
||||||
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||||
node: 'node20',
|
node: 'node20',
|
||||||
@@ -46,7 +49,7 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// extendTsConfig (tsConfig) {}
|
// extendTsConfig (tsConfig) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||||
// vueRouterBase,
|
// vueRouterBase,
|
||||||
// vueDevtools,
|
// vueDevtools,
|
||||||
// vueOptionsAPI: false,
|
// vueOptionsAPI: false,
|
||||||
@@ -65,13 +68,32 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// extendViteConf (viteConf) {},
|
// extendViteConf (viteConf) {},
|
||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
extendViteConf () {
|
extendViteConf() {
|
||||||
return {
|
return {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules/leaflet')) return 'vendor-leaflet-core';
|
||||||
|
if (id.includes('leaflet-routing-machine')) return 'vendor-leaflet-routing';
|
||||||
|
if (id.includes('leaflet-geosearch')) return 'vendor-leaflet-geosearch';
|
||||||
|
if (id.includes('leaflet-extra-markers')) return 'vendor-leaflet-markers';
|
||||||
|
if (id.includes('openrouteservice-js')) return 'vendor-openrouteservice';
|
||||||
|
if (id.includes('socket.io-client')) return 'vendor-socketio';
|
||||||
|
if (id.includes('node_modules/quasar')) return 'vendor-quasar';
|
||||||
|
if (id.includes('node_modules/vue') || id.includes('node_modules/pinia')) {
|
||||||
|
return 'vendor-vue-core';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: [
|
hmr: {
|
||||||
'localhost',
|
// overlay: false,
|
||||||
'strixx.famor.org',
|
},
|
||||||
],
|
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}"',
|
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{ts,js,mjs,cjs,vue}"',
|
||||||
useFlatConfig: true,
|
useFlatConfig: true,
|
||||||
},
|
},
|
||||||
|
overlay: false,
|
||||||
},
|
},
|
||||||
{ server: false },
|
{ server: false },
|
||||||
],
|
],
|
||||||
@@ -95,30 +118,41 @@ export default defineConfig((/* ctx */) => {
|
|||||||
devServer: {
|
devServer: {
|
||||||
// https: true,
|
// https: true,
|
||||||
open: false, // opens browser window automatically
|
open: false, // opens browser window automatically
|
||||||
public: 'http://strixx.famor.org:9000',
|
// public: 'https://simloc.strixx.intrepidnet.org',
|
||||||
proxy: {
|
proxy: {
|
||||||
// proxy all requests starting with /api to jsonplaceholder
|
// proxy all requests starting with /api to jsonplaceholder
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:49151',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
},
|
},
|
||||||
'/socket.io': {
|
'/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://
|
secure: false, // Set to true if using wss://
|
||||||
ws: true, // Enable WebSocket proxying
|
ws: true, // Enable WebSocket proxying
|
||||||
rewriteWsOrigin: true,
|
rewriteWsOrigin: true,
|
||||||
changeOrigin: 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
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||||
framework: {
|
framework: {
|
||||||
config: {
|
config: {
|
||||||
dark: true,
|
dark: true,
|
||||||
},
|
},
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
iconSet: 'mdi-v7',
|
||||||
// lang: 'en-US', // Quasar language pack
|
// lang: 'en-US', // Quasar language pack
|
||||||
|
|
||||||
// For special cases outside of where the auto-import strategy can have an impact
|
// For special cases outside of where the auto-import strategy can have an impact
|
||||||
@@ -129,14 +163,11 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// directives: [],
|
// directives: [],
|
||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: [
|
plugins: ['Dialog', 'Notify'],
|
||||||
'Dialog',
|
|
||||||
'Notify',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
// https://v2.quasar.dev/options/animations
|
// https://v2.quasar.dev/options/animations
|
||||||
animations: [],
|
animations: ['slideInLeft', 'slideOutLeft', 'slideOutRight'],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
|
||||||
// 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 {
|
interface ComponentCustomProperties {
|
||||||
$axios: AxiosInstance;
|
$axios: AxiosInstance;
|
||||||
$api: AxiosInstance;
|
$api: AxiosInstance;
|
||||||
|
$osm: AxiosInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = axios.create({ baseURL: '/api' });
|
const api = axios.create({ baseURL: '/api' });
|
||||||
|
const osm = axios.create({ baseURL: '/osm' });
|
||||||
|
|
||||||
export default defineBoot(({ app }) => {
|
export default defineBoot(({ app }) => {
|
||||||
app.config.globalProperties.$axios = axios
|
app.config.globalProperties.$axios = axios
|
||||||
app.config.globalProperties.$api = api
|
app.config.globalProperties.$api = api
|
||||||
|
app.config.globalProperties.$osm = osm
|
||||||
})
|
})
|
||||||
|
|
||||||
export { axios, api };
|
export { axios, api, osm };
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
18
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>
|
<template>
|
||||||
<q-dialog ref="dlgRef" persistent>
|
<q-dialog ref="dlgRef" persistent>
|
||||||
<q-card>
|
<q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm">
|
||||||
<q-card-section class="row items-center">
|
<q-toolbar>
|
||||||
<q-avatar icon="add_location" color="primary" text-color="white" />
|
<q-avatar icon="mdi-map-marker" color="primary" text-color="white" />
|
||||||
<span class="q-ml-sm"> Are you sure you want to set location to {{ lat }}, {{ lng }} ?</span>
|
<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-section>
|
||||||
<q-card-actions align="right">
|
<q-separator />
|
||||||
<q-btn flat label="OK" color="primary" @click="onOkClick" />
|
<q-card-actions align="between">
|
||||||
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" />
|
<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-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
@@ -15,21 +62,88 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDialogPluginComponent } from 'quasar';
|
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({
|
const props = defineProps({
|
||||||
lat: { type: Number, required: true },
|
lat: { type: Number, required: true },
|
||||||
lng: { type: Number, required: true }
|
lng: { type: Number, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits([
|
const $q = useQuasar();
|
||||||
...useDialogPluginComponent.emits
|
const loading = ref(true);
|
||||||
])
|
const delay = ref(300);
|
||||||
|
|
||||||
|
defineEmits([...useDialogPluginComponent.emits]);
|
||||||
|
|
||||||
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||||
|
|
||||||
function onOkClick () {
|
const address = ref<NominatimResponse>();
|
||||||
onDialogOK()
|
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>
|
</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">
|
<script setup lang="ts">
|
||||||
import { socket } from 'boot/socket';
|
import { socket } from 'boot/socketio';
|
||||||
import { useStatusStore } from 'stores/status';
|
import { computed, ref } from 'vue';
|
||||||
import { computed } 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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
||||||
<q-btn round icon="webhook">
|
<q-btn icom="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
|
||||||
<q-badge floating :color="socketStatus" rounded />
|
<q-btn label="Connect" @click="socketStore.connect()" />
|
||||||
</q-btn>
|
<q-btn label="Disconnect" @click="socketStore.disconnect()" />
|
||||||
<q-btn label="Connect" @click="statusStore.socketConnect" />
|
</div>
|
||||||
<q-btn label="Disconnect" @click="statusStore.socketDisconnect" />
|
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
||||||
<q-btn label="Emit Hello" @click="socket.emit('message', 'Hello from Vue!')" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,38 +1,90 @@
|
|||||||
<script setup lang="ts">
|
<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';
|
const socketStore = useSocketStore();
|
||||||
import { api } from 'boot/axios';
|
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' });
|
function statusDevColor(state: string | boolean | null | undefined): string {
|
||||||
|
if (state === null || state === undefined) {
|
||||||
async function getStatus() {
|
return 'grey';
|
||||||
try {
|
}
|
||||||
const { data } = await api({
|
if (typeof state === 'boolean') {
|
||||||
method: 'get',
|
return state ? 'green' : 'red';
|
||||||
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 {
|
} else {
|
||||||
console.error('Error setting location:', error);
|
switch (state.toLowerCase()) {
|
||||||
|
case 'paused':
|
||||||
|
return 'yellow';
|
||||||
|
case 'running':
|
||||||
|
return 'green';
|
||||||
|
case 'stopped':
|
||||||
|
return 'red';
|
||||||
|
default:
|
||||||
|
return 'grey';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await getStatus();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-toolbar class="bg-primary text-white q-my-md shadow-2">
|
<q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
|
||||||
<q-badge :color="statusDev.color" rounded class="q-mr-sm" />{{ statusDev.deviceName }}
|
<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>
|
</q-toolbar>
|
||||||
</template>
|
</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 {
|
export interface Meta {
|
||||||
totalCount: number;
|
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.
|
// to match your app's branding.
|
||||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||||
|
|
||||||
$primary: #1976d2;
|
$primary: #161B36;
|
||||||
$secondary: #26a69a;
|
$secondary: #8FA9BF;
|
||||||
$accent: #9c27b0;
|
$accent: #5a728a;
|
||||||
|
|
||||||
$dark: #1d1d1d;
|
$dark: #1d1d1d;
|
||||||
$dark-page: #121212;
|
$dark-page: #0B1026;
|
||||||
|
|
||||||
$positive: #21ba45;
|
$positive: #4CAF50;
|
||||||
$negative: #c10015;
|
$negative: #EF5350;
|
||||||
$info: #31ccec;
|
$info: #42A5F5;
|
||||||
$warning: #f2c037;
|
$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>
|
<template>
|
||||||
<q-layout view="lHh lpr lFf">
|
<q-layout view="hhh lpr fff">
|
||||||
<q-header elevated>
|
<q-header>
|
||||||
<q-toolbar>
|
<MenuBar @drawer="drawer = !drawer" />
|
||||||
<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-header>
|
</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>
|
<q-page-container>
|
||||||
<router-view />
|
<router-view />
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
@@ -17,4 +43,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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>
|
</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>
|
||||||
@@ -7,25 +7,26 @@
|
|||||||
<q-btn color="purple" @click="showNotif" label="Show Notification" />
|
<q-btn color="purple" @click="showNotif" label="Show Notification" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="border: 5px pink dashed;" >
|
<div class="col" style="border: 5px pink dashed">
|
||||||
<q-icon :name="darkStatus" size="100px" @click="toggleDarkLight" />
|
<q-icon :name="darkStatus" size="100px" @click="toggleDarkLight" />
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar';
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue';
|
||||||
import SocketTest from 'components/SocketTest.vue'
|
import SocketTest from 'components/SocketTest.vue';
|
||||||
const $q = useQuasar()
|
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() {
|
function toggleDarkLight() {
|
||||||
$q.dark.toggle()
|
$q.dark.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function showNotif() {
|
function showNotif() {
|
||||||
$q.notify({
|
$q.notify({
|
||||||
message: 'This is a notification',
|
message: 'This is a notification',
|
||||||
@@ -33,9 +34,16 @@ function showNotif() {
|
|||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
actions: [
|
actions: [
|
||||||
{ label: 'Dismiss', color: 'white', handler: () => { /* Dismiss action */ } }
|
{
|
||||||
]
|
label: 'Dismiss',
|
||||||
})
|
color: 'white',
|
||||||
|
handler: () => {
|
||||||
|
/* Dismiss action */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -5,8 +5,28 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('layouts/MainLayout.vue'),
|
component: () => import('layouts/MainLayout.vue'),
|
||||||
children: [
|
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
|
// but you can also remove it
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)*',
|
path: '/:catchAll(.*)*',
|
||||||
|
name: 'ErrorNotFound',
|
||||||
component: () => import('pages/ErrorNotFound.vue'),
|
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';
|
||||||