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
This commit is contained in:
2026-04-29 13:17:49 -04:00
parent 52f05550e0
commit ddaf682d16
35 changed files with 4838 additions and 1486 deletions

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
@@ -151,7 +151,8 @@ export default defineConfig((/* ctx */) => {
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

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

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

@@ -1,30 +1,24 @@
<template> <template>
<div>
<div style="white-space: pre-line"> <div style="white-space: pre-line">
{{ formattedAddressLine1 }} {{ formattedAddressLine1 }}
<q-inner-loading v-if="loading">
<q-spinner-dots color="primary" />
</q-inner-loading>
</div> </div>
<div> <div>
{{ formattedAddressLine2 }} {{ formattedAddressLine2 }}
<q-inner-loading v-if="loading"> </div>
<q-spinner-dots color="primary" />
</q-inner-loading>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed } from 'vue';
import type { NominatimResponse } from 'components/models'; import type { NominatimResponse } from 'components/models';
const props = defineProps({ const props = defineProps<{
address: Object as () => NominatimResponse, address?: NominatimResponse | undefined;
}); }>();
const loading = ref(false);
const formattedAddressLine1 = computed(() => { const formattedAddressLine1 = computed(() => {
if (!loading.value && props.address) { if (props.address) {
const place = [props.address.leisure, props.address.shop].filter(Boolean); const place = [props.address.leisure, props.address.shop].filter(Boolean);
const addy = [props.address.house_number, props.address.road].filter(Boolean).join(' '); const addy = [props.address.house_number, props.address.road].filter(Boolean).join(' ');
return [place, addy].filter(Boolean).join('\n'); return [place, addy].filter(Boolean).join('\n');
@@ -34,15 +28,11 @@ const formattedAddressLine1 = computed(() => {
}); });
const formattedAddressLine2 = computed(() => { const formattedAddressLine2 = computed(() => {
if (!loading.value && props.address) { if (props.address) {
const town: string = props.address.city const town = props.address.city ?? props.address.village ?? props.address.town;
? props.address.city
: props.address.village
? props.address.village
: ' ';
const stateAbbr = stateAbbrevMap[props.address.state] ?? props.address.state ?? ' '; const stateAbbr = stateAbbrevMap[props.address.state] ?? props.address.state ?? ' ';
const formAddress: string = town + ', ' + stateAbbr + ' ' + props.address.postcode; const cityState = [town, stateAbbr].filter(Boolean).join(', ');
return formAddress; return [cityState, props.address.postcode].filter(Boolean).join(' ');
} else { } else {
return ''; return '';
} }

View File

@@ -16,7 +16,7 @@
class="q-mr-sm" class="q-mr-sm"
dense dense
flat flat
icon="menu" icon="mdi-menu"
round round
size="sm" size="sm"
@click="qLocDrawer = !qLocDrawer" @click="qLocDrawer = !qLocDrawer"
@@ -35,11 +35,16 @@
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown>
<q-btn-dropdown label="Routing" size="sm" stretch v-if="routeSet.start && routeSet.end"> <q-btn-dropdown label="Routing" size="sm" stretch v-if="routeIsSet">
<q-list> <q-list>
<q-item v-close-popup v-ripple clickable @click="routeToQueue"> <q-item v-close-popup v-ripple clickable @click="routeToQueue">
<q-item-section> <q-item-section>
<q-item-label>Add Route to Sim Queue</q-item-label> <q-item-label>Route Waypoints to SimQ</q-item-label>
</q-item-section>
</q-item>
<q-item v-close-popup v-ripple clickable @click="routeAllPointsToQueue">
<q-item-section>
<q-item-label>Route to SimQ</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-close-popup v-ripple clickable @click="clearRoute"> <q-item v-close-popup v-ripple clickable @click="clearRoute">
@@ -53,35 +58,43 @@
</q-footer> </q-footer>
<q-drawer <q-drawer
v-model="qLocDrawer" v-model="qLocDrawer"
behavior="mobile"
show-if-above
:width="300" :width="300"
side="left" side="left"
:breakpoint="500" :breakpoint="500"
@mouseenter="miniState = false"
@mouseleave="miniState = true"
class="leafletDrawer" class="leafletDrawer"
> >
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '50' }"> <div class="full-height column no-wrap">
<q-list padding> <q-list padding>
<q-item-label header <q-item class="q-gutter-x-md">
set service dns forwarding options cname=qbittorrent.famor.org,docker.famor.org <q-item-section avatar>
><span class="bold">Location Queue: </span> {{ simulationState }}</q-item-label <q-item-label
><span class="text-weight-bolder text-accent text-h6">Queue: </span><span :class="(simulationRunning)? 'text-positive' : 'text-negative'">{{ toTitleCase(simulationState) }}</span></q-item-label
> >
</q-item-section>
<q-item-section>
<q-select
v-model="showItems"
:options="drawerOptions"
label="Items to Show"
dense
style="max-width: 125px"
/>
</q-item-section>
</q-item>
</q-list>
<q-separator /> <q-separator />
<q-scroll-area :horizontal-thumb-style="{ opacity: '50' }" class="col">
<q-list padding>
<VueDraggable <VueDraggable
ref="el" ref="el"
v-model="locationQueueOrderFiltered" v-model="locationQueueOrderFiltered"
handle=".drag-handle" handle=".drag-handle"
filter=".undraggable" :isDisabled="disableDraggable"
> >
<div <LocationItem
v-for="(key, index) in locationQueueOrderFiltered" v-for="(key, index) in locationQueueOrderFiltered"
:class="isDraggable(key)" :class="isDraggable(key)"
:key="key" :key="key"
@contextmenu.prevent="onDrawerContextMenu($event, key)"
>
<LocationItem
:loc_id="key" :loc_id="key"
:active=" :active="
(locationQueueData as Record<string, any>)[key]?.loc_id === (locationQueueData as Record<string, any>)[key]?.loc_id ===
@@ -101,8 +114,9 @@
:end="(locationQueueData as Record<string, any>)[key]?.end ?? undefined" :end="(locationQueueData as Record<string, any>)[key]?.end ?? undefined"
:status="(locationQueueData as Record<string, any>)[key]?.status ?? undefined" :status="(locationQueueData as Record<string, any>)[key]?.status ?? undefined"
@item-clicked="zoomToCoords" @item-clicked="zoomToCoords"
@contextmenu.prevent="onDrawerContextMenu($event, key)"
active-class="text-orange"
/> />
</div>
</VueDraggable> </VueDraggable>
</q-list> </q-list>
</q-scroll-area> </q-scroll-area>
@@ -119,6 +133,7 @@
</q-item> </q-item>
</q-list> </q-list>
</q-menu> </q-menu>
</div>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
@@ -151,8 +166,8 @@
<q-item clickable v-ripple @click="handleAddLocation"> <q-item clickable v-ripple @click="handleAddLocation">
<q-item-section avatar> <q-item-section avatar>
<q-avatar <q-avatar
icon="add_location" icon="mdi-map-marker"
color="primary" color="accent"
text-color="white" text-color="white"
size="sm" size="sm"
/> />
@@ -161,25 +176,36 @@
</q-item> </q-item>
<q-item clickable v-ripple @click="setStartRoute"> <q-item clickable v-ripple @click="setStartRoute">
<q-item-section avatar> <q-item-section avatar>
<q-avatar <q-avatar color="accent" size="sm">
icon="add_location" <q-icon class="small-icon" v-html="iconElement(startIcon).outerHTML" />
color="primary" </q-avatar>
text-color="white"
size="sm"
/>
</q-item-section> </q-item-section>
<q-item-section>Set Route Start</q-item-section> <q-item-section>Set Route Start</q-item-section>
</q-item> </q-item>
<q-item clickable v-ripple @click="setEndRoute"> <q-item clickable v-ripple @click="setEndRoute">
<q-item-section avatar>
<q-avatar color="accent" size="sm">
<q-icon class="small-icon" v-html="iconElement(endIcon).outerHTML" />
</q-avatar>
</q-item-section>
<q-item-section>Set Route End</q-item-section>
</q-item>
<q-item clickable v-ripple @click="handleFavorite('set')" v-if="!isFavorite">
<q-item-section avatar> <q-item-section avatar>
<q-avatar <q-avatar
icon="add_location" icon="mdi-star-outline"
color="primary" color="accent"
text-color="white" text-color="yellow"
size="sm" size="sm"
/> />
</q-item-section> </q-item-section>
<q-item-section>Set Route End</q-item-section> <q-item-section>Set Favorite</q-item-section>
</q-item>
<q-item clickable v-ripple @click="handleFavorite('clear')" v-else>
<q-item-section avatar>
<q-avatar icon="mdi-star" color="accent" text-color="yellow" size="sm" />
</q-item-section>
<q-item-section>Clear Favorite</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</L-Popup> </L-Popup>
@@ -197,7 +223,7 @@
> >
</L-Marker> </L-Marker>
</L-Layer-Group> </L-Layer-Group>
<L-Layer-Group v-if="routeSet.start && routeSet.end"> <L-Layer-Group v-if="routeSet.start || routeSet.end">
<LRoutingMachine <LRoutingMachine
v-bind="routingOptions" v-bind="routingOptions"
@routingstart="debugRoutingEvent" @routingstart="debugRoutingEvent"
@@ -205,12 +231,48 @@
@routingerror="debugRoutingEvent" @routingerror="debugRoutingEvent"
/> />
</L-Layer-Group> </L-Layer-Group>
<L-Layer-Group v-if="findMyUpdate"> <L-Layer-Group v-if="findMyUpdate && showFindMy">
<L-Marker <L-Marker
v-if="findMyUpdate" v-if="findMyUpdate"
:icon="fmIcon as any" :icon="fmIcon as any"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]" :lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
> >
<L-Popup
:options="{ closeOnClick: true, closeButton: false, className: 'marker-popup' }"
>
<q-list dense seperator style="min-width: 100px" class="bg-grey-10">
<q-item clickable v-ripple @click="handleAddLocation">
<q-item-section avatar>
<q-avatar
icon="add_location"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section>
<q-item-section>Add Location</q-item-section>
</q-item>
<q-item clickable v-ripple @click="setStartRoute">
<q-item-section avatar>
<q-avatar size="sm">
<q-icon v-html="iconElement(startIcon).outerHTML" />
</q-avatar>
</q-item-section>
<q-item-section>Set Route Start</q-item-section>
</q-item>
<q-item clickable v-ripple @click="setEndRoute">
<q-item-section avatar>
<q-avatar
icon="add_location"
color="primary"
text-color="white"
size="sm"
/>
</q-item-section>
<q-item-section>Set Route End</q-item-section>
</q-item>
</q-list>
</L-Popup>
<L-Tooltip> <L-Tooltip>
{{ findMyTimePast }} {{ findMyTimePast }}
</L-Tooltip> </L-Tooltip>
@@ -266,10 +328,12 @@ import * as LeafLet from 'leaflet';
import LRoutingMachine from 'components/LRoutingMachine.vue'; import LRoutingMachine from 'components/LRoutingMachine.vue';
import LocationItem from 'components/LocationItem.vue'; import LocationItem from 'components/LocationItem.vue';
import SetLocationDialog from 'components/SetLocationDialog.vue'; import SetLocationDialog from 'components/SetLocationDialog.vue';
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
import { customRouter } from 'functions/serviceURL'; import { customRouter } from 'functions/serviceURL';
import { useRoutingEvents } from '../composables/useRoutingEvents'; import { useRoutingEvents } from '../composables/useRoutingEvents';
import { useMarkerContextMenu } from '../composables/useMarkerContextMenu'; import { useMarkerContextMenu } from '../composables/useMarkerContextMenu';
import type { IRouter } from 'leaflet-routing-machine'; import type { IRouter } from 'leaflet-routing-machine';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
// Types // Types
import type { coords, SearchControlProps } from 'components/models'; import type { coords, SearchControlProps } from 'components/models';
@@ -277,31 +341,30 @@ import type { LeafletMouseEvent, Map } from 'leaflet';
// Stores // Stores
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useSocketioStore } from 'stores/socketio'; import { useSocketStore } from 'stores/socket';
import { useSimulationStore } from 'stores/simulation';
import { useLeafletStore } from 'stores/leaflet'; import { useLeafletStore } from 'stores/leaflet';
import { useIcloudStore } from 'stores/icloud';
import { favorites } from 'constants/favorites'; import { favorites } from 'constants/favorites';
const socketStore = useSocketStore();
const leafletStore = useLeafletStore(); const leafletStore = useLeafletStore();
const { zoom, center, markerLatLng, qLocDrawer, routeSet, routeDirections } = const { zoom, center, markerLatLng, qLocDrawer, routeSet, routeDirections, routeCoordinates } =
storeToRefs(leafletStore); storeToRefs(leafletStore);
const socketStore = useSocketioStore(); const simulationStore = useSimulationStore();
const { const { currentLocation, locationQueueData, locationQueueOrder, simulationState, simulationRunning } =
currentLocation, storeToRefs(simulationStore);
nextLocation,
locationQueueData, const icloudStore = useIcloudStore();
locationQueueOrder, const { findMyUpdate } = storeToRefs(icloudStore);
findMyUpdate,
simulationState,
} = storeToRefs(socketStore);
const $q = useQuasar(); const $q = useQuasar();
const now = ref(Date.now()); const now = ref(Date.now());
const mapRef = ref(); const mapRef = ref();
const responseMessage = ref(''); const responseMessage = ref('');
const miniState = ref(true);
const safeCenter = computed<[number, number]>(() => { const safeCenter = computed<[number, number]>(() => {
const lat = center.value?.[0]; const lat = center.value?.[0];
const lng = center.value?.[1]; const lng = center.value?.[1];
@@ -319,6 +382,12 @@ const safeMarkerLatLng = computed<[number, number] | null>(() => {
return null; return null;
}); });
const drawerOptions = ['All', 'Done', 'Queued', 'Deleted'];
const showItems = ref('All');
const disableDraggable = computed(() => {
return showItems.value === 'All';
});
const onMapReady = (map: Map) => { const onMapReady = (map: Map) => {
const provider = new OpenStreetMapProvider(); const provider = new OpenStreetMapProvider();
const searchOptions: SearchControlProps = { const searchOptions: SearchControlProps = {
@@ -358,6 +427,10 @@ const startIcon = new Icon({
svg: PinTriangle, svg: PinTriangle,
}); });
const iconElement = (icon: Icon) => {
return icon.createIcon();
};
const endIcon = new Icon({ const endIcon = new Icon({
color: '#a23337', color: '#a23337',
accentColor: 'rgba(0,0,0,0.25)', accentColor: 'rgba(0,0,0,0.25)',
@@ -390,20 +463,22 @@ const locationQueueOrderFiltered = computed({
} }
}, },
set: (val) => { set: (val) => {
socketStore.updateLocationQueueOrder(val); simulationStore.updateLocationQueueOrder(val);
}, },
}); });
const isDraggable = (locid: string) => { const isDraggable = (locid: string) => {
const currentIndex = currentLocation.value ? locationQueueOrder.value.indexOf(currentLocation.loc_id) : 0; const currentIndex = currentLocation.value
? locationQueueOrderFiltered.value.indexOf(currentLocation.value.loc_id)
: 0;
const myIndex = locationQueueOrder.value.indexOf(locid); const myIndex = locationQueueOrder.value.indexOf(locid);
const myUpdatedIndex = myIndex - currentIndex; const myUpdatedIndex = myIndex - currentIndex;
if ( myUpdatedIndex > 0 ) { if (myUpdatedIndex > 1) {
return 'draggable'; return 'draggable';
} else { } else {
return 'undraggable'; return 'undraggable';
} }
} };
const getCustomIcon = (locid: string) => { const getCustomIcon = (locid: string) => {
const currentIndex = currentLocation.value const currentIndex = currentLocation.value
@@ -497,6 +572,7 @@ async function routeToQueue() {
await addLocation( await addLocation(
{ lat: Number(direction.coordinates.lat), lng: Number(direction.coordinates.lng) }, { lat: Number(direction.coordinates.lat), lng: Number(direction.coordinates.lng) },
direction.time, direction.time,
true,
); );
} }
await delay(1000); await delay(1000);
@@ -504,6 +580,24 @@ async function routeToQueue() {
} }
} }
} }
async function routeAllPointsToQueue() {
if (routeSet.value.start && routeSet.value.end && routeCoordinates) {
if (routeCoordinates.value && routeCoordinates.value.length > 0) {
for (const coord of routeCoordinates.value) {
if (coord) {
await addLocation(
{ lat: Number(coord.lat), lng: Number(coord.lng) },
coord.timeFromPrev,
false,
);
}
await delay(1000);
}
}
}
}
/* function routeToQueue() { /* function routeToQueue() {
console.log('routeToQueue'); console.log('routeToQueue');
if (routeSet.value.start && routeSet.value.end && routeSegments) { if (routeSet.value.start && routeSet.value.end && routeSegments) {
@@ -548,15 +642,26 @@ const updateRoute = () => {
routingOptions.waypoints = waypoints; routingOptions.waypoints = waypoints;
}; };
const { clickedLatLng, handleMarkerClick, setStartRoute, setEndRoute } = useMarkerContextMenu( const { isFavorite, handleMarkerClick, clickedLatLng, setStartRoute, setEndRoute } =
routeSet, useMarkerContextMenu(routeSet, updateRoute, closeAllPopups);
updateRoute,
closeAllPopups, const routeIsSet = computed(() => {
); return (
routeSet.value.start.lat &&
routeSet.value.start.lng &&
routeSet.value.end.lat &&
routeSet.value.end.lng
);
});
const selectedItem = ref(); const selectedItem = ref();
const contextMenu = ref(); const contextMenu = ref();
function toTitleCase(word: string | null | undefined) {
if (!word) return '';
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
function onDrawerContextMenu(e: MouseEvent, item: string) { function onDrawerContextMenu(e: MouseEvent, item: string) {
console.log('onDrawerContextMenu: ', item); console.log('onDrawerContextMenu: ', item);
selectedItem.value = item; selectedItem.value = item;
@@ -570,7 +675,7 @@ function handleDrawerContextMenu(command: string) {
break; break;
case 'delete': case 'delete':
try { try {
const ack = socketStore.simulationControl({ const ack = simulationStore.simulationControl({
command: 'delete', command: 'delete',
latitude: 0, latitude: 0,
longitude: 0, longitude: 0,
@@ -606,24 +711,66 @@ function handleDrawerContextMenu(command: string) {
$q.notify(`context menu: ${command} ${selectedItem.value}`); $q.notify(`context menu: ${command} ${selectedItem.value}`);
} }
function handleFavorite(operation: string) {
if (clickedLatLng.value) {
const latlng = clickedLatLng.value;
closeAllPopups();
$q.notify(`${operation} favorite...${latlng.toString()}`);
$q.dialog({
component: EditFavoriteDialog,
componentProps: {
lat: Number(latlng.lat),
lng: Number(latlng.lng),
isFavorite: isFavorite.value,
operation: operation,
},
})
.onOk(({ operation, lat, lng, name, category, icon }) => {
// void setFavorite({ lat: Number(lat), lng: Number(lng) }, name, category, icon);
console.log(
'Confirmed favorite ' +
operation +
': ' +
lat +
', ' +
lng +
', name: ' +
name +
', category: ' +
category +
', icon: ' +
icon,
);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
}
function handleAddLocation() { function handleAddLocation() {
if (clickedLatLng.value) { if (clickedLatLng.value) {
const latlng = clickedLatLng.value; const latlng = clickedLatLng.value;
closeAllPopups(); closeAllPopups();
$q.notify(`add location...${latlng.toString()}`); $q.notify(`add location...${latlng.toString()}`);
// reverseGeocode(latlng.lat, latlng.lng)
// .then((data) => {
// const NomAddress = data.address as unknown as NominatimAddress;
$q.dialog({ $q.dialog({
component: SetLocationDialog, component: SetLocationDialog,
componentProps: { componentProps: {
lat: Number(latlng.lat), lat: Number(latlng.lat),
lng: Number(latlng.lng), lng: Number(latlng.lng),
// address: NomAddress,
}, },
}) })
.onOk(({ delay, address }) => { .onOk(({ delay, address }) => {
void addLocation({ lat: Number(latlng.lat), lng: Number(latlng.lng) }, delay, address); const revGeo = !address;
void addLocation(
{ lat: Number(latlng.lat), lng: Number(latlng.lng) },
delay,
revGeo,
address,
);
console.log( console.log(
'Confirmed location add: latitude: ' + 'Confirmed location add: latitude: ' +
latlng.lat + latlng.lat +
@@ -639,10 +786,6 @@ function handleAddLocation() {
.onDismiss(() => { .onDismiss(() => {
console.log('Dialog dismissed'); console.log('Dialog dismissed');
}); });
// })
// .catch((error) => {
// console.error('Error fetching reverse geocode:', error);
// });
} }
} }
/* /*
@@ -659,19 +802,29 @@ async function reverseGeocode(lat: number, lng: number) {
loading.value = false; loading.value = false;
} }
} }
*/
async function addLocation(coords: coords, delay: number, address?: string) {
async function addLocation(
coords: coords,
delay: number | undefined,
getRevGeocode: boolean,
address?: string,
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let notType: string = 'positive'; let notType: string = 'positive';
try { try {
const setCmdRsp = socketStore.simulationControl({ let revGeo;
if (!address && getRevGeocode) {
revGeo = reverseGeocodeRateLimited(coords.lat, coords.lng);
}
const addy = address ?? revGeo?.address;
const setCmdRsp = simulationStore.simulationControl({
command: 'add', command: 'add',
latitude: coords.lat, latitude: coords.lat,
longitude: coords.lng, longitude: coords.lng,
loc_id: '', loc_id: '',
delay: delay, delay: delay,
address: address, address: addy,
}); });
if (setCmdRsp.msg) { if (setCmdRsp.msg) {
responseMessage.value = setCmdRsp.msg; responseMessage.value = setCmdRsp.msg;
@@ -697,8 +850,41 @@ async function addLocation(coords: coords, delay: number, address?: string) {
} }
}); });
} }
*/
async function addLocation(
coords: coords,
delay: number | undefined,
getRevGeocode: boolean,
address?: string,
) {
let notType: string = 'positive';
let revGeo;
if (!address && getRevGeocode) {
revGeo = await reverseGeocodeRateLimited(coords.lat, coords.lng);
}
const addy = address ?? revGeo?.address;
const setCmdRsp = simulationStore.simulationControl({
command: 'add',
latitude: coords.lat,
longitude: coords.lng,
loc_id: '',
delay: delay,
address: addy,
});
if (setCmdRsp.sts === 'error') {
notType = 'negative';
responseMessage.value = 'Failed to set location: ' + setCmdRsp.msg;
console.error('Error setting location:', setCmdRsp.msg);
}
if (setCmdRsp.msg) {
responseMessage.value = setCmdRsp.msg;
}
$q.notify({ type: notType, message: responseMessage.value });
}
function zoomToCoords(arg: string) { function zoomToCoords(arg: string) {
console.log('zoomToCoords: ', arg);
const item = locationQueueData.value[arg]; const item = locationQueueData.value[arg];
if (!item || item.latitude == null || item.longitude == null) { if (!item || item.latitude == null || item.longitude == null) {
return; return;
@@ -708,6 +894,15 @@ function zoomToCoords(arg: string) {
qLocDrawer.value = false; qLocDrawer.value = false;
} }
const showFindMy = computed(() => {
if (findMyUpdate.value) {
const diffInSec = Math.floor(Math.abs(now.value - findMyUpdate.value.timeStamp) / 1000);
return diffInSec < 120;
} else {
return false;
}
});
const findMyTimePast = computed(() => { const findMyTimePast = computed(() => {
if (findMyUpdate.value) { if (findMyUpdate.value) {
const diffInMs = Math.abs(now.value - findMyUpdate.value.timeStamp); const diffInMs = Math.abs(now.value - findMyUpdate.value.timeStamp);
@@ -752,13 +947,6 @@ function zoomTo(loc: string) {
$q.notify({ type: 'negative', message: 'Simulation Location not available' }); $q.notify({ type: 'negative', message: 'Simulation Location not available' });
} }
break; break;
case 'nextLoc':
if (nextLocation.value && nextLocation.value.latitude && nextLocation.value.longitude) {
leafletStore.setCenter(nextLocation.value.latitude, nextLocation.value.longitude);
} else {
$q.notify({ type: 'negative', message: 'Next Location not available' });
}
break;
default: default:
$q.notify({ type: 'negative', message: 'Invalid location' }); $q.notify({ type: 'negative', message: 'Invalid location' });
break; break;
@@ -800,4 +988,15 @@ onUnmounted(() => {
background-color: $dark background-color: $dark
.marker-popup .leaflet-popup-content .marker-popup .leaflet-popup-content
margin: 13px 10px 13px 10px margin: 13px 10px 13px 10px
.small-icon svg
transform: scale(0.6)
transform-origin: center
bottom: 0
right: 0
.small-icon
position: absolute
margin-left: 14px
margin-top: 48px
</style> </style>

View File

@@ -1,20 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useSocketioStore } from 'stores/socketio'; import { useSimulationStore } from 'stores/simulation';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import type { NominatimResponse } from 'components/models'; import type { NominatimResponse } from 'components/models';
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers'; import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
import FormattedAddress from 'components/FormattedAddress.vue'; import FormattedAddress from 'components/FormattedAddress.vue';
//import NestedKnob from 'components/NestedKnob.vue';
const socketStore = useSocketioStore(); const simulationStore = useSimulationStore();
const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } = const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } =
storeToRefs(socketStore); storeToRefs(simulationStore);
const props = defineProps({ const props = defineProps({
address: { address: {
type: Object as () => NominatimResponse, type: Object as () => NominatimResponse,
required: true, required: false,
}, },
latitude: { latitude: {
type: Number, type: Number,
@@ -26,7 +27,7 @@ const props = defineProps({
}, },
icon: { icon: {
type: String, type: String,
default: 'location_on', default: 'mdi-map-marker',
}, },
start: { start: {
type: String, type: String,
@@ -136,19 +137,8 @@ const calculateDeltaTime = computed(() => {
} }
return delta; return delta;
}); });
/*
const secondsToTime = computed(() => {
const seconds = props.delay;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds
.toString()
.padStart(2, '0')}`;
});
const secondsToHhMmSs = (seconds: number) => {
const secondsToTime = (seconds: number) => {
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60; const remainingSeconds = seconds % 60;
@@ -156,7 +146,17 @@ const secondsToTime = (seconds: number) => {
.toString() .toString()
.padStart(2, '0')}`; .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 timeToSeconds = (timeIn: string) => {
const a = timeIn.split(':'); const a = timeIn.split(':');
const seconds = +a[0] * 60 * 60 + +a[1] * 60 + +a[2]; const seconds = +a[0] * 60 * 60 + +a[1] * 60 + +a[2];
@@ -173,78 +173,6 @@ const humanReadableDateTime = (iso: string) => {
second: '2-digit', second: '2-digit',
}); });
}; };
/*
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',
};
function formatAddress(input: string): string {
const parts = input.split(',').map((p) => p.trim());
if (parts.length < 5) return input; // fallback safety
const streetNumber = parts[0];
const streetName = parts[1];
const zip = parts.at(-2) ?? '';
const stateFull = parts.at(-3) ?? '';
const cityRaw = parts.at(-5) ?? '';
const city = cityRaw.replace(/^City of\s+/i, '');
const state = stateAbbrevMap[stateFull] ?? stateFull;
return `${streetNumber} ${streetName}, ${city}, ${state} ${zip}`;
}
*/
const currentIndex = computed(() => { const currentIndex = computed(() => {
return currentLocation.value ? locationQueueOrder.value.indexOf(currentLocation.value.loc_id) : 0; return currentLocation.value ? locationQueueOrder.value.indexOf(currentLocation.value.loc_id) : 0;
@@ -258,12 +186,6 @@ const myUpdatedIndex = computed(() => {
return myIndex.value - currentIndex.value; return myIndex.value - currentIndex.value;
}); });
/*
const markerIndex = computed(() => {
return props.active ? '*' : myUpdatedIndex.value.toString();
});
*/
const itemClass = computed(() => { const itemClass = computed(() => {
if (myUpdatedIndex.value > 0) return 'future'; if (myUpdatedIndex.value > 0) return 'future';
else if (myUpdatedIndex.value < 0) return 'past'; else if (myUpdatedIndex.value < 0) return 'past';
@@ -313,21 +235,80 @@ onMounted(() => {
timerId = requestAnimationFrame(update); timerId = requestAnimationFrame(update);
}); });
const delayValue = computed({ const delayConverted = computed({
get: () => props.delay, get: () => {
set: (val) => socketStore.updateLocationMark(props.loc_id, 'delay', val), 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(() => { const getDelayAttrs = computed(() => {
if (props.delay <= 260) { if (props.delay <= 260) {
return { units: 's', delay: props.delay, step: 10, max: 300 } as const; return {
units: 's',
display: Math.floor((props.delay % 3600) / 60),
step: 10,
max: 80,
} as const;
} else if (props.delay <= 3600) { } else if (props.delay <= 3600) {
return { units: 'm', delay: props.delay / 60, step: 60, max: 4500 } as const; return {
units: 'm',
display: Math.floor((props.delay % 3600) / 60),
step: 1,
max: 80,
} as const;
} else { } else {
return { units: 'h', delay: props.delay / 3600, step: 3600, max: 21600 } as const; 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(() => { const displayDelay = computed(() => {
return props.index > currentIndex.value; return props.index > currentIndex.value;
}); });
@@ -336,58 +317,74 @@ const delayActive = computed(() => {
return props.index === currentIndex.value + 1; return props.index === currentIndex.value + 1;
}); });
const canDrag = computed(() => {
return props.index > currentIndex.value + 1;
});
onUnmounted(() => { onUnmounted(() => {
cancelAnimationFrame(timerId); cancelAnimationFrame(timerId);
}); });
</script> </script>
<template> <template>
<q-item v-if="displayDelay" :active="delayActive"> <div>
<q-item-section avatar> <q-item
<q-icon name="access_time" color="secondary" /> v-if="displayDelay"
</q-item-section> :active="delayActive"
<q-item-section> :class="delayActive ? 'text-orange bg-dark' : ''"
<q-knob active-class="text-orange bg-dark"
show-value
v-model="delayValue"
size="50px"
color="secondary"
track-color="dark-page"
:step="getDelayAttrs.step"
:max="getDelayAttrs.max"
> >
{{ getDelayAttrs.delay }} {{ getDelayAttrs.units }} <q-item-section class="flex flex-center">
</q-knob> <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 <q-slider
v-model="delayValue" v-model="delayConverted"
:min="0" :min="0"
:step="getDelayAttrs.step" :step="getDelayAttrs.step"
:max="getDelayAttrs.max" :max="getDelayAttrs.max"
label label
switch-label-side
:label-value="delayConverted + ' ' + getDelayAttrs.units"
markers markers
snap snap
label-always
:label-value="getDelayAttrs.delay + getDelayAttrs.units"
:marker-labels="markerLabel"
color="primary" color="primary"
style="max-width: 75%"
/> />
-->
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator inset spaced v-if="displayDelay" /> <q-separator inset spaced v-if="displayDelay" />
<q-item v-ripple clickable :active="active" @click="itemClicked" :class="itemClass"> <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-section style="width: 70%">
<q-item-label> <q-item-label v-if="address">
<FormattedAddress :address="props.address" /> <FormattedAddress :address="props.address" />
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip> <q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
</q-item-label> </q-item-label>
<q-item-label v-else> {{ latitude }}, {{ longitude }} </q-item-label>
<q-item-label caption lines="1" v-if="start && simulationRunning"> <q-item-label caption lines="1" v-if="start && simulationRunning">
start: {{ humanReadableDateTime(start) }} start: {{ humanReadableDateTime(start) }}
</q-item-label> </q-item-label>
<q-item-label caption lines="1" v-if="end && simulationRunning"> <q-item-label caption lines="1" v-if="end && simulationRunning">
end: {{ humanReadableDateTime(end) }} end: {{ humanReadableDateTime(end) }}
</q-item-label> </q-item-label>
<q-item-label caption lines="1" v-else> delay: {{ delay }} seconds </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>
<q-item-section <q-item-section
side side
@@ -400,13 +397,14 @@ onUnmounted(() => {
align-items: center; align-items: center;
" "
> >
<q-icon class="drag-handle" v-html="iconElement.outerHTML" /> <q-icon :class="{ 'drag-handle': canDrag }" v-html="iconElement.outerHTML" />
<q-item-label caption lines="1" v-if="simulationRunning"> <q-item-label caption lines="1" v-if="simulationRunning">
{{ calculateDeltaTime }} {{ calculateDeltaTime }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-separator spaced inset v-if="!isLast" /> <q-separator spaced inset v-if="!isLast" />
</div>
</template> </template>
<style lang="sass" scoped> <style lang="sass" scoped>
.past .past

View File

@@ -1,36 +1,56 @@
<script setup lang="ts"> <script setup lang="ts">
import { useLeafletStore } from 'stores/leaflet';
import type { coords } 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 { DeviceCommands } from 'components/models';
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 icloudStore = useIcloudStore();
const tunneldStore = useTunneldStore();
const { center, markerLatLng, zoom } = storeToRefs(leafletStore); const { center, markerLatLng, zoom } = storeToRefs(leafletStore);
const { const {
simulationRunning, simulationRunning,
simulationState, simulationState,
simulationQueueLength, simulationQueueLength,
icloudMonitor, locationQueueOrder,
testMode, testMode,
gpsNoise, gpsNoise,
} = storeToRefs(socketStore); currentLocation,
} = storeToRefs(simulationStore);
const { icloudMonitor } = storeToRefs(icloudStore);
const menuOpen = ref(false); const menuOpen = ref(false);
const favoritesMap = favorites as Record<string, unknown>; 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 = { type ControlAction = {
name: string; name: string;
cmd: string; cmd: string;
@@ -67,7 +87,7 @@ function handleFavClick(coords: coords) {
} }
function handleTestToggle() { function handleTestToggle() {
const response = socketStore.simulationControl({ const response = simulationStore.simulationControl({
command: 'test-mode', command: 'test-mode',
latitude: null, latitude: null,
longitude: null, longitude: null,
@@ -81,12 +101,14 @@ function handleTestToggle() {
} }
function handleGpsNoiseToggle() { function handleGpsNoiseToggle() {
const response = socketStore.simulationControl({command: 'gps-noise', latitude: null, const response = simulationStore.simulationControl({
command: 'gps-noise',
latitude: null,
longitude: null, longitude: null,
loc_id: null, loc_id: null,
delay: 0, delay: 0,
address: null, address: null,
}); });
if (response.sts === 'error') { if (response.sts === 'error') {
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' }); $q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
} }
@@ -106,7 +128,14 @@ function handleControlClick(cmdAttr: ControlAction) {
let notType: string = 'positive'; let notType: string = 'positive';
let notMsg: string = ''; let notMsg: string = '';
try { try {
const ack = socketStore.simulationControl({ command: cmdAttr['cmd'], latitude: null, longitude: null, loc_id: null, delay: cmdAttr.delay, address: null }); const ack = simulationStore.simulationControl({
command: cmdAttr['cmd'],
latitude: null,
longitude: null,
loc_id: null,
delay: cmdAttr.delay,
address: null,
});
if (ack.sts === 'error') { if (ack.sts === 'error') {
notType = 'negative'; notType = 'negative';
} }
@@ -126,15 +155,53 @@ function handleControlClick(cmdAttr: ControlAction) {
$q.notify({ type: notType, message: notMsg }); $q.notify({ type: notType, message: notMsg });
} }
} }
if (cmdAttr.cmdClass === 'dev_cntrl_class') { if (cmdAttr.cmdClass === 'dev_cntrl_class') {
socket.emit( let notType: string = 'positive';
'device_control', let notMsg: string = '';
{ command: cmdAttr.cmd as DeviceCommands, delay: 0 }, try {
(response) => { const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
console.log(response.command_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('Device Command ERROR: ', error.message);
notMsg = `Device Command Error: ${error.message}`;
} else {
console.error('Device Command Error: ', error);
notMsg = 'Device Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
}
}
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
try {
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
if (ack.sts === 'error') {
notType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
}
} catch (error: unknown) {
notType = 'negative';
if (error instanceof Error) {
console.error('Tunneld Command ERROR: ', error.message);
notMsg = `Tunneld Command Error: ${error.message}`;
} else {
console.error('Tunneld Command Error: ', error);
notMsg = 'Tunneld Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
}
} }
}) })
.onCancel(() => { .onCancel(() => {
@@ -148,7 +215,7 @@ function handleControlClick(cmdAttr: ControlAction) {
let notType: string = 'positive'; let notType: string = 'positive';
let notMsg: string = ''; let notMsg: string = '';
try { try {
const ack = socketStore.simulationControl({ const ack = simulationStore.simulationControl({
command: cmdAttr.cmd, command: cmdAttr.cmd,
latitude: null, latitude: null,
longitude: null, longitude: null,
@@ -179,7 +246,7 @@ function handleControlClick(cmdAttr: ControlAction) {
let notType: string = 'positive'; let notType: string = 'positive';
let notMsg: string = ''; let notMsg: string = '';
try { try {
const ack = socketStore.icloudMonitorControl(cmdAttr.cmd); const ack = icloudStore.icloudMonitorControl(cmdAttr.cmd);
if (ack.sts === 'error') { if (ack.sts === 'error') {
notType = 'negative'; notType = 'negative';
} }
@@ -201,13 +268,52 @@ function handleControlClick(cmdAttr: ControlAction) {
} }
if (cmdAttr.cmdClass === 'dev_cntrl_class') { if (cmdAttr.cmdClass === 'dev_cntrl_class') {
socket.emit( let notType: string = 'positive';
'device_control', let notMsg: string = '';
{ command: cmdAttr.cmd as DeviceCommands, delay: 0 }, try {
(response) => { const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
console.log(response.command_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('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 });
}
} }
} }
} }
@@ -227,20 +333,23 @@ function handleControlClick(cmdAttr: ControlAction) {
class="q-mr-sm" 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 <q-btn
:icon-right="menuOpen ? 'arrow_drop_up' : 'arrow_drop_down'" :icon-right="menuOpen ? 'mdi-menu-up' : 'mdi-menu-down'"
stretch stretch
flat flat
label="Favorites" label="Favorites"
v-if="route.name === 'Leaflet'" 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 dense dark> <q-list dark>
<template v-for="(favObj, favId) in favoritesMap" :key="favId"> <template v-for="(favObj, favId) in favoritesMap" :key="favId">
<q-item <q-item
dense
dark dark
v-if="hasCoords(favObj)" v-if="hasCoords(favObj)"
clickable clickable
@@ -255,7 +364,7 @@ function handleControlClick(cmdAttr: ControlAction) {
<q-item-label>{{ favObj.name }}</q-item-label> <q-item-label>{{ favObj.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item v-else-if="hasSubitems(favObj)" clickable v-ripple dense dark> <q-item v-else-if="hasSubitems(favObj)" clickable v-ripple dark>
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="favObj.icon" color="secondary" size="sm" text-color="black" /> <q-avatar :icon="favObj.icon" color="secondary" size="sm" text-color="black" />
</q-item-section> </q-item-section>
@@ -263,12 +372,11 @@ function handleControlClick(cmdAttr: ControlAction) {
<q-item-label>{{ favObj.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="bottom start" self="bottom end"> <q-menu anchor="bottom start" self="bottom end">
<q-list dense dark> <q-list dark>
<q-item <q-item
dense
dark dark
v-for="(favSubObj, favSubId) in favObj.subitems" v-for="(favSubObj, favSubId) in favObj.subitems"
:key="favSubId" :key="favSubId"
@@ -296,11 +404,18 @@ function handleControlClick(cmdAttr: ControlAction) {
</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 dense dark> <q-list dark>
<q-item-label header>Simulation Controls</q-item-label> <q-expansion-item
<q-item dense dark tag="label" v-ripple> group="controls"
<q-item-section avatar> 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 <q-toggle
v-model="testMode" v-model="testMode"
size="sm" size="sm"
@@ -308,18 +423,19 @@ function handleControlClick(cmdAttr: ControlAction) {
@update:model-value="handleTestToggle" @update:model-value="handleTestToggle"
dark dark
dense dense
:disabled="simulationRunning"
/> />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label>Test Mode</q-item-label> <q-item-label>Test Mode</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item dense dark tag="label" v-ripple> <q-item dark tag="label" v-ripple>
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-toggle <q-toggle
v-model="gpsNoise" v-model="gpsNoise"
size="sm" size="sm"
color="brown" color="accent"
@update:model-value="handleGpsNoiseToggle" @update:model-value="handleGpsNoiseToggle"
dark dark
dense dense
@@ -330,7 +446,6 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark dark
v-if="!simulationRunning" v-if="!simulationRunning"
clickable clickable
@@ -338,7 +453,7 @@ function handleControlClick(cmdAttr: ControlAction) {
v-close-popup v-close-popup
@click="handleControlClick(controls.simulation.start)" @click="handleControlClick(controls.simulation.start)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.simulation.start.icon" :icon="controls.simulation.start.icon"
color="secondary" color="secondary"
@@ -351,7 +466,6 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark dark
v-if="simulationState === 'RUNNING' && simulationRunning" v-if="simulationState === 'RUNNING' && simulationRunning"
clickable clickable
@@ -359,7 +473,7 @@ function handleControlClick(cmdAttr: ControlAction) {
v-close-popup v-close-popup
@click="handleControlClick(controls.simulation.pause)" @click="handleControlClick(controls.simulation.pause)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.simulation.pause.icon" :icon="controls.simulation.pause.icon"
color="secondary" color="secondary"
@@ -372,7 +486,6 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark dark
v-if="simulationState === 'PAUSED'" v-if="simulationState === 'PAUSED'"
clickable clickable
@@ -380,7 +493,7 @@ function handleControlClick(cmdAttr: ControlAction) {
v-close-popup v-close-popup
@click="handleControlClick(controls.simulation.resume)" @click="handleControlClick(controls.simulation.resume)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.simulation.resume.icon" :icon="controls.simulation.resume.icon"
color="secondary" color="secondary"
@@ -393,15 +506,14 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark dark
v-if="simulationQueueLength && simulationQueueLength > 0" v-if="simulationQueueLength && simulationQueueLength > 0 && !isLast"
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.simulation.clear)" @click="handleControlClick(controls.simulation.clear)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.simulation.clear.icon" :icon="controls.simulation.clear.icon"
color="secondary" color="secondary"
@@ -414,7 +526,26 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense 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 dark
v-if="simulationRunning" v-if="simulationRunning"
clickable clickable
@@ -422,7 +553,7 @@ function handleControlClick(cmdAttr: ControlAction) {
v-close-popup v-close-popup
@click="handleControlClick(controls.simulation.end)" @click="handleControlClick(controls.simulation.end)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.simulation.end.icon" :icon="controls.simulation.end.icon"
color="secondary" color="secondary"
@@ -434,10 +565,15 @@ function handleControlClick(cmdAttr: ControlAction) {
<q-item-label> {{ controls.simulation.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>iCloud Monitor Controls</q-item-label> <q-expansion-item
group="controls"
icon="mdi-cloud"
dense-toggle
label="iCloud Monitor Controls"
color="accent"
>
<q-item <q-item
dense
dark dark
v-if="!icloudMonitor" v-if="!icloudMonitor"
clickable clickable
@@ -445,7 +581,7 @@ function handleControlClick(cmdAttr: ControlAction) {
v-close-popup v-close-popup
@click="handleControlClick(controls.icloudmonitor.start)" @click="handleControlClick(controls.icloudmonitor.start)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.icloudmonitor.start.icon" :icon="controls.icloudmonitor.start.icon"
color="secondary" color="secondary"
@@ -458,7 +594,6 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark dark
v-if="icloudMonitor" v-if="icloudMonitor"
clickable clickable
@@ -467,7 +602,7 @@ function handleControlClick(cmdAttr: ControlAction) {
@click="handleControlClick(controls.icloudmonitor.stop)" @click="handleControlClick(controls.icloudmonitor.stop)"
size="sm" size="sm"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.icloudmonitor.stop.icon" :icon="controls.icloudmonitor.stop.icon"
color="secondary" color="secondary"
@@ -479,17 +614,163 @@ function handleControlClick(cmdAttr: ControlAction) {
<q-item-label> {{ controls.icloudmonitor.stop.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-separator spaced /> </q-expansion-item>
<q-item-label header>Device Controls</q-item-label> <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 <q-item
dense
dark dark
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.device.reboot)" @click="handleControlClick(controls.device.reboot)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.device.reboot.icon" :icon="controls.device.reboot.icon"
color="secondary" color="secondary"
@@ -502,14 +783,13 @@ function handleControlClick(cmdAttr: ControlAction) {
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item
dense
dark dark
clickable clickable
v-ripple v-ripple
v-close-popup v-close-popup
@click="handleControlClick(controls.device.shutdown)" @click="handleControlClick(controls.device.shutdown)"
> >
<q-item-section avatar> <q-item-section avatar class="q-pl-lg">
<q-avatar <q-avatar
:icon="controls.device.shutdown.icon" :icon="controls.device.shutdown.icon"
color="secondary" color="secondary"
@@ -521,9 +801,21 @@ function handleControlClick(cmdAttr: ControlAction) {
<q-item-label> {{ controls.device.shutdown.name }} </q-item-label> <q-item-label> {{ controls.device.shutdown.name }} </q-item-label>
</q-item-section> </q-item-section>
</q-item> </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

@@ -2,15 +2,27 @@
<q-dialog ref="dlgRef" persistent> <q-dialog ref="dlgRef" persistent>
<q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm"> <q-card class="bg-dark text-grey-1 add-loc-card q-pa-sm">
<q-toolbar> <q-toolbar>
<q-avatar icon="add_location" color="primary" text-color="white" /> <q-avatar icon="mdi-map-marker" color="primary" text-color="white" />
<q-toolbar-title>Add Location to Queue</q-toolbar-title> <q-toolbar-title>Add Location to Queue</q-toolbar-title>
</q-toolbar> </q-toolbar>
<q-card-section class="q-ml-lg"> <q-card-section class="q-ml-lg">
<div class="q-mb-sm">Are you sure you want to set location to:</div> <div>Are you sure you want to set location to:</div>
<div class="q-ml-lg"> <div class="q-ml-md">
<formatted-address :address="address" /> <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 <q-input
class="q-mt-sm" class="q-mt-md"
style="max-width: 150px" style="max-width: 150px"
v-model.number="delay" v-model.number="delay"
filled filled
@@ -19,13 +31,30 @@
type="number" type="number"
suffix="seconds" suffix="seconds"
color="grey-4" color="grey-4"
autofocus
/> />
</div> </div>
</q-card-section> </q-card-section>
<q-separator /> <q-separator />
<q-card-actions align="right"> <q-card-actions align="between">
<div class="text-yellow">
<q-checkbox
class="cursor-pointer"
v-model="isFavorite"
checked-icon="mdi-star"
unchecked-icon="mdi-star-outline"
indeterminate-icon="mdi-help"
color="yellow"
@click="handleFavoriteClick"
:label="favoriteName"
>
<q-tooltip>Favorite</q-tooltip>
</q-checkbox>
</div>
<div>
<q-btn flat label="OK" @click="onOkClick" /> <q-btn flat label="OK" @click="onOkClick" />
<q-btn flat label="Cancel" @click="onDialogCancel" /> <q-btn flat label="Cancel" @click="onDialogCancel" />
</div>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@@ -34,39 +63,83 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket'; import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
import { ref, onMounted } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useQuasar } from 'quasar';
import FormattedAddress from 'components/FormattedAddress.vue'; 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 $q = useQuasar();
const loading = ref(true); const loading = ref(true);
const delay = ref(0); 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(); 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 () => { onMounted(async () => {
try { try {
loading.value = true; loading.value = true;
const response = await reverseGeocodeRateLimited(props.lat, props.lng); const response = await reverseGeocodeRateLimited(props.lat, props.lng);
console.log('reverse geocode response: ', response); console.log('reverse geocode response: ', response);
address.value = response; address.value = response.address;
if (response.favorite) {
favorite.value = response.favorite;
}
} catch (error) { } catch (error) {
console.error('Error fetching reverse geocode:', error); console.error('Error fetching reverse geocode:', error);
throw error; throw error;
} finally { } finally {
await sleep(500);
loading.value = false; 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({delay: delay.value, address: address.value}); onDialogOK({ delay: delay.value, address: address.value });
} }
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@@ -1,33 +1,21 @@
<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'; import type { ClientToServerEvents } from 'components/models';
const socketioStore = useSocketioStore(); const socketStore = useSocketStore();
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;
@@ -37,33 +25,14 @@ function handleEmit() {
sockEvent.value = ''; sockEvent.value = '';
eventArgs.value = ''; eventArgs.value = '';
} }
const timeWithSeconds = ref('00:00:45');
watch(
messageList,
(newVal: string[], oldVal: string[]) => {
const 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>
@@ -73,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,10 +1,23 @@
<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 { sockConnected, deviceConnected, tunnelConnected, simulationState, icloudMonitor, testMode } = const simulationStore = useSimulationStore();
storeToRefs(socketioStore); const icloudStore = useIcloudStore();
const deviceStore = useDeviceStore();
const tunneldStore = useTunneldStore();
const { sockConnected } = storeToRefs(socketStore);
const { deviceConnected } = storeToRefs(deviceStore);
const { tunnelConnected } = storeToRefs(tunneldStore);
const { simulationState, testMode } = storeToRefs(simulationStore);
const { icloudMonitor } = storeToRefs(icloudStore);
function statusDevColor(state: string | boolean | null | undefined): string { function statusDevColor(state: string | boolean | null | undefined): string {
if (state === null || state === undefined) { if (state === null || state === undefined) {
return 'grey'; return 'grey';
@@ -28,58 +41,47 @@ function statusDevColor(state: string | boolean | null | undefined): string {
<template> <template>
<q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'"> <q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
<div class="flex col q-gutter-md align-center justify-start content-center"> <div class="flex col">
<q-space /> <q-space />
<!--
<div style="width: 80vw" class="flex justify-end"> <div style="width: 80vw" class="flex justify-end">
<q-btn -->
rounded <div class="q-gutter-x-sm">
push <q-btn dense rounded push size="sm" icon="mdi-cog" @click="socketStore.toggleSock()">
size="sm" <q-badge :color="statusDevColor(sockConnected)" rounded floating />
icon="settings"
class="q-mr-sm"
@click="socketioStore.toggleSock()"
>
<q-badge :color="statusDevColor(sockConnected)" rounded floating class="q-mr-sm" />
</q-btn> </q-btn>
<q-btn <q-btn
dense
rounded rounded
push push
size="sm" size="sm"
icon="phone_iphone" icon="mdi-cellphone"
class="q-mr-sm" @click="socketStore.requestUpdate()"
@click="socketioStore.requestUpdate()"
> >
<q-badge :color="statusDevColor(deviceConnected)" rounded floating class="q-mr-sm" /> <q-badge :color="statusDevColor(deviceConnected)" rounded floating />
</q-btn>
<q-btn dense rounded push size="sm" icon="mdi-subway" @click="socketStore.requestUpdate()">
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating />
</q-btn> </q-btn>
<q-btn <q-btn
dense
rounded rounded
push push
size="sm" size="sm"
icon="subway" icon="mdi-map-marker"
class="q-mr-sm" @click="socketStore.requestUpdate()"
@click="socketioStore.requestUpdate()"
> >
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating class="q-mr-sm" /> <q-badge :color="statusDevColor(simulationState)" rounded floating />
</q-btn> </q-btn>
<q-btn <q-btn
dense
rounded rounded
push push
size="sm" size="sm"
icon="location_on" icon="mdi-cloud"
class="q-mr-sm" @click="icloudStore.icloudMonitorControl('refresh')"
@click="socketioStore.requestUpdate()"
> >
<q-badge :color="statusDevColor(simulationState)" rounded floating class="q-mr-sm" /> <q-badge :color="statusDevColor(icloudMonitor)" rounded floating />
</q-btn>
<q-btn
rounded
push
size="sm"
icon="cloud"
class="q-mr-sm"
@click="socketioStore.icloudMonitorControl('refresh')"
>
<q-badge :color="statusDevColor(icloudMonitor)" rounded floating class="q-mr-sm" />
</q-btn> </q-btn>
</div> </div>
</div> </div>

View File

@@ -26,11 +26,14 @@ export interface DevCtrlAttr {
} }
*/ */
export type FavoriteCommands = 'set' | 'delete' | 'get';
export type SimulationCommands = export type SimulationCommands =
'restart' 'restart'
| 'start' | 'start'
| 'pause' | 'pause'
| 'resume' | 'resume'
| 'reset'
| 'clear' | 'clear'
| 'end' | 'end'
| 'add' | 'add'
@@ -41,7 +44,7 @@ export type SimulationCommands =
export type DeviceCommands = 'shutdown' | 'reboot'; export type DeviceCommands = 'shutdown' | 'reboot';
export type TunnelCommands = export type TunneldCommands =
| 'start' | 'start'
| 'start-watcher' | 'start-watcher'
| 'end-watcher' | 'end-watcher'
@@ -128,6 +131,14 @@ export interface SimulationStatus {
longitude: number; longitude: number;
} }
export interface icloudData {
consumer_queue: string | number | boolean;
consumer_task: string | boolean;
monitor_enabled: boolean;
monitor_task: string | boolean;
monitor_running: boolean;
}
export interface QueueData { export interface QueueData {
active: boolean; active: boolean;
data: LocationQueue; data: LocationQueue;
@@ -149,13 +160,7 @@ export interface StatusUpdate {
}; };
device_name: string | undefined | null; device_name: string | undefined | null;
fmf_location: FindMyUpdate | undefined | null; fmf_location: FindMyUpdate | undefined | null;
icloud: { icloud: icloudData;
consumer_queue: number | undefined | null;
consumer_task: string | undefined | null;
monitor_enabled: boolean;
monitor_task: string | undefined | null;
monitor_running: boolean;
};
next_move?: number | undefined | null; next_move?: number | undefined | null;
set_location_enabled: boolean; set_location_enabled: boolean;
simulation_queue: QueueData simulation_queue: QueueData
@@ -224,6 +229,20 @@ export interface ClientToServerEvents {
// status // status
request_update: (callback: (response: StatusUpdate) => void) => void; request_update: (callback: (response: StatusUpdate) => void) => void;
// control // 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: ( simulation_control: (
args: { args: {
command: SimulationCommands; command: SimulationCommands;
@@ -231,7 +250,7 @@ export interface ClientToServerEvents {
longitude?: number | null | undefined; longitude?: number | null | undefined;
delay?: number | null | undefined; delay?: number | null | undefined;
loc_id?: string | null | undefined; loc_id?: string | null | undefined;
address?: string | null | undefined; address?: NominatimResponse | string | null | undefined;
}, },
callback: (response: SimulationControlResponse) => void, callback: (response: SimulationControlResponse) => void,
) => void; ) => void;
@@ -242,13 +261,6 @@ export interface ClientToServerEvents {
}, },
callback?: (response: DeviceControlResponse) => void, callback?: (response: DeviceControlResponse) => void,
) => void; ) => void;
tunnel_control: (
args: {
command: TunnelCommands;
delay?: number;
},
callback?: (response: DeviceControlResponse) => void,
) => void;
icloud_monitor_control: ( icloud_monitor_control: (
args: { args: {
command: string; command: string;
@@ -276,17 +288,26 @@ export interface ClientToServerEvents {
latitude: number; latitude: number;
longitude: number; longitude: number;
}, },
callback?: (response: NominatimResponse) => void, callback?: (response: GeoCacheResponse) => void,
) => void; ) => void;
} }
export interface FavoriteControlResponse {
command_status: string;
command: FavoriteCommands;
command_class: string;
data?: {
favorites: Favorite[] | undefined | null;
};
message?: string | undefined;
}
export interface SimulationControlResponse { export interface SimulationControlResponse {
command_status: string; command_status: string;
command: SimulationCommands; command: SimulationCommands;
command_class: string; command_class: string;
data?: SimulationControlResponseData | undefined | null; data?: SimulationControlResponseData | undefined | null;
message?: string | undefined; message?: string | undefined;
} }
interface SimulationControlResponseData { interface SimulationControlResponseData {
@@ -301,6 +322,14 @@ interface DeviceControlResponse {
message?: string; message?: string;
} }
interface TunneldControlResponse {
command_status: string;
command_class: string;
command: TunneldCommands;
message?: string;
}
export interface iCloudMonitorResponse { export interface iCloudMonitorResponse {
command_status: string; command_status: string;
command: string; command: string;
@@ -401,20 +430,42 @@ export interface LatLng {
} }
export interface routeDirections { export interface routeDirections {
dirIndex: number; dirIndex?: number | undefined;
coordinateIndex: number; coordinateIndex: number;
text: string; text?: string | undefined;
distance: number; distance?: number | undefined;
time: number; time?: number | undefined;
coordinates: LatLng | null | undefined; coordinates: LatLng | null | undefined;
} }
export interface routeCoordinates {
coordinateIndex: number;
lat: number;
lng: number;
distanceFromPrev: number;
timeFromPrev: number;
instruction: string;
}
export interface RouteSet { export interface RouteSet {
start: [number, number] | [null, null] | [undefined, undefined] | null | undefined; start: [number, number] | [null, null] | [undefined, undefined] | null | undefined;
end: [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; 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 // TypeScript Interface for Reverse Geocoding Response
export interface NominatimResponse { export interface NominatimResponse {
shop?: string | null | undefined; shop?: string | null | undefined;
@@ -424,8 +475,10 @@ export interface NominatimResponse {
neighbourhood?: string | null | undefined; neighbourhood?: string | null | undefined;
suburb?: string | null | undefined; suburb?: string | null | undefined;
county: string; county: string;
town? : string | null | undefined;
city?: string | null | undefined; city?: string | null | undefined;
village?: string | null | undefined; village?: string | null | undefined;
municipality?: string | null | undefined;
state: string; state: string;
'ISO3166-2-lvl4': string; 'ISO3166-2-lvl4': string;
postcode: string; postcode: string;

View File

@@ -1,6 +1,7 @@
import { ref, type Ref } from 'vue'; import { ref, type Ref } from 'vue';
import * as LeafLet from 'leaflet'; import * as LeafLet from 'leaflet';
import type { LeafletMouseEvent } from 'leaflet'; import type { LeafletMouseEvent } from 'leaflet';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
type RouteSet = { type RouteSet = {
start?: LeafLet.LatLng | null | undefined; start?: LeafLet.LatLng | null | undefined;
@@ -19,11 +20,14 @@ export function useMarkerContextMenu(
closeAllPopups: () => void, closeAllPopups: () => void,
) { ) {
const clickedLatLng = ref<LeafLet.LatLng | null>(null); const clickedLatLng = ref<LeafLet.LatLng | null>(null);
const isFavorite = ref(false);
const handleMarkerClick = (event: LeafletMouseEvent) => { const handleMarkerClick = async (event: LeafletMouseEvent) => {
console.log('marker clicked', event); console.log('marker clicked', event);
closeAllPopups(); closeAllPopups();
clickedLatLng.value = event.latlng; 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); LeafLet.DomEvent.stopPropagation(event.originalEvent);
}; };
@@ -44,6 +48,7 @@ export function useMarkerContextMenu(
}; };
return { return {
isFavorite,
clickedLatLng, clickedLatLng,
handleMarkerClick, handleMarkerClick,
setStartRoute, setStartRoute,

View File

@@ -1,8 +1,9 @@
import { useLeafletStore } from 'stores/leaflet'; import { useLeafletStore } from 'stores/leaflet';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import type { routeCoordinates as StoreRouteCoordinates } from 'components/models';
const leafletStore = useLeafletStore(); const leafletStore = useLeafletStore();
const { routeSegments, routeDirections } = storeToRefs(leafletStore); const { routeSegments, routeDirections, routeCoordinates } = storeToRefs(leafletStore);
type RouteSummary = { type RouteSummary = {
totalDistance: number; totalDistance: number;
@@ -37,6 +38,23 @@ type RouteResult = {
coordinates?: RouteCoordinates[]; 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() { export function useRoutingEvents() {
const handleRoutesFound = (event: { routes?: RouteResult[] }) => { const handleRoutesFound = (event: { routes?: RouteResult[] }) => {
const route = event.routes?.[0]; const route = event.routes?.[0];
@@ -81,7 +99,73 @@ export function useRoutingEvents() {
routeDirections.value = directionsSummary; 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 { return {
handleRoutesFound, handleRoutesFound,

View File

@@ -4,7 +4,7 @@ export const controls = {
name: 'Start Location Sim', name: 'Start Location Sim',
cmd: 'start', cmd: 'start',
cmdClass: 'sim_cntrl_class', cmdClass: 'sim_cntrl_class',
icon: 'play_arrow', icon: 'mdi-play',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
@@ -12,7 +12,7 @@ export const controls = {
name: 'Pause Location Sim', name: 'Pause Location Sim',
cmd: 'pause', cmd: 'pause',
cmdClass: 'sim_cntrl_class', cmdClass: 'sim_cntrl_class',
icon: 'pause', icon: 'mdi-pause',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
@@ -20,23 +20,31 @@ export const controls = {
name: 'Resume Location Simulation', name: 'Resume Location Simulation',
cmd: 'resume', cmd: 'resume',
cmdClass: 'sim_cntrl_class', cmdClass: 'sim_cntrl_class',
icon: 'play_arrow', icon: 'mdi-play-pause',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
clear: { clear: {
name: 'Clear Location Queue', name: 'Clear Future Items',
cmd: 'clear', cmd: 'clear',
cmdClass: 'sim_cntrl_class', cmdClass: 'sim_cntrl_class',
icon: 'directions_off', icon: 'msi-map-marker-remove',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
reset: {
name: 'Reset Location Queue',
cmd: 'reset',
cmdClass: 'sim_cntrl_class',
icon: 'mdi-restart',
cnfrm: true,
delay: 0,
},
end: { end: {
name: 'End Location Sim', name: 'End Location Sim',
cmd: 'end', cmd: 'end',
cmdClass: 'sim_cntrl_class', cmdClass: 'sim_cntrl_class',
icon: 'stop', icon: 'mdi-stop',
cnfrm: true, cnfrm: true,
delay: 0, delay: 0,
}, },
@@ -46,7 +54,7 @@ export const controls = {
name: 'Shutdown', name: 'Shutdown',
cmd: 'shutdown', cmd: 'shutdown',
cmdClass: 'dev_cntrl_class', cmdClass: 'dev_cntrl_class',
icon: 'power_settings_new', icon: 'mdi-power',
cnfrm: true, cnfrm: true,
delay: 5, delay: 5,
}, },
@@ -54,7 +62,7 @@ export const controls = {
name: 'Reboot', name: 'Reboot',
cmd: 'reboot', cmd: 'reboot',
cmdClass: 'dev_cntrl_class', cmdClass: 'dev_cntrl_class',
icon: 'restart_alt', icon: 'mdi-restart',
cnfrm: true, cnfrm: true,
delay: 5, delay: 5,
}, },
@@ -64,7 +72,7 @@ export const controls = {
name: 'Start iCloud Monitor', name: 'Start iCloud Monitor',
cmd: 'start', cmd: 'start',
cmdClass: 'icloud-monitor_cntrl_class', cmdClass: 'icloud-monitor_cntrl_class',
icon: 'play_arrow', icon: 'mdi-play',
cnfrm: false, cnfrm: false,
delay: 0, delay: 0,
}, },
@@ -72,7 +80,65 @@ export const controls = {
name: 'Stop iCloud Monitor', name: 'Stop iCloud Monitor',
cmd: 'stop', cmd: 'stop',
cmdClass: 'icloud-monitor_cntrl_class', cmdClass: 'icloud-monitor_cntrl_class',
icon: 'stop', 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, cnfrm: false,
delay: 0, delay: 0,
}, },

View File

@@ -1,7 +1,7 @@
export const favorites = { export const favorites = {
home: { home: {
name: 'Home', name: 'Home',
icon: 'home', icon: 'mdi-home',
coords: { coords: {
lat: 40.910773020811, lat: 40.910773020811,
lng: -73.891069806448, lng: -73.891069806448,
@@ -9,11 +9,11 @@ export const favorites = {
}, },
work_places: { work_places: {
name: 'Work Places', name: 'Work Places',
icon: 'work', icon: 'mdi-briefcase',
subitems: { subitems: {
jeong: { jeong: {
name: 'Jeong', name: 'Jeong',
icon: 'dermatology', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.76624975651346, lat: 40.76624975651346,
lng: -73.81444335286128, lng: -73.81444335286128,
@@ -22,7 +22,7 @@ export const favorites = {
}, },
santos: { santos: {
name: 'Santos', name: 'Santos',
icon: 'syringe', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.74504671877868, lat: 40.74504671877868,
lng: -73.8880099638491, lng: -73.8880099638491,
@@ -31,7 +31,7 @@ export const favorites = {
}, },
natalya_qns: { natalya_qns: {
name: 'Natalya (Qns)', name: 'Natalya (Qns)',
icon: 'healing', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.69644966409178, lat: 40.69644966409178,
lng: -73.837453217826, lng: -73.837453217826,
@@ -40,7 +40,7 @@ export const favorites = {
}, },
natalya_bx: { natalya_bx: {
name: 'Natalya (Bronx)', name: 'Natalya (Bronx)',
icon: 'healing', icon: 'mdi-doctor',
coords: { coords: {
lat: 40.85384419116598, lat: 40.85384419116598,
lng: -73.86314767911834, lng: -73.86314767911834,
@@ -49,7 +49,7 @@ export const favorites = {
}, },
office: { office: {
name: 'Linwood Plaza', name: 'Linwood Plaza',
icon: 'allergy', icon: 'mdi-allergy',
coords: { coords: {
lat: 40.86141832913106, lat: 40.86141832913106,
lng: -73.96997583196286, lng: -73.96997583196286,
@@ -60,7 +60,7 @@ export const favorites = {
}, },
strg: { 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,
@@ -69,7 +69,7 @@ export const favorites = {
}, },
acme: { acme: {
name: 'Acme', name: 'Acme',
icon: 'grocery', icon: 'mdi-cart',
coords: { coords: {
lat: 40.90930366920829, lat: 40.90930366920829,
lng: -73.87658695470259, lng: -73.87658695470259,

View File

@@ -1,15 +1,24 @@
// services/nominatimService.ts // services/nominatimService.ts
import { useSocketioStore } from 'stores/socketio'; import { socket } from 'boot/socketio';
import type { NominatimResponse, NominatimRequest } from 'components/models'; import type { NominatimRequest, GeoCacheResponse } from 'components/models';
const socketStore = useSocketioStore();
let lastRequestTime = 0; let lastRequestTime = 0;
export const reverseGeocodeRateLimited = async ( function revGeoCode(nomRequest: NominatimRequest): Promise<GeoCacheResponse> {
lat: number, return new Promise((resolve) => {
lon: number, socket.emit(
): Promise<NominatimResponse> => { '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 now = Date.now();
const timeSinceLast = now - lastRequestTime; const timeSinceLast = now - lastRequestTime;
@@ -17,7 +26,10 @@ export const reverseGeocodeRateLimited = async (
if (timeSinceLast < 1000) { if (timeSinceLast < 1000) {
await new Promise((resolve) => setTimeout(resolve, 1000 - timeSinceLast)); await new Promise((resolve) => setTimeout(resolve, 1000 - timeSinceLast));
} }
const response: NominatimResponse = await socketStore.revGeoCode({latitude: lat, longitude: lon} as NominatimRequest); const response: GeoCacheResponse = await revGeoCode({
latitude: lat,
longitude: lon,
} as NominatimRequest);
lastRequestTime = Date.now(); lastRequestTime = Date.now();
return response; return response;

View File

@@ -45,77 +45,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
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 socketStore = useSocketioStore(); const simulationStore = useSimulationStore();
const socketStore = useSocketStore();
const icloudStore = useIcloudStore();
const favoriteStore = useFavoriteStore();
const drawer = ref(false); const drawer = ref(false);
/*
const route = useRoute();
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(() => {
socketStore.bindEvents(); socketStore.bindEvents();
simulationStore.bindEvents();
icloudStore.bindEvents();
socketStore.connect(); 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>

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,6 +1,12 @@
import { defineStore, acceptHMRUpdate } from 'pinia'; import { defineStore, acceptHMRUpdate } from 'pinia';
import { favorites } from 'constants/favorites' import { favorites } from 'constants/favorites'
import type { RoutesSet, LatLng, routeSegments, routeDirections } from 'components/models'; import type {
RoutesSet,
LatLng,
routeSegments,
routeDirections,
routeCoordinates,
} from 'components/models';
interface State { interface State {
zoom: number; zoom: number;
@@ -13,8 +19,9 @@ interface State {
wayPoints?: LatLng[] | null | undefined; wayPoints?: LatLng[] | null | undefined;
}; };
routesSet: RoutesSet[] | null; routesSet: RoutesSet[] | null;
routeSegments?: routeSegments[] | null; routeSegments: routeSegments[] | null;
routeDirections?: routeDirections[] | null; routeDirections: routeDirections[] | null;
routeCoordinates: routeCoordinates[] | null;
} }
export const useLeafletStore = defineStore('leaflet', { export const useLeafletStore = defineStore('leaflet', {
@@ -32,6 +39,7 @@ export const useLeafletStore = defineStore('leaflet', {
routesSet: [], routesSet: [],
routeSegments: [], routeSegments: [],
routeDirections: [], routeDirections: [],
routeCoordinates: [],
}; };
}, },
actions: { actions: {

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,54 +1,24 @@
import { defineStore, acceptHMRUpdate } from 'pinia'; import { defineStore, acceptHMRUpdate } from 'pinia';
import { socket } from 'boot/socketio';
import { useQuasar } from 'quasar';
import iCloudCodeDialog from 'components/iCloudCodeDialog.vue';
import type {
CurrentLocation,
NextLocation,
ErrorFull,
LocationQueue,
SimulationControlResponse,
StatusUpdate,
FindMyUpdate,
iCloudMonitorResponse,
SimulationStatus,
LocationMarkUpdateResponse,
LocationItemUpdate,
NominatimResponse,
NominatimRequest,
AppError,
QueueData,
} from 'components/models';
const $q = useQuasar();
const debugLog: boolean = true;
export const useSocketioStore = defineStore('socketio', { export const useSocketioStore = defineStore('socketio', {
state: () => { state: () => {
return { return {
sockConnected: false as boolean,
socketID: null as string | null | undefined,
testMode: null as boolean | undefined | null,
gpsNoise: null as boolean | undefined | null, // nextLocation: null as NextLocation | null,
deviceConnected: false as boolean, // messageList: [''] as string[],
tunnelConnected: false as boolean,
simulationRunning: false as boolean | undefined | null,
simulationState: null as string | null | undefined,
simulationQueueLength: 0 as number | null | undefined,
currentLocation: null as CurrentLocation | null | undefined,
nextLocation: null as NextLocation | null,
messageList: [''] as string[],
errorList: [] as ErrorFull[],
locationQueueData: {} as LocationQueue,
locationQueueOrder: [] as string[],
locationQueueDeletedItems: [] as string[],
leafletZoom: 10 as number, leafletZoom: 10 as number,
icloudMonitor: false as boolean,
findMyUpdate: null as FindMyUpdate | null | undefined,
}; };
}, },
getters: { getters: {
sockState: (state) => state.sockConnected, /* sockState: (state) => state.sockConnected,
deviceState: (state) => state.deviceConnected, deviceState: (state) => state.deviceConnected,
lMarkerLatLng: (state): [number, number] => { lMarkerLatLng: (state): [number, number] => {
if ( if (
@@ -64,687 +34,27 @@ export const useSocketioStore = defineStore('socketio', {
return this.lMarkerLatLng; return this.lMarkerLatLng;
}, },
lZoom: (state): number => state.leafletZoom, lZoom: (state): number => state.leafletZoom,
*/
}, },
actions: { actions: {
setSockStatus() {
this.sockConnected = socket.connected;
this.socketID = socket.id;
},
bindEvents() {
this.setSockStatus();
// connect
socket.on('connect', () => {
this.setSockStatus();
socket.emit('message', 'Hello from client', (e: boolean) => {
if (debugLog) {
console.log('Message delivered: ' + e);
}
});
if (debugLog) {
console.log('Connected to server');
}
});
// disconnect
socket.on('disconnect', () => {
this.setSockStatus();
console.log('Disconnected from server');
});
// message
socket.on('message', (e: string) => {
this.setSockStatus();
this.messageList.push(e);
if (debugLog) {
console.log('Websock message received!');
}
});
// 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 (debugLog) {
console.log('StatusUpdate received: ', data);
}
this.digestUpdate(data);
});
// fmf_update
socket.on('fmf_update', (data: FindMyUpdate): void => {
if (debugLog) {
console.log('event: fmf_update received: ', data);
}
this.findMyUpdate = data;
});
// 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
};
});
// 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');
}
});
});
// 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;
});
/*
socket.onAny((eventName, ...args) => {
console.log('Event received: ', eventName, ' Args: ', args, '');
if (serverToClientKnownEvents.includes(eventName)) {
console.log('Known event received: ', eventName, ' Args: ', args, '');
} else {
console.log(
'Known events: ',
serverToClientKnownEvents,
' Received Event: ',
eventName,
' Args: ',
args,
'',
);
console.log(`Received UNKNOWN Event: ${eventName}`, args);
}
});
*/
},
connect() {
if (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();
},
setSimulationRunning(isRunning: boolean, states: string): void {
this.setSockStatus();
this.simulationState = states;
this.simulationRunning = isRunning;
},
icloudMonitorControl(command: string) {
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;
},
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?: string | null | undefined;
}) {
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
this.setSockStatus();
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', delay: 0, latitude: null, longitude: null },
(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 '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 = '{latitude}, {longitude}';
}
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;
},
requestUpdate(): void {
socket.emit('request_update', (response: StatusUpdate) => {
this.digestUpdate(response);
});
},
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;
},
digestUpdate(data: StatusUpdate): void {
if (data.simulation_queue) {
this.digestQueueUpdate(data.simulation_queue)
}
this.deviceConnected = !!(data.udid && data.tunnel);
this.tunnelConnected = !!data.tunnel;
this.icloudMonitor = data.icloud.monitor_enabled;
this.currentLocation = {
loc_id: data.current_location.loc_id,
latitude: data.current_location.latitude,
longitude: data.current_location.longitude,
next_move: data.next_move,
};
this.findMyUpdate = data.fmf_location;
},
setDeviceState(state: boolean) {
this.deviceConnected = state;
},
revGeoCode(nomRequest: NominatimRequest): Promise<NominatimResponse> {
return new Promise((resolve) => {
socket.emit(
'reverse_geocode',
{ latitude: nomRequest.latitude, longitude: nomRequest.longitude },
(response) => {
resolve(response);
},
);
});
},
updateLocationMark(loc_id: string, key: string, value: string | number) {
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[]) {
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;
});
},
}, },
}); });

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