Compare commits

..

9 Commits

Author SHA1 Message Date
ddaf682d16 add EditFavoriteDialog component and simloc_logo.svg
split socketio strore into seperate stores
added all routing points to sim
add vuedraggable
built icon picker for favorites
and more
2026-04-29 13:17:49 -04:00
52f05550e0 fixes 2026-04-22 11:32:47 -04:00
d72550b3c1 fixes 2026-04-18 08:08:47 -04:00
32c4f2a835 lots of changes 2026-04-14 09:53:12 -04:00
27a2904bab osr proxy 2026-04-04 11:29:19 -04:00
9b8b9ec664 lint 2026-04-01 16:05:32 -04:00
a6264fad67 customIcon 2026-04-01 15:47:38 -04:00
e63a8a6329 leaflet routimg machine - lical osr 2026-04-01 10:30:34 -04:00
05e63a28f1 extensive changes 2026-03-27 17:11:13 -04:00
55 changed files with 9248 additions and 2654 deletions

View File

@@ -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

3660
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,23 +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-extra-markers": "^2.0.1",
"leaflet-geosearch": "^4.2.2", "leaflet-geosearch": "^4.2.2",
"leaflet-routing-machine": "^3.2.12", "leaflet-routing-machine": "^3.2.12",
"openrouteservice-js": "^0.4.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"quasar": "^2.16.0", "quasar": "^2.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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -20,15 +20,15 @@ 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
@@ -70,14 +70,30 @@ export default defineConfig((/* ctx */) => {
extendViteConf() { extendViteConf() {
return { return {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules/leaflet')) return 'vendor-leaflet-core';
if (id.includes('leaflet-routing-machine')) return 'vendor-leaflet-routing';
if (id.includes('leaflet-geosearch')) return 'vendor-leaflet-geosearch';
if (id.includes('leaflet-extra-markers')) return 'vendor-leaflet-markers';
if (id.includes('openrouteservice-js')) return 'vendor-openrouteservice';
if (id.includes('socket.io-client')) return 'vendor-socketio';
if (id.includes('node_modules/quasar')) return 'vendor-quasar';
if (id.includes('node_modules/vue') || id.includes('node_modules/pinia')) {
return 'vendor-vue-core';
}
return undefined;
},
},
},
},
server: { server: {
hmr: { hmr: {
// overlay: false, // overlay: false,
}, },
allowedHosts: [ allowedHosts: ['localhost', 'strixx.famor.org', 'simloc.strixx.intrepidnet.org'],
'localhost',
'strixx.famor.org'
],
}, },
}; };
}, },
@@ -102,31 +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/, ''), // 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
@@ -141,7 +167,7 @@ export default defineConfig((/* ctx */) => {
}, },
// 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: {

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

1978
src/assets/simloc_logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 538 KiB

BIN
src/assets/simloc_logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -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 };

View File

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

18
src/boot/sentry.ts Normal file
View 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,
});
})

View File

@@ -1,15 +1,17 @@
<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="icon" color="secondary" text-color="black" />
<span class="q-ml-sm"> <q-toolbar-title>Command Control</q-toolbar-title>
Are you sure you want to {{ name }} ? </q-toolbar>
</span> <q-card-section class="q-ml-lg">
<div class="q-mb-sm">Are you sure you want to {{ name }} ? </div>
</q-card-section> </q-card-section>
<q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="OK" color="primary" @click="onOkClick" /> <q-btn flat label="OK" @click="onOkClick" />
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" /> <q-btn flat label="Cancel" @click="onDialogCancel" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@@ -18,8 +20,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
const props = defineProps({ defineProps({
name: { type: String, required: true }, name: { type: String, required: true },
icon: { type: String, required: true },
}); });
defineEmits([...useDialogPluginComponent.emits]); defineEmits([...useDialogPluginComponent.emits]);

View 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>

View 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>

View 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.

View File

@@ -1,84 +1,83 @@
<template>
<div style="display: none"></div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'; import {
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'; ref,
import L from 'leaflet'; 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';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'; import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import type { IRouter, IGeocoder, LineOptions } from 'leaflet-routing-machine';
// Props type RoutingControlInstance = {
const props = defineProps<{ on: (listeners: unknown) => void;
mapObject?: any; setWaypoints: (waypoints: unknown[]) => void;
visible?: boolean; remove: () => void;
waypoints: any[]; };
router?: IRouter;
plan?: L.Routing.Plan; // ---- Emits ----
geocoder?: IGeocoder; const emit = defineEmits<{
fitSelectedRoutes?: string | boolean; (e: 'ready', value: RoutingControlInstance): void;
lineOptions?: LineOptions;
routeLine?: Function;
autoRoute?: boolean;
routeWhileDragging?: boolean;
routeDragInterval?: number;
waypointMode?: string;
useZoomParameter?: boolean;
showAlternatives?: boolean;
altLineOptions?: LineOptions;
}>(); }>();
// Defaults // ---- Props ----
const visible = props.visible ?? true; const props = defineProps(routingControlProps);
const fitSelectedRoutes = props.fitSelectedRoutes ?? 'smart';
const autoRoute = props.autoRoute ?? true;
const routeWhileDragging = props.routeWhileDragging ?? false;
const routeDragInterval = props.routeDragInterval ?? 500;
const waypointMode = props.waypointMode ?? 'connect';
const useZoomParameter = props.useZoomParameter ?? false;
const showAlternatives = props.showAlternatives ?? false;
// State // ---- Attrs (for events) ----
const ready = ref(false); const attrs = useAttrs();
const layer = ref<any>(null);
// Methods // ---- Injections ----
function add() { const { UseGlobalLeafletInjection, RegisterControlInjection } = InjectionKeys;
if (!props.mapObject) return; const { WINDOW_OR_GLOBAL, assertInject, propsBinder, remapEvents } = Utilities;
const options = { const useGlobalLeaflet = inject(UseGlobalLeafletInjection, false);
waypoints: props.waypoints, const registerControl = assertInject(RegisterControlInjection);
fitSelectedRoutes,
autoRoute,
routeWhileDragging,
routeDragInterval,
waypointMode,
useZoomParameter,
showAlternatives,
};
console.log(L.Routing);
const routingLayer = L.Routing.control(options);
routingLayer.addTo(props.mapObject);
layer.value = routingLayer;
ready.value = true; // ---- State ----
} const leafletObject = ref<RoutingControlInstance | null>(null);
// Watchers // ---- Setup logic ----
watch(
() => props.mapObject,
(val) => {
if (!val) return;
add();
},
);
// Lifecycle const { options, methods } = setupRoutingControl(props);
onMounted(() => {
add(); 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(() => { onBeforeUnmount(() => {
if (layer.value) { if (leafletObject.value) {
layer.value.remove(); leafletObject.value.setWaypoints([]);
leafletObject.value.remove();
} }
}); });
</script> </script>

File diff suppressed because it is too large Load Diff

View 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>

View File

@@ -1,64 +1,207 @@
<script setup lang="ts"> <script setup lang="ts">
import { useLeafletStore } from 'stores/leaflet';
import type { Control, coords, CtrlAttr, CtrlAttrs } from 'components/models';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { socket } from 'boot/socketio';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useSocketioStore } from 'stores/socketio'; import { computed, ref } from 'vue';
import { ref } from 'vue';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
import { favorites } from 'constants/favorites'; import { favorites } from 'constants/favorites';
import { controls } from 'constants/controls'; 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 route = useRoute();
const $q = useQuasar(); const $q = useQuasar();
const deviceStore = useDeviceStore();
const leafletStore = useLeafletStore(); const leafletStore = useLeafletStore();
const socketStore = useSocketioStore(); const simulationStore = useSimulationStore();
const { center, markerLatLng } = storeToRefs(leafletStore); const icloudStore = useIcloudStore();
const tunneldStore = useTunneldStore();
const { center, markerLatLng, zoom } = storeToRefs(leafletStore);
const {
simulationRunning,
simulationState,
simulationQueueLength,
locationQueueOrder,
testMode,
gpsNoise,
currentLocation,
} = storeToRefs(simulationStore);
const { simulationRunning, simulationState, simulationQueueLength } = storeToRefs(socketStore); const { icloudMonitor } = storeToRefs(icloudStore);
const menuOpen = ref(); 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) { function handleFavClick(coords: coords) {
center.value = [coords.lat, coords.lng]; center.value = [coords.lat, coords.lng];
markerLatLng.value = [coords.lat, coords.lng]; markerLatLng.value = [coords.lat, coords.lng];
zoom.value = 15;
} }
function handleControlClick(cmdAttr: CtrlAttr) { 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) { if (cmdAttr.cnfrm) {
$q.dialog({ $q.dialog({
component: ConfirmCommandDialog, component: ConfirmCommandDialog,
componentProps: { componentProps: {
name: cmdAttr.name, name: cmdAttr.name,
icon: cmdAttr.icon,
}, },
}) })
.onOk(() => { .onOk(() => {
if (cmdAttr.cmdClass === 'simulation_control') { if (cmdAttr.cmdClass === 'sim_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
try { try {
const ack = socketStore.simulationControl(cmdAttr.cmd, cmdAttr.delay); const ack = simulationStore.simulationControl({
$q.notify({ type: 'positive', message: ack }); 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) { } catch (error: unknown) {
notType = 'negative';
if (error instanceof Error) { if (error instanceof Error) {
console.error('Simulation Command ERROR: ', error.message); console.error('Simulation Command ERROR: ', error.message);
const ack = `Simulation Command Error: ${error.message}`; notMsg = `Simulation Command Error: ${error.message}`;
$q.notify({ type: 'negative', message: ack });
} else { } else {
console.error('Simmulation Command Error: ', error); console.error('Simulation Command Error: ', error);
const ack = 'Simulation Command Error: Unknow error'; notMsg = 'Simulation Command Error: Unknow error';
$q.notify({ type: 'negative', message: ack }); }
} 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 });
} }
if (ctrl.cmdClass === 'device_control') {
socket.emit('device_control', { command: ctrl.cmd, delay: 0 }, (response) => {
console.log(response.status, response.command);
});
} }
}) })
.onCancel(() => { .onCancel(() => {
@@ -68,77 +211,190 @@ function handleControlClick(cmdAttr: CtrlAttr) {
console.log('Dialog dismissed'); console.log('Dialog dismissed');
}); });
} else { } else {
if (ctrl.cmdClass === 'simulation_control') { if (cmdAttr.cmdClass === 'sim_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
try { try {
const response = socketStore.simulationControl(ctrl.cmd); const ack = simulationStore.simulationControl({
$q.notify({ type: 'positive', message: response }); command: cmdAttr.cmd,
} catch (error: unknown) { latitude: null,
if (error instanceof Error) { longitude: null,
console.error('Error: ' + error.message); loc_id: null,
$q.notify({ type: 'negative', message: error.toString() }); delay: cmdAttr.delay,
} else { address: null,
console.error('Error setting location:', error);
$q.notify({ type: 'negative', message: error.toString() });
}
}
}
if (ctrl.cmdClass === 'device_control') {
socket.emit('device_control', { command: ctrl.cmd, delay: 0 }, (response) => {
console.log(response.status, response.command);
}); });
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> </script>
<template> <template>
<q-toolbar class="bg-primary text-white"> <q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
<q-btn @click="$emit('drawer')" flat round dense icon="menu" class="q-mr-sm" /> <!-- <q-btn
@click="
$emit('drawer');
leafletStore.toggleQLocDrawer();
"
flat
round
dense
icon="menu"
class="q-mr-sm"
/>
-->
<q-avatar>
<q-img src="~assets/simloc_logo2.png" class="logo" />
</q-avatar>
<q-separator dark inset /> <q-separator dark inset />
<q-space /> <q-space />
<q-btn :icon-right="menuOpen ? 'arrow_drop_up' : 'arrow_drop_down'" stretch flat label="Favorites" v-if="route.name === 'Leaflet'"> <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-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
<q-list> <q-list dark>
<template v-for="fav, index) in favorites" :key="index"> <template v-for="(favObj, favId) in favoritesMap" :key="favId">
<q-item <q-item
v-if="fav.coords" dark
v-if="hasCoords(favObj)"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleFavClick(fav.coords)" @click="handleFavClick(favObj.coords)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="fav.icon" color="primary" text-color="white" /> <q-avatar :icon="favObj.icon" color="secondary" size="sm" text-color="black" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label>{{ fav.name }}</q-item-label> <q-item-label>{{ favObj.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-else clickable v-ripple> <q-item v-else-if="hasSubitems(favObj)" clickable v-ripple dark>
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="fav.icon" color="primary" text-color="white" /> <q-avatar :icon="favObj.icon" color="secondary" size="sm" text-color="black" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label>{{ fav.name }}</q-item-label> <q-item-label>{{ favObj.name }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-icon name="keyboard_arrow_right" /> <q-icon name="mdi-chevron-right" />
</q-item-section> </q-item-section>
<q-menu anchor="top end" self="top start"> <q-menu anchor="bottom start" self="bottom end">
<q-list> <q-list dark>
<q-item <q-item
v-for="(f, indx) in fav.subitems" dark
:key="indx" v-for="(favSubObj, favSubId) in favObj.subitems"
:key="favSubId"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleFavClick(f.coords)" @click="handleFavClick(favSubObj.coords)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="f.icon" color="primary" text-color="white" /> <q-avatar
:icon="favSubObj.icon"
color="secondary"
size="sm"
text-color="black"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label>{{ f.name }}</q-item-label> <q-item-label>{{ favSubObj.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@@ -148,99 +404,418 @@ function handleControlClick(cmdAttr: CtrlAttr) {
</q-list> </q-list>
</q-menu> </q-menu>
</q-btn> </q-btn>
<q-btn-dropdown stretch flat label="Controls"> <q-btn-dropdown class="text-weight-bold" stretch flat label="Controls">
<q-list> <q-list dark>
<q-item-label header>Simulation Controls</q-item-label> <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 <q-item
dark
v-if="!simulationRunning" v-if="!simulationRunning"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.sim_start)"> @click="handleControlClick(controls.simulation.start)"
<q-item-section avatar> >
<q-avatar :icon="controls.sim_start.icon" color="primary" text-color="white" /> <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-section> <q-item-section>
<q-item-label> {{ controls.sim_start.name }} </q-item-label> <q-item-label> {{ controls.simulation.start.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dark
v-if="simulationState === 'RUNNING' && simulationRunning" v-if="simulationState === 'RUNNING' && simulationRunning"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.sim_pause)" @click="handleControlClick(controls.simulation.pause)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar :icon="controls.sim_pause.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.pause.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.sim_pause.name }} </q-item-label> <q-item-label> {{ controls.simulation.pause.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dark
v-if="simulationState === 'PAUSED'" v-if="simulationState === 'PAUSED'"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.sim_resume)" @click="handleControlClick(controls.simulation.resume)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar :icon="controls.sim_resume.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.resume.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.sim_resume.name }} </q-item-label> <q-item-label> {{ controls.simulation.resume.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
v-if="simulationQueueLength > 0" dark
v-if="simulationQueueLength && simulationQueueLength > 0 && !isLast"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.sim_clear)" @click="handleControlClick(controls.simulation.clear)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar :icon="controls.sim_clear.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.clear.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.sim_clear.name }} </q-item-label> <q-item-label> {{ controls.simulation.clear.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
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" v-if="simulationRunning"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.sim_end)" @click="handleControlClick(controls.simulation.end)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar :icon="controls.sim_end.icon" color="primary" text-color="white" /> <q-avatar
:icon="controls.simulation.end.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label> {{ controls.sim_end.name }} </q-item-label> <q-item-label> {{ controls.simulation.end.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator spaced /> </q-expansion-item>
<q-item-label header>Device Controls</q-item-label> <q-expansion-item
<q-item clickable v-ripple v-close-popup @click="handleControlClick(controls.dev_reboot)"> group="controls"
<q-item-section avatar> icon="mdi-cloud"
<q-avatar :icon="controls.dev_reboot.icon" color="primary" text-color="white" /> 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-section> <q-item-section>
<q-item-label> {{ controls.dev_reboot.name }} </q-item-label> <q-item-label> {{ controls.icloudmonitor.start.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item clickable v-ripple v-close-popup @click="handleControlClick(controls.dev_shutdown)"> <q-item
<q-item-section avatar> dark
<q-avatar :icon="controls.dev_shutdown.icon" color="primary" text-color="white" /> 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-section> <q-item-section>
<q-item-label> {{ controls.dev_shutdown.name }} </q-item-label> <q-item-label> {{ controls.icloudmonitor.stop.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </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-list>
</q-btn-dropdown> </q-btn-dropdown>
</q-toolbar> </q-toolbar>
</template> </template>
<style scoped></style> <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>

View 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>

View File

@@ -1,19 +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="text-h6"> Add Simulated Location to Queue</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-section> <q-separator />
<span class="q-ml-sm"> <q-card-actions align="between">
Are you sure you want to set location to {{ latitude }}, {{ longitude }} ? <div class="text-yellow">
</span> <q-checkbox
<q-input dense v-model="delay" autofocus @keyup.enter="onOkClick" label="Delay (seconds)" type="number" /> class="cursor-pointer"
</q-card-section> v-model="isFavorite"
<q-card-actions align="right"> checked-icon="mdi-star"
<q-btn flat label="OK" color="primary" @click="onOkClick" /> unchecked-icon="mdi-star-outline"
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" /> 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>
@@ -21,22 +62,88 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { ref } from 'vue'; 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 },
}); });
const delay = ref(0); const $q = useQuasar();
const latitude = props.lat; const loading = ref(true);
const longitude = props.lng; const delay = ref(300);
defineEmits([...useDialogPluginComponent.emits]); defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent(); const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
const address = ref<NominatimResponse>();
const isFavorite = ref(false);
const favorite = ref();
const favoriteName = computed(() => favorite.value?.name ?? '');
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
onMounted(async () => {
try {
loading.value = true;
const response = await reverseGeocodeRateLimited(props.lat, props.lng);
console.log('reverse geocode response: ', response);
address.value = response.address;
if (response.favorite) {
favorite.value = response.favorite;
}
} catch (error) {
console.error('Error fetching reverse geocode:', error);
throw error;
} finally {
await sleep(500);
loading.value = false;
}
});
function handleFavoriteClick() {
if (!favorite.value) {
favorite.value = {
name: '',
icon: '',
category: '',
latitude: props.lat,
longitude: props.lng,
};
}
$q.dialog({
component: EditFavoriteDialog,
componentProps: {
lat: props.lat,
lng: props.lng,
fav: favorite.value,
},
})
.onOk(({ data }) => {
favorite.value = data;
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
function onOkClick() { function onOkClick() {
onDialogOK(); onDialogOK({ delay: delay.value, address: address.value });
} }
</script> </script>
<style lang="sass" scoped>
.add-loc-card
width: 100%
max-width: 450px
</style>

View File

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

View File

@@ -1,68 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { socket } from 'boot/socketio'; import { socket } from 'boot/socketio';
import { useQuasar } from 'quasar'; import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue'; import { useSocketStore } from 'stores/socket';
import { useSocketioStore } from 'stores/socketio';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import type { ClientToServerEvents } from 'components/models';
const socketStore = useSocketStore();
const socketioStore = useSocketioStore();
const $q = useQuasar();
const msgInput = ref('');
const sockEvent = ref(''); const sockEvent = ref('');
const eventArgs = ref(''); const eventArgs = ref('');
const { sockConnected, messageList } = storeToRefs(socketioStore); const { sockConnected } = storeToRefs(socketStore);
const sockStatColor = computed(() => { const sockStatColor = computed(() => {
return sockConnected.value ? 'green' : 'red'; return sockConnected.value ? 'green' : 'red';
}); });
function sendMessage() {
const msgToSend = msgInput.value;
console.log('Sending message... ' + msgToSend);
socket.emit('message', msgToSend, (e, r) => {
console.log('Message: ' + msgToSend + ' delivered: ' + e + ', response: ' + r);
});
msgInput.value = '';
}
function handleEmit() { function handleEmit() {
const event = sockEvent.value; const event = sockEvent.value;
const jsonArgs = eventArgs.value; const jsonArgs = eventArgs.value;
socket.emit(event, jsonArgs, (resp) => { socket.emit(event as keyof ClientToServerEvents, jsonArgs, (resp: unknown) => {
console.log('Server Reponse: ' + resp); console.log('Server Response: %s', resp);
}); });
sockEvent.value = ''; sockEvent.value = '';
eventArgs.value = ''; eventArgs.value = '';
}; }
const timeWithSeconds = ref('00:00:45');
watch(
messageList,
(newVal: string[], oldVal: string[]) => {
let newMsg: string;
newMsg = newVal[newVal.length - 1];
console.log('New message received: ', newMsg);
console.log('Past List', oldVal);
$q.notify(newMsg);
},
{ deep: true },
);
</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 icom="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn> <q-btn icom="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
<q-btn label="Connect" @click="socketioStore.connect()" /> <q-btn label="Connect" @click="socketStore.connect()" />
<q-btn label="Disconnect" @click="socketioStore.disconnect()" /> <q-btn label="Disconnect" @click="socketStore.disconnect()" />
<q-input v-model="msgInput" filled label="Message" />
<div>
<q-btn label="Send" @click="sendMessage" />
</div>
<q-list bordered seperator v-if="socketioStore.messageList.length > 0">
<q-item v-for="(msg, index) in socketioStore.messageList" :key="index">
<q-item-section>{{ msg }}</q-item-section>
</q-item>
</q-list>
</div> </div>
<div class="flex flex-center col q-ma-lg q-gutter-md"> <div class="flex flex-center col q-ma-lg q-gutter-md">
<div class="text-h3">SocketIO Functions</div> <div class="text-h3">SocketIO Functions</div>
@@ -72,5 +42,7 @@ watch(
<q-btn label="Emit" @click="handleEmit" /> <q-btn label="Emit" @click="handleEmit" />
</div> </div>
</div> </div>
<div>
<q-time v-model="timeWithSeconds" with-seconds format24h default-view="seconds" />
</div>
</template> </template>

View File

@@ -1,21 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSocketioStore } from 'stores/socketio'; 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 { storeToRefs } from 'pinia';
const socketioStore = useSocketioStore(); const socketStore = useSocketStore();
const { const simulationStore = useSimulationStore();
sockConnected, const icloudStore = useIcloudStore();
deviceConnected, const deviceStore = useDeviceStore();
tunnelConnected, const tunneldStore = useTunneldStore();
simulationRunning,
currentLocation, const { sockConnected } = storeToRefs(socketStore);
nextLocation, const { deviceConnected } = storeToRefs(deviceStore);
} = storeToRefs(socketioStore); const { tunnelConnected } = storeToRefs(tunneldStore);
function statusDevColor(state: string | boolean): string { const { simulationState, testMode } = storeToRefs(simulationStore);
const { icloudMonitor } = storeToRefs(icloudStore);
function statusDevColor(state: string | boolean | null | undefined): string {
if (state === null || state === undefined) {
return 'grey';
}
if (typeof state === 'boolean') { if (typeof state === 'boolean') {
return state ? 'green' : 'red'; return state ? 'green' : 'red';
} else { } else {
switch (state) { switch (state.toLowerCase()) {
case 'paused': case 'paused':
return 'yellow'; return 'yellow';
case 'running': case 'running':
@@ -30,26 +40,50 @@ function statusDevColor(state: string | boolean): string {
</script> </script>
<template> <template>
<q-toolbar class="bg-primary text-white"> <q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
<div class="flex col q-gutter-md align-center justify-start content-center"> <div class="flex col">
<q-space /> <q-space />
<span>Status:</span> <!--
<span> <div style="width: 80vw" class="flex justify-end">
<q-badge :color="statusDevColor(sockConnected)" rounded class="q-mr-sm" /> -->
<q-btn label="WebSocket" @click="socketioStore.toggleSock()" /> <div class="q-gutter-x-sm">
</span> <q-btn dense rounded push size="sm" icon="mdi-cog" @click="socketStore.toggleSock()">
<span> <q-badge :color="statusDevColor(sockConnected)" rounded floating />
<q-badge :color="statusDevColor(deviceConnected)" rounded class="q-mr-sm" /> </q-btn>
Device Connection <q-btn
</span> dense
<span> rounded
<q-badge :color="statusDevColor(tunnelConnected)" rounded class="q-mr-sm" /> push
tunneld size="sm"
</span> icon="mdi-cellphone"
<span> @click="socketStore.requestUpdate()"
<q-badge :color="statusDevColor(simulationRunning)" rounded class="q-mr-sm" /> >
Location Simulation <q-badge :color="statusDevColor(deviceConnected)" rounded floating />
</span> </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> </div>
</q-toolbar> </q-toolbar>
</template> </template>

View 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>

View File

@@ -1,7 +1,7 @@
/*
export type SimulationCommands = "start" | "pause" | "resume" | "clear" | "end" | "add"; export interface Meta {
totalCount: number;
export type DeviceCommands= "start_tunnel" | "stop_tunnel" | "shutdown"; }
export interface CtrlAttrs { export interface CtrlAttrs {
[key: string]: CtrlAttr; [key: string]: CtrlAttr;
@@ -16,53 +16,158 @@ export interface CtrlAttr {
delay: number; 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 { export interface LocationQueue {
[key: string]: LocationMark [key: string]: LocationMark;
} }
interface 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; loc_id: string;
latitude: number | undefined | null; latitude: number | undefined | null;
longitude: number | undefined | null; longitude: number | undefined | null;
address?: string | undefined | null;
delay?: number | undefined | null; delay?: number | undefined | null;
start_time: string | undefined | null; start: string | undefined | null;
end_time?: string | undefined | null ; status: string | undefined | null;
end?: string | undefined | null;
} }
export interface SimulationControlResponse { // SERVER TO CLIENT
status: string;
command: SimulationCommands; 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; loc_id: string;
message?: string | undefined; data: LocationMark;
latitude?: number | undefined | null;
longitude?: number | undefined | null;
delay?: number | undefined | null;
start_time?: string | undefined | null;
end_time?: string | undefined | null;
} }
interface DeviceControlResponse { export interface SimulationStatus {
status: string; loc_id: string;
command: DeviceCommands; latitude: number;
delay?: number; longitude: number;
} }
interface StatusUpdate { export interface icloudData {
simulation_active: boolean; consumer_queue: string | number | boolean;
set_location_enabled: boolean; consumer_task: string | boolean;
queue: number, 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; latitude: number | undefined | null;
longitude: 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; next_move?: number | undefined | null;
queue_list: LocationQueue[] set_location_enabled: boolean;
queue_state: string | undefined | null; simulation_queue: QueueData
queue_status: boolean;
simulation_task: string | undefined | null;
test_mode: boolean
tunnel: string | undefined | null; tunnel: string | undefined | null;
tunnel_watcher_running: boolean;
device_count?: number | undefined | null; device_count?: number | undefined | null;
udid?: string | null; udid?: string | null;
device_name?: string | null | undefined;
product_version?: string | null | undefined; product_version?: string | null | undefined;
phone_number?: string | null | undefined; phone_number?: string | null | undefined;
developer_mode_enabled?: boolean | undefined | null; developer_mode_enabled?: boolean | undefined | null;
@@ -73,55 +178,9 @@ interface StatusUpdate {
lockdown_untrusted_port?: number | undefined | null; lockdown_untrusted_port?: number | undefined | null;
lockdown_trusted_reachable?: boolean | undefined | null; lockdown_trusted_reachable?: boolean | undefined | null;
lockdown_untrusted_reachable?: boolean | undefined | null; lockdown_untrusted_reachable?: boolean | undefined | null;
dtservicehub_reachable?: boolean | undefined | null dtservicehub_reachable?: boolean | undefined | null;
} }
export interface ServerToClientEvents {
noArg: () => void;
withAck: (a: string, callback: (b: number) => void) => void;
simulationStatus: (c: SimulationStatus) => void;
status: (d: StatusUpdate) => void;
device_status: (d: DeviceStatus) => void;
error: (data: ErrorFull) => void;
message: (e: string) => void;
}
export interface ClientToServerEvents {
message: (e: string, callback: (b: boolean, r: string) => void) => void;
simulation_control: (
args: {
command: SimulationCommands,
latitude?: number | null | undefined,
longitude?: number | null | undefined,
delay?: number | undefined
},
callback: (response: SimulationControlResponse) => void
) => void;
device_control: (
args: {
command: DeviceCommands,
delay?: number
},
callback?: (response: DeviceControlResponse) => void
) => void;
}
interface SimulationStatus {
status: boolean;
data: {
latitude: number;
longitude: number;
start: string;
end?: string;
next_move?: number;
};
}
export interface Meta {
totalCount: number;
}
interface DeviceStatus { interface DeviceStatus {
device_connected: boolean; device_connected: boolean;
device_count: number; device_count: number;
@@ -140,26 +199,149 @@ interface DeviceStatus {
dtservicehub_reachable?: boolean; dtservicehub_reachable?: boolean;
} }
export type Control = DeviceControl | SimulationControl; export interface ErrorFull {
type: string;
export interface DeviceControl { error: string;
id: number;
name: string;
cmd: DeviceCommands
cmdClass: "device_control"
icon: string;
confirm: boolean;
} }
export interface SimulationControl { export interface FindMyUpdate {
id: number; altitude: number;
batteryLevel: number;
deviceDisplayName: string;
deviceStatus: number;
horizontalAccuracy: number;
latitude: number;
longitude: number;
name: string; name: string;
cmd: SimulationCommands; timeStamp: number;
cmdClass: 'simulation_control'; verticalAccuracy: number;
icon: string;
confirm: boolean;
} }
// 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 { export interface coords {
lat: number; lat: number;
lng: number; lng: number;
@@ -183,9 +365,11 @@ export interface SearchControlProps {
export interface CurrentLocation { export interface CurrentLocation {
loc_id: string; loc_id: string;
latitude: number; latitude: number | null | undefined;
longitude: number; longitude: number | null | undefined;
next_move?: number | null // start_time?: string | null | undefined;
// end_time?: string | null | undefined
next_move?: number | null | undefined;
} }
export interface NextLocation { export interface NextLocation {
@@ -195,7 +379,114 @@ export interface NextLocation {
time_at_location?: number | null; time_at_location?: number | null;
} }
export interface ErrorFull { /*
export interface NominatimReverseResponse {
place_id: number;
licence: string;
osm_type: string;
osm_id: number;
lat: string;
lon: string;
class: string;
type: string; type: string;
error: 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;
} }

View 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,
};
}

View 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,
};
}

View File

@@ -1,62 +1,146 @@
import type { CtrlAttrs } from 'components/models'; export const controls = {
simulation: {
export const controls: CtrlAttrs = { start: {
sim_start: {
name: 'Start Location Sim', name: 'Start Location Sim',
cmd: 'start', cmd: 'start',
cmdClass: 'simulation_control', cmdClass: 'sim_cntrl_class',
icon: 'play_arrow', icon: 'mdi-play',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
sim_pause: { pause: {
name: 'Pause Location Sim', name: 'Pause Location Sim',
cmd: 'pause', cmd: 'pause',
cmdClass: 'simulation_control', cmdClass: 'sim_cntrl_class',
icon: 'pause', icon: 'mdi-pause',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
sim_resume: { resume: {
name: 'Resume Location Simulation', name: 'Resume Location Simulation',
cmd: 'resume', cmd: 'resume',
cmdClass: 'simulation_control', cmdClass: 'sim_cntrl_class',
icon: 'play_arrow', icon: 'mdi-play-pause',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
sim_clear: { clear: {
name: 'Clear Location Queue', name: 'Clear Future Items',
cmd: 'clear', cmd: 'clear',
cmdClass: 'simulation_control', cmdClass: 'sim_cntrl_class',
icon: 'directions_off', icon: 'msi-map-marker-remove',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
sim_end: { reset: {
name: 'Reset Location Queue',
cmd: 'reset',
cmdClass: 'sim_cntrl_class',
icon: 'mdi-restart',
cnfrm: true,
delay: 0,
},
end: {
name: 'End Location Sim', name: 'End Location Sim',
cmd: 'end', cmd: 'end',
cmdClass: 'simulation_control', cmdClass: 'sim_cntrl_class',
icon: 'stop', icon: 'mdi-stop',
cnfrm: true, cnfrm: true,
delay: 0, delay: 0,
}, },
dev_shutdown: { },
device: {
shutdown: {
name: 'Shutdown', name: 'Shutdown',
cmd: 'shutdown', cmd: 'shutdown',
cmdClass: 'device_control', cmdClass: 'dev_cntrl_class',
icon: 'power_settings_new', icon: 'mdi-power',
cnfrm: true, cnfrm: true,
delay: 5, delay: 5,
}, },
dev_reboot: { reboot: {
name: 'Reboot', name: 'Reboot',
cmd: 'reboot', cmd: 'reboot',
cmdClass: 'device_control', cmdClass: 'dev_cntrl_class',
icon: 'restart_alt', icon: 'mdi-restart',
cnfrm: true, cnfrm: true,
delay: 5, 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,
},
},
}; };

View File

@@ -1,79 +1,79 @@
export const favorites = [ export const favorites = {
{ home: {
name: 'Home', name: 'Home',
icon: 'home', icon: 'mdi-home',
coords: { coords: {
lat: 40.910773020811, lat: 40.910773020811,
lng: -73.891069806448, lng: -73.891069806448,
}, },
}, },
{ work_places: {
name: "Work Places", name: 'Work Places',
icon: "work", icon: 'mdi-briefcase',
subitems: [ subitems: {
{ jeong: {
name: 'Jeong', name: 'Jeong',
icon: 'language_korean_latin', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.76624975651346, lat: 40.76624975651346,
lng: -73.81444335286128, lng: -73.81444335286128,
}, },
address: '35-02 150th Pl, Flushing, NY 11354', address: '35-02 150th Pl, Flushing, NY 11354',
}, },
{ santos: {
name: 'Santos', name: 'Santos',
icon: 'rice_bowl', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.74504671877868, lat: 40.74504671877868,
lng: -73.8880099638491, lng: -73.8880099638491,
}, },
address: '77-08 Broadway, Elmhurst, NY 11373' address: '77-08 Broadway, Elmhurst, NY 11373',
}, },
{ natalya_qns: {
name: 'Natalyaa (Qns)', name: 'Natalya (Qns)',
icon: 'currency_ruble', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.69644966409178, lat: 40.69644966409178,
lng: -73.837453217826, lng: -73.837453217826,
}, },
address: '110-14 Jamaica Ave, Richmond Hill, NY 11418', address: '110-14 Jamaica Ave, Richmond Hill, NY 11418',
}, },
{ natalya_bx: {
name: 'Natalyaa (Bronx)', name: 'Natalya (Bronx)',
icon: 'currency_ruble', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.85384419116598, lat: 40.85384419116598,
lng: -73.86314767911834, lng: -73.86314767911834,
}, },
address: '2109 Matthews Ave, Bronx, NY 10462', address: '2109 Matthews Ave, Bronx, NY 10462',
}, },
{ office: {
name: 'Linwood Plaza', name: 'Linwood Plaza',
icon: 'dermatology', icon: 'mdi-allergy',
coords: { coords: {
lat: 40.86141832913106, lat: 40.86141832913106,
lng: -73.96997583196286, lng: -73.96997583196286,
}, },
address: '158 Linwood Plaza, Fort Lee, NJ 07024', address: '158 Linwood Plaza, Fort Lee, NJ 07024',
}, },
],
}, },
{ },
strg: {
name: 'Man Mini Storage', name: 'Man Mini Storage',
icon: 'box', icon: 'mdi-dolly',
coords: { coords: {
lat: 40.75158955085288, lat: 40.75158955085288,
lng: -73.9328988710467, lng: -73.9328988710467,
}, },
address: '31-08 Northern Blvd, Long Island City, NY 11101', address: '31-08 Northern Blvd, Long Island City, NY 11101',
}, },
{ acme: {
name: 'Acmd', name: 'Acme',
icon: 'grocery', icon: 'mdi-cart',
coords: { coords: {
lat: 40.90930366920829, lat: 40.90930366920829,
lng: -73.87658695470259, lng: -73.87658695470259,
}, },
address: '31-08 Northern Blvd, Long Island City, NY 11101', address: '31-08 Northern Blvd, Long Island City, NY 11101',
}, },
]; };

View File

@@ -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: #02006c; $primary: #161B36;
$secondary: #010057; $secondary: #8FA9BF;
$accent: #9c27b0; $accent: #5a728a;
$dark: #1d1d1d; $dark: #1d1d1d;
$dark-page: #03002e; $dark-page: #0B1026;
$positive: #21ba45; $positive: #4CAF50;
$negative: #c10015; $negative: #EF5350;
$info: #31ccec; $info: #42A5F5;
$warning: #f2c037; $warning: #FFCA28;

View 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;
};

View 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;
};

View File

@@ -1,48 +1,56 @@
import { Utilities } from "@vue-leaflet/vue-leaflet"; import { Utilities } from '@vue-leaflet/vue-leaflet';
import type * as L from "leaflet"; import type { PropType } from 'vue';
import type { IRouter, IGeocoder, LineOptions } from "leaflet-routing-machine"; import type { IRouter, LineOptions } from 'leaflet-routing-machine';
import type * as L from 'leaflet';
// Props typing
export interface RoutingControlProps { export interface RoutingControlProps {
waypoints: L.LatLng[]; waypoints: unknown[];
router?: IRouter; router?: IRouter | undefined;
plan?: any; // L.Routing.Plan (can refine if you typed it) plan?: unknown;
fitSelectedRoutes?: string | boolean; fitSelectedRoutes?: string | boolean;
lineOptions?: LineOptions; lineOptions?: LineOptions | undefined;
routeLine?: (...args: any[]) => any; routeLine?: ((route: unknown) => unknown) | undefined;
createMarker?:
| ((i: number, waypoint: { latLng: L.LatLng }, n: number) => L.Marker | false)
| undefined;
autoRoute?: boolean; autoRoute?: boolean;
routeWhileDragging?: boolean; routeWhileDragging?: boolean;
routeDragInterval?: number; routeDragInterval?: number;
waypointMode?: string; waypointMode?: string;
useZoomParameter?: boolean; useZoomParameter?: boolean;
showAlternatives?: boolean; showAlternatives?: boolean;
altLineOptions?: LineOptions; altLineOptions?: LineOptions | undefined;
} }
// Vue-style prop definitions (still needed for runtime)
export const routingControlProps = { export const routingControlProps = {
waypoints: { waypoints: {
type: Array as () => L.LatLng[], type: Array as PropType<unknown[]>,
default: () => [], default: () => [],
}, },
router: { router: {
type: Object as () => IRouter, type: Object as PropType<IRouter | undefined>,
default: undefined, default: undefined,
}, },
plan: { plan: {
type: Object as () => any, type: Object as PropType<unknown>,
default: undefined, default: undefined,
}, },
fitSelectedRoutes: { fitSelectedRoutes: {
type: [String, Boolean] as () => string | boolean, type: [String, Boolean] as PropType<string | boolean>,
default: "smart", default: 'smart',
}, },
lineOptions: { lineOptions: {
type: Object as () => LineOptions, type: Object as PropType<LineOptions | undefined>,
default: undefined, default: undefined,
}, },
routeLine: { routeLine: {
type: Function as () => (...args: any[]) => any, 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, default: undefined,
}, },
autoRoute: { autoRoute: {
@@ -59,7 +67,7 @@ export const routingControlProps = {
}, },
waypointMode: { waypointMode: {
type: String, type: String,
default: "connect", default: 'connect',
}, },
useZoomParameter: { useZoomParameter: {
type: Boolean, type: Boolean,
@@ -70,17 +78,13 @@ export const routingControlProps = {
default: false, default: false,
}, },
altLineOptions: { altLineOptions: {
type: Object as () => LineOptions, type: Object as PropType<LineOptions | undefined>,
default: undefined, default: undefined,
}, },
}; };
// Setup function
export const setupRoutingControl = (props: RoutingControlProps) => { export const setupRoutingControl = (props: RoutingControlProps) => {
const options = Utilities.propsToLeafletOptions( const options = Utilities.propsToLeafletOptions(props, routingControlProps);
props,
routingControlProps
);
return { return {
options, options,

View 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
//});

View File

@@ -6,6 +6,7 @@
<q-footer> <q-footer>
<StatusBar /> <StatusBar />
</q-footer> </q-footer>
<!--
<q-drawer <q-drawer
v-model="drawer" v-model="drawer"
:width="200" :width="200"
@@ -34,6 +35,7 @@
</q-list> </q-list>
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
-->
<q-page-container> <q-page-container>
<router-view /> <router-view />
</q-page-container> </q-page-container>
@@ -42,76 +44,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useSocketioStore } from 'stores/socketio'; 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 MenuBar from 'components/MenuBar.vue';
import StatusBar from 'components/StatusBar.vue'; import StatusBar from 'components/StatusBar.vue';
const socketioStore = useSocketioStore(); const simulationStore = useSimulationStore();
const drawer = ref(false); const socketStore = useSocketStore();
const route = useRoute(); const icloudStore = useIcloudStore();
const favoriteStore = useFavoriteStore();
const drawer = ref(false);
const menuList = [
{
icon: 'map',
label: 'Map',
separator: false,
route: 'Leaflet',
},
{
icon: 'info',
label: 'Device Info',
separator: false,
route: 'DeviceInfo',
},
{
icon: 'inbox',
label: 'Inbox',
separator: true,
route: 'ErrorNotFound',
},
{
icon: 'send',
label: 'Outbox',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'delete',
label: 'Trash',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'error',
label: 'Spam',
separator: true,
route: 'ErrorNotFound',
},
{
icon: 'settings',
label: 'Settings',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'feedback',
label: 'Send Feedback',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'help',
iconColor: 'primary',
label: 'Help',
separator: false,
route: 'ErrorNotFound',
},
];
onMounted(() => { onMounted(() => {
socketioStore.bindEvents(); socketStore.bindEvents();
socketioStore.connect(); simulationStore.bindEvents();
icloudStore.bindEvents();
socketStore.connect();
favoriteStore.initialize();
}); });
</script> </script>

View File

@@ -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>

View File

@@ -20,7 +20,7 @@ const routes: RouteRecordRaw[] = [
{ {
path: 'test', path: 'test',
name: 'Test', name: 'Test',
component: () => import('pages/TestPage.vue') component: () => import('pages/TestPage.vue'),
}, },
{ {
path: 'device-info', path: 'device-info',

47
src/stores/device.ts Normal file
View 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
View 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
View 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));
}

View File

@@ -1,19 +1,67 @@
import { defineStore, acceptHMRUpdate } from 'pinia'; import { defineStore, acceptHMRUpdate } from 'pinia';
import { favorites } from 'constants/favorites'
import type {
RoutesSet,
LatLng,
routeSegments,
routeDirections,
routeCoordinates,
} from 'components/models';
interface State { interface State {
zoom: number zoom: number;
center: [number, number] center: [number, number] | [null, null] | null;
markerLatLng: [number, number] 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', { export const useLeafletStore = defineStore('leaflet', {
state: (): State => { state: (): State => {
return { return {
zoom: 10, zoom: 10,
center: [40.71278, -74.00594], center: [favorites.home.coords.lat, favorites.home.coords.lng],
markerLatLng: [40.71278, -74.00594], 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) { if (import.meta.hot) {

443
src/stores/simulation.ts Normal file
View 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
View 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));
}

View File

@@ -1,15 +1,5 @@
import { defineStore, acceptHMRUpdate } from 'pinia'; import { defineStore, acceptHMRUpdate } from 'pinia';
import { socket } from 'boot/socketio';
import type {
CurrentLocation,
NextLocation,
ErrorFull,
SimulationControl,
SimulationCommands,
LocationQueue,
SimulationControlResponse,
StatusUpdate
} from 'components/models';
@@ -17,214 +7,54 @@ import type {
export const useSocketioStore = defineStore('socketio', { export const useSocketioStore = defineStore('socketio', {
state: () => { state: () => {
return { return {
sockConnected: false as boolean,
socketID: null as string | null | undefined,
deviceConnected: false as boolean,
tunnelConnected: false as boolean, // nextLocation: null as NextLocation | null,
simulationRunning: false as boolean | string, // messageList: [''] as string[],
simulationState: null as string | null | undefined,
simulationQueneLength: 0 as number, leafletZoom: 10 as number,
currentLocation: null as CurrentLocation | null,
nextLocation: null as NextLocation | null,
messageList: [''] as string[],
errorList: [] as ErrorFull[],
locationQueue: [] as LocationQueue[],
leafLetZoom: 10 as number,
}; };
}, },
getters: { getters: {
sockState: (state) => state.sockConnected, /* sockState: (state) => state.sockConnected,
deviceState: (state) => state.deviceConnected, deviceState: (state) => state.deviceConnected,
markerLatLng: (state) => { lMarkerLatLng: (state): [number, number] => {
return [state.currentLocation.latitude, state.currentLocation.longitude] if (
state.currentLocation == null ||
!state.currentLocation.latitude ||
!state.currentLocation.longitude
) {
return [0, 0];
}
return [state.currentLocation.latitude, state.currentLocation.longitude];
}, },
center(): [number, number] { lCenter(): [number, number] {
return this.leafletCurrentMarker return this.lMarkerLatLng;
}, },
zoom: (state) => state.leafletZoom, lZoom: (state): number => state.leafletZoom,
*/
}, },
actions: { actions: {
setSockStatus() {
this.sockConnected = socket.connected;
this.socketID = socket.id;
},
bindEvents() {
this.setSockStatus();
socket.on('connect', () => {
this.setSockStatus();
socket.emit('message', 'Hello from client', (e: boolean) => {
console.log('Message delivered: ' + e);
});
console.log('Connected to server');
});
socket.on('disconnect', () => {
this.setSockStatus();
console.log('Disconnected from server');
});
socket.on('message', (e: string) => {
this.setSockStatus();
this.messageList.push(e);
console.log('Websock message received!');
});
socket.on('error', (data: ErrorFull) => {
this.setSockStatus();
const errorFull = { type: data.type, error: data.error };
this.errorList.push(errorFull);
});
socket.on('status', (data: StatusUpdate): void => {
console.log("StatusUpdate received: ", data);
this.simulationRunning = data.simulation_active;
this.simulationState = data.queue_state;
this.simulationQueneLength = data.quene_length;
this.tunnelConnected = !!data.tunnel;
this.currentLocation = { loc_id: data.loc_id, latitude: data.latitude, longitude: data.longitude, next_move: data.next_mode };
});
socket.onAny((eventName, ...args) => {
this.setSockStatus();
console.log(`Received event: ${eventName}`, args);
});
},
connect() {
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();
},
setSimulationRunning(isRunning: boolean, states: string ): void {
this.setSockStatus();
this.simulationState = states;
this.simulationRunning = isRunning;
},
simulationControl(command: SimulationCommands, delay?: number, latitude?: number | null, longitude?: number | null): string | never {
this.setSockStatus();
switch (command) {
case 'start':
console.log("socketStore: got command: start");
if (this.simulationRunning || this.simulationState == "RUNNING" || this.simulationState == "PAUSED") {
throw new Error('Simulation is already running' + this.simulationState);
}
console.log("Emmitting simulation_control: start")
socket.emit('simulation_control', { command: 'start', delay: 0, latitude: null, longitude: null}, (response: SimulationControlResponse) => {
if (response.status == "error") {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
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.status === "error") {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
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.status == "error") {
throw new Error(response.message)
} else {
this.simulationState = response.status;
console.log(response.message)
return response.message
}
});
break;
case 'clear':
if (this.simulationQueueLength== 0) {
throw new Error('Simulation queue is empty');
}
if (this.simulationState == "STOPPED " || !this.simulationRunning) {
throw new Error('Simulation is not running');
}
socket.emit('simulation_control', { command: 'clear' }, (response) => {
if (response.status == 'error') {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
return response.message;
}
});
break;
case 'end':
if (this.simulationState == "ENDED" || !this.simulationRunning) {
throw new Error('Simulation is already ended');
}
socket.emit('simulation_control', { command: 'end' }, (response) => {
if (response.status == 'error') {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
return response.message;
}
});
break;
case 'add':
if (this.simulationState == "ENDED" || !this.simulationRunning) {
throw new Error('Simulation is not running');
}
if (!latitude || !longitude) {
throw new Error ("latitude or longitude not set");
}
socket.emit('simulation_control',{ command: 'add', latitude: latitude, longitude: longitude, delay: delay }, (response) => {
if (response.status == "error") {
throw new Error(response.message)
} else {
this.simulationState = response.status;
console.log("response from simulate_control_add: ", response);
const locMrk = {
[response.loc_id]: {
loc_id: response.loc_id,
latitude: response.latitude,
longitude: response.longitude,
delay: response.delay,
start_time: response.start_time,
},
};
this.locationQueue.push(locMrk);
return response.message;
}
});
break;
default:
throw new Error('Invalid command');
}
},
setDeviceState(state: boolean) {
this.deviceConnected = state;
},
}, },
}); });

48
src/stores/tunneld.ts Normal file
View 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));
}

View File

@@ -1,46 +0,0 @@
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
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;
}
export interface coords {
lat: number;
lng: number;
}
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;
}

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

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

View File

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

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

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