milestones

This commit is contained in:
2026-06-10 11:00:10 -04:00
parent ddaf682d16
commit 1c3ec92b1f
50 changed files with 7314 additions and 2320 deletions
+28
View File
@@ -0,0 +1,28 @@
import * as L from 'leaflet';
interface TooltipCircleOptions extends L.CircleOptions {
tooltipContent?: string;
}
export class TipCircle extends L.Circle {
constructor(latlng: L.LatLngExpression, options: TooltipCircleOptions) {
super(latlng, options);
if (options?.tooltipContent) {
this.bindTooltip(options.tooltipContent, {
permanent: false,
direction: 'top',
});
}
}
override setRadius(radius: number): this {
super.setRadius(radius);
if (this.getTooltip()) {
this.setTooltipContent(`New radius: ${radius}m`);
}
return this;
}
}
export const tipCircle = (latlng: L.LatLngExpression, options: TooltipCircleOptions) => {
return new TipCircle(latlng, options);
}
View File
+1
View File
@@ -0,0 +1 @@
{}
+279
View File
@@ -0,0 +1,279 @@
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { useLeafletStore } from 'stores/leaflet';
import { useSimulationStore } from 'stores/simulation';
import { useTunneldStore } from 'stores/tunneld';
import { useDeviceStore } from 'stores/device';
import { useIcloudStore } from 'stores/icloud';
import { useControls } from 'constants/controls';
import type {
ControlAction,
ControlCategoryKey,
DeviceCommands,
TunneldCommands,
} from 'components/models';
import { computed } from 'vue';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
const $q = useQuasar();
const leafletStore = useLeafletStore();
const simulationStore = useSimulationStore();
const tunneldStore = useTunneldStore();
const deviceStore = useDeviceStore();
const icloudStore = useIcloudStore();
const { categories: controlCategories } = useControls();
type NotificationResult = {
type: string;
message: string;
};
type CommandAck = {
status: string;
message: string;
};
function setOpenedAppFab(fab: ControlCategoryKey) {
leafletStore.fabs.appControl.openedFab = fab;
}
function handleOpenedAppFabUpdate(val: boolean, key: ControlCategoryKey) {
if (val) setOpenedAppFab(key);
}
function formatUnknownError(commandLabel: string, error: unknown): NotificationResult {
if (error instanceof Error) {
console.error(`${commandLabel} ERROR: `, error.message);
return {
type: 'negative',
message: `${commandLabel} Error: ${error.message}`,
};
}
console.error(`${commandLabel} Error: `, error);
return {
type: 'negative',
message: `${commandLabel} Error: Unknown error`,
};
}
function notificationFromAck(ack: CommandAck, fallbackMessage: string): NotificationResult {
return {
type: ack.status.toLowerCase() === 'error' ? 'negative' : 'positive',
message: ack.message || fallbackMessage,
};
}
async function runSimulationCommand(cmdAttr: ControlAction): Promise<NotificationResult> {
try {
const ack = await simulationStore.simulationControlAsync({
command: cmdAttr.cmd,
latitude: null,
longitude: null,
loc_id: null,
delay: cmdAttr.delay,
address: null,
});
return notificationFromAck(ack, `${cmdAttr.name} requested`);
} catch (error: unknown) {
return formatUnknownError('Simulation Command', error);
}
}
async function runDeviceCommand(cmdAttr: ControlAction): Promise<NotificationResult> {
try {
const ack = await deviceStore.deviceControlAsync(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
return notificationFromAck(ack, `${cmdAttr.name} requested`);
} catch (error: unknown) {
return formatUnknownError('Device Command', error);
}
}
async function runTunneldCommand(cmdAttr: ControlAction): Promise<NotificationResult> {
try {
const ack = await tunneldStore.tunneldControlAsync(cmdAttr.cmd as TunneldCommands);
return notificationFromAck(ack, `${cmdAttr.name} requested`);
} catch (error: unknown) {
return formatUnknownError('Tunneld Command', error);
}
}
async function runIcloudMonitorCommand(cmdAttr: ControlAction): Promise<NotificationResult> {
try {
const ack = await icloudStore.icloudMonitorControlAsync(cmdAttr.cmd);
return notificationFromAck(ack, `${cmdAttr.name} requested`);
} catch (error: unknown) {
return formatUnknownError('iCloud Monitor Command', error);
}
}
async function runRoutingCommand(cmdAttr: ControlAction): Promise<NotificationResult> {
try {
if (cmdAttr.cmd === 'waypoints_to_simq') {
const result = await leafletStore.routeToQueue();
return {
type: result.type,
message: result.message || `${cmdAttr.name} requested`,
};
}
if (cmdAttr.cmd === 'points_to_simq') {
const result = await leafletStore.routeAllPointsToQueue();
return {
type: result.type,
message: result.message || `${cmdAttr.name} requested`,
};
}
if (cmdAttr.cmd === 'clear') {
const result = leafletStore.clearRoute();
return {
type: result.type,
message: result.message || `${cmdAttr.name} requested`,
};
}
return {
type: 'negative',
message: `Unsupported routing command: ${cmdAttr.cmd}`,
};
} catch (error: unknown) {
return formatUnknownError('Routing Command', error);
}
}
async function runControlCommand(cmdAttr: ControlAction): Promise<NotificationResult> {
switch (cmdAttr.cmdClass) {
case 'sim_cntrl_class':
return await runSimulationCommand(cmdAttr);
case 'dev_cntrl_class':
return await runDeviceCommand(cmdAttr);
case 'tunneld_cntrl_class':
return await runTunneldCommand(cmdAttr);
case 'icloud-monitor_cntrl_class':
return await runIcloudMonitorCommand(cmdAttr);
case 'routing_cntrl_class':
return await runRoutingCommand(cmdAttr);
default:
return {
type: 'negative',
message: `Unsupported command class: ${cmdAttr.cmdClass}`,
};
}
}
async function notifyControlCommand(cmdAttr: ControlAction) {
const notification = await runControlCommand(cmdAttr);
$q.notify(notification);
}
function handleControlClick(cmdAttr: ControlAction) {
if (cmdAttr.cnfrm) {
$q.dialog({
component: ConfirmCommandDialog,
componentProps: {
name: cmdAttr.name,
icon: cmdAttr.icon,
},
})
.onOk(() => {
void notifyControlCommand(cmdAttr);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
} else {
void notifyControlCommand(cmdAttr);
}
}
function closeAllFabs() {
leafletStore.fabs.appControl.openedFab = null;
}
const buttonClass = 'bg-ios-blur-brdr-h';
const buttonText = computed(() => ($q.dark.isActive ? 'grey-10' : 'grey-9'));
const activeButtonText = computed(() => ($q.dark.isActive ? 'white' : 'black'));
</script>
<template>
<q-fab
flat
round
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs xs"
direction="left"
v-model="leafletStore.fabs.appControl.opened"
id="app-ctrl"
type="button"
@show="leafletStore.closeOtherFabs('appControl')"
@hide="closeAllFabs"
>
<template v-slot:tooltip>
<q-tooltip anchor="center end" self="center start">App Controls</q-tooltip>
</template>
<template v-slot:icon>
<q-icon name="mdi-dots-vertical" :color="buttonText" size="sm" />
</template>
<template v-slot:active-icon>
<q-icon name="mdi-dots-vertical" :color="activeButtonText" size="sm" />
</template>
<template v-for="category in controlCategories" :key="category.key">
<q-fab
flat
round
type="button"
:icon="category.icon"
:class="buttonClass"
class="q-ma-none q-pa-none q-mx-xs"
:text-color="buttonText"
padding="0px 0px"
direction="up"
v-model="category.openedFab.value"
v-show="category.isVisible"
@update:model-value="handleOpenedAppFabUpdate($event, category.key)"
>
<template v-slot:tooltip
><q-tooltip>{{ category.name }}</q-tooltip></template
>
<template v-slot:icon>
<q-icon :name="category.icon" :color="buttonText" size="sm" />
</template>
<template v-slot:active-icon>
<q-icon :name="category.icon" :color="activeButtonText" size="sm" />
</template>
<q-fab-action
flat
round
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs xs"
type="button"
v-for="cmd in category.commands"
:key="cmd.renderKey"
:disabled="cmd.disabled"
v-show="cmd.isVisible"
@click="handleControlClick(cmd)"
>
<template v-slot:icon>
<q-icon :name="cmd.icon" :color="cmd.iconColor" size="sm" />
</template>
<q-tooltip anchor="center left" self="center end">{{ cmd.name }}</q-tooltip>
</q-fab-action>
</q-fab>
</template>
</q-fab>
</template>
<style lang="sass"></style>
+76 -111
View File
@@ -1,6 +1,6 @@
<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-card class="bg-dark text-grey-1 add-loc-card q-pa-sm" v-if="operation == 'delete'">
<q-toolbar>
<q-avatar icon="mdi-star" color="secondary" text-color="yellow" />
<q-toolbar-title>Clear Favorite</q-toolbar-title>
@@ -24,78 +24,90 @@
>Edit Favorite</q-toolbar-title
>
</q-toolbar>
<q-card-section class="q-ml-lg">
<q-card-section horizontal class="q-ml-lg">
<FormattedAddress class="q-ml-md" :address="address" />
catId: {{ formData.category_id }}
</q-card-section>
<q-card-section>
<q-input
color="secondary"
v-model="formData.name"
label="Favorite Name"
autofocus
@keyup.enter="onOkClick"
style="max-width: 250px"
style="max-width: 360px"
/>
<div class="flex col q-col-gutter-md">
<q-select
dense
options-dense
color="accent"
v-model="formData.category"
v-model="formData.category_id"
: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"
label="Select Category"
options-value="id"
option-label="name"
option-value="icon"
emit-value
map-options
use-input
clearable
label="Select Icon"
@filter="iconFilterFcn"
style="max-width: 150px"
style="max-width: 175px"
>
<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">
<template v-slot:selected-item="scope: any">
<q-item>
<q-item-section avatar>
<q-icon :name="scope.opt.icon" />
<q-icon :name="scope.opt.icon" :style="{ color: scope.opt.color }" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template v-slot:option="scope">
<q-item>
<q-item-section avatar>
<q-icon :name="scope.opt.icon" :style="{ color: scope.opt.color }" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template v-slot:append>
<q-icon
v-if="formData.category_id"
name="mdi-close"
class="cursor-pointer"
@click.stop="formData.category_id = 0"
/>
</template>
</q-select>
<IconPicker v-model="formData.icon" />
</div>
<div class="flex col q-col-gutter-md">
<div class="flex">
<div class="row q-col-gutter-md">
<q-input
v-model="formData.longitude"
label="Longitude"
style="max-width: 150px"
style="max-width: 118px"
color="secondary"
readonly
/>
<q-input
v-model="formData.latitude"
label="Latitude"
style="max-width: 150px"
style="max-width: 118px"
color="secondary"
readonly
/>
<q-input
v-model="formData.radius"
label="Radius"
style="max-width: 118px"
color="secondary"
suffix="m"
readonly
/>
</div>
</div>
</q-card-section>
<q-separator />
@@ -110,16 +122,18 @@
<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';
import type { NewFavorite, FavoriteCommands, GeoCacheResponse } from 'components/models';
import { computed, reactive, ref } from 'vue';
import FormattedAddress from 'components/FormattedAddress.vue';
import IconPicker from 'components/IconPicker.vue';
interface Props {
id?: number | undefined | null;
lat: number;
lng: number;
isFavorite: boolean;
operation: string;
radius: number;
geoCacheResp: GeoCacheResponse;
operation: FavoriteCommands;
}
const favoriteStore = useFavoriteStore();
@@ -130,86 +144,37 @@ defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
const iconsList = ref(iconList);
const categories = computed(() => favoriteStore.categories);
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 address = props.geoCacheResp.address;
const isFavorite = !!props.geoCacheResp.favorite;
const icon = ref(props.geoCacheResp.favorite?.icon as string);
const name = ref(props.geoCacheResp.favorite?.name);
const categoryId = ref(props.geoCacheResp.favorite?.category_id);
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({
const formData: NewFavorite = reactive({
name: name.value,
longitude: props.lng,
latitude: props.lat,
category: category.value,
category_id: categoryId.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);
}
});
}
radius: props.radius,
address: address,
}) as NewFavorite;
function onOkClick() {
if (props.isFavorite && props.operation === 'clear') {
favoriteStore.favoriteControl({ command: 'delete', favorite: formData });
const response = { operation: props.operation, data: formData };
onDialogOK(response);
}
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;
/*
onMounted(() => {
if (props.geoCacheResp.favorite) {
formData.name = props.geoCacheResp.favorite.name;
formData.icon = props.geoCacheResp.favorite.icon;
formData.category_id = props.geoCacheResp.favorite.category_id;
}
});
*/
</script>
<style lang="sass" scoped>
.add-loc-card
-27
View File
@@ -1,27 +0,0 @@
<template>
<q-item clickable tag="a" target="_blank" :href="link">
<q-item-section v-if="icon" avatar>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
export interface EssentialLinkProps {
title: string;
caption?: string;
link?: string;
icon?: string;
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',
link: '#',
icon: '',
});
</script>
+193
View File
@@ -0,0 +1,193 @@
<script setup lang="ts">
import { useFavoriteStore } from 'stores/favorite';
import { useLeafletStore } from 'stores/leaflet';
import type { Category, Favorite } from 'components/models';
import { useQuasar } from 'quasar';
import { computed } from 'vue';
const $q = useQuasar();
const leafletStore = useLeafletStore();
const favoriteStore = useFavoriteStore();
type CatFavs = Category[] | Favorite[];
const buttonClass = 'bg-ios-blur-brdr-h';
const buttonText = computed(() => ($q.dark.isActive ? 'grey-10' : 'grey-9'));
const activeButtonText = computed(() => ($q.dark.isActive ? 'white' : 'black'));
function closeAllFabs() {
favoriteStore.catsWithFavs.forEach((cat) => {
cat.openedFab = false;
});
favoriteStore.favorites.forEach((fav) => {
fav.openedFab = false;
});
leafletStore.fabs.favorites.opened = false;
}
function closeOtherFabs(items: CatFavs, currentIndex: number, parentCategory: Category | null) {
console.log(
'Closing other fabs, items: %s, currentIndex: %d, parentCategory: %o',
items,
currentIndex,
parentCategory,
);
if (parentCategory) {
parentCategory.favorites.forEach((fav, index) => {
if (index !== currentIndex) {
fav.openedFab = false;
}
});
favoriteStore.catsWithFavs.forEach((cat) => {
if (cat.id !== parentCategory.id) {
cat.openedFab = false;
}
});
} else {
items.forEach((item, index) => {
if (index !== currentIndex) {
item.openedFab = false;
}
});
}
}
function handleCategoryFabUpdate(val: boolean | null, category: Category, currentIndex: number) {
category.openedFab = val;
if (val) closeOtherFabs(favoriteStore.catsWithFavs, currentIndex, null);
}
function handleFavoriteFabUpdate(
val: boolean | null,
favorite: Favorite,
favorites: Favorite[],
currentIndex: number,
parentCategory: Category,
) {
favorite.openedFab = val;
if (val) closeOtherFabs(favorites, currentIndex, parentCategory);
}
function handleZoomClick(lat: number, lng: number) {
leafletStore.flyTo(lat, lng, 16);
closeAllFabs();
}
</script>
<template>
<q-fab
round
flat
padding="xs xs"
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
direction="left"
v-model="leafletStore.fabs.favorites.opened"
id="favorite-ctrl"
type="button"
@show="leafletStore.closeOtherFabs('favorites')"
@hide="closeAllFabs"
>
<template v-slot:tooltip
><q-tooltip anchor="center end" self="center start">Favorites</q-tooltip></template
>
<template v-slot:icon>
<q-icon name="mdi-star" :color="buttonText" size="sm" />
</template>
<template v-slot:active-icon>
<q-icon name="mdi-star" :color="activeButtonText" size="sm" />
</template>
<template v-for="(category, cIndex) in favoriteStore.catsWithFavs" :key="category.id">
<q-fab
round
flat
padding="0px 0px"
type="button"
:class="buttonClass"
class="q-ma-none q-pa-none q-mx-xs"
:text-color="buttonText"
direction="up"
:icon="category.icon"
:model-value="category.openedFab ?? false"
@update:model-value="handleCategoryFabUpdate($event, category, cIndex)"
>
<template v-slot:tooltip
><q-tooltip anchor="bottom middle" self="bottom middle" :offset="[10, 40]">{{
category.name
}}</q-tooltip></template
>
<template v-slot:icon>
<q-icon :name="category.icon" :color="buttonText" size="sm" />
</template>
<template v-slot:active-icon>
<q-icon :name="category.icon" :color="activeButtonText" size="sm" />
</template>
<q-fab
small
round
flat
padding="0px 0px"
type="button"
:class="buttonClass"
class="q-ma-none q-pa-none q-my-xs"
:text-color="buttonText"
direction="left"
v-for="(fav, fIndex) in category.favorites"
:key="fav.id"
vertical-actions-align="left"
external-label
:label="fav.name"
:model-value="fav.openedFab ?? false"
@update:model-value="
handleFavoriteFabUpdate($event, fav, category.favorites, fIndex, category)
"
>
<template v-slot:tooltip
><q-tooltip anchor="center end" self="center start">{{ fav.name }}</q-tooltip></template
>
<template v-slot:icon>
<q-icon :name="fav.icon" :color="buttonText" size="sm" />
</template>
<template v-slot:active-icon>
<q-icon :name="fav.icon" :color="activeButtonText" size="sm" />
</template>
<q-fab-action
small
round
flat
padding="xs xs"
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
type="button"
@click="handleZoomClick(fav.latitude, fav.longitude)"
icon="mdi-loupe"
>
<q-tooltip anchor="top middle" self="center middle" :offset="[10, 25]"
>Zoom To {{ fav.name }}</q-tooltip
>
</q-fab-action>
<q-fab-action
small
round
flat
padding="xs xs"
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
type="button"
@click="handleZoomClick(fav.latitude, fav.longitude)"
icon="mdi-map-marker"
>
<q-tooltip anchor="top middle" self="center middle" :offset="[10, 25]"
>Set Route Start To {{ fav.name }}</q-tooltip
>
</q-fab-action>
</q-fab>
</q-fab>
</template>
</q-fab>
</template>
<style lang="sass"></style>
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div>
<div style="white-space: pre-line">
<div>
{{ formattedAddressLine1 }}
</div>
<div>
+83
View File
@@ -0,0 +1,83 @@
<template>
<div class="q-pa-md" style="max-width: 350px">
<q-select
v-model="selectedLocation"
use-input
input-debounce="400"
label="Search Location"
:options="options"
option-label="label"
@filter="filterFn"
@update:model-value="onSelectLocation"
clearable
outlined
dense
popup-content-class="bg-ios-blur"
popup-content-style="width: 300px;"
style="width: 100%"
class="bg-ios-blur"
behavior="menu"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey"> No results found </q-item-section>
</q-item>
</template>
<template v-slot:append>
<q-icon name="mdi-magnify" />
</template>
</q-select>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useLeafletStore } from 'stores/leaflet';
import { OpenStreetMapProvider } from 'leaflet-geosearch';
import type { SearchResult } from 'components/models';
const leafletStore = useLeafletStore();
const selectedLocation = ref(null);
const options = ref();
const provider = new OpenStreetMapProvider({
// Optional: Add your provider API keys here if using OpenCage or Google
});
const emit = defineEmits(['update:visible']);
const onSelectLocation = (location: SearchResult) => {
leafletStore.moveToLocation(location);
selectedLocation.value = null;
emit('update:visible', false);
};
type FilterFn = (
val: string,
update: (cb: () => void) => void,
abort: () => void,
) => Promise<void> | void;
const filterFn: FilterFn = async (val, update, abort) => {
if (val.length < 2) {
abort();
return;
}
try {
const results = await provider.search({ query: val });
update(() => {
// Format the results for QSelect
options.value = results.map((r) => ({
label: r.label,
x: r.x, // longitude
y: r.y, // latitude
bounds: r.bounds,
}));
});
} catch (error) {
console.error('Geosearch error:', error);
abort();
}
};
</script>
+97
View File
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { type PropType, ref, watch } from 'vue';
import iconList from '@quasar/extras/mdi-v7/icons.json';
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
modelValue: {
type: String as PropType<string | undefined>,
required: false,
},
});
const selectedIcon = ref(props.modelValue);
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));
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);
}
});
}
watch(selectedIcon, (newValue) => {
emit('update:modelValue', newValue);
});
</script>
<template>
<q-select
dense
options-dense
color="accent"
v-model="selectedIcon"
:options="filteredIcons"
option-label="name"
option-value="icon"
emit-value
map-options
use-input
label="Select Icon"
@filter="iconFilterFcn"
style="max-width: 200px"
>
<template v-slot:selected-item="scope">
<q-item>
<q-item-section avatar>
<q-icon size="xs" :name="scope.opt.icon" />
</q-item-section>
<q-item-section>
<q-item-label class="text-body2">{{ scope.opt.name }}</q-item-label>
</q-item-section>
</q-item>
</template>
<template v-slot:option="scope">
<q-item>
<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>
<template v-slot:append>
<q-icon
v-if="selectedIcon"
name="mdi-close"
class="cursor-pointer"
@click.stop="selectedIcon = ''"
/>
</template>
</q-select>
</template>
<style lang="sass" scoped></style>
+147
View File
@@ -0,0 +1,147 @@
<script setup lang="ts">
import { useLeafletStore } from 'stores/leaflet';
import { computed } from 'vue';
import { useQuasar } from 'quasar';
const leafletStore = useLeafletStore();
const $q = useQuasar();
type LiteralLayers = 'marker' | 'favorites' | 'findMy' | 'simQueue' | 'routing';
function handleLayerClick(layer: LiteralLayers) {
leafletStore.layerControl[layer] = !leafletStore.layerControl[layer];
}
const buttonClass = 'bg-ios-blur-brdr-h';
const buttonText = computed(() => ($q.dark.isActive ? 'grey-10' : 'grey-9'));
const activeButtonText = computed(() => ($q.dark.isActive ? 'white' : 'black'));
</script>
<template>
<q-fab
flat
round
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs"
icon="mdi-layers-outline"
direction="left"
v-model="leafletStore.fabs.layerControl.opened"
id="layer-ctrl"
@show="leafletStore.closeOtherFabs('layerControl')"
>
<template v-slot:icon>
<q-icon name="mdi-layers-outline" :color="buttonText" />
</template>
<template v-slot:active-icon>
<q-icon name="mdi-layers-outline" :color="activeButtonText" />
</template>
<template v-slot:tooltip>
<q-tooltip anchor="center end" self="center start">Layers</q-tooltip>
</template>
<q-fab-action
round
flat
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs"
color="primary"
type="button"
@click.stop.prevent="handleLayerClick('marker')"
>
<template v-slot:icon>
<q-icon
name="mdi-map-marker"
:color="leafletStore.layerControl.marker ? activeButtonText : buttonText"
size="sm"
/>
</template>
<q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">Marker</q-tooltip>
</q-fab-action>
<q-fab-action
round
flat
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs"
type="button"
direction="left"
@click.stop.prevent="handleLayerClick('favorites')"
>
<template v-slot:icon>
<q-icon
name="mdi-star"
:color="leafletStore.layerControl.favorites ? activeButtonText : buttonText"
/>
</template>
<q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">Favorites</q-tooltip>
</q-fab-action>
<q-fab-action
round
flat
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs"
type="button"
direction="left"
@click.stop.prevent="handleLayerClick('routing')"
>
<template v-slot:icon>
<q-icon
name="mdi-routes"
:color="leafletStore.layerControl.routing ? activeButtonText : buttonText"
/>
</template>
<q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">Routing</q-tooltip>
</q-fab-action>
<q-fab-action
round
flat
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs"
type="button"
direction="left"
@click.stop.prevent="handleLayerClick('findMy')"
>
<template v-slot:icon>
<q-icon
name="mdi-cellphone-marker"
:color="leafletStore.layerControl.findMy ? activeButtonText : buttonText"
/>
</template>
<q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">FindMy</q-tooltip>
</q-fab-action>
<q-fab-action
round
flat
:class="buttonClass"
class="q-ma-none q-pa-none"
:text-color="buttonText"
padding="xs"
type="button"
@click.stop.prevent="handleLayerClick('simQueue')"
>
<template v-slot:icon>
<q-icon
name="mdi-map-marker-multiple"
:color="leafletStore.layerControl.simQueue ? activeButtonText : buttonText"
/>
</template>
<q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]"
>Simulation Queue</q-tooltip
>
</q-fab-action>
</q-fab>
</template>
<style lang="sass">
.bg-grey-9-custom
background: rgba(33, 33, 33, 0.33)
.bg-white-custom
background: rgba(255, 255, 255, 0.2)
</style>
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useLeafletStore } from 'stores/leaflet';
import { LocateControl } from 'leaflet.locatecontrol';
const leafletStore = useLeafletStore();
const locateControl = new LocateControl({
position: 'topleft',
flyTo: true,
keepCurrentZoomLevel: true,
returnToPrevBounds: true,
layer: leafletStore.layerGroups.locateControl ?? undefined,
});
const locationControlInit = ref(false);
const locateControlActive = ref(false);
const locateControlFollowing = ref(false);
const locationFound = ref(false);
const iconColor = computed(() =>
locateControlActive.value
? locationFound.value
? locateControlFollowing.value
? '#FC8428FF'
: '#2074B6FF'
: '#FFFFFF'
: '#212121',
);
const iconClass = computed(() => {
if (locateControlActive.value && !locationFound.value && !locateControlFollowing.value) {
return 'locate-control-icon-spinner';
} else {
return '';
}
});
const handleClick = () => {
initialize();
if (locateControlActive.value) {
console.log('Stopping locate control');
locateControl.stop();
} else {
console.log('Starting locate control');
locateControl.start();
}
};
function initialize() {
if (!locationControlInit.value) {
if (leafletStore.mapObject) {
locationControlInit.value = true;
leafletStore.mapObject.addControl(locateControl);
leafletStore.mapObject?.on('locateactivate', function () {
console.log('Locate control activated');
locateControlActive.value = true;
});
leafletStore.mapObject?.on('locatedeactivate', function () {
console.log('Locate control deactivated');
locateControlActive.value = false;
leafletStore.layerGroups.locateControl?.clearLayers();
});
leafletStore.mapObject?.on('locationfound', function (e) {
console.log('Location Found: ', e);
console.log('Location found:', e.latlng);
console.log('Accuracy:', e.accuracy, 'meters');
locateControlFollowing.value = true;
locationFound.value = true;
});
leafletStore.mapObject?.on('moveend', function () {
if (locateControlFollowing.value) {
locateControlFollowing.value = false;
}
});
leafletStore.mapObject?.on('locationtimeout', function (e) {
const timeoutEvent = e as typeof e & { count?: number };
console.log('Location timeout count:', timeoutEvent.count);
locationFound.value = false;
leafletStore.layerGroups.locateControl?.clearLayers();
});
}
}
}
onMounted(() => {
if (leafletStore.mapObject) {
console.log('MapObject found on initial mount, adding locate control');
initialize();
}
});
</script>
<template>
<q-btn small flat round size="0.8em" class="bg-ios-blur-brdr-h" @click="handleClick">
<q-icon
name="mdi-navigation"
:style="'color: ' + iconColor + ';'"
class="locate-control-icon"
:class="iconClass"
aria-label="Locate me"
/>
</q-btn>
</template>
<style lang="sass">
.locate-control-icon
display: inline-block
width: 16px
height: 16px
margin: 7px
mask-repeat: no-repeat
mask-position: center
.locate-control-icon-spinner
animation: leaflet-control-locate-spin 2s linear infinite
@keyframes leaflet-control-locate-spin
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
</style>
+114 -69
View File
@@ -3,69 +3,42 @@ import { storeToRefs } from 'pinia';
import { useSimulationStore } from 'stores/simulation';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import type { NominatimResponse } from 'components/models';
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
import {
Icon,
PinCirclePanel,
PinStarPanel,
PointCircle,
TackDiamondBorder,
} from 'leaflet-extra-markers';
import FormattedAddress from 'components/FormattedAddress.vue';
//import NestedKnob from 'components/NestedKnob.vue';
const simulationStore = useSimulationStore();
const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } =
storeToRefs(simulationStore);
const {
currentLocation,
locationQueueOrder,
locationQueueData,
locationQueueDeletedItems,
simulationRunning,
} = storeToRefs(simulationStore);
const props = defineProps({
address: {
type: Object as () => NominatimResponse,
required: false,
},
latitude: {
type: Number,
required: true,
},
longitude: {
type: Number,
required: true,
},
icon: {
type: String,
default: 'mdi-map-marker',
},
start: {
type: String,
required: true,
},
end: {
type: String,
required: false,
},
delay: {
type: Number,
default: 0,
},
active: {
type: Boolean,
default: false,
},
isCurrentDelay: {
type: Boolean,
default: false,
},
loc_id: {
type: String,
required: true,
},
isLast: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
status: {
type: String,
default: 'queued',
},
});
const props = defineProps<{
address?: NominatimResponse | undefined;
latitude: number;
longitude: number;
icon?: string;
start: string;
end?: string | undefined;
delay: number;
active: boolean;
isCurrentDelay: boolean;
loc_id: string;
isLast: boolean;
index: number;
status?: string | undefined;
type?: string | undefined;
instruction?: string | undefined;
}>();
// Define custom events that this component can emit
const emit = defineEmits(['item-clicked']);
@@ -75,6 +48,14 @@ function itemClicked() {
emit('item-clicked', param1);
}
const myData = computed(() => {
return simulationStore.getLocationData(props.loc_id);
});
const isDeleted = computed(() => {
return myData.value.status === 'deleted' ? true : false;
});
const currentTime = ref(new Date());
let timerId: number;
@@ -103,7 +84,24 @@ function formatInAgo(seconds: number) {
const calculateDeltaTime = computed(() => {
let delta;
if (props.active) {
delta = 'now';
const startTime = new Date(props.start);
const nowMs: number = currentTime.value.getTime();
const pastMs: number = startTime.getTime();
const diffMs: number = Math.abs(nowMs - pastMs);
const seconds: number = Math.floor(diffMs / 1000);
const minutes: number = Math.floor(seconds / 60);
const remainderSeconds: number = seconds % 60;
const hours: number = Math.floor(minutes / 60);
const days: number = Math.floor(hours / 24);
if (days > 0) {
delta = `for ${days} d${days > 1 ? 's' : ''}`;
} else if (hours > 0) {
delta = `for ${hours} h${hours > 1 ? 's' : ''}`;
} else if (minutes > 0) {
delta = `for ${minutes}:${remainderSeconds}`;
} else {
delta = `for ${seconds} s${seconds > 1 ? 's' : ''}`;
}
} else if (props.end && props.end !== '') {
const pastTime: Date = new Date(props.end);
const nowMs: number = currentTime.value.getTime();
@@ -124,7 +122,7 @@ const calculateDeltaTime = computed(() => {
}
} else {
let startSlice;
if (currentIndex.value > 0) {
if (currentIndex.value && currentIndex.value > 0) {
startSlice = currentIndex.value;
} else {
startSlice = 0;
@@ -175,15 +173,26 @@ const humanReadableDateTime = (iso: string) => {
};
const currentIndex = computed(() => {
return currentLocation.value ? locationQueueOrder.value.indexOf(currentLocation.value.loc_id) : 0;
return currentLocation.value.loc_id
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: null;
});
const myIndex = computed(() => {
if (isDeleted.value) {
return locationQueueDeletedItems.value.indexOf(props.loc_id);
} else {
return locationQueueOrder.value.indexOf(props.loc_id);
}
});
const myUpdatedIndex = computed(() => {
if (isDeleted.value) {
return myIndex.value + 1;
} else {
if (currentIndex.value === null) return myIndex.value + 1;
return myIndex.value - currentIndex.value;
}
});
const itemClass = computed(() => {
@@ -193,7 +202,27 @@ const itemClass = computed(() => {
});
const customIcon = computed(() => {
if (myData.value.status === 'deleted') {
return new Icon({
color: 'black',
accentColor: 'black',
content: myUpdatedIndex.value.toString(),
contentColor: 'white',
scale: 1,
svg: TackDiamondBorder,
});
} else {
if (myUpdatedIndex.value > 0) {
if (props.type === 'coord') {
return new Icon({
color: 'blue',
accentColor: 'firebrick',
content: myUpdatedIndex.value.toString(),
contentColor: 'white',
scale: 0.25,
svg: PointCircle,
});
} else {
return new Icon({
color: 'blue',
accentColor: 'firebrick',
@@ -202,7 +231,18 @@ const customIcon = computed(() => {
scale: 1,
svg: PinCirclePanel,
});
}
} else if (myUpdatedIndex.value < 0) {
if (props.type === 'coord') {
return new Icon({
color: 'black',
accentColor: 'grey',
content: myUpdatedIndex.value.toString(),
contentColor: 'white',
scale: 0.25,
svg: PointCircle,
});
} else {
return new Icon({
color: 'black',
accentColor: 'grey',
@@ -211,6 +251,7 @@ const customIcon = computed(() => {
scale: 1,
svg: PinCirclePanel,
});
}
} else {
return new Icon({
color: 'pink',
@@ -221,6 +262,7 @@ const customIcon = computed(() => {
svg: PinStarPanel,
});
}
}
});
const iconElement = computed(() => {
@@ -310,14 +352,16 @@ const delayInputMask = computed(() => {
*/
const displayDelay = computed(() => {
if (currentIndex.value === null) return true;
return props.index > currentIndex.value;
});
const delayActive = computed(() => {
return props.index === currentIndex.value + 1;
return props.index === currentIndex.value;
});
const canDrag = computed(() => {
if (currentIndex.value === null) return true;
return props.index > currentIndex.value + 1;
});
@@ -330,8 +374,8 @@ onUnmounted(() => {
<q-item
v-if="displayDelay"
:active="delayActive"
:class="delayActive ? 'text-orange bg-dark' : ''"
active-class="text-orange bg-dark"
:class="delayActive ? 'text-orange' : ''"
active-class="text-orange"
>
<q-item-section class="flex flex-center">
<q-input
@@ -368,18 +412,19 @@ onUnmounted(() => {
:active="active"
@click="itemClicked"
:class="itemClass"
active-class="text-orange bg-dark"
active-class="text-orange"
>
<q-item-section style="width: 70%">
<q-item-label v-if="address">
<FormattedAddress :address="props.address" />
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
</q-item-label>
<q-item-label v-else-if="instruction" lines="2">{{ instruction }}</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 && !isDeleted">
start: {{ humanReadableDateTime(start) }}
</q-item-label>
<q-item-label caption lines="1" v-if="end && simulationRunning">
<q-item-label caption lines="1" v-if="end && simulationRunning && !isDeleted">
end: {{ humanReadableDateTime(end) }}
</q-item-label>
<q-item-label caption lines="1" v-if="!active && !end">
@@ -389,7 +434,7 @@ onUnmounted(() => {
<q-item-section
side
style="
padding: 30px 0 0 10px;
padding: 30px 0 0 0;
height: 100%;
width: 35%;
display: flex;
@@ -398,7 +443,7 @@ onUnmounted(() => {
"
>
<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 && !isDeleted">
{{ calculateDeltaTime }}
</q-item-label>
</q-item-section>
+145 -157
View File
@@ -1,15 +1,14 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import { computed, ref } from 'vue';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
import { favorites } from 'constants/favorites';
import { controls } from 'constants/controls';
import { useControls } from 'constants/controls';
import type { coords, DeviceCommands, TunneldCommands } from 'components/models';
import type { coords, DeviceCommands, TunneldCommands, ControlAction } from 'components/models';
import { useDeviceStore } from 'stores/device';
import { useLeafletStore } from 'stores/leaflet';
@@ -17,7 +16,6 @@ import { useSimulationStore } from 'stores/simulation';
import { useIcloudStore } from 'stores/icloud';
import { useTunneldStore } from 'stores/tunneld';
const route = useRoute();
const $q = useQuasar();
const deviceStore = useDeviceStore();
@@ -25,6 +23,7 @@ const leafletStore = useLeafletStore();
const simulationStore = useSimulationStore();
const icloudStore = useIcloudStore();
const tunneldStore = useTunneldStore();
const { actions: controls } = useControls();
const { center, markerLatLng, zoom } = storeToRefs(leafletStore);
const {
@@ -43,7 +42,7 @@ const menuOpen = ref(false);
const favoritesMap = favorites as Record<string, unknown>;
const isLast = computed(() => {
if (locationQueueOrder.value && locationQueueOrder.value.length > 0 && currentLocation.value) {
if (locationQueueOrder.value && locationQueueOrder.value.length > 0 && currentLocation.value.loc_id) {
const currentIndex = locationQueueOrder.value.indexOf(currentLocation.value.loc_id);
return locationQueueOrder.value.length - 1 === currentIndex;
} else {
@@ -51,15 +50,6 @@ const isLast = computed(() => {
}
});
type ControlAction = {
name: string;
cmd: string;
cmdClass: string;
icon: string;
cnfrm: boolean;
delay: number;
};
type FavoriteWithCoords = {
name: string;
icon: string;
@@ -95,8 +85,8 @@ function handleTestToggle() {
delay: 0,
address: null,
});
if (response.sts === 'error') {
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
if (response.status === 'error') {
$q.notify({ type: 'negative', message: response.message ?? 'Failed to toggle test mode' });
}
}
@@ -109,8 +99,8 @@ function handleGpsNoiseToggle() {
delay: 0,
address: null,
});
if (response.sts === 'error') {
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
if (response.status === 'error') {
$q.notify({ type: 'negative', message: response.message ?? 'Failed to toggle test mode' });
}
}
@@ -125,8 +115,8 @@ function handleControlClick(cmdAttr: ControlAction) {
})
.onOk(() => {
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = simulationStore.simulationControl({
command: cmdAttr['cmd'],
@@ -136,71 +126,71 @@ function handleControlClick(cmdAttr: ControlAction) {
delay: cmdAttr.delay,
address: null,
});
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Simulation Command ERROR: ', error.message);
notMsg = `Simulation Command Error: ${error.message}`;
notificationMessage = `Simulation Command Error: ${error.message}`;
} else {
console.error('Simulation Command Error: ', error);
notMsg = 'Simulation Command Error: Unknow error';
notificationMessage = 'Simulation Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Device Command ERROR: ', error.message);
notMsg = `Device Command Error: ${error.message}`;
notificationMessage = `Device Command Error: ${error.message}`;
} else {
console.error('Device Command Error: ', error);
notMsg = 'Device Command Error: Unknow error';
notificationMessage = 'Device Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Tunneld Command ERROR: ', error.message);
notMsg = `Tunneld Command Error: ${error.message}`;
notificationMessage = `Tunneld Command Error: ${error.message}`;
} else {
console.error('Tunneld Command Error: ', error);
notMsg = 'Tunneld Command Error: Unknow error';
notificationMessage = 'Tunneld Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
})
@@ -212,8 +202,8 @@ function handleControlClick(cmdAttr: ControlAction) {
});
} else {
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = simulationStore.simulationControl({
command: cmdAttr.cmd,
@@ -223,96 +213,96 @@ function handleControlClick(cmdAttr: ControlAction) {
delay: cmdAttr.delay,
address: null,
});
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Simulation Command ERROR: ', error.message);
notMsg = `Simulation Command Error: ${error.message}`;
notificationMessage = `Simulation Command Error: ${error.message}`;
} else {
console.error('Simulation Command Error: ', error);
notMsg = 'Simulation Command Error: Unknow error';
notificationMessage = 'Simulation Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
if (cmdAttr.cmdClass === 'icloud-monitor_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = icloudStore.icloudMonitorControl(cmdAttr.cmd);
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Simulation Command ERROR: ', error.message);
notMsg = `Simulation Command Error: ${error.message}`;
notificationMessage = `Simulation Command Error: ${error.message}`;
} else {
console.error('Simulation Command Error: ', error);
notMsg = 'Simulation Command Error: Unknow error';
notificationMessage = 'Simulation Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Device Command ERROR: ', error.message);
notMsg = `Device Command Error: ${error.message}`;
notificationMessage = `Device Command Error: ${error.message}`;
} else {
console.error('Device Command Error: ', error);
notMsg = 'Device Command Error: Unknow error';
notificationMessage = 'Device Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
let notType: string = 'positive';
let notMsg: string = '';
let notificationType: string = 'positive';
let notificationMessage: string = '';
try {
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
if (ack.sts === 'error') {
notType = 'negative';
if (ack.status === 'error') {
notificationType = 'negative';
}
if (ack.msg) {
notMsg = ack.msg;
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
notificationType = 'negative';
if (error instanceof Error) {
console.error('Tunneld Command ERROR: ', error.message);
notMsg = `Tunneld Command Error: ${error.message}`;
notificationMessage = `Tunneld Command Error: ${error.message}`;
} else {
console.error('Tunneld Command Error: ', error);
notMsg = 'Tunneld Command Error: Unknow error';
notificationMessage = 'Tunneld Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notMsg });
$q.notify({ type: notificationType, message: notificationMessage });
}
}
}
@@ -343,7 +333,6 @@ function handleControlClick(cmdAttr: ControlAction) {
stretch
flat
label="Favorites"
v-if="route.name === 'Leaflet'"
class="text-weight-bold"
>
<q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
@@ -451,18 +440,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.simulation.start)"
@click="handleControlClick(controls.simulation.commands.start)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.simulation.start.icon"
:icon="controls.simulation.commands.start.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.simulation.start.name }} </q-item-label>
<q-item-label> {{ controls.simulation.commands.start.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -471,18 +460,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.simulation.pause)"
@click="handleControlClick(controls.simulation.commands.pause)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.simulation.pause.icon"
:icon="controls.simulation.commands.pause.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.simulation.pause.name }} </q-item-label>
<q-item-label> {{ controls.simulation.commands.pause.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -491,18 +480,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.simulation.resume)"
@click="handleControlClick(controls.simulation.commands.resume)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.simulation.resume.icon"
:icon="controls.simulation.commands.resume.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.simulation.resume.name }} </q-item-label>
<q-item-label> {{ controls.simulation.commands.resume.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -511,18 +500,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.simulation.clear)"
@click="handleControlClick(controls.simulation.commands.clear)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.simulation.clear.icon"
:icon="controls.simulation.commands.clear.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.simulation.clear.name }} </q-item-label>
<q-item-label> {{ controls.simulation.commands.clear.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -531,18 +520,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.simulation.reset)"
@click="handleControlClick(controls.simulation.commands.reset)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.simulation.reset.icon"
:icon="controls.simulation.commands.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-label> {{ controls.simulation.commands.reset.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -551,18 +540,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.simulation.end)"
@click="handleControlClick(controls.simulation.commands.end)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.simulation.end.icon"
:icon="controls.simulation.commands.end.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.simulation.end.name }} </q-item-label>
<q-item-label> {{ controls.simulation.commands.end.name }} </q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
@@ -579,18 +568,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.icloudmonitor.start)"
@click="handleControlClick(controls.icloudmonitor.commands.start)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.icloudmonitor.start.icon"
:icon="controls.icloudmonitor.commands.start.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.icloudmonitor.start.name }} </q-item-label>
<q-item-label> {{ controls.icloudmonitor.commands.start.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -599,19 +588,19 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.icloudmonitor.stop)"
@click="handleControlClick(controls.icloudmonitor.commands.stop)"
size="sm"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.icloudmonitor.stop.icon"
:icon="controls.icloudmonitor.commands.stop.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.icloudmonitor.stop.name }} </q-item-label>
<q-item-label> {{ controls.icloudmonitor.commands.stop.name }} </q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
@@ -622,18 +611,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.start)"
@click="handleControlClick(controls.tunneld.commands.start)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.start.icon"
:icon="controls.tunneld.commands.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-label> {{ controls.tunneld.commands.start.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -642,18 +631,37 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.start_watcher)"
@click="handleControlClick(controls.tunneld.commands.start_watcher)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.start_watcher.icon"
:icon="controls.tunneld.commands.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-label> {{ controls.tunneld.commands.start_watcher.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
dark
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.commands.restart)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.commands.restart.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.tunneld.commands.restart.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -662,18 +670,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.restart)"
@click="handleControlClick(controls.tunneld.commands.clear)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.restart.icon"
:icon="controls.tunneld.commands.clear.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-label> {{ controls.tunneld.commands.clear.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -682,38 +690,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.clear)"
@click="handleControlClick(controls.tunneld.commands.cancel)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.clear.icon"
:icon="controls.tunneld.commands.cancel.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-label> {{ controls.tunneld.commands.cancel.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -722,18 +710,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.end_watcher)"
@click="handleControlClick(controls.tunneld.commands.end_watcher)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.end_watcher.icon"
:icon="controls.tunneld.commands.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-label> {{ controls.tunneld.commands.end_watcher.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -742,18 +730,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.tunneld.shutdown)"
@click="handleControlClick(controls.tunneld.commands.shutdown)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.tunneld.shutdown.icon"
:icon="controls.tunneld.commands.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-label> {{ controls.tunneld.commands.shutdown.name }} </q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
@@ -768,18 +756,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.device.reboot)"
@click="handleControlClick(controls.device.commands.reboot)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.device.reboot.icon"
:icon="controls.device.commands.reboot.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.device.reboot.name }} </q-item-label>
<q-item-label> {{ controls.device.commands.reboot.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
@@ -787,18 +775,18 @@ function handleControlClick(cmdAttr: ControlAction) {
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.device.shutdown)"
@click="handleControlClick(controls.device.commands.shutdown)"
>
<q-item-section avatar class="q-pl-lg">
<q-avatar
:icon="controls.device.shutdown.icon"
:icon="controls.device.commands.shutdown.icon"
color="secondary"
text-color="black"
size="sm"
/>
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.device.shutdown.name }} </q-item-label>
<q-item-label> {{ controls.device.commands.shutdown.name }} </q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
-110
View File
@@ -1,110 +0,0 @@
<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>
+12 -5
View File
@@ -52,6 +52,7 @@
</q-checkbox>
</div>
<div>
<q-btn flat label="No Delay" @click="onNoDelayClick" />
<q-btn flat label="OK" @click="onOkClick" />
<q-btn flat label="Cancel" @click="onDialogCancel" />
</div>
@@ -62,13 +63,15 @@
<script setup lang="ts">
import { useDialogPluginComponent } from 'quasar';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
import { useLocationStore } from 'stores/location';
import { ref, onMounted, computed } from 'vue';
import { useQuasar } from 'quasar';
import FormattedAddress from 'components/FormattedAddress.vue';
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
import type { NominatimResponse } from 'components/models';
const locationStore = useLocationStore();
const props = defineProps({
lat: { type: Number, required: true },
lng: { type: Number, required: true },
@@ -92,11 +95,11 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
onMounted(async () => {
try {
loading.value = true;
const response = await reverseGeocodeRateLimited(props.lat, props.lng);
const response = await locationStore.getCachedLocation({ lat: props.lat, lng: props.lng});
console.log('reverse geocode response: ', response);
address.value = response.address;
if (response.favorite) {
favorite.value = response.favorite;
address.value = response?.address;
if (response?.favorite) {
favorite.value = response?.favorite;
}
} catch (error) {
console.error('Error fetching reverse geocode:', error);
@@ -141,6 +144,10 @@ function handleFavoriteClick() {
function onOkClick() {
onDialogOK({ delay: delay.value, address: address.value });
}
function onNoDelayClick() {
onDialogOK({ delay: 0, address: address.value });
}
</script>
<style lang="sass" scoped>
.add-loc-card
-1
View File
@@ -1 +0,0 @@
export {};
+1 -1
View File
@@ -30,7 +30,7 @@ const timeWithSeconds = ref('00:00:45');
</script>
<template>
<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 icon="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
<q-btn label="Connect" @click="socketStore.connect()" />
<q-btn label="Disconnect" @click="socketStore.disconnect()" />
</div>
+125 -16
View File
@@ -5,6 +5,7 @@ import { useIcloudStore } from 'stores/icloud';
import { useDeviceStore } from 'stores/device';
import { useTunneldStore } from 'stores/tunneld';
import { storeToRefs } from 'pinia';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
const socketStore = useSocketStore();
const simulationStore = useSimulationStore();
@@ -15,9 +16,23 @@ const tunneldStore = useTunneldStore();
const { sockConnected } = storeToRefs(socketStore);
const { deviceConnected } = storeToRefs(deviceStore);
const { tunnelConnected } = storeToRefs(tunneldStore);
const { simulationState, testMode } = storeToRefs(simulationStore);
const { simulationState } = storeToRefs(simulationStore);
const { icloudMonitor } = storeToRefs(icloudStore);
const props = defineProps<{
transparentBg?: boolean;
}>();
const bgClass = () => {
if (props.transparentBg) {
return 'transparent-background';
} else {
return 'white-background';
}
};
const uptimeSeconds = ref();
let timer: ReturnType<typeof setInterval>;
function statusDevColor(state: string | boolean | null | undefined): string {
if (state === null || state === undefined) {
return 'grey';
@@ -37,55 +52,149 @@ function statusDevColor(state: string | boolean | null | undefined): string {
}
}
}
const formattedUptime = computed(() => {
const days = Math.floor(uptimeSeconds.value / 86400);
const hours = Math.floor((uptimeSeconds.value % 86400) / 3600);
const minutes = Math.floor((uptimeSeconds.value % 3600) / 60);
const seconds = Math.floor(uptimeSeconds.value % 60);
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
});
watch(
() => deviceStore.lastKnownUptime,
(newVal: number) => {
uptimeSeconds.value = newVal;
},
);
onMounted(() => {
timer = setInterval(() => {
uptimeSeconds.value++;
}, 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
</script>
<template>
<q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
<div class="flex col">
<q-space />
<!--
<div style="width: 80vw" class="flex justify-end">
<q-img src="~assets/simloc_logo2.png" class="logo" @click="showStatus = !showStatus" />
<span class="text-grey-9">SimPi</span>
<q-space />
-->
<div class="q-gutter-x-sm">
<q-btn dense rounded push size="sm" icon="mdi-cog" @click="socketStore.toggleSock()">
<div class="flex status-bar-container">
<div class="row justify-center q-gutter-x-xs">
<q-btn
round
flat
color="bg-grey-9"
:class="bgClass"
class="text-grey-9"
dense
size="sm"
icon="mdi-cog"
aria-label="Uptime"
>
<q-badge color="positive" rounded floating />
<q-tooltip>{{ formattedUptime }}</q-tooltip>
</q-btn>
<q-btn
round
flat
color="bg-grey-9"
:class="bgClass"
class="text-grey-9"
dense
size="sm"
icon="mdi-cog"
@click="socketStore.toggleSock()"
aria-label="socketIO Status"
>
<q-badge :color="statusDevColor(sockConnected)" rounded floating />
</q-btn>
<q-btn
round
flat
color="bg-grey-9"
:class="bgClass"
class="text-grey-9"
dense
rounded
push
size="sm"
icon="mdi-cellphone"
@click="socketStore.requestUpdate()"
aria-label="Device Status"
>
<q-badge :color="statusDevColor(deviceConnected)" rounded floating />
</q-btn>
<q-btn dense rounded push size="sm" icon="mdi-subway" @click="socketStore.requestUpdate()">
<q-btn
round
flat
color="bg-grey-9"
:class="bgClass"
class="text-grey-9"
dense
size="sm"
icon="mdi-subway"
@click="socketStore.requestUpdate()"
aria-label="Tunnel Status"
>
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating />
</q-btn>
<q-btn
round
flat
color="bg-grey-9"
:class="bgClass"
class="text-grey-9"
dense
rounded
push
size="sm"
icon="mdi-map-marker"
@click="socketStore.requestUpdate()"
aria-label="Simulation Status"
>
<q-badge :color="statusDevColor(simulationState)" rounded floating />
</q-btn>
<q-btn
small
round
flat
color="bg-grey-9"
:class="bgClass"
class="text-grey-9"
dense
rounded
push
size="sm"
icon="mdi-cloud"
@click="icloudStore.icloudMonitorControl('refresh')"
aria-label="iCloud Monitor Status"
>
<q-badge :color="statusDevColor(icloudMonitor)" rounded floating />
</q-btn>
</div>
<!--
<q-btn round
flat
color="bg-grey-9"
class="white-background text-grey-9"
dense
size="sm"
icon="mdi-account-hard-hat"
@click="socketStore.requestSendTestPrompt('test')"
/>
<q-space />
-->
</div>
</q-toolbar>
</template>
<style scoped></style>
<style scoped lang="sass">
.logo
max-width: 20px
height: 100%
.status-bar-container
width: 165px
box-shadow: none
.white-background
background: rgba(255,255,255,0.3)
.transparent-background
background-color: rgba(0,0,0,0)
</style>
+222
View File
@@ -0,0 +1,222 @@
<script setup lang="ts">
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 { useLeafletStore } from 'stores/leaflet';
import { useQuasar } from 'quasar';
import { storeToRefs } from 'pinia';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
const socketStore = useSocketStore();
const simulationStore = useSimulationStore();
const icloudStore = useIcloudStore();
const deviceStore = useDeviceStore();
const tunneldStore = useTunneldStore();
const leafletStore = useLeafletStore();
const $q = useQuasar();
const { sockConnected } = storeToRefs(socketStore);
const { deviceConnected } = storeToRefs(deviceStore);
const { tunnelConnected } = storeToRefs(tunneldStore);
const { simulationState } = storeToRefs(simulationStore);
const { icloudMonitor } = storeToRefs(icloudStore);
const uptimeSeconds = ref();
let timer: ReturnType<typeof setInterval>;
function statusDevColor(state: string | boolean | null | undefined): string {
if (state === null || state === undefined) {
return 'grey';
}
if (typeof state === 'boolean') {
return state ? 'green' : 'red';
} else {
switch (state.toLowerCase()) {
case 'paused':
return 'yellow';
case 'running':
return 'green';
case 'stopped':
return 'red';
default:
return 'grey';
}
}
}
function statusIconColor(state: string | boolean | null | undefined): string {
if (state === null || state === undefined) {
return 'grey-6';
}
if (typeof state === 'boolean') {
return state ? 'grey-10' : 'grey-6';
} else {
if (simulationStore.testMode) {
return 'yellow';
} else {
switch (state.toLowerCase()) {
case 'paused':
return 'grey-6';
case 'running':
return 'grey-10';
case 'stopped':
return 'grey-6';
default:
return 'grey-6';
}
}
}
}
const formattedUptime = computed(() => {
const days = Math.floor(uptimeSeconds.value / 86400);
const hours = Math.floor((uptimeSeconds.value % 86400) / 3600);
const minutes = Math.floor((uptimeSeconds.value % 3600) / 60);
const seconds = Math.floor(uptimeSeconds.value % 60);
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
});
const buttonText = computed(() => ($q.dark.isActive ? 'grey-10' : 'grey-9'));
const activeButtonText = computed(() => ($q.dark.isActive ? 'white' : 'black'));
watch(
() => deviceStore.lastKnownUptime,
(newVal: number) => {
uptimeSeconds.value = newVal;
},
);
onMounted(() => {
timer = setInterval(() => {
uptimeSeconds.value++;
}, 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
</script>
<template>
<q-fab
round
flat
padding="xs xs"
class="q-ma-none q-pa-none bg-ios-blur-brdr-h"
:text-color="buttonText"
direction="left"
v-model="leafletStore.fabs.statusBar.opened"
type="button"
id="status-bar"
@show="leafletStore.closeOtherFabs('statusBar')"
@click="socketStore.requestUpdate()"
>
<template v-slot:icon>
<q-icon name="mdi-list-status" :color="buttonText" size="sm" />
</template>
<template v-slot:active-icon>
<q-icon name="mdi-list-status" :color="activeButtonText" size="sm" />
</template>
<template v-slot:tooltip>
<q-tooltip anchor="center end" self="center start">Status</q-tooltip>
</template>
<q-fab-action
round
flat
padding="xs"
class="bg-ios-blur-brdr-h q-ma-none q-pa-none"
:text-color="buttonText"
type="button"
@click="socketStore.requestUpdate()"
>
<template v-slot:icon>
<q-icon name="mdi-server" :color="statusIconColor(sockConnected)" size="sm" />
</template>
<q-badge color="positive" rounded floating />
<q-tooltip>{{ formattedUptime }}</q-tooltip>
</q-fab-action>
<q-fab-action
round
flat
padding="xs"
class="bg-ios-blur-brdr-h q-ma-none q-pa-none"
:text-color="buttonText"
direction="left"
type="button"
@click="socketStore.toggleSock()"
>
<template v-slot:icon>
<q-icon name="mdi-cog" :color="statusIconColor(sockConnected)" size="sm" />
</template>
<q-badge :color="statusDevColor(sockConnected)" rounded floating />
</q-fab-action>
<q-fab-action
round
flat
padding="xs"
class="bg-ios-blur-brdr-h q-ma-none q-pa-none"
:text-color="buttonText"
direction="left"
type="button"
@click="socketStore.requestUpdate()"
>
<template v-slot:icon>
<q-icon name="mdi-cellphone" :color="statusIconColor(deviceConnected)" size="sm" />
</template>
<q-badge :color="statusDevColor(deviceConnected)" rounded floating />
</q-fab-action>
<q-fab-action
round
flat
padding="xs"
class="bg-ios-blur-brdr-h q-ma-none q-pa-none"
:text-color="buttonText"
direction="left"
type="button"
icon="mdi-subway"
@click="socketStore.requestUpdate()"
>
<template v-slot:icon>
<q-icon name="mdi-subway" :color="statusIconColor(tunnelConnected)" size="sm" />
</template>
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating />
</q-fab-action>
<q-fab-action
round
flat
padding="xs"
class="bg-ios-blur-brdr-h q-ma-none q-pa-none"
:text-color="buttonText"
direction="left"
type="button"
@click="socketStore.requestUpdate()"
>
<template v-slot:icon>
<q-icon name="mdi-map-marker" :color="statusIconColor(simulationState)" size="sm" />
</template>
<q-badge :color="statusDevColor(simulationState)" rounded floating />
</q-fab-action>
<q-fab-action
round
flat
padding="xs"
class="bg-ios-blur-brdr-h q-ma-none q-pa-none"
:text-color="buttonText"
direction="left"
type="button"
@click="icloudStore.icloudMonitorControl('refresh')"
>
<template v-slot:icon>
<q-icon name="mdi-cloud" :color="statusIconColor(icloudMonitor)" size="sm" />
</template>
<q-badge :color="statusDevColor(icloudMonitor)" rounded floating />
</q-fab-action>
</q-fab>
</template>
<style lang="sass">
.white-background
background: rgba(255,255,255,0.3)
.transparent-background
background-color: rgba(0,0,0,0)
</style>
+1 -1
View File
@@ -14,7 +14,7 @@
@keyup.enter="onOkClick"
label="Delay (seconds)"
type="number"
:rules="[(val) => val.length === 6 || 'Must be 6 digits']"
:rules="[(val: string) => val.length === 6 || 'Must be 6 digits']"
/>
</q-card-section>
<q-card-actions align="right">
+557
View File
@@ -0,0 +1,557 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
// Leaflet imports
import * as L from 'leaflet';
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import {
Icon,
PinCirclePanel,
PinStar,
PinStarPanel,
TackCirclePanel,
} from 'leaflet-extra-markers';
import 'leaflet-geosearch/dist/geosearch.css';
import 'leaflet/dist/leaflet.css';
import '@quasar/extras/mdi-v7/mdi-v7.css';
import {
LCircle,
LLayerGroup,
LMap,
LMarker,
LTileLayer,
LTooltip,
} from '@vue-leaflet/vue-leaflet';
// Custom Components
import LRoutingMachine from 'components/LRoutingMachine.vue';
import { useRoutingEvents } from 'src/composables/useRoutingEvents';
import MarkerMenu from 'components/leaflet/MarkerMenu.vue';
// Types
import type { SearchControlProps } from 'components/models';
import type { LayerGroup, LeafletMouseEvent, Map } from 'leaflet';
// Stores
import { storeToRefs } from 'pinia';
import { useSocketStore } from 'stores/socket';
import { useSimulationStore } from 'stores/simulation';
import { useLeafletStore } from 'stores/leaflet';
import { useIcloudStore } from 'stores/icloud';
import { useFavoriteStore } from 'stores/favorite';
import { useLocationStore } from 'stores/location';
const socketStore = useSocketStore();
const favoriteStore = useFavoriteStore();
const locationStore = useLocationStore();
const { favorites } = storeToRefs(favoriteStore);
const leafletStore = useLeafletStore();
const { clickedLatLng, center, markerLatLng, layerControl } =
storeToRefs(leafletStore);
const simulationStore = useSimulationStore();
const { currentLocation, locationQueueData, locationQueueOrder } = storeToRefs(simulationStore);
const icloudStore = useIcloudStore();
const { findMyUpdate } = storeToRefs(icloudStore);
const now = ref(Date.now());
const home = computed(() => favorites.value.find((f) => f.name === 'Home'));
const safeCenter = computed<[number | undefined, number | undefined]>(() => {
const lat = center.value?.[0];
const lng = center.value?.[1];
if (typeof lat === 'number' && typeof lng === 'number') {
return [lat, lng];
}
return [home.value?.latitude, home.value?.longitude];
});
function updateMarker(event: LeafletMouseEvent) {
markerLatLng.value = [event.latlng.lat, event.latlng.lng];
center.value = [event.latlng.lat, event.latlng.lng];
}
const markerIcon = () => {
return new Icon({
color: 'grey',
accentColor: 'rgba(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi mdi-crosshairs-gps" />`,
contentColor: 'white',
scale: 1,
svg: TackCirclePanel,
});
};
const fmIcon = () => {
return new Icon({
color: 'indianred',
accentColor: 'firebrick',
content: 'FM',
contentColor: 'white',
scale: 0.75,
svg: PinCirclePanel,
});
};
const getFavIcon = (icon: string) => {
return new Icon({
color: '#f5bb39',
accentColor: 'rbga(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi ${icon}" />`,
contentColor: 'black',
scale: 1,
svg: PinStar,
});
};
const safeMarkerLatLng = computed<[number, number] | null>(() => {
const lat = markerLatLng.value?.[0];
const lng = markerLatLng.value?.[1];
if (typeof lat === 'number' && typeof lng === 'number') {
return [lat, lng];
}
return null;
});
const onFavLayerGroupReady = (layerGroup: LayerGroup) => {
leafletStore.setFavLayerGroup(layerGroup);
};
const onMapReady = (map: Map) => {
leafletStore.setMap(map);
const provider = new OpenStreetMapProvider();
const searchOptions: SearchControlProps = {
provider: provider,
showMarker: false,
autoClose: true,
updateMap: true,
showPopup: true,
style: 'button',
acceptAutoLoad: true,
autoComplete: true,
autoCompleteDelay: 250,
retainZoomLevel: false,
animateZoom: true,
keepResult: true,
position: 'topleft',
};
const searchControl = GeoSearchControl(searchOptions);
map.addControl(searchControl);
};
const { handleRoutesFound } = useRoutingEvents();
const debugRoutingEvent = (event: Event) => {
console.log(`${event.type} event: `, event);
};
const findMyTimePast = computed(() => {
if (findMyUpdate.value) {
const diffInMs = Math.abs(now.value - findMyUpdate.value.timeStamp);
const seconds = Math.floor((diffInMs / 1000) % 60);
const minutes = Math.floor((diffInMs / (1000 * 60)) % 60);
const hours = Math.floor((diffInMs / (1000 * 60 * 60)) % 24);
const days = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
if (days > 1) {
return (
days + ' days, ' + hours + ' hours, ' + minutes + 'minutes, ' + seconds + 'seconds ago'
);
} else if (hours > 1) {
return hours + ' hours, ' + minutes + 'minutes, ' + seconds + 'seconds ago';
} else if (minutes > 1) {
return minutes + ' minutes, ' + seconds + ' seconds ago';
} else {
return seconds + ' seconds ago';
}
} else {
return 'Find My Location not available';
}
});
/*
type AddLocationDialogPayload = {
delay: number | undefined;
address?: string;
};
async function addDialogLocation(latlng: L.LatLng, { delay, address }: AddLocationDialogPayload) {
const revGeo = !address;
const { notificationType, responseMessage } = await useAddLocation(
{ lat: Number(latlng.lat), lng: Number(latlng.lng) },
delay,
revGeo,
address,
);
$q.notify({ type: notificationType, message: responseMessage });
console.log(
'Confirmed location add: latitude: ' +
latlng.lat +
', longitude: ' +
latlng.lng +
', delay: ' +
delay,
);
}
function handleAddLocation() {
if (clickedLatLng.value) {
const latlng = clickedLatLng.value;
closeAllPopups();
$q.notify(`add location...${latlng.toString()}`);
$q.dialog({
component: SetLocationDialog,
componentProps: {
lat: Number(latlng.lat),
lng: Number(latlng.lng),
},
})
.onOk((payload: AddLocationDialogPayload) => {
void addDialogLocation(latlng, payload);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
}
const iconElement = (icon: Icon) => {
return icon.createIcon();
};
const fmIcon = new Icon({
color: 'indianred',
accentColor: 'firebrick',
content: 'FM',
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
const startIcon = new Icon({
color: '#006838',
accentColor: 'rgba(0,0,0,0.25)',
content: 'S',
contentColor: 'white',
scale: 1,
svg: PinTriangle,
});
const endIcon = new Icon({
color: '#a23337',
accentColor: 'rgba(0,0,0,0.25)',
content: 'E',
contentColor: 'white',
scale: 1,
svg: PinSquare,
});
const intermediateIcon = new Icon({
color: 'cornflowerblue',
accentColor: 'rgba(0,0,0,0.25)',
contentColor: 'white',
svg: ChipCircle,
scale: 1,
});
const getFavIcon = (icon: string) => {
return new Icon({
color: '#f5bb39',
accentColor: 'rbga(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi ${icon}" />`,
contentColor: 'black',
scale: 1,
svg: PinStar,
});
};
*/
const getCustomIcon = (locid: string) => {
const currentIndex = currentLocation.value.loc_id
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: 0;
const locationIndex = locationQueueOrder.value.indexOf(locid);
const updatedIndex = locationIndex - currentIndex;
if (currentLocation.value && currentLocation.value.loc_id === locid) {
return new Icon({
color: 'pink',
accentColor: 'black',
content: '*',
contentColor: 'black',
scale: 1,
svg: PinStarPanel,
});
}
if (updatedIndex > 0) {
return new Icon({
color: 'blue',
accentColor: 'firebrick',
content: updatedIndex.toString(),
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
} else {
return new Icon({
color: 'black',
accentColor: 'grey',
content: updatedIndex.toString(),
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
}
};
const isFavorite = ref(false);
const handleMarkerClick = async (event: LeafletMouseEvent) => {
console.log('marker clicked', event);
// closeAllPopups();
clickedLatLng.value = event.latlng;
const cachedLocation = await locationStore.getCachedLocation(
L.latLng(event.latlng.lat, event.latlng.lng),
);
console.log('cachedLocation: ', cachedLocation);
isFavorite.value = !!cachedLocation?.favorite;
console.log('isFavorite: ', isFavorite.value);
L.DomEvent.stopPropagation(event.originalEvent);
};
const showFindMy = computed(() => {
if (findMyUpdate.value) {
const diffInSec = Math.floor(Math.abs(now.value - findMyUpdate.value.timeStamp) / 1000);
return diffInSec < 120;
} else {
return false;
}
});
/*
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),
radius: favoriteRadius.value,
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');
});
}
}
*/
const timer = ref();
onMounted(() => {
if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) {
leafletStore.setCenter(findMyUpdate.value.latitude, findMyUpdate.value.longitude);
} else {
if (home.value) {
if (home.value?.latitude && home.value?.longitude) {
console.log('favorites: home: ', home.value?.latitude, home.value?.longitude);
leafletStore.setCenter(home.value?.latitude, home.value?.longitude);
}
}
}
socketStore.requestUpdate();
timer.value = setInterval(() => {
now.value = Date.now();
}, 1000);
});
onUnmounted(() => {
clearInterval(timer.value);
});
</script>
<template>
<q-page>
<div style="height: 550px; width: 100vw; max-width: 800px; color: #000000">
<L-Map
id="map"
ref="map"
:center="[safeCenter[0] ?? 0, safeCenter[1] ?? 0]"
:zoom="leafletStore.zoom"
style="height: 550px; width: 100vw; max-width: 800px"
@click="updateMarker"
@ready="onMapReady"
>
<L-Tile-Layer
layer-type="base"
name="OpenStreetMap"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
></L-Tile-Layer>
<L-Layer-Group v-if="layerControl.marker" name="Marker">
<L-Marker
:icon="markerIcon() as any"
v-if="safeMarkerLatLng"
:lat-lng="[safeMarkerLatLng[0] ?? 0, safeMarkerLatLng[1] ?? 0]"
@click="handleMarkerClick"
>
<MarkerMenu
:lat-lng="[safeMarkerLatLng[0] ?? 0, safeMarkerLatLng[1] ?? 0]"
:is-favorite="isFavorite"
/>
</L-Marker>
</L-Layer-Group>
<L-Layer-Group name="Find My" v-if="layerControl.findMy && findMyUpdate && showFindMy">
<L-Marker
v-if="findMyUpdate"
:icon="fmIcon() as any"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
>
<MarkerMenu
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
:is-favorite="isFavorite"
/>
<L-Tooltip>
{{ findMyTimePast }}
</L-Tooltip>
</L-Marker>
<L-Circle
:fillOpacity="0.5"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
:radius="findMyUpdate.horizontalAccuracy"
color="#f5bb39"
fillColor="indianred"
></L-Circle>
</L-Layer-Group>
<L-Layer-Group v-if="layerControl.favorites" name="Favorites" @ready="onFavLayerGroupReady">
<div v-for="fav in favorites" :key="fav.id">
<L-Marker
:icon="getFavIcon(fav.icon ?? 'mdi-star') as any"
:lat-lng="[
(fav as Record<string, any>)?.latitude ?? 0,
(fav as Record<string, any>)?.longitude ?? 0,
]"
@click="handleMarkerClick"
>
<MarkerMenu
:lat-lng="[
(fav as Record<string, any>)?.latitude ?? 0,
(fav as Record<string, any>)?.longitude ?? 0,
]"
:is-favorite="true"
/>
</L-Marker>
<L-Circle
:lat-lng="[
(fav as Record<string, any>)?.latitude ?? 0,
(fav as Record<string, any>)?.longitude ?? 0,
]"
:radius="fav.radius"
color="black"
fillColor="#f5bb39"
:fillOpacity="0.25"
/>
<l-tooltip>{{ fav.name }}</l-tooltip>
</div>
</L-Layer-Group>
<L-Layer-Group v-if="layerControl.simQueue && locationQueueOrder" name="Sim Queue">
<L-Marker
v-for="locid in locationQueueOrder"
:key="locid"
:icon="getCustomIcon(locid) as any"
:lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
>
</L-Marker>
</L-Layer-Group>
<L-Layer-Group name="Route" v-if="layerControl.routing">
<LRoutingMachine
v-bind="leafletStore.routingOptions"
@routingstart="debugRoutingEvent"
@routesfound="handleRoutesFound"
@routingerror="debugRoutingEvent"
/>
</L-Layer-Group>
</L-Map>
</div>
</q-page>
</template>
<style lang="sass">
.l-map
height: 100vh
width: 100vw
.marker-popup .leaflet-popup-content-wrapper, .marker-popup .leaflet-popup-tip
background-color: $dark
.marker-popup .leaflet-popup-content
margin: 13px 10px 13px 10px
.leaflet-circle-actions
pointer-events: none
.leaflet-circle-actions button
border: 2px solid rgba(0,0,0,0.2)
background-clip: padding-box
cursor: pointer
text-align: center
font-weight: bold
box-shadow: 0 1px 5px rgba(0,0,0,0.4)
.circle-btn-accept
background-color: $positive
color: black
.circle-btn-clear
background-color: $negative
color: black
.leaflet-circle-actions button:hover
background-color: #f4f4f4
.leaflet-overlay-pane svg
position: absolute
.small-icon svg
transform: scale(0.6)
transform-origin: center
bottom: 0
right: 0
.small-icon
position: absolute
margin-left: 14px
margin-top: 44px
</style>
+379
View File
@@ -0,0 +1,379 @@
<script setup lang="ts">
import { useSimulationStore } from 'stores/simulation';
import { storeToRefs } from 'pinia';
import { computed, ref, watch } from 'vue';
import { useLeafletStore } from 'stores/leaflet';
import { useQuasar } from 'quasar';
import { zoomTo } from 'src/composables/useZoom';
// Draggable imports
import { VueDraggable } from 'vue-draggable-plus';
// Custom Components
import LocationItem from 'components/LocationItem.vue';
defineProps<{
persistent?: boolean;
side: 'right' | 'left';
breakpoint?: number;
}>();
defineEmits<{
(event: 'update:modelValue', payload: string): void;
}>();
const noSwipeClose = computed(() => $q.platform.is.desktop);
const noSwipeOpen = computed(() => $q.platform.is.desktop);
const overlay = computed(() => !leafletStore.locationDrawerSettings.pinned);
const $q = useQuasar();
const simulationStore = useSimulationStore();
const {
currentLocation,
locationQueueData,
locationQueueOrder,
simulationState,
simulationRunning,
locationQueueFilteredDone,
locationQueueFilteredActive,
locationQueueDeletedItems,
} = storeToRefs(simulationStore);
const activeList = ref([...locationQueueFilteredActive.value]);
const leafletStore = useLeafletStore();
const showItems = ref('All');
const disableDraggable = computed(() => {
return showItems.value === 'All';
});
watch(
() => simulationStore.locationQueueFilteredActive,
(newVal) => {
activeList.value = [...newVal];
},
);
const handleDragEnd = () => {
simulationStore.updateLocationQueueCombined(activeList.value);
};
function handleDrawerContextMenu(command: string) {
let notType: string = 'positive';
let notificationMessage: string = '';
switch (command) {
case 'zoom':
zoomToCoords(selectedItem.value);
break;
case 'delete':
try {
const ack = simulationStore.simulationControl({
command: 'delete',
latitude: 0,
longitude: 0,
loc_id: selectedItem.value,
delay: 0,
});
if (ack.status === 'error') {
notType = 'negative';
}
if (ack.message) {
notificationMessage = ack.message;
}
if (ack.status === 'error') {
notType = 'negative';
}
if (ack.message) {
notificationMessage = ack.message;
}
} catch (error: unknown) {
notType = 'negative';
if (error instanceof Error) {
console.error('Simulation Command ERROR: ', error.message);
notificationMessage = `Simulation Command Error: ${error.message}`;
} else {
console.error('Simulation Command Error: ', error);
notificationMessage = 'Simulation Command Error: Unknow error';
}
} finally {
$q.notify({ type: notType, message: notificationMessage });
}
}
$q.notify(`context menu: ${command} ${selectedItem.value}`);
}
/*
const locationQueueOrderFiltered = computed({
get: () => {
if (locationQueueOrder.value) {
return locationQueueOrder.value.filter(
(loc_id) => locationQueueData.value[loc_id]?.status !== 'deleted',
);
} else {
return [];
}
},
set: (val) => {
simulationStore.updateLocationQueueOrder(val);
},
});
*/
const selectedItem = ref();
const panel = ref('active');
const isDraggable = (locid: string) => {
const currentIndex = currentLocation.value.loc_id
? locationQueueFilteredActive.value.indexOf(currentLocation.value.loc_id)
: null;
const myIndex = locationQueueOrder.value.indexOf(locid);
const myUpdatedIndex = () => {
if (currentIndex === null) return myIndex + 1;
return myIndex - currentIndex;
};
if (myUpdatedIndex() > 1) {
return 'draggable';
} else {
return 'undraggable';
}
};
function toTitleCase(word: string | null | undefined) {
if (!word) return '';
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
function onDrawerContextMenu(e: MouseEvent, item: string) {
console.log('onDrawerContextMenu: ', item);
selectedItem.value = item;
}
function zoomToCoords(arg: string) {
console.log('zoomToCoords: ', arg);
const item = locationQueueData.value[arg];
if (!item || item.latitude == null || item.longitude == null) {
return;
}
leafletStore.flyTo(item.latitude, item.longitude, 16);
leafletStore.leafletDrawerOpen = false;
}
</script>
<template>
<q-drawer
id="location-drawer"
:overlay="overlay"
v-model="leafletStore.locationDrawerSettings.opened"
:width="300"
:side="side"
:breakpoint="breakpoint ?? 500"
:no-swipe-open="noSwipeOpen"
:no-swipe-close="noSwipeClose"
:persistent="persistent"
class="location-drawer bg-ios-blur-brdr"
>
<div class="full-height column no-wrap">
<q-list>
<q-item style="height: 50px; line-height: 58px !important">
<q-item-section avatar>
<q-btn
flat
round
small
icon="mdi-chevron-left"
style="margin-right: 15px"
@click="leafletStore.locationsOpenedHandler(false)"
aria-label="Location List"
/>
</q-item-section>
<q-item-section>
<q-item-label
><span class="text-weight-bolder text-black text-h6">Queue: </span><br /><span
:class="simulationRunning ? 'text-positive' : 'text-negative'"
>{{ toTitleCase(simulationState) }}</span
></q-item-label
>
</q-item-section>
<q-item-section side>
<q-btn
flat
round
small
v-if="$q.platform.is.desktop"
:icon="!leafletStore.locationDrawerSettings.pinned ? 'mdi-pin' : 'mdi-pin-outline'"
class="text-grey"
:class="{ unpinned: !leafletStore.locationDrawerSettings.pinned }"
@click="
leafletStore.locationDrawerSettings.pinned =
!leafletStore.locationDrawerSettings.pinned
"
aria-label="Pin Location List"
>
<q-tooltip v-if="leafletStore.locationDrawerSettings.pinned"
>Unpin Sim Queue</q-tooltip
>
<q-tooltip v-else>Pin sim queue</q-tooltip>
</q-btn>
</q-item-section>
</q-item>
</q-list>
<q-separator />
<q-tabs
v-model="panel"
dense
class="text-grey-10"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab name="active" label="Active" />
<q-tab name="done" label="Past" />
<q-tab name="deleted" label="Deleted" />
</q-tabs>
<q-tab-panels v-model="panel" animated class="full-height bg-transparent">
<q-tab-panel name="active" class="column no-wrap bg-transparent">
<q-scroll-area class="col">
<q-list padding class="q-pb-xs" id="active">
<VueDraggable
ref="el"
v-model="activeList"
handle=".drag-handle"
:animation="200"
:isDisabled="disableDraggable"
@end="handleDragEnd"
>
<LocationItem
v-for="(key, index) in activeList"
:class="isDraggable(key)"
:key="key"
:loc_id="key"
:active="
(locationQueueData as Record<string, any>)[key]?.loc_id ===
currentLocation?.loc_id
"
:isCurrentDelay="
(locationQueueData as Record<string, any>)[key]?.loc_id ===
locationQueueFilteredActive[locationQueueOrder.indexOf(key) + 1]
"
:index="locationQueueOrder.indexOf(key)"
:isLast="index == locationQueueFilteredActive.length - 1"
:start="(locationQueueData as Record<string, any>)[key]?.start ?? ''"
:address="(locationQueueData as Record<string, any>)[key]?.address ?? undefined"
:latitude="(locationQueueData as Record<string, any>)[key]?.latitude ?? 0"
:longitude="(locationQueueData as Record<string, any>)[key]?.longitude ?? 0"
:delay="(locationQueueData as Record<string, any>)[key]?.delay ?? 0"
:end="(locationQueueData as Record<string, any>)[key]?.end ?? undefined"
:status="(locationQueueData as Record<string, any>)[key]?.status ?? undefined"
:type="(locationQueueData as Record<string, any>)[key]?.type ?? undefined"
:instruction="
(locationQueueData as Record<string, any>)[key]?.instruction ?? undefined
"
@item-clicked="zoomToCoords"
@contextmenu.prevent="onDrawerContextMenu($event, key)"
active-class="text-orange"
/>
</VueDraggable>
</q-list>
</q-scroll-area>
</q-tab-panel>
<q-tab-panel name="done" class="column no-wrap bg-transparent">
<q-scroll-area :horizontal-thumb-style="{ opacity: '50' }" class="col">
<q-list padding class="q-pb-xl" id="done">
<LocationItem
v-for="(key, index) in locationQueueFilteredDone"
:key="key"
:loc_id="key"
:active="false"
:isCurrentDelay="false"
:index="locationQueueOrder.indexOf(key)"
:isLast="index == locationQueueFilteredDone.length - 1"
:start="(locationQueueData as Record<string, any>)[key]?.start ?? ''"
:address="(locationQueueData as Record<string, any>)[key]?.address ?? undefined"
:latitude="(locationQueueData as Record<string, any>)[key]?.latitude ?? 0"
:longitude="(locationQueueData as Record<string, any>)[key]?.longitude ?? 0"
:delay="(locationQueueData as Record<string, any>)[key]?.delay ?? 0"
:end="(locationQueueData as Record<string, any>)[key]?.end ?? undefined"
:status="(locationQueueData as Record<string, any>)[key]?.status ?? undefined"
:type="(locationQueueData as Record<string, any>)[key]?.type ?? undefined"
:instruction="
(locationQueueData as Record<string, any>)[key]?.instruction ?? undefined
"
@item-clicked="zoomToCoords"
@contextmenu.prevent="onDrawerContextMenu($event, key)"
active-class="text-orange"
/>
</q-list>
</q-scroll-area>
</q-tab-panel>
<q-tab-panel name="deleted" class="column no-wrap bg-transparent">
<q-scroll-area :horizontal-thumb-style="{ opacity: '50' }" class="col">
<q-list padding class="q-pb-xl" id="deleted">
<LocationItem
v-for="(key, index) in locationQueueDeletedItems"
:key="key"
:loc_id="key"
:active="
(locationQueueData as Record<string, any>)[key]?.loc_id ===
currentLocation?.loc_id
"
:isCurrentDelay="
(locationQueueData as Record<string, any>)[key]?.loc_id ===
locationQueueDeletedItems[index + 1]
"
:index="index"
:isLast="index == locationQueueDeletedItems.length - 1"
:start="(locationQueueData as Record<string, any>)[key]?.start ?? ''"
:address="(locationQueueData as Record<string, any>)[key]?.address ?? undefined"
:latitude="(locationQueueData as Record<string, any>)[key]?.latitude ?? 0"
:longitude="(locationQueueData as Record<string, any>)[key]?.longitude ?? 0"
:delay="(locationQueueData as Record<string, any>)[key]?.delay ?? 0"
:end="(locationQueueData as Record<string, any>)[key]?.end ?? undefined"
:status="(locationQueueData as Record<string, any>)[key]?.status ?? undefined"
:type="(locationQueueData as Record<string, any>)[key]?.type ?? undefined"
:instruction="
(locationQueueData as Record<string, any>)[key]?.instruction ?? undefined
"
@item-clicked="zoomToCoords"
@contextmenu.prevent="onDrawerContextMenu($event, key)"
active-class="text-orange"
/>
</q-list>
</q-scroll-area>
</q-tab-panel>
</q-tab-panels>
<q-separator />
<q-list padding>
<q-item style="height: 50px; line-height: 58px !important">
<q-item-label>Route Stats</q-item-label>
<q-item-section>[TIME]</q-item-section>
<q-item-section>[DISTANCE]</q-item-section>
</q-item>
</q-list>
<q-menu :target="'#' + panel" context-menu touch-position>
<q-list>
<q-item clickable ripple v-close-popup @click="handleDrawerContextMenu('zoom')">
<q-item-section label="Zoom to Location"> Zoom </q-item-section>
</q-item>
<q-item clickable ripple v-close-popup @click="handleDrawerContextMenu('delete')">
<q-item-section label="Zoom to Location"> Delete </q-item-section>
</q-item>
<q-item clickable ripple v-close-popup @click="handleDrawerContextMenu('gp')">
<q-item-section label="Zoom to Location"> Go Now </q-item-section>
</q-item>
</q-list>
</q-menu>
</div>
</q-drawer>
</template>
<style scoped lang="sass">
.q-tab-panel
padding: 0 !important
.q-item.q-router-link--active, .q-item--active
background-color: $accent
color: $primary
.unpinned
transform: rotate(45deg)
.with-separator
border-top: 1px solid #666
.background-transparent
background-color: transparent !important
</style>
+754
View File
@@ -0,0 +1,754 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
// Leaflet imports
import * as L from 'leaflet';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import {
Icon,
PinCirclePanel,
PinStar,
PinStarPanel,
TackCirclePanel,
TackDiamondBorder,
PointCircle,
} from 'leaflet-extra-markers';
import 'leaflet/dist/leaflet.css';
import '@quasar/extras/mdi-v7/mdi-v7.css';
import { LCircle, LLayerGroup, LMap, LMarker, LTooltip } from '@vue-leaflet/vue-leaflet';
import 'leaflet.locatecontrol/dist/L.Control.Locate.min.css';
// Custom Components
import LRoutingMachine from 'components/LRoutingMachine.vue';
import { useRoutingEvents } from 'src/composables/useRoutingEvents';
import MarkerMenu from 'components/leaflet/MarkerMenu.vue';
// Types
import type { LayerGroup, LeafletMouseEvent, Map } from 'leaflet';
// Stores
import { storeToRefs } from 'pinia';
import { useSocketStore } from 'stores/socket';
import { useSimulationStore } from 'stores/simulation';
import { useLeafletStore } from 'stores/leaflet';
import { useIcloudStore } from 'stores/icloud';
import { useFavoriteStore } from 'stores/favorite';
import { useLocationStore } from 'stores/location';
const socketStore = useSocketStore();
const favoriteStore = useFavoriteStore();
const locationStore = useLocationStore();
const { favorites } = storeToRefs(favoriteStore);
const leafletStore = useLeafletStore();
const { clickedLatLng, center, markerLatLng, layerControl, getOverlayLayer, getBaseLayer } =
storeToRefs(leafletStore);
const simulationStore = useSimulationStore();
const { currentLocation, locationQueueData, locationQueueOrder, locationQueueDeletedItems } =
storeToRefs(simulationStore);
const icloudStore = useIcloudStore();
const { findMyUpdate } = storeToRefs(icloudStore);
const now = ref(Date.now());
const mapReady = ref(false);
const home = computed(() => favorites.value.find((f) => f.name === 'Home'));
const defaultCenter: [number, number] = [0, 0];
const safeCenter = computed<[number, number]>(() => {
const lat = center.value?.[0];
const lng = center.value?.[1];
if (typeof lat === 'number' && typeof lng === 'number') {
return [lat, lng];
}
const homeLat = home.value?.latitude;
const homeLng = home.value?.longitude;
if (typeof homeLat === 'number' && typeof homeLng === 'number') {
return [homeLat, homeLng];
}
return defaultCenter;
});
function updateMarker(event: LeafletMouseEvent) {
markerLatLng.value = [event.latlng.lat, event.latlng.lng];
center.value = [event.latlng.lat, event.latlng.lng];
}
const markerIcon = () => {
return new Icon({
color: 'grey',
accentColor: 'rgba(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi mdi-crosshairs-gps" />`,
contentColor: 'white',
scale: 1,
svg: TackCirclePanel,
});
};
const fmIcon = () => {
return new Icon({
color: 'indianred',
accentColor: 'firebrick',
content: 'FM',
contentColor: 'white',
scale: 0.75,
svg: PinCirclePanel,
});
};
const getFavIcon = (icon: string) => {
return new Icon({
color: '#f5bb39',
accentColor: 'rbga(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi ${icon}" />`,
contentColor: 'black',
scale: 1,
svg: PinStar,
});
};
const safeMarkerLatLng = computed<[number, number] | null>(() => {
const lat = markerLatLng.value?.[0];
const lng = markerLatLng.value?.[1];
if (typeof lat === 'number' && typeof lng === 'number') {
return [lat, lng];
}
return null;
});
const onFavLayerGroupReady = (layerGroup: LayerGroup) => {
leafletStore.setFavLayerGroup(layerGroup);
};
const onLCLayerGroupReady = (layerGroup: LayerGroup) => {
console.log('LCLayerGroup ready');
leafletStore.setLCLayerGroup(layerGroup);
};
const onMapReady = (map: Map) => {
mapReady.value = true;
leafletStore.setMap(map);
leafletStore.setBaseLayer();
};
const { handleRoutesFound } = useRoutingEvents();
const debugRoutingEvent = (event: Event) => {
console.log(`${event.type} event: `, event);
};
const findMyTimePast = computed(() => {
if (findMyUpdate.value) {
const diffInMs = Math.abs(now.value - findMyUpdate.value.timeStamp);
const seconds = Math.floor((diffInMs / 1000) % 60);
const minutes = Math.floor((diffInMs / (1000 * 60)) % 60);
const hours = Math.floor((diffInMs / (1000 * 60 * 60)) % 24);
const days = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
if (days > 1) {
return (
days + ' days, ' + hours + ' hours, ' + minutes + 'minutes, ' + seconds + 'seconds ago'
);
} else if (hours > 1) {
return hours + ' hours, ' + minutes + 'minutes, ' + seconds + 'seconds ago';
} else if (minutes > 1) {
return minutes + ' minutes, ' + seconds + ' seconds ago';
} else {
return seconds + ' seconds ago';
}
} else {
return 'Find My Location not available';
}
});
/*
type AddLocationDialogPayload = {
delay: number | undefined;
address?: string;
};
async function addDialogLocation(latlng: L.LatLng, { delay, address }: AddLocationDialogPayload) {
const revGeo = !address;
const { notificationType, responseMessage } = await useAddLocation(
{ lat: Number(latlng.lat), lng: Number(latlng.lng) },
delay,
revGeo,
address,
);
$q.notify({ type: notificationType, message: responseMessage });
console.log(
'Confirmed location add: latitude: ' +
latlng.lat +
', longitude: ' +
latlng.lng +
', delay: ' +
delay,
);
}
function handleAddLocation() {
if (clickedLatLng.value) {
const latlng = clickedLatLng.value;
closeAllPopups();
$q.notify(`add location...${latlng.toString()}`);
$q.dialog({
component: SetLocationDialog,
componentProps: {
lat: Number(latlng.lat),
lng: Number(latlng.lng),
},
})
.onOk((payload: AddLocationDialogPayload) => {
void addDialogLocation(latlng, payload);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
}
const iconElement = (icon: Icon) => {
return icon.createIcon();
};
const fmIcon = new Icon({
color: 'indianred',
accentColor: 'firebrick',
content: 'FM',
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
const startIcon = new Icon({
color: '#006838',
accentColor: 'rgba(0,0,0,0.25)',
content: 'S',
contentColor: 'white',
scale: 1,
svg: PinTriangle,
});
const endIcon = new Icon({
color: '#a23337',
accentColor: 'rgba(0,0,0,0.25)',
content: 'E',
contentColor: 'white',
scale: 1,
svg: PinSquare,
});
const intermediateIcon = new Icon({
color: 'cornflowerblue',
accentColor: 'rgba(0,0,0,0.25)',
contentColor: 'white',
svg: ChipCircle,
scale: 1,
});
const getFavIcon = (icon: string) => {
return new Icon({
color: '#f5bb39',
accentColor: 'rbga(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi ${icon}" />`,
contentColor: 'black',
scale: 1,
svg: PinStar,
});
};
*/
const getDeletedIcon = (locid: string) => {
const locationIndex = locationQueueDeletedItems.value.indexOf(locid);
const updatedIndex = locationIndex + 1;
return new Icon({
color: 'black',
accentColor: 'black',
content: updatedIndex.toString(),
contentColor: 'white',
scale: 1,
svg: TackDiamondBorder,
});
};
const getCustomIcon = (locid: string) => {
const currentIndex = currentLocation.value.loc_id
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
: null;
const locationIndex = locationQueueOrder.value.indexOf(locid);
const updatedIndex = () => {
if (currentIndex === null) {
return locationIndex + 1;
} else {
return locationIndex - currentIndex;
}
};
const type = locationQueueData.value[locid]?.type;
if (updatedIndex() > 0) {
if (type === 'coord') {
return new Icon({
color: 'blue',
accentColor: 'firebrick',
content: updatedIndex().toString(),
contentColor: 'white',
scale: 0.25,
svg: PointCircle,
});
} else {
return new Icon({
color: 'blue',
accentColor: 'firebrick',
content: updatedIndex().toString(),
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
}
} else if (updatedIndex() < 0) {
if (type === 'coord') {
return new Icon({
color: 'black',
accentColor: 'grey',
content: updatedIndex().toString(),
contentColor: 'white',
scale: 0.25,
svg: PointCircle,
});
} else {
return new Icon({
color: 'black',
accentColor: 'grey',
content: updatedIndex().toString(),
contentColor: 'white',
scale: 1,
svg: PinCirclePanel,
});
}
} else {
if (currentLocation.value && currentLocation.value.loc_id === locid) {
return new Icon({
color: 'pink',
accentColor: 'black',
content: '*',
contentColor: 'black',
scale: 1,
svg: PinStarPanel,
});
}
}
};
const isFavorite = ref(false);
const handleMarkerClick = async (event: LeafletMouseEvent) => {
console.log('marker clicked', event);
// closeAllPopups();
clickedLatLng.value = event.latlng;
const cachedLocation = await locationStore.getCachedLocation(
L.latLng(event.latlng.lat, event.latlng.lng),
);
console.log('cachedLocation: ', cachedLocation);
isFavorite.value = !!cachedLocation?.favorite;
console.log('isFavorite: ', isFavorite.value);
L.DomEvent.stopPropagation(event.originalEvent);
};
const showFindMy = computed(() => {
if (findMyUpdate.value) {
const diffInSec = Math.floor(Math.abs(now.value - findMyUpdate.value.timeStamp) / 1000);
return diffInSec < 120;
} else {
return false;
}
});
/*
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),
radius: favoriteRadius.value,
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');
});
}
}
*/
watch(getBaseLayer, (newVal: number, oldVal: number) => {
console.log('Base layer changed from', oldVal, 'to', newVal);
if (leafletStore.mapObject) {
leafletStore.setBaseLayer(newVal, oldVal);
}
});
watch(getOverlayLayer, () => {
console.log('OverlayLayer toggled');
leafletStore.toggleWmsLayer();
});
const timer = ref();
onMounted(() => {
if (findMyUpdate.value && findMyUpdate.value.latitude && findMyUpdate.value.longitude) {
leafletStore.setCenter(findMyUpdate.value.latitude, findMyUpdate.value.longitude);
} else {
if (home.value) {
if (home.value?.latitude && home.value?.longitude) {
console.log('favorites: home: ', home.value?.latitude, home.value?.longitude);
leafletStore.setCenter(home.value?.latitude, home.value?.longitude);
}
}
}
socketStore.requestUpdate();
timer.value = setInterval(() => {
now.value = Date.now();
}, 1000);
});
onUnmounted(() => {
clearInterval(timer.value);
});
</script>
<template>
<div class="map-wrapper absolute-top-left absolute-bottom-right" style="transform: translateZ(0)">
<L-Map
id="map"
ref="map"
:center="safeCenter"
:zoom="leafletStore.zoom"
style="height: 100%; width: 100%"
@click="updateMarker"
@ready="onMapReady"
:options="{ zoomControl: false }"
>
<!--
<L-Tile-Layer
name="Topography"
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
layer-type="base"
:min-zoom="2"
:max-zoom="17"
no-wrap
attribution="&copy; OpenTopoMap"
/>
<L-Tile-Layer
name="Satellite"
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
layer-type="base"
:min-zoom="2"
:max-zoom="19"
no-wrap
attribution="© ArcGIS"
/>
<L-Tile-Layer
name="OpenStreetMap"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
visible
:min-zoom="2"
:max-zoom="19"
no-wrap
/>
<L-Wms-Tile-Layer
name="OpenStreetMap Transparent"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layers="semitransparent"
transparent
format="image/png"
:max-zoom="19"
:opacity="0.5"
layer-type="overlay"
attribution="© OpenStreetMap"
/>
-->
<L-Layer-Group :visible="layerControl.marker" name="Marker">
<L-Marker
:icon="markerIcon() as any"
v-if="safeMarkerLatLng"
:lat-lng="[safeMarkerLatLng[0] ?? 0, safeMarkerLatLng[1] ?? 0]"
@click="handleMarkerClick"
>
<MarkerMenu
:lat-lng="[safeMarkerLatLng[0] ?? 0, safeMarkerLatLng[1] ?? 0]"
:is-favorite="isFavorite"
/>
</L-Marker>
</L-Layer-Group>
<L-Layer-Group
name="Locate Control"
:visible="layerControl.locateControl"
@ready="onLCLayerGroupReady"
/>
<L-Layer-Group
name="Find My"
:visible="(layerControl.findMy && findMyUpdate && showFindMy) as boolean"
>
<L-Marker
v-if="findMyUpdate"
:icon="fmIcon() as any"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
>
<MarkerMenu
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
:is-favorite="isFavorite"
/>
<L-Tooltip>
{{ findMyTimePast }}
</L-Tooltip>
</L-Marker>
<L-Circle
v-if="findMyUpdate"
:fillOpacity="0.5"
:lat-lng="[findMyUpdate.latitude ?? 0, findMyUpdate.longitude ?? 0]"
:radius="findMyUpdate.horizontalAccuracy"
color="#f5bb39"
fillColor="indianred"
></L-Circle>
</L-Layer-Group>
<L-Layer-Group
:visible="layerControl.favorites"
name="Favorites"
@ready="onFavLayerGroupReady"
>
<template v-for="fav in favorites" :key="fav.id">
<div>
<L-Marker
:icon="getFavIcon(fav.icon ?? 'mdi-star') as any"
:lat-lng="[
(fav as Record<string, any>)?.latitude ?? 0,
(fav as Record<string, any>)?.longitude ?? 0,
]"
@click="handleMarkerClick"
>
<MarkerMenu
:lat-lng="[
(fav as Record<string, any>)?.latitude ?? 0,
(fav as Record<string, any>)?.longitude ?? 0,
]"
:is-favorite
/>
</L-Marker>
<L-Circle
:lat-lng="[
(fav as Record<string, any>)?.latitude ?? 0,
(fav as Record<string, any>)?.longitude ?? 0,
]"
:radius="fav.radius"
color="black"
fillColor="#f5bb39"
:fillOpacity="0.25"
/>
</div>
<l-tooltip>{{ fav.name }}</l-tooltip>
</template>
</L-Layer-Group>
<L-Layer-Group
:visible="layerControl.simQueue && locationQueueOrder.length > 0"
name="Sim Queue"
>
<L-Marker
v-for="locid in locationQueueOrder"
:key="locid"
:icon="getCustomIcon(locid) as any"
:lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
>
<MarkerMenu
:lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
:is-favorite="isFavorite"
/>
</L-Marker>
<L-Marker
v-for="locid in locationQueueDeletedItems"
:key="locid"
:icon="getDeletedIcon(locid) as any"
:lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
>
<MarkerMenu
:lat-lng="[
(locationQueueData as Record<string, any>)[locid]?.latitude ?? 0,
(locationQueueData as Record<string, any>)[locid]?.longitude ?? 0,
]"
:is-favorite="isFavorite"
/>
</L-Marker>
</L-Layer-Group>
<L-Layer-Group name="Route" :visible="layerControl.routing">
<LRoutingMachine
v-bind="leafletStore.routingOptions"
@routingstart="debugRoutingEvent"
@routesfound="handleRoutesFound"
@routingerror="debugRoutingEvent"
/>
</L-Layer-Group>
</L-Map>
</div>
</template>
<style lang="sass">
.l-map
height: 100vh
width: 100vw
.leaflet-routing-container
margin-right: 50px !important
background: rgba(255, 255, 255, 0.2) !important
backdrop-filter: blur(16px) saturate(120%) !important
-webkit-backdrop-filter: blur(16px) saturate(120%) !important
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1) !important
transition: background-color 0.3s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease !important
border: 1px solid rgba(255, 255, 255, 0.4) !important
border-radius: 12px !important
.leaflet-top.leaflet-left, .leaflet-top.leaflet-right, .leaflet-bottom.leaflet-left
display: none !important
.leaflet-layer,
.leaflet-control-attribution
filter: invert(100%) hue-rotate(180deg) brightness(95%) contrast(90%)
.marker-popup .leaflet-popup-content-wrapper, .marker-popup .leaflet-popup-tip
background-color: $dark
.marker-popup .leaflet-popup-content
margin: 13px 10px 13px 10px
.leaflet-circle-actions
pointer-events: none
.leaflet-circle-actions button
border: 2px solid rgba(0,0,0,0.2)
background-clip: padding-box
cursor: pointer
text-align: center
font-weight: bold
box-shadow: 0 1px 5px rgba(0,0,0,0.4)
.circle-btn-accept
background-color: $positive
color: black
.circle-btn-clear
background-color: $negative
color: black
.leaflet-circle-actions button:hover
background-color: #f4f4f4
.leaflet-overlay-pane svg
position: absolute
.small-icon svg
transform: scale(0.6)
transform-origin: center
bottom: 0
right: 0
.small-icon
position: absolute
margin-left: 14px
margin-top: 44px
.leaflet-bar a
background: none !important
.leaflet-container.crosshair-cursor-enabled
cursor: crosshair
.leaflet-control.leaflet-bar
top: 50px
color: #212121 !important
.my-flag-icon__inner
font-size: 35px
position: relative
top: -20px
left: 2px
.my-div-icon
z-index: 2000!important
.my-div-icon__name
line-height: 20px
font-size: .9rem
font-weight: bolder
position: absolute
top: 0
left: 30px
max-width: 200px
text-overflow: ellipsis
overflow: hidden
background-color: rgba(0,0,0,0.5)
color: #fff
border-radius: 5px
padding: 0 5px
border: 1px solid white
box-shadow: 3px 3px 10px #999
.direction
border: 2px solid black
border-radius: 50% 0 50% 50%
background-color: white
opacity: .5
height: 20px
width: 20px
.my-round-marker
height: 10px
border-radius: 50%
background-color: $red-7
transform: scale(1)
box-shadow: 0 0 0 0 rgba(255, 82, 82, 1)
animation: pulse 2s infinite
@keyframes pulse
0%
transform: scale(0.95)
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7)
70%
transform: scale(1)
box-shadow: 0 0 0 10px rgba(255, 82, 82, 0)
100%
transform: scale(0.95)
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0)
</style>
+422
View File
@@ -0,0 +1,422 @@
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue';
import { useQuasar } from 'quasar';
import SetLocationDialog from 'components/SetLocationDialog.vue';
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
import { useAddLocation } from 'composables/useAddLocation';
import { actionCircle } from 'components/leaflet/actionCircle';
import { useLeafletStore } from 'stores/leaflet';
import { useLocationStore } from 'stores/location';
import { useFavoriteStore } from 'stores/favorite';
import { storeToRefs } from 'pinia';
import * as L from 'leaflet';
import { LPopup } from '@vue-leaflet/vue-leaflet';
import { Icon, PinStar } from 'leaflet-extra-markers';
import { startIcon, endIcon, iconElement } from 'constants/icons';
import type { LatLngExpression } from 'leaflet';
import type { Favorite, FavoriteCommands, GeoCacheResponse } from 'components/models';
import FormattedAddress from 'components/FormattedAddress.vue';
const props = defineProps({
latLng: {
type: Object as () => LatLngExpression,
required: true,
},
});
const isFavorite = ref(false);
const favorite = ref();
const loading = ref(true);
const $q = useQuasar();
const favoriteStore = useFavoriteStore();
const locationStore = useLocationStore();
const leafletStore = useLeafletStore();
const { clickedLatLng, routeSet, layerControl, layerGroups } = storeToRefs(leafletStore);
type AddLocationDialogPayload = {
delay: number | undefined;
address?: string;
};
async function addDialogLocation(latlng: L.LatLng, { delay, address }: AddLocationDialogPayload) {
const revGeo = !address;
const { notificationType, responseMessage } = await useAddLocation(
{ lat: Number(latlng.lat), lng: Number(latlng.lng) },
revGeo,
delay,
address,
undefined,
undefined,
);
$q.notify({ type: notificationType, message: responseMessage });
console.log(
'Confirmed location add: latitude: ' +
latlng.lat +
', longitude: ' +
latlng.lng +
', delay: ' +
delay,
);
}
function handleAddLocation() {
if (props.latLng) {
const latlng = props.latLng;
closeAllPopups();
$q.dialog({
component: SetLocationDialog,
componentProps: {
lat: Number(L.latLng(latlng).lat),
lng: Number(L.latLng(latlng).lng),
},
})
.onOk((payload: AddLocationDialogPayload) => {
void addDialogLocation(L.latLng(latlng), payload);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
}
async function addFavorite() {
console.log('Adding favorite for', props.latLng);
if (props.latLng) {
const defaultRadius = 50;
const latlng = props.latLng;
layerControl.value.favorites = true;
layerControl.value.marker = false;
let geoCacheResp: GeoCacheResponse | undefined = await locationStore.getCachedLocation(latlng);
if (geoCacheResp) {
console.log('Using cached location for favorite:', geoCacheResp);
} else {
console.log('No cached location found, creating new favorite');
}
if (layerGroups.value.favorites) {
closeAllPopups();
const favoriteIcon = new Icon({
color: '#f5bb39',
accentColor: 'rbga(0,0,0,0.25)',
contentHtml: '<icon class="q-icon mdi mdi-star" />',
contentColor: 'black',
scale: 1,
svg: PinStar,
});
const favoriteMarker = L.marker(L.latLng(latlng), {
icon: favoriteIcon,
draggable: true,
contextmenu: false,
contextmenuItems: [],
});
const circle = actionCircle(L.latLng(latlng), {
radius: defaultRadius,
color: 'black',
fillColor: '#f5bb39',
fillOpacity: 0.25,
onAccept: (instance) => {
console.log(
'Confirm favorite coordinates: ' +
instance.getLatLng().toString() +
', radius: ' +
instance.getRadius() +
', geoCacheResp: ',
);
$q.notify(`Adding favorite...${instance.getLatLng().toString()}`);
$q.dialog({
component: EditFavoriteDialog,
componentProps: {
lat: Number(instance.getLatLng().lat),
lng: Number(instance.getLatLng().lng),
radius: instance.getRadius(),
operation: 'set',
geoCacheResp: geoCacheResp,
},
})
.onOk((response: { operation: FavoriteCommands; data: Favorite }) => {
void favoriteStore.favoriteControl({
command: response.operation,
favorite: response.data,
});
newFavGrp.clearLayers();
layerControl.value.marker = true;
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
},
onClear: () => {
console.log('Clear favorite radius');
newFavGrp.clearLayers();
layerControl.value.marker = true;
},
});
const handleLatLng = (latlng: L.LatLngExpression, dist: number = 50): L.LatLngExpression => {
const lat = L.latLng(latlng).lat;
const lng = L.latLng(latlng).lng;
const brearing = 270;
const brng = (brearing * Math.PI) / 180;
const R = 6371e3;
const lat1 = (lat * Math.PI) / 180;
const lon1 = (lng * Math.PI) / 180;
const lat2 = Math.asin(
Math.sin(lat1) * Math.cos(dist / R) +
Math.cos(lat1) * Math.sin(dist / R) * Math.cos(brng),
);
const lon2 =
lon1 +
Math.atan2(
Math.sin(brng) * Math.sin(dist / R) * Math.cos(lat1),
Math.cos(dist / R) - Math.sin(lat1) * Math.sin(lat2),
);
return [(lat2 * 180) / Math.PI, (lon2 * 180) / Math.PI];
};
const handleIcon = L.divIcon({
html: '<i class="q-icon mdi mdi-hand-pointing-right text-black" style="font-size: 32px;" />',
className: 'custom-marker-with-buttons',
iconSize: [32, 32],
iconAnchor: [28, 16],
});
const handleMarker = L.marker(handleLatLng(latlng), {
draggable: true,
icon: handleIcon,
contextmenu: false,
contextmenuItems: [],
});
const newFavGrp: L.FeatureGroup = L.featureGroup([favoriteMarker, circle, handleMarker]);
leafletStore.addToFavLayer(newFavGrp);
let startPosition: L.LatLng;
handleMarker.on('dragstart', function (e) {
startPosition = e.target.getLatLng();
});
handleMarker.on('drag', function (e) {
const newLatLng = e.target.getLatLng();
const circleLatLng = circle.getLatLng();
let lng: number;
if (newLatLng.lng > circleLatLng.lng) {
lng = circleLatLng.lng;
} else {
lng = newLatLng.lng;
}
const forcedLatLng = L.latLng({ lat: startPosition.lat, lng: lng });
handleMarker.setLatLng(forcedLatLng);
const newRadius = leafletStore.getDistance(circleLatLng, forcedLatLng);
if (newRadius) {
circle.setRadius(newRadius);
}
});
favoriteMarker.on('drag', function (e) {
const newLatLng = e.target.getLatLng();
circle.setLatLng(newLatLng);
handleMarker.setLatLng(handleLatLng(newLatLng, circle.getRadius()));
});
favoriteMarker.on('dragend', function (e) {
const newLatLng = e.target.getLatLng();
void (async () => {
try {
geoCacheResp = await locationStore.getCachedLocation(newLatLng);
console.log('Marker dragend');
} catch (error) {
console.error('Error handling marker dragend:', error);
}
})();
});
}
}
leafletStore.zoom = 20;
}
/*
function clearFavorite() {
closeAllPopups();
if (!props.latLng) return;
const latlng = L.latLng(props.latLng);
let geoCacheResp: GeoCacheResponse | undefined;
void (async () => {
try {
geoCacheResp = await locationStore.getCachedLocation(latlng);
} catch (error) {
console.error('Error finding cached location:', error);
}
if (geoCacheResp?.favorite) {
$q.notify(`Clearing favorite...${geoCacheResp.favorite.name?.toString()}`);
$q.dialog({
component: EditFavoriteDialog,
componentProps: {
lat: Number(latlng.lat),
lng: Number(latlng.lng),
radius: geoCacheResp.favorite.radius,
operation: 'clear',
geoCacheResp: geoCacheResp,
},
})
.onOk((response: { operation: FavoriteCommands; data: Favorite }) => {
void favoriteStore.favoriteControl({
command: response.operation,
favorite: response.data,
});
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
})();
}
*/
function closeAllPopups() {
leafletStore.closeAllPopups();
}
const setStartRoute = () => {
leafletStore.layerControl.routing = true;
if (!clickedLatLng.value) return;
closeAllPopups();
leafletStore.routeSet.start = clickedLatLng.value;
console.log('setStartRoute: ', routeSet.value.start);
};
const setEndRoute = () => {
leafletStore.layerControl.routing = true;
if (!clickedLatLng.value) return;
leafletStore.routeSet.end = clickedLatLng.value;
closeAllPopups();
console.log('setEndRoute: ', leafletStore.routeSet.end);
leafletStore.updateRouteWaypoints();
console.log('updating Route');
};
onBeforeMount(async () => {
try {
console.log('Fetching geoCacheResp...');
const geoCacheResp = await locationStore.getCachedLocation(props.latLng);
console.log('geoCacheResp: ', geoCacheResp);
if (geoCacheResp?.favorite) {
favorite.value = geoCacheResp.favorite;
}
isFavorite.value = !!geoCacheResp?.favorite;
} catch (error) {
console.error('Error fetching cached location:', error);
throw error;
} finally {
loading.value = false;
}
});
</script>
<template>
<L-Popup :options="{ closeOnClick: true, closeButton: false, className: 'marker-popup' }">
<q-list dense seperator style="min-width: 100px" class="bg-grey-10">
<q-item v-if="isFavorite" class="text-center">
<q-item-section>
<q-item-label>{{ favorite.name }}</q-item-label>
<q-item-label caption lines="2" v-if="favorite.address"
><FormattedAddress :address="favorite.address"></FormattedAddress
></q-item-label>
</q-item-section>
</q-item>
<q-separator v-if="isFavorite" class="q-my-sm" />
<q-item clickable v-ripple @click="handleAddLocation">
<q-item-section avatar>
<q-avatar icon="mdi-map-marker" color="accent" 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 color="accent" size="sm">
<q-icon class="small-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 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 v-if="loading">
<q-item-section avatar>
<q-spinner-hourglass color="accent" size="2em" />
</q-item-section>
<q-item-section>Loading...</q-item-section>
</q-item>
<q-item clickable v-ripple @click="addFavorite" v-else-if="!isFavorite && !loading">
<q-item-section avatar>
<q-avatar icon="mdi-star-outline" color="accent" text-color="yellow" size="sm" />
</q-item-section>
<q-item-section>Set Favorite</q-item-section>
</q-item>
<q-item clickable v-ripple @click="addFavorite" v-else-if="isFavorite && !loading">
<q-item-section avatar>
<q-avatar icon="mdi-star-outline" color="accent" text-color="yellow" size="sm" />
</q-item-section>
<q-item-section>Edit Favorite</q-item-section>
</q-item>
</q-list>
</L-Popup>
</template>
<style lang="sass">
.marker-popup .leaflet-popup-content-wrapper, .marker-popup .leaflet-popup-tip
background-color: $dark
.marker-popup .leaflet-popup-content
margin: 13px 10px 13px 10px
.leaflet-circle-actions
pointer-events: none
.leaflet-circle-actions button
border: 2px solid rgba(0,0,0,0.2)
background-clip: padding-box
cursor: pointer
text-align: center
font-weight: bold
box-shadow: 0 1px 5px rgba(0,0,0,0.4)
.circle-btn-accept
background-color: $positive
color: black
.circle-btn-clear
background-color: $negative
color: black
.leaflet-circle-actions button:hover
background-color: #f4f4f4
.leaflet-overlay-pane svg
position: absolute
.small-icon svg
transform: scale(0.6)
transform-origin: center
bottom: 0
right: 0
.small-icon
position: absolute
margin-left: 14px
margin-top: 44px
</style>
+227
View File
@@ -0,0 +1,227 @@
<script setup lang="ts">
import { useLeafletStore } from 'stores/leaflet';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { zoomTo } from 'src/composables/useZoom';
import { useQuasar } from 'quasar';
import { useAddLocation, type AddLocationResult } from 'src/composables/useAddLocation';
const $q = useQuasar();
const leafletStore = useLeafletStore();
const { leafletDrawerOpen, layerControl, routeSet, routeDirections, routeCoordinates } =
storeToRefs(leafletStore);
const routeIsSet = computed(() => {
return (
routeSet.value.start.lat &&
routeSet.value.start.lng &&
routeSet.value.end.lat &&
routeSet.value.end.lng
);
});
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
async function routeToQueue() {
if (routeSet.value.start && routeSet.value.end && routeDirections) {
if (routeDirections.value && routeDirections.value.length > 0) {
for (const direction of routeDirections.value) {
if (direction.coordinates) {
await useAddLocation(
{ lat: Number(direction.coordinates.lat), lng: Number(direction.coordinates.lng) },
false,
direction.time,
undefined,
'instruction',
direction.text,
);
}
await delay(1000);
}
}
}
}
async function routeAllPointsToQueue() {
let result: AddLocationResult = {
notificationType: 'negative',
responseMessage: 'No route points to add',
};
if (routeSet.value.start && routeSet.value.end && routeCoordinates) {
if (routeCoordinates.value && routeCoordinates.value.length > 0) {
for (const coord of routeCoordinates.value) {
if (coord) {
const type = coord.instruction ? 'instruction' : 'coord';
result = await useAddLocation(
{ lat: Number(coord.lat), lng: Number(coord.lng) },
false,
coord.timeFromPrev,
undefined,
type,
coord.instruction,
);
}
await delay(1000);
}
}
}
$q.notify({ type: result.notificationType, message: result.responseMessage });
}
/* function routeToQueue() {
console.log('routeToQueue');
if (routeSet.value.start && routeSet.value.end && routeSegments) {
console.log('routeToQueue: start: ', routeSet.value.start);
setLocation(
{ lat: Number(routeSet.value.start.lat), lng: Number(routeSet.value.start.lng) },
0,
);
if (routeSegments.value) {
routeSegments.value.forEach((segment: routeSegments) => {
console.log('routeToQueue: segment: ', segment);
setLocation(
{ lat: Number(segment.toCoordinates.lat), lng: Number(segment.toCoordinates.lng) },
segment.timeSeconds,
);
});
}
console.log('routeToQueue: end: ', routeSet.value.end);
setLocation({ lat: Number(routeSet.value.end.lat), lng: Number(routeSet.value.end.lng) }, 0);
}
}
*/
function clearRoute() {
leafletStore.layerControl.routing = false;
leafletStore.clearRouteSegments();
routeSet.value.start = { lat: null, lng: null };
routeSet.value.end = { lat: null, lng: null };
$q.notify({ type: 'positive', message: 'Route cleared' });
}
</script>
<template>
<q-toolbar>
<q-btn
class="q-mr-sm"
dense
flat
icon="mdi-menu"
round
size="sm"
@click="leafletDrawerOpen = !leafletDrawerOpen"
/>
<q-btn-dropdown flat label="Zoom To" size="sm" stretch>
<q-list>
<q-item v-close-popup v-ripple clickable @click="zoomTo('fmLoc')">
<q-item-section>
<q-item-label>Find My Location</q-item-label>
</q-item-section>
</q-item>
<q-item v-close-popup v-ripple clickable @click="zoomTo('simLoc')">
<q-item-section>
<q-item-label>Sim Location</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn-dropdown label="Routing" size="sm" stretch v-if="routeIsSet">
<q-list>
<q-item v-close-popup v-ripple clickable @click="routeToQueue">
<q-item-section>
<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>
<q-item v-close-popup v-ripple clickable @click="clearRoute">
<q-item-section>
<q-item-label>Clear</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn-dropdown label="Map Layers" size="sm" stretch>
<q-list dense class="bg-primary">
<q-item dense tag="label" v-ripple>
<q-item-section>
<q-item-label>Marker</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
dense
color="accent"
v-model="layerControl.marker"
@update:model-value="leafletStore.onLayerToggle"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Sim Queue</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
dense
color="accent"
v-model="layerControl.simQueue"
@update:model-value="leafletStore.onLayerToggle"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Favorites</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
dense
color="accent"
v-model="layerControl.favorites"
@update:model-value="leafletStore.onLayerToggle"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>Routing</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
dense
color="accent"
v-model="layerControl.routing"
@update:model-value="leafletStore.onLayerToggle"
/>
</q-item-section>
</q-item>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label>FindMy</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
dense
color="accent"
v-model="layerControl.findMy"
@update:model-value="leafletStore.onLayerToggle"
/>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar>
</template>
<style scoped lang="sass">
.leafletFooter
height: 48px
</style>
+126
View File
@@ -0,0 +1,126 @@
import * as L from 'leaflet';
/**
* Options interface extending standard Circle options
* to include custom callback hooks.
*/
interface ActionCircleOptions extends L.CircleOptions {
onAccept: (instance: ActionCircle) => void;
onClear: (instance: ActionCircle) => void;
}
class ActionCircle extends L.Circle {
declare options: ActionCircleOptions;
private _btnContainer: HTMLDivElement | null = null;
constructor(latlng: L.LatLngExpression, options: ActionCircleOptions) {
super(latlng, options);
}
// Called when the circle is added to the map
override onAdd(map: L.Map): this {
super.onAdd(map);
this._initUI(map);
// map.on('zoom, zoomend, move, moveend', this._repositionUI, this);
// Keep buttons synced with circle position during pan/zoom
map.on('zoom, zoomend', this._repositionUI, this);
map.on('move, moveend', this._repositionUI, this);
map.on('viewreset', this._repositionUI, this);
return this;
}
override setLatLng(latLng: L.LatLngExpression): this {
super.setLatLng(latLng);
this._repositionUI();
return this;
}
// Called when circle.remove() or map.removeLayer(circle) is executed
override onRemove(map: L.Map): this {
if (this._btnContainer) {
L.DomUtil.remove(this._btnContainer);
this._btnContainer = null;
}
map.off('zoom, zoomend', this._repositionUI, this);
map.off('move, moveend', this._repositionUI, this);
map.off('viewreset', this._repositionUI, this);
super.onRemove(map);
return this;
}
override setRadius(radius: number): this {
super.setRadius(radius);
this._repositionUI();
return this;
}
private _initUI(map: L.Map): void {
// Create container in the markerPane (above the circle path)
const container = L.DomUtil.create('div', 'leaflet-circle-actions', map.getPane('markerPane'));
this._btnContainer = container;
// Prevent map clicks/drags from triggering when interacting with buttons
L.DomEvent.disableClickPropagation(container);
// Accept Button
const acceptBtn = L.DomUtil.create('button', 'circle-btn-accept', container);
L.DomUtil.create('i', 'q-icon mdi mdi-check', acceptBtn);
L.DomEvent.on(acceptBtn, 'click', (e) => {
L.DomEvent.stopPropagation(e);
if (this.options.onAccept) this.options.onAccept(this);
});
// Clear Button
const clearBtn = L.DomUtil.create('button', 'circle-btn-clear', container);
L.DomUtil.create('i', 'q-icon mdi mdi-close', clearBtn);
L.DomEvent.on(clearBtn, 'click', (e) => {
L.DomEvent.stopPropagation(e);
if (this.options.onClear) this.options.onClear(this);
this.remove(); // Self-destruct
});
this._repositionUI();
}
private readonly _repositionUI = (): void => {
if (!this._btnContainer || !this._map) return;
const bounds = this.getBounds();
const southEast = bounds.getSouthEast();
const pixelPoint = this._map.latLngToLayerPoint(southEast);
const radius = this.getRadius();
const radiusRat = radius / 1000;
const translate = `translate(${radiusRat}, ${radiusRat})`;
/*
if (radius < 30) {
translate = 'translate(25%, 25%)';
}
if (radius > 200) {
translate = 'translate(75%, 75%)';
}
*/
// Convert LatLng center to pixel coordinates
// const centerPoint = this._map.latLngToLayerPoint(this.getLatLng());
// Apply styles to overlay the center
Object.assign(this._btnContainer.style, {
position: 'absolute',
left: `${pixelPoint.x}px`,
top: `${pixelPoint.y}px`,
zIndex: '1000',
display: 'flex',
gap: '8px',
transform: translate, // Centering trick
pointerEvents: 'auto',
});
};
}
// Factory function for cleaner initialization
export const actionCircle = (latlng: L.LatLngExpression, options: ActionCircleOptions) => {
return new ActionCircle(latlng, options);
};
+274 -308
View File
@@ -1,112 +1,10 @@
/*
export interface Meta {
totalCount: number;
}
import type { ComputedRef, shallowRef } from 'vue';
import type * as L from 'leaflet';
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
export interface CtrlAttrs {
[key: string]: CtrlAttr;
}
export interface CtrlAttr {
name: string;
cmd: string;
cmdClass: string;
icon: string;
cnfrm: boolean;
delay: number;
}
export interface DevCtrlAttr {
name: string;
cmd: DeviceCommands;
cmdClass: 'device_control';
icon: string;
cnfrm: boolean;
delay: number;
}
*/
export type FavoriteCommands = 'set' | 'delete' | 'get';
export type SimulationCommands =
'restart'
| 'start'
| 'pause'
| 'resume'
| 'reset'
| 'clear'
| 'end'
| 'add'
| 'test-mode'
| 'delete'
| 'next'
| 'gps-noise';
export type DeviceCommands = 'shutdown' | 'reboot';
export type TunneldCommands =
| 'start'
| 'start-watcher'
| 'end-watcher'
| 'shutdown'
| 'restart'
| 'clear'
| 'cancel';
export interface LocationQueue {
[key: string]: LocationMark;
}
export interface LocationMarkUpdateResponse {
command_status: string;
command: string;
command_class: string;
message: string;
data: LocationMark;
}
export interface QueueOrderUpdateResponse {
command_status: string;
command: string;
command_class: string;
message: string;
data: string[];
}
export interface LocationMark {
loc_id: string;
latitude: number | undefined | null;
longitude: number | undefined | null;
address?: string | undefined | null;
delay?: number | undefined | null;
start: string | undefined | null;
status: string | undefined | null;
end?: string | undefined | null;
}
// SERVER TO CLIENT
export interface ServerToClientEvents {
// built-in
noArg: () => void;
withAck: (a: string, callback: (b: number) => void) => void;
message: (e: string) => void;
error: (data: ErrorFull) => void;
// testing
test_prompt: (callback: (s: string) => void) => void;
// deprecated
simulation_status: (c: SimulationStatus) => void;
// future
device_status: (d: DeviceStatus) => void;
// in-use
status: (d: StatusUpdate) => void;
app_error: (data: AppError) => void;
icloud_2fa_request: (callback: (e: number) => void) => void;
fmf_update: (d: FindMyUpdate) => void;
queue_data_update: (d: QueueData) => void;
location_item_update: (d: LocationItemUpdate) => void;
}
export interface AppError {
type: string;
message: string;
@@ -120,90 +18,129 @@ export interface AppError {
};
}
export interface LocationItemUpdate {
loc_id: string;
data: LocationMark;
}
export interface SimulationStatus {
loc_id: string;
latitude: number;
longitude: number;
}
export interface icloudData {
consumer_queue: string | number | boolean;
consumer_task: string | boolean;
monitor_enabled: boolean;
monitor_task: string | boolean;
monitor_running: boolean;
}
export interface QueueData {
active: boolean;
data: LocationQueue;
order: string[];
deleted_items: string[];
state: string | undefined | null;
worker_task: string | undefined | null;
gps_noise: boolean;
test_mode: boolean;
}
export interface StatusUpdate {
connected_clients: { [key: string]: string };
current_location: {
loc_id: string;
latitude: number | undefined | null;
longitude: number | undefined | null;
start?: string | undefined | null;
};
device_name: string | undefined | null;
fmf_location: FindMyUpdate | undefined | null;
icloud: icloudData;
next_move?: number | undefined | null;
set_location_enabled: boolean;
simulation_queue: QueueData
tunnel: string | undefined | null;
tunnel_watcher_running: boolean;
device_count?: number | undefined | null;
udid?: string | null;
product_version?: string | null | undefined;
phone_number?: string | null | undefined;
developer_mode_enabled?: boolean | undefined | null;
ddi_mounted?: boolean;
rsd_address?: string | null | undefined;
rsd_port?: number | undefined | null;
lockdown_trusted_port?: number | undefined | null;
lockdown_untrusted_port?: number | undefined | null;
lockdown_trusted_reachable?: boolean | undefined | null;
lockdown_untrusted_reachable?: boolean | undefined | null;
dtservicehub_reachable?: boolean | undefined | null;
}
interface DeviceStatus {
device_connected: boolean;
device_count: number;
udid?: string | null;
device_name?: string | null;
product_version?: string | null;
phone_number?: string | null;
developer_mode_enabled?: boolean;
ddi_mounted?: boolean;
rsd_address?: string | null;
rsd_port?: number;
lockdown_trusted_port?: number;
lockdown_untrusted_port?: number;
lockdown_trusted_reachable?: boolean;
lockdown_untrusted_reachable?: boolean;
dtservicehub_reachable?: boolean;
}
export interface ErrorFull {
type: string;
error: string;
}
// Testing
export interface SendTestPromptResp {
prompt_name: string;
command_status: string;
message?: string;
}
export interface TestPromptData {
prompt_name: string;
}
export type TestPromptReturn = {
confirmed: boolean;
};
export interface ServerToClientEvents {
// built-in
noArg: () => void;
withAck: (a: string, callback: (b: number) => void) => void;
message: (e: string) => void;
error: (data: ErrorFull) => void;
// testing
test_prompt: (data: TestPromptData, callback: (s: TestPromptReturn) => void) => void;
// deprecated
/*
simulation_status: (c: SimulationStatus) => void;
fmf_update: (d: FindMyUpdate) => void;
queue_data_update: (d: QueueData) => void;
location_item_update: (d: LocationItemUpdate) => void;
device_status: (d: DeviceStatus) => void;
*/
// in-use
status: (d: StatusUpdate) => void;
app_error: (data: AppError) => void;
icloud_2fa_request: (callback: (e: number) => void) => void;
}
export interface StatusUpdate {
device?: DeviceStatusUpdate;
simulation?: SimulationStatusUpdate;
tunneld?: {
tunnel: string | undefined | null;
tunnel_watcher_running: boolean;
};
icloud?: icloudStatusUpdate;
socket?: {
connected_clients: { [key: string]: string };
};
}
interface DeviceStatusUpdate {
device_connected?: boolean;
uptime?: number | undefined;
udid?: string | null;
device_name?: string | null;
}
export interface SimulationState {
currentLocation: {
loc_id: string | null;
latitude: number | null;
longitude: number | null;
};
gpsNoise: boolean | null;
locationQueueData: LocationQueue;
locationQueueDeletedItems: string[];
locationQueueOrder: string[];
simulationQueueLength: number | null | undefined;
simulationRunning: boolean | undefined | null;
simulationState: string | null | undefined;
testMode: boolean | null | undefined;
workerTask: string | undefined | null;
}
export interface SimulationStatusUpdate {
active?: boolean;
data?: LocationQueue;
order?: string[];
deleted_items?: string[];
state?: string | undefined | null;
worker_task?: string | undefined | null;
gps_noise?: boolean;
test_mode?: boolean;
current_location?: {
loc_id?: string;
latitude?: number;
longitude?: number;
};
}
export interface LocationQueue {
[key: string]: LocationItem;
}
export interface LocationItem {
loc_id: string;
latitude: number;
longitude: number ;
address?: string;
delay?: number | null;
start: string | null;
status?: string | null;
end?: string | null;
type?: string | undefined | null;
instruction?: boolean | null;
}
export interface icloudStatusUpdate {
consumer_queue?: string | number | boolean;
consumer_task?: string | boolean;
monitor_enabled?: boolean;
monitor_task?: string | boolean;
monitor_running?: boolean;
fmf_location?: FindMyUpdate | undefined | null;
}
export interface FindMyUpdate {
altitude: number;
batteryLevel: number;
@@ -217,15 +154,16 @@ export interface FindMyUpdate {
verticalAccuracy: number;
}
// END SERVER TO CLIENT
// CLIENT TO SERVER
export interface ClientToServerEvents {
// built-in
message: (e: string, callback: (b: boolean, r: string) => void) => void;
// testing
send_test_prompt: (p: string, callback: (response: string) => void) => void;
send_test_prompt: (
args: { prompt_name: string },
callback: (response: SendTestPromptResp) => void,
) => void;
// status
request_update: (callback: (response: StatusUpdate) => void) => void;
// control
@@ -234,14 +172,21 @@ export interface ClientToServerEvents {
command: TunneldCommands;
udid?: string;
},
callback?: (response: TunneldControlResponse) => void,
callback?: (response: CommandResponse) => void,
) => void;
favorite_control: (
args: {
command: FavoriteCommands;
favorite?: Favorite | undefined | null;
},
callback: (response: FavoriteControlResponse) => void,
callback: (response: FavControlResponse) => void,
) => void;
category_control: (
args: {
command: CategoryCommands;
category?: Category | undefined | null;
},
callback: (response: FavControlResponse) => void,
) => void;
simulation_control: (
args: {
@@ -251,21 +196,23 @@ export interface ClientToServerEvents {
delay?: number | null | undefined;
loc_id?: string | null | undefined;
address?: NominatimResponse | string | null | undefined;
type?: 'instruction' | 'coord' | null | undefined;
instruction?: string | null | undefined;
},
callback: (response: SimulationControlResponse) => void,
callback: (response: CommandResponse) => void,
) => void;
device_control: (
args: {
command: DeviceCommands;
delay?: number;
},
callback?: (response: DeviceControlResponse) => void,
callback?: (response: CommandResponse) => void,
) => void;
icloud_monitor_control: (
args: {
command: string;
},
callback?: (response: iCloudMonitorResponse) => void,
callback?: (response: CommandResponse) => void,
) => void;
// data updates
location_item_update: (
@@ -274,13 +221,13 @@ export interface ClientToServerEvents {
key: string;
value: string | number;
},
callback?: (response: LocationMarkUpdateResponse) => void,
callback?: (response: CommandResponse) => void,
) => void;
queue_order_update: (
args: {
newOrder: string[];
},
callback?: (response: QueueOrderUpdateResponse) => void,
callback?: (response: CommandResponse) => void,
) => void;
// data requests
reverse_geocode: (
@@ -292,126 +239,52 @@ export interface ClientToServerEvents {
) => void;
}
export interface FavoriteControlResponse {
command_status: string;
command: FavoriteCommands;
command_class: string;
data?: {
favorites: Favorite[] | undefined | null;
};
message?: string | undefined;
}
// Commands
export type FavoriteCommands = 'set' | 'delete' | 'get' | 'edit';
export type CategoryCommands = 'get' | 'add' | 'delete' | 'edit';
export type SimulationCommands =
| 'restart'
| 'start'
| 'pause'
| 'resume'
| 'reset'
| 'clear'
| 'end'
| 'add'
| 'test-mode'
| 'delete'
| 'next'
| 'gps-noise';
export type DeviceCommands = 'shutdown' | 'reboot';
export type TunneldCommands =
| 'start'
| 'start-watcher'
| 'end-watcher'
| 'shutdown'
| 'restart'
| 'clear'
| 'cancel';
export interface SimulationControlResponse {
command_status: string;
command: SimulationCommands;
command_class: string;
data?: SimulationControlResponseData | undefined | null;
message?: string | undefined;
}
interface SimulationControlResponseData {
simulation_queue?: QueueData;
location_item?: LocationMark;
}
interface DeviceControlResponse {
command_status: string;
command_class: string;
command: DeviceCommands;
message?: string;
}
interface TunneldControlResponse {
command_status: string;
command_class: string;
command: TunneldCommands;
message?: string;
}
export interface iCloudMonitorResponse {
export interface CommandResponse {
command_status: string;
command: string;
command_class: string;
icloud_monitor_enabled?: boolean | undefined | null;
icloud_monitor_running?: boolean | undefined | null;
message?: string | undefined | null;
error?: string | undefined | null;
message: string;
data?: StatusUpdate;
}
// END CLIENT TO SERVER
export interface coords {
lat: number;
lng: number;
export interface FavControlResponse {
command_status: string;
command: string;
command_class: string;
message?: string;
data?: {
categories?: Category[];
favorites?: Favorite[];
}
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
export interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
}
export interface CurrentLocation {
loc_id: string;
latitude: number | null | undefined;
longitude: number | null | undefined;
// start_time?: string | null | undefined;
// end_time?: string | null | undefined
next_move?: number | null | undefined;
}
export interface NextLocation {
loc_id: string;
latitude: number;
longitude: number;
time_at_location?: number | null;
}
/*
export interface NominatimReverseResponse {
place_id: number;
licence: string;
osm_type: string;
osm_id: number;
lat: string;
lon: string;
class: string;
type: string;
place_rank: number;
importance: number;
addresstype: string;
name: string;
display_name: string;
address: NominatimAddress;
boundingbox: string[];
}
export interface NominatimAddress {
house_number: string;
road: string;
village?: string;
city?: string;
county: string;
state: string;
'ISO3166-2-lvl4': string;
postcode: string;
country: string;
country_code: string;
}
*/
export interface routeSegments {
fromWaypoint: number;
toWaypoint: number;
@@ -453,12 +326,51 @@ export interface RouteSet {
wayPoints?: [number, number][] | [null, null] | [undefined, undefined] | null | undefined;
}
export interface Favorite {
name: string | null | undefined;
export interface NewFavorite {
id?: number;
name?: string;
latitude: number;
longitude: number;
icon: string | null;
category: string;
icon?: string;
category_id?: number;
address: NominatimResponse;
radius: number;
}
export interface SearchResult {
x: number;
y: number;
label: string;
bounds: [[number, number], [number, number]] | null;
}
export interface Favorite {
id: number;
name: string;
latitude: number;
longitude: number;
icon: string;
category_id: number;
address: NominatimResponse;
radius: number;
openedFab?: boolean | null;
}
export interface NewCategory {
id?: number;
name?: string;
icon?: string;
color?: string;
favorite_list?: number[];
}
export interface Category {
id: number;
name: string;
icon: string;
color: string;
favorite_list?: number[];
favorites: Favorite[];
openedFab?: boolean | null;
}
export interface GeoCacheResponse {
@@ -490,3 +402,57 @@ export interface NominatimRequest {
latitude: number;
longitude: number;
}
export interface ControlAction {
name: string;
cmd: string;
cmdClass: string;
icon: ComputedRef<string | null | undefined> | string;
cnfrm: boolean;
disabled: boolean;
delay: number;
iconColor: ComputedRef<string | null | undefined> | string;
isVisible?: ComputedRef<false | true | null | undefined> | boolean;
}
export type ControlCategoryKey = 'simulation' | 'device' | 'icloudmonitor' | 'tunneld' | 'routing';
export interface ReturnedStatusMessage {
status: string;
message: string;
}
export interface BaseLayer {
value: number;
label: string;
layer: ReturnType<typeof shallowRef<L.TileLayer>>;
}
export interface BaseLayerWMS {
selected: boolean;
label: string;
layer: ReturnType<typeof shallowRef<L.TileLayer.WMS>>;
}
export interface coords {
lat: number;
lng: number;
}
export interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
position: 'topleft' | 'topright' | 'bottomleft' | 'bottomright';
}
+604
View File
@@ -0,0 +1,604 @@
import type { ComputedRef, shallowRef } from 'vue';
export type FavoriteCommands = 'set' | 'delete' | 'get' | 'edit';
export type CategoryCommands = 'get' | 'add' | 'delete' | 'edit';
export type SimulationCommands =
| 'restart'
| 'start'
| 'pause'
| 'resume'
| 'reset'
| 'clear'
| 'end'
| 'add'
| 'test-mode'
| 'delete'
| 'next'
| 'gps-noise';
export type DeviceCommands = 'shutdown' | 'reboot';
export type TunneldCommands =
| 'start'
| 'start-watcher'
| 'end-watcher'
| 'shutdown'
| 'restart'
| 'clear'
| 'cancel';
export interface LocationQueue {
[key: string]: LocationMark;
}
export interface CommandResponse {
command_status: string;
command: string;
command_class: string;
message: string;
data?: StatusUpdate;
}
export interface LocationMarkUpdateResponse {
command_status: string;
command: string;
command_class: string;
message: string;
data: LocationMark;
}
export interface QueueOrderUpdateResponse {
command_status: string;
command: string;
command_class: string;
message: string;
data: string[];
}
export interface LocationMark {
loc_id: string;
latitude: number | undefined | null;
longitude: number | undefined | null;
address?: string | undefined | null;
delay?: number | undefined | null;
start: string | undefined | null;
status: string | undefined | null;
end?: string | undefined | null;
type?: string | undefined | null;
instruction?: boolean | undefined | null;
}
// SERVER TO CLIENT
export interface ServerToClientEvents {
// built-in
noArg: () => void;
withAck: (a: string, callback: (b: number) => void) => void;
message: (e: string) => void;
error: (data: ErrorFull) => void;
// testing
test_prompt: (data: TestPromptData, callback: (s: TestPromptReturn) => void) => void;
// deprecated
simulation_status: (c: SimulationStatus) => void;
// future
device_status: (d: DeviceStatus) => void;
// in-use
status: (d: StatusUpdate) => void;
app_error: (data: AppError) => void;
icloud_2fa_request: (callback: (e: number) => void) => void;
fmf_update: (d: FindMyUpdate) => void;
queue_data_update: (d: QueueData) => void;
location_item_update: (d: LocationItemUpdate) => void;
}
export interface AppError {
type: string;
message: string;
error: string;
data: {
udids?: string | null | undefined;
udid?: string | null | undefined;
disconnected_udids?: string | null | undefined;
tunnel_timeout_seconds?: string | null | undefined;
dvt_timeout_seconds?: string | null | undefined;
};
}
export interface LocationItemUpdate {
loc_id: string;
data: LocationMark;
}
export interface SimulationStatus {
loc_id: string;
latitude: number;
longitude: number;
}
export interface icloudData {
consumer_queue: string | number | boolean;
consumer_task: string | boolean;
monitor_enabled: boolean;
monitor_task: string | boolean;
monitor_running: boolean;
fmf_location: FindMyUpdate | undefined | null;
}
export interface SimulationState {
currentLocation: {
loc_id: string | null;
latitude: number | null;
longitude: number | null;
};
gpsNoise: boolean | null;
locationQueueData: LocationQueue;
locationQueueDeletedItems: string[];
locationQueueOrder: string[];
simulationQueueLength: number | null | undefined;
simulationRunning: boolean | undefined | null;
simulationState: string | null | undefined;
testMode: boolean | null | undefined;
workerTask: string | undefined | null;
}
export interface SimulationStatusUpdate {
active?: boolean;
data?: LocationQueue;
order?: string[];
deleted_items?: string[];
state?: string | undefined | null;
worker_task?: string | undefined | null;
gps_noise?: boolean;
test_mode?: boolean;
current_location?: {
loc_id?: string;
latitude?: number;
longitude?: number;
};
}
export interface QueueData {
active: boolean;
data: LocationQueue;
order: string[];
deleted_items: string[];
state: string | undefined | null;
worker_task: string | undefined | null;
gps_noise: boolean;
test_mode: boolean;
current_location?: string;
}
export interface StatusUpdate {
device?: DeviceStatus;
simulation?: SimulationStatusUpdate;
tunneld?: {
tunnel: string | undefined | null;
tunnel_watcher_running: boolean;
};
icloud?: icloudData;
socket?: {
connected_clients: { [key: string]: string };
};
}
interface DeviceStatus {
device_connected?: boolean;
uptime?: number | undefined;
device_count?: number;
udid?: string | null;
device_name?: string | null;
product_version?: string | null;
phone_number?: string | null;
developer_mode_enabled?: boolean;
ddi_mounted?: boolean;
rsd_address?: string | null;
rsd_port?: number;
lockdown_trusted_port?: number;
lockdown_untrusted_port?: number;
lockdown_trusted_reachable?: boolean;
lockdown_untrusted_reachable?: boolean;
dtservicehub_reachable?: boolean;
}
export interface ErrorFull {
type: string;
error: string;
}
export interface FindMyUpdate {
altitude: number;
batteryLevel: number;
deviceDisplayName: string;
deviceStatus: number;
horizontalAccuracy: number;
latitude: number;
longitude: number;
name: string;
timeStamp: number;
verticalAccuracy: number;
}
// END SERVER TO CLIENT
// CLIENT TO SERVER
export interface ClientToServerEvents {
// built-in
message: (e: string, callback: (b: boolean, r: string) => void) => void;
// testing
send_test_prompt: (
args: { prompt_name: string },
callback: (response: SendTestPromptResp) => void,
) => void;
// status
request_update: (callback: (response: StatusUpdate) => void) => void;
// control
tunneld_control: (
args: {
command: TunneldCommands;
udid?: string;
},
callback?: (response: TunneldControlResponse) => void,
) => void;
favorite_control: (
args: {
command: FavoriteCommands;
favorite?: Favorite | undefined | null;
},
callback: (response: FavoriteControlResponse) => void,
) => void;
category_control: (
args: {
command: CategoryCommands;
category?: Category | undefined | null;
},
callback: (response: CategoryControlResponse) => void,
) => void;
simulation_control: (
args: {
command: SimulationCommands;
latitude?: number | null | undefined;
longitude?: number | null | undefined;
delay?: number | null | undefined;
loc_id?: string | null | undefined;
address?: NominatimResponse | string | null | undefined;
type?: 'instruction' | 'coord' | null | undefined;
instruction?: string | null | undefined;
},
callback: (response: SimulationControlResponse) => void,
) => void;
device_control: (
args: {
command: DeviceCommands;
delay?: number;
},
callback?: (response: DeviceControlResponse) => void,
) => void;
icloud_monitor_control: (
args: {
command: string;
},
callback?: (response: iCloudMonitorResponse) => void,
) => void;
// data updates
location_item_update: (
args: {
loc_id: string;
key: string;
value: string | number;
},
callback?: (response: LocationMarkUpdateResponse) => void,
) => void;
queue_order_update: (
args: {
newOrder: string[];
},
callback?: (response: QueueOrderUpdateResponse) => void,
) => void;
// data requests
reverse_geocode: (
args: {
latitude: number;
longitude: number;
},
callback?: (response: GeoCacheResponse) => void,
) => void;
}
export interface SendTestPromptResp {
prompt_name: string;
command_status: string;
message?: string;
}
export interface TestPromptData {
prompt_name: string;
}
export type TestPromptReturn = {
confirmed: boolean;
};
export interface FavoriteControlResponse {
command_status: string;
command: FavoriteCommands;
command_class: string;
data?: {
favorites: Favorite[] | undefined | null;
};
message?: string | undefined;
}
export interface CategoryControlResponse {
command_status: string;
command: CategoryCommands;
command_class: string;
data?: {
categories: Category[] | undefined | null;
};
message?: string | undefined;
}
export interface SimulationControlResponse {
command_status: string;
command: SimulationCommands;
command_class: string;
data?: SimulationStatusUpdate;
message: string;
}
interface SimulationControlResponseData {
simulation_queue?: QueueData;
location_item?: LocationMark;
}
interface DeviceControlResponse {
command_status: string;
command_class: string;
command: DeviceCommands;
message?: string;
}
interface TunneldControlResponse {
command_status: string;
command_class: string;
command: TunneldCommands;
message?: string;
}
export interface iCloudMonitorResponse {
command_status: string;
command: string;
command_class: string;
icloud_monitor_enabled?: boolean | undefined | null;
icloud_monitor_running?: boolean | undefined | null;
message?: string | undefined | null;
error?: string | undefined | null;
data?: FindMyUpdate | undefined | null;
}
// END CLIENT TO SERVER
export interface coords {
lat: number;
lng: number;
}
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
import type * as L from 'leaflet';
export interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
position: 'topleft' | 'topright' | 'bottomleft' | 'bottomright';
}
export interface CurrentLocation {
loc_id: string;
latitude: number | null | undefined;
longitude: number | null | undefined;
// start_time?: string | null | undefined;
// end_time?: string | null | undefined
next_move?: number | null | undefined;
}
export interface NextLocation {
loc_id: string;
latitude: number;
longitude: number;
time_at_location?: number | null;
}
/*
export interface NominatimReverseResponse {
place_id: number;
licence: string;
osm_type: string;
osm_id: number;
lat: string;
lon: string;
class: string;
type: string;
place_rank: number;
importance: number;
addresstype: string;
name: string;
display_name: string;
address: NominatimAddress;
boundingbox: string[];
}
export interface NominatimAddress {
house_number: string;
road: string;
village?: string;
city?: string;
county: string;
state: string;
'ISO3166-2-lvl4': string;
postcode: string;
country: string;
country_code: string;
}
*/
export interface routeSegments {
fromWaypoint: number;
toWaypoint: number;
distanceMeters: number;
timeSeconds: number;
toCoordinates: LatLng;
}
export interface RoutesSet {
[key: string]: RouteSet;
}
export interface LatLng {
lat: number | null | undefined;
lng: number | null | undefined;
}
export interface routeDirections {
dirIndex?: number | undefined;
coordinateIndex: number;
text?: string | undefined;
distance?: number | undefined;
time?: number | undefined;
coordinates: LatLng | null | undefined;
}
export interface routeCoordinates {
coordinateIndex: number;
lat: number;
lng: number;
distanceFromPrev: number;
timeFromPrev: number;
instruction: string;
}
export interface RouteSet {
start: [number, number] | [null, null] | [undefined, undefined] | null | undefined;
end: [number, number] | [null, null] | [undefined, undefined] | null | undefined;
wayPoints?: [number, number][] | [null, null] | [undefined, undefined] | null | undefined;
}
export interface NewFavorite {
id?: number;
name?: string;
latitude: number;
longitude: number;
icon?: string;
category_id?: number;
address: NominatimResponse;
radius: number;
}
export interface SearchResult {
x: number;
y: number;
label: string;
bounds: [[number, number], [number, number]] | null;
}
export interface Favorite {
id: number;
name: string;
latitude: number;
longitude: number;
icon: string;
category_id: number;
address: NominatimResponse;
radius: number;
openedFab?: boolean | null;
}
export interface NewCategory {
id?: number;
name?: string;
icon?: string;
color?: string;
favorite_list?: number[];
}
export interface Category {
id: number;
name: string;
icon: string;
color: string;
favorite_list?: number[];
favorites: Favorite[];
openedFab?: boolean | null;
}
export interface GeoCacheResponse {
address: NominatimResponse;
favorite?: Favorite;
}
// TypeScript Interface for Reverse Geocoding Response
export interface NominatimResponse {
shop?: string | null | undefined;
leisure?: string | null | undefined;
house_number?: string | null | undefined;
road: string;
neighbourhood?: string | null | undefined;
suburb?: string | null | undefined;
county: string;
town?: string | null | undefined;
city?: string | null | undefined;
village?: string | null | undefined;
municipality?: string | null | undefined;
state: string;
'ISO3166-2-lvl4': string;
postcode: string;
country: string;
country_code: string;
}
export interface NominatimRequest {
latitude: number;
longitude: number;
}
export interface ControlCategories {
[key: string]: ControlAction[];
}
export interface ControlAction {
name: string;
cmd: string;
cmdClass: string;
icon: ComputedRef<string | null | undefined> | string;
cnfrm: boolean;
disabled: boolean;
delay: number;
iconColor: ComputedRef<string | null | undefined> | string;
isVisible?: ComputedRef<false | true | null | undefined> | boolean;
}
export type ControlCategoryKey = 'simulation' | 'device' | 'icloudmonitor' | 'tunneld' | 'routing';
export interface ReturnedStatusMessage {
status: string;
message: string;
}
export interface BaseLayer {
value: number;
label: string;
layer: ReturnType<typeof shallowRef<L.TileLayer>>;
}
export interface BaseLayerWMS {
selected: boolean;
label: string;
layer: ReturnType<typeof shallowRef<L.TileLayer.WMS>>;
}
+91
View File
@@ -0,0 +1,91 @@
import { socket } from 'boot/socketio';
import { useLocationStore } from 'stores/location';
import { useSimulationStore } from 'stores/simulation';
import type { coords, NominatimResponse, CommandResponse } from 'components/models';
import * as L from 'leaflet';
export type AddLocationResult = {
notificationType: 'positive' | 'negative';
responseMessage: string;
};
function addLocationRequest({
coords,
delay,
address,
type,
instruction,
}: {
coords: coords;
delay?: number | null | undefined;
address?: NominatimResponse | string | null;
type?: 'instruction' | 'coord' | null | undefined;
instruction?: string | null | undefined;
}) {
return new Promise<CommandResponse>((resolve) => {
socket.emit(
'simulation_control',
{
command: 'add',
latitude: coords.lat,
longitude: coords.lng,
delay,
address,
type,
instruction,
},
(response) => {
resolve(response);
},
);
});
}
export async function useAddLocation(
coords: coords,
getRevGeocode: boolean,
delay?: number | null,
address?: NominatimResponse | string | null,
type?: 'instruction' | 'coord' | null,
instruction?: string | null,
): Promise<AddLocationResult> {
const simulationStore = useSimulationStore();
const locationStore = useLocationStore();
try {
const revGeo =
!address && getRevGeocode
/* ? await reverseGeocodeRateLimited(coords.lat, coords.lng) */
? await locationStore.getCachedLocation(L.latLng(coords.lat, coords.lng))
: undefined;
const addy = address ?? revGeo?.address ?? '';
const response = await addLocationRequest({ coords, delay, address: addy, type, instruction });
if (response.command_status === 'ERROR') {
return {
notificationType: 'negative',
responseMessage: 'Failed to add location: ' + (response.message ?? 'Unknown error'),
};
}
if (!response.data?.simulation) {
return {
notificationType: 'negative',
responseMessage: response.message ?? 'Simulation queue data missing',
};
}
simulationStore.digestUpdate(response.data.simulation);
return {
notificationType: 'positive',
responseMessage: response.message ?? 'Location added',
};
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Error adding location:', error);
return {
notificationType: 'negative',
responseMessage: 'Failed to add location: ' + message,
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import { onMounted, onUnmounted } from 'vue';
export function useEscKey(callback: () => void) {
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
callback();
}
};
onMounted(() => {
window.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
});
}
+44 -11
View File
@@ -1,11 +1,13 @@
import { ref, type Ref } from 'vue';
import * as LeafLet from 'leaflet';
import * as L from 'leaflet';
import type { LeafletMouseEvent } from 'leaflet';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
import { useLocationStore } from 'stores/location';
import { useLeafletStore } from 'stores/leaflet';
type RouteSet = {
start?: LeafLet.LatLng | null | undefined;
end?: LeafLet.LatLng | null | undefined;
start?: L.LatLng | null | undefined;
end?: L.LatLng | null | undefined;
};
type RouteSetLike = {
@@ -14,35 +16,64 @@ type RouteSetLike = {
[key: string]: unknown;
};
const locationStore = useLocationStore();
const leafletStore = useLeafletStore();
export function useMarkerContextMenu(
routeSet: Ref<RouteSetLike>,
updateRoute: () => void,
closeAllPopups: () => void,
) {
const clickedLatLng = ref<LeafLet.LatLng | null>(null);
const clickedLatLng = ref<L.LatLng | null>(null);
const isFavorite = ref(false);
const handleMarkerClick = async (event: LeafletMouseEvent) => {
console.log('marker clicked', event);
closeAllPopups();
clickedLatLng.value = event.latlng;
const resp = await reverseGeocodeRateLimited(Number(event.latlng.lat), Number(event.latlng.lng));
isFavorite.value = !!resp.favorite;
LeafLet.DomEvent.stopPropagation(event.originalEvent);
const resp = await locationStore.getCachedLocation(
L.latLng(Number(event.latlng.lat), Number(event.latlng.lng)),
);
/* const resp = await reverseGeocodeRateLimited(
Number(event.latlng.lat),
Number(event.latlng.lng),
);
*/
isFavorite.value = !!resp?.favorite;
console.log('isFavorite: %s, address: %s', isFavorite.value, resp?.address?.road);
L.DomEvent.stopPropagation(event.originalEvent);
};
const handleMarkerDrag = (latlng: L.LatLng) => {
clickedLatLng.value = latlng;
};
const handleMarkerDragEnd = async (latlng: L.LatLng) => {
clickedLatLng.value = latlng;
const resp = await locationStore.getCachedLocation(latlng);
isFavorite.value = !!resp?.favorite;
/*
const resp = await reverseGeocodeRateLimited(Number(latlng.lat), Number(latlng.lng));
*/
};
const setStartRoute = () => {
leafletStore.layerControl.routing = true;
if (!clickedLatLng.value) return;
closeAllPopups();
(routeSet.value as RouteSet).start = clickedLatLng.value;
(leafletStore.routeSet as RouteSet).start = clickedLatLng.value;
console.log('setStartRoute: ', routeSet.value.start);
};
const setEndRoute = () => {
leafletStore.layerControl.routing = true;
if (!clickedLatLng.value) return;
(routeSet.value as RouteSet).end = clickedLatLng.value;
(leafletStore.routeSet as RouteSet).end = clickedLatLng.value;
closeAllPopups();
console.log('setEndRoute: ', routeSet.value.end);
console.log('setEndRoute: ', leafletStore.routeSet.end);
updateRoute();
console.log('updating Route');
};
@@ -53,5 +84,7 @@ export function useMarkerContextMenu(
handleMarkerClick,
setStartRoute,
setEndRoute,
handleMarkerDrag,
handleMarkerDragEnd,
};
}
+46
View File
@@ -0,0 +1,46 @@
import { useQuasar } from 'quasar';
import { useLeafletStore } from 'stores/leaflet';
import { useIcloudStore } from 'stores/icloud';
import { useSimulationStore } from 'stores/simulation';
const $q = useQuasar();
const leafletStore = useLeafletStore();
const simulationStore = useSimulationStore();
const icloudStore = useIcloudStore();
export function zoomTo(loc: string) {
switch (loc) {
case 'fmLoc':
if (
icloudStore.findMyUpdate &&
icloudStore.findMyUpdate.latitude &&
icloudStore.findMyUpdate.longitude
) {
leafletStore.setCenter(
icloudStore.findMyUpdate.latitude,
icloudStore.findMyUpdate.longitude,
);
} else {
$q.notify({ type: 'negative', message: 'Find My Location not available' });
}
break;
case 'simLoc':
if (
simulationStore.currentLocation &&
simulationStore.currentLocation.latitude &&
simulationStore.currentLocation.longitude
) {
leafletStore.setCenter(
simulationStore.currentLocation.latitude,
simulationStore.currentLocation.longitude,
);
} else {
$q.notify({ type: 'negative', message: 'Simulation Location not available' });
}
break;
default:
$q.notify({ type: 'negative', message: 'Invalid location' });
break;
}
}
+329 -12
View File
@@ -1,12 +1,73 @@
export const controls = {
import { computed, type WritableComputedRef, type ComputedRef } from 'vue';
import { useIcloudStore } from 'stores/icloud';
import { useLeafletStore } from 'stores/leaflet';
import { useSimulationStore } from 'stores/simulation';
import { useTunneldStore } from 'stores/tunneld';
import type { ControlAction, ControlCategoryKey } from 'components/models';
export type ControlCategoryView = {
key: ControlCategoryKey;
name: string;
icon: string;
openedFab: WritableComputedRef<boolean>;
isVisible: boolean;
commands: ControlCommandView[];
};
export type ControlCommandView = ControlAction & {
key: string;
renderKey: string;
icon: string;
iconColor: string;
disabled: boolean;
isVisible: boolean;
};
type ControlCategoryDefinition = {
key: ControlCategoryKey;
name: string;
icon: string;
commands: Record<string, ControlAction>;
};
const baseControls = {
simulation: {
key: 'simulation',
name: 'Simulation',
icon: 'mdi-map-marker-multiple',
commands: {
testMode: {
name: 'Test Mode',
cmd: 'test-mode',
cmdClass: 'sim_cntrl_class',
icon: 'mdi-test-tube-off',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
gpsNoise: {
name: 'GPS Noise',
cmd: 'gps-noise',
cmdClass: 'sim_cntrl_class',
icon: 'mdi-satellite-variant',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
start: {
name: 'Start Location Sim',
name: 'Start',
cmd: 'start',
cmdClass: 'sim_cntrl_class',
icon: 'mdi-play',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
pause: {
name: 'Pause Location Sim',
@@ -14,7 +75,10 @@ export const controls = {
cmdClass: 'sim_cntrl_class',
icon: 'mdi-pause',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
resume: {
name: 'Resume Location Simulation',
@@ -22,23 +86,32 @@ export const controls = {
cmdClass: 'sim_cntrl_class',
icon: 'mdi-play-pause',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
clear: {
name: 'Clear Future Items',
cmd: 'clear',
cmdClass: 'sim_cntrl_class',
icon: 'msi-map-marker-remove',
icon: 'mdi-map-marker-remove',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
reset: {
name: 'Reset Location Queue',
cmd: 'reset',
cmdClass: 'sim_cntrl_class',
icon: 'mdi-restart',
cnfrm: true,
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
end: {
name: 'End Location Sim',
@@ -46,17 +119,28 @@ export const controls = {
cmdClass: 'sim_cntrl_class',
icon: 'mdi-stop',
cnfrm: true,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
},
},
device: {
key: 'device',
name: 'Device',
icon: 'mdi-raspberry-pi',
commands: {
shutdown: {
name: 'Shutdown',
cmd: 'shutdown',
cmdClass: 'dev_cntrl_class',
icon: 'mdi-power',
cnfrm: true,
disabled: false,
delay: 5,
iconColor: 'grey-10',
isVisible: true,
},
reboot: {
name: 'Reboot',
@@ -64,35 +148,57 @@ export const controls = {
cmdClass: 'dev_cntrl_class',
icon: 'mdi-restart',
cnfrm: true,
disabled: false,
delay: 5,
iconColor: 'grey-10',
isVisible: true,
},
},
},
icloudmonitor: {
key: 'icloudmonitor',
icon: 'mdi-cloud',
name: 'iCloud Monitor',
commands: {
start: {
name: 'Start iCloud Monitor',
name: 'Start',
cmd: 'start',
cmdClass: 'icloud-monitor_cntrl_class',
icon: 'mdi-play',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
stop: {
name: 'Stop iCloud Monitor',
name: 'Stop',
cmd: 'stop',
cmdClass: 'icloud-monitor_cntrl_class',
icon: 'mdi-stop',
cnfrm: false,
delay: 0,
disabled: false,
delay: 5,
iconColor: 'grey-10',
isVisible: true,
},
},
},
tunneld: {
key: 'tunneld',
name: 'tunneld',
icon: 'mdi-subway',
commands: {
start: {
name: 'Start Tunneld',
name: 'Start',
cmd: 'start',
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-play',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
start_watcher: {
name: 'Start TunneldWatcher',
@@ -100,7 +206,10 @@ export const controls = {
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-play',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
end_watcher: {
name: 'End TunneldWatcher',
@@ -108,39 +217,247 @@ export const controls = {
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-stop',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
clear: {
name: 'Clear Tunneld',
name: 'Clear',
cmd: 'clear',
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-backspace',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
cancel: {
name: 'Cancel Tunneld',
name: 'Cancel',
cmd: 'cancel',
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-cancel',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
restart: {
name: 'Restart Tunneld',
name: 'Restart',
cmd: 'restart',
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-restart',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
shutdown: {
name: 'Shutdown Tunneld',
name: 'Shutdown',
cmd: 'shutdown',
cmdClass: 'tunneld_cntrl_class',
icon: 'mdi-stop',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
},
},
routing: {
key: 'routing',
name: 'routing',
icon: 'mdi-routes',
commands: {
clear: {
name: 'Clear',
cmd: 'clear',
cmdClass: 'routing_cntrl_class',
icon: 'mdi-backspace',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
waypoints_to_queue: {
name: 'Waypoints to Simulation Queue',
cmd: 'waypoints_to_simq',
cmdClass: 'routing_cntrl_class',
icon: 'mdi-directions',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
allpoints_to_queue: {
name: 'All Points to Simulation Queue',
cmd: 'points_to_simq',
cmdClass: 'routing_cntrl_class',
icon: 'mdi-map-marker-path',
cnfrm: false,
disabled: false,
delay: 0,
iconColor: 'grey-10',
isVisible: true,
},
},
},
} satisfies Record<ControlCategoryKey, ControlCategoryDefinition>;
function renderKey(
commandKey: string,
command: Pick<ControlCommandView, 'icon' | 'iconColor' | 'disabled' | 'isVisible'>,
) {
return [commandKey, command.icon, command.iconColor, command.disabled, command.isVisible].join(
':',
);
}
export function useControls() {
const simulationStore = useSimulationStore();
const icloudStore = useIcloudStore();
const tunneldStore = useTunneldStore();
const leafletStore = useLeafletStore();
const openedFabFor = (key: ControlCategoryKey) =>
computed({
get() {
return leafletStore.fabs.appControl.openedFab === key;
},
set(val) {
if (val) leafletStore.fabs.appControl.openedFab = key;
},
});
const categoryIsVisible = {
simulation: computed(() => {
return true;
}),
device: computed(() => {
return true;
}),
icloudmonitor: computed(() => {
return true;
}),
tunneld: computed(() => {
return true;
}),
routing: computed(() => {
return Boolean(leafletStore.routeSet.start.lng && leafletStore.routeSet.end.lng);
}),
} satisfies Record<ControlCategoryKey, ComputedRef<boolean>>;
const openedFabs = {
simulation: openedFabFor('simulation'),
device: openedFabFor('device'),
icloudmonitor: openedFabFor('icloudmonitor'),
tunneld: openedFabFor('tunneld'),
routing: openedFabFor('routing'),
} satisfies Record<ControlCategoryKey, WritableComputedRef<boolean>>;
const actions = baseControls;
const categories = computed<ControlCategoryView[]>(() =>
Object.values(baseControls).map((category) => {
const commands = Object.entries(category.commands).map(([commandKey, action]) => {
const command: ControlCommandView = {
...action,
key: commandKey,
renderKey: '',
icon: action.icon,
iconColor: action.iconColor,
disabled: Boolean(action.disabled),
isVisible: Boolean(action.isVisible),
};
if (category.key === 'simulation') {
const queueLength = simulationStore.simulationQueueLength ?? 0;
if (commandKey === 'testMode') {
command.icon = simulationStore.testMode ? 'mdi-test-tube' : 'mdi-test-tube-off';
command.iconColor = simulationStore.testMode ? 'white' : 'grey-10';
command.disabled = Boolean(simulationStore.simulationRunning);
}
if (commandKey === 'gpsNoise') {
command.iconColor = simulationStore.gpsNoise ? 'white' : 'grey-10';
}
if (commandKey === 'start') {
command.isVisible = !simulationStore.simulationRunning;
}
if (commandKey === 'pause') {
command.isVisible =
simulationStore.simulationState === 'RUNNING' &&
Boolean(simulationStore.simulationRunning);
}
if (commandKey === 'resume') {
command.isVisible =
simulationStore.simulationState === 'PAUSED' &&
Boolean(simulationStore.simulationRunning);
}
if (commandKey === 'clear') {
command.isVisible = queueLength > 0 && !simulationStore.isLastLocationItem;
}
if (commandKey === 'reset') {
command.isVisible = queueLength > 0;
}
if (commandKey === 'end') {
command.isVisible = Boolean(simulationStore.simulationRunning);
}
}
if (category.key === 'icloudmonitor') {
if (commandKey === 'start') command.isVisible = !icloudStore.icloudMonitor;
if (commandKey === 'stop') command.isVisible = Boolean(icloudStore.icloudMonitor);
}
if (category.key === 'tunneld') {
if (commandKey === 'start') command.isVisible = !tunneldStore.tunnelConnected;
if (commandKey === 'start_watcher') command.isVisible = !tunneldStore.tunneldWatcher;
if (commandKey === 'end_watcher')
command.isVisible = Boolean(tunneldStore.tunneldWatcher);
if (['clear', 'cancel', 'shutdown'].includes(commandKey)) {
command.isVisible = Boolean(tunneldStore.tunnelConnected);
}
}
if (category.key === 'routing') {
if (commandKey === 'waypoints_to_queue')
command.isVisible = !!leafletStore.routeDirections;
if (commandKey === 'allpoints_to_queue')
command.isVisible = !!leafletStore.routeCoordinates;
if (commandKey === 'clear') command.isVisible = Boolean(leafletStore.routeSet);
}
command.renderKey = renderKey(commandKey, command);
return command;
});
return {
key: category.key,
name: category.name,
icon: category.icon,
openedFab: openedFabs[category.key],
isVisible: categoryIsVisible[category.key].value,
commands,
};
}),
);
return {
actions,
categories,
};
}
+72
View File
@@ -0,0 +1,72 @@
import {
Icon,
PinCirclePanel,
PointCircle,
PinTriangle,
PinSquare,
ChipCircle,
PinStar,
} from 'leaflet-extra-markers';
import 'leaflet/dist/leaflet.css';
import '@quasar/extras/mdi-v7/mdi-v7.css';
export const iconElement = (icon: Icon) => {
return icon.createIcon();
};
export const markerIcon = new Icon({
color: 'black',
accentColor: 'rgba(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi mdi-crosshairs-gps" />`,
contentColor: 'white',
scale: 1,
svg: PointCircle,
});
export const fmIcon = new Icon({
color: 'indianred',
accentColor: 'firebrick',
content: 'FM',
contentColor: 'white',
scale: 2,
svg: PinCirclePanel,
});
export const startIcon = new Icon({
color: '#006838',
accentColor: 'rgba(0,0,0,0.25)',
content: 'S',
contentColor: 'white',
scale: 1,
svg: PinTriangle,
});
export const intermediateIcon = new Icon({
color: 'cornflowerblue',
accentColor: 'rgba(0,0,0,0.25)',
contentColor: 'white',
svg: ChipCircle,
scale: 1,
});
export const endIcon = new Icon({
color: '#a23337',
accentColor: 'rgba(0,0,0,0.25)',
content: 'E',
contentColor: 'white',
scale: 1,
svg: PinSquare,
});
export const getFavIcon = (icon: string) => {
return new Icon({
color: '#f5bb39',
accentColor: 'rbga(0,0,0,0.25)',
contentHtml: `<icon class="q-icon mdi ${icon}" />`,
contentColor: 'black',
scale: 1,
svg: PinStar,
});
};
+81
View File
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue';
import { useMeta } from 'quasar';
import { useLeafletStore } from 'stores/leaflet';
import { useSimulationStore } from 'stores/simulation';
import { useSocketStore } from 'stores/socket';
import { useIcloudStore } from 'stores/icloud';
import { useFavoriteStore } from 'stores/favorite';
import type { LeafletMouseEvent } from 'leaflet';
import LocationDrawer from 'components/leaflet/LocationDrawer.vue';
useMeta({
meta: {
viewport: {
name: 'viewport',
content:
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover',
},
},
});
function layoutClickHandler(event: LeafletMouseEvent) {
// if (!leafletStore.locationDrawerSettings.opened && !leafletStore.controlDrawerSettings.opened && !leafletStore.statusBarSettings.opened) {
if (!leafletStore.locationDrawerSettings.opened && !leafletStore.fabs.statusBar) {
return;
}
if (
!event.target.closest('#location-drawer') &&
!leafletStore.locationDrawerSettings.pinned &&
leafletStore.locationDrawerSettings.opened
) {
leafletStore.locationsOpenedHandler(false);
}
if (!event.target.closest('#status-bar') && leafletStore.fabs.statusBar) {
leafletStore.fabs.statusBar.opened = false;
}
/*
if (!event.target.closest('#control-drawer') && !leafletStore.controlDrawerSettings.pinned) {
leafletStore.locationsOpenedHandler(false);
}
if(!event.target.closest('#status-bar') || !event.target.closest('#status-bar-button')) {
leafletStore.statusBarOpenedHandler(false);
}
*/
}
const leafletStore = useLeafletStore();
const simulationStore = useSimulationStore();
const socketStore = useSocketStore();
const icloudStore = useIcloudStore();
const favoriteStore = useFavoriteStore();
onMounted(() => {
socketStore.bindEvents();
simulationStore.bindEvents();
icloudStore.bindEvents();
socketStore.connect();
favoriteStore.initialize();
watch([favoriteStore.categories, favoriteStore.favorites], () => {
favoriteStore.mapCategoriesAndFavorites();
});
});
</script>
<template>
<q-layout ref="layout" view="hHh LpR lFf" @click.stop="layoutClickHandler">
<LocationDrawer persistent :breakpoint="576" side="left" class="location-drawer" />
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped lang="sass">
.transparent-footer
background-color: rgba(0, 0, 0, 0.5)
box-shadow: none
backdrop-filter: blur(5px)
</style>
+3 -35
View File
@@ -1,41 +1,11 @@
<template>
<q-layout view="hhh lpr fff">
<q-header>
<MenuBar @drawer="drawer = !drawer" />
<MenuBar />
</q-header>
<q-footer>
<StatusBar />
</q-footer>
<!--
<q-drawer
v-model="drawer"
:width="200"
:breakpoint="500"
overlay
:class="$q.dark.isActive ? 'bg-grey-8' : 'bg-grey-3'"
>
<q-scroll-area class="fit">
<q-list>
<template v-for="(menuItem, index) in menuList" :key="index">
<q-item
clickable
:active="menuItem.route === route.name"
v-ripple
@click="$router.push({ name: menuItem.route })"
>
<q-item-section avatar>
<q-icon :name="menuItem.icon" />
</q-item-section>
<q-item-section>
{{ menuItem.label }}
</q-item-section>
</q-item>
<q-separator :key="'sep' + index" v-if="menuItem.separator" />
</template>
</q-list>
</q-scroll-area>
</q-drawer>
-->
<q-page-container>
<router-view />
</q-page-container>
@@ -43,7 +13,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { onMounted } from 'vue';
import { useSimulationStore } from 'stores/simulation';
import { useSocketStore } from 'stores/socket';
@@ -51,15 +21,13 @@ import { useIcloudStore } from 'stores/icloud';
import { useFavoriteStore } from 'stores/favorite';
import MenuBar from 'components/MenuBar.vue';
import StatusBar from 'components/StatusBar.vue';
import StatusBar from '../components/StatusBar.vue';
const simulationStore = useSimulationStore();
const socketStore = useSocketStore();
const icloudStore = useIcloudStore();
const favoriteStore = useFavoriteStore();
const drawer = ref(false);
onMounted(() => {
socketStore.bindEvents();
simulationStore.bindEvents();
+327
View File
@@ -0,0 +1,327 @@
<template>
<q-page>
<q-btn
small
round
flat
size="md"
class="floated drawermenuleft bg-ios-blur-brdr-h text-grey-10"
icon="mdi-menu"
:disable="simulationStore.simulationQueueLength === 0"
v-if="!leafletStore.locationDrawerSettings.opened"
@click.stop="leafletStore.locationsOpenedHandler(!locationDrawerSettings.opened)"
/>
<q-avatar
class="floated logo"
:class="[
leafletStore.locationDrawerSettings.opened && !leafletStore.locationDrawerSettings.pinned? 'shift-logo' : '',
simulationStore.testMode ? 'bg-yellow' : 'bg-ios-blur',
]"
>
<q-img class="logo" src="~assets/simloc_logo2.png" />
</q-avatar>
<q-btn
small
flat
round
v-if="errors.length"
color="bg-grey-9"
size="md"
icon="mdi-bell"
class="floated notifications bg-ios-blur-h text-grey-9"
@click="resetNewErrorsCounter"
>
<q-chip
v-if="newErrorsCounter"
color="red"
class="absolute-top-right q-pa-xs text-white"
style="font-size: 0.6rem"
>{{ newErrorsCounter }}</q-chip
>
<q-menu fit>
<q-list no-border link separator style="max-height: 200px" class="scroll">
<q-item v-for="(error, index) in errors" :key="index" style="cursor: default">
<q-item-section>
<q-item-label>{{ error }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<LocateControl class="floated locate-control" />
<q-btn
small
flat
round
size="0.8em"
class="floated geosearch bg-ios-blur-brdr-h"
@click="geoSearchForm = !geoSearchForm"
>
<q-icon
name="mdi-map-search"
:color="geoSearchForm ? 'white' : 'grey-10'"
:class="{ 'rotate-180': geoSearchForm }"
class="transition-rotate"
/>
</q-btn>
<div class="geosearch-container">
<GeoSearch
class="geosearchform"
v-if="geoSearchForm"
@update:visible="geoSearchForm = $event"
/>
</div>
<div class="floated zoom-controls q-py-md q-gutter-y-md column items-start">
<q-btn-group flat round class="zoom-rocker">
<q-btn
small
flat
round
size="0.8em"
class="zoom-in"
icon="mdi-plus-thick"
color="grey-10"
@click="leafletStore.zoomIn"
/>
<q-btn
small
flat
round
size="0.8em"
class="zoom-out"
icon="mdi-minus-thick"
color="grey-10"
@click="leafletStore.zoomOut"
/>
</q-btn-group>
</div>
<q-btn
small
flat
round
size="0.8em"
class="floated base-layers bg-ios-blur-brdr-h"
@click="baseLayerSelect = true"
>
<q-icon
name="mdi-layers-triple-outline"
:color="baseLayerSelect ? 'white' : 'grey-10'"
:class="{ 'rotate-180': baseLayerSelect }"
class="transition-rotate"
/>
</q-btn>
<q-dialog v-model="baseLayerSelect" class="bg-ios-blur">
<q-card style="width: 300px" class="q-px-sm q-pb-md bg-ios-blur-brdr">
<q-card-section>
<div class="text-h6">Base Layers</div>
</q-card-section>
<q-card-section>
<q-option-group
v-model="leafletStore.baseLayer.selected"
:options="leafletStore.baseLayer.options"
type="radio"
color="accent"
/>
<q-checkbox
color="accent"
v-model="leafletStore.baseLayerWMS.selected"
label="OSM Transparent Overlay"
/>
</q-card-section>
</q-card>
</q-dialog>
<LayerFab class="floated layers" />
<FavoriteFab class="floated favorites" />
<ControlFab class="floated appctrls" />
<StatusBarFab class="floated status-fab" />
<map-container />
</q-page>
</template>
<script setup lang="ts">
import { useSimulationStore } from 'stores/simulation';
import { useLeafletStore } from 'stores/leaflet';
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import MapContainer from 'components/leaflet/MapContainer.vue';
import StatusBarFab from 'components/StatusBarFab.vue';
import FavoriteFab from 'components/FavoriteFab.vue';
import ControlFab from 'components/ControlFab.vue';
import LayerFab from 'components/LayerFab.vue';
import GeoSearch from 'components/GeoSearch.vue';
import { useEscKey } from 'composables/useEscKey';
import LocateControl from 'components/LocateControl.vue';
const simulationStore = useSimulationStore();
const leafletStore = useLeafletStore();
const { locationDrawerSettings } = storeToRefs(leafletStore);
const errors = ref([]);
const newErrorsCounter = ref();
function resetNewErrorsCounter() {
newErrorsCounter.value = 0;
}
const geoSearchForm = ref(false);
const baseLayerSelect = ref(false);
useEscKey(() => {
geoSearchForm.value = false;
});
</script>
<style lang="sass">
.floated
z-index: 2000
position: absolute
&.logo
top: 5px
left: 10px
opacity: .9
transition: transform 0.3s ease-in-out
cursor: pointer
&.shift-logo
transform: translateX(300px)
&.logo img
height: 40px
display: inline-block
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)
&.logo img:hover
animation: boing 1s ease infinite
transform-origin: bottom center
filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.5))
&.drawermenuleft
bottom: 15px
left: 10px
&.github
top: 5px
right: 100px
&.telemetry
top: 5px
right: 55px
&.zoom-controls
top: 80px
right: 10px
&.notifications
top: 5px
right: 145px
&.menu
bottom: 60px
right: 10px
width: 65px
&.locate-control
right: 10px
top: 170px
&.geosearch
top: 210px
right: 10px
&.favorites
right: 10px
bottom: 145px
&.base-layers
right: 10px
top: 250px
&.layers
right: 10px
bottom: 105px
&.appctrls
right: 10px
bottom: 65px
&.status
bottom: 25px
right: 50px
&.statusbar
bottom: 25px
right: 100px
&.date
top: 60px
right: 10px
background-color: white
padding: 1px
border-radius: 3px
box-shadow: 0 0 15px rgba(0,0,0,0.5)
&.no-devices
bottom: 150px
text-align: center
width: 100%
pointer-events: none
user-select: none
.no-devices__message
font-size: 3rem
opacity: .5
font-weight: bolder
text-transform: uppercase
&.plus
top: 50px
left: 50px
&.white-background
background: rgba(255,255,255,0.3)
&.status-fab
bottom: 25px
right: 10px
.geosearchform
position: fixed
top: 15%
left: 50%
transform: translate(-50%, -50%)
z-index: 999
.q-drawer
background: none
.bg-ios-blur, .bg-ios-blur-brdr, .bg-ios-blur-brdr-h, .bg-ios-blur-h
background: rgba(255, 255, 255, 0.2)
backdrop-filter: blur(16px) saturate(120%)
-webkit-backdrop-filter: blur(16px) saturate(120%)
.bg-ios-blur-brdr, .bg-ios-blur-brdr-h
/* Subtle glass border */
border: 1px solid rgba(255, 255, 255, 0.4)
border-radius: 12px
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1)
transition: background-color 0.3s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease
.transition-rotate
transition: transform 0.3s ease
.rotate-180
transform: rotate(180deg)
.zoom-rocker
flex-direction: column
border-radius: 12px
background: rgba(255, 255, 255, 0.2)
backdrop-filter: blur(16px) saturate(120%)
-webkit-backdrop-filter: blur(16px) saturate(120%)
border: 1px solid rgba(255, 255, 255, 0.4)
overflow: hidden
.zoom-in
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1)
transition: background-color 0.3s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease
border-radius: 12px 12px 0 0
.zoom-out
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1)
transition: background-color 0.3s ease, backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease
border-radius: 0 0 12px 12px
.zoom-in:hover
border-radius: 12px 12px 0 0
background-color: rgba(255, 255, 255, 0.4)
backdrop-filter: blur(24px) saturate(200%)
-webkit-backdrop-filter: blur(24px) saturate(200%)
.zoom-out:hover
border-radius: 0 0 12px 12px
background-color: rgba(255, 255, 255, 0.4)
backdrop-filter: blur(24px) saturate(200%)
-webkit-backdrop-filter: blur(24px) saturate(200%)
.bg-ios-blur-h:hover, .bg-ios-blur-brdr-h:hover
background-color: rgba(255, 255, 255, 0.4)
backdrop-filter: blur(24px) saturate(200%)
-webkit-backdrop-filter: blur(24px) saturate(200%)
@keyframes boing
0%, 100%
transform: scale(1, 1)
30%
transform: scale(1.15, 0.85)
40%
transform: scale(0.9, 1.1)
50%
transform: scale(1.05, 0.95)
65%
transform: scale(1, 1)
</style>
-3
View File
@@ -1,10 +1,7 @@
<template>
<q-page class="row items-center justify-evenly">
<LeafletTest />
</q-page>
</template>
<script setup lang="ts">
import LeafletTest from 'components/LeafletTest.vue'
</script>
+27
View File
@@ -0,0 +1,27 @@
<template>
<q-page class="row items-center justify-evenly">
<q-layout container view="hHh Lpr fFf" class="leafletLayout rounded-borders">
<q-footer :class="$q.dark.isActive ? 'bg-primary' : 'bg-black'" class="leafletFooter">
<TheFooter />
</q-footer>
<LocationDrawer persistent :breakpoint="576" side="left" />
<q-page-container>
<LeafletMap />
</q-page-container>
</q-layout>
</q-page>
</template>
<script setup lang="ts">
import LocationDrawer from 'components/leaflet/LocationDrawer.vue';
import TheFooter from 'components/leaflet/TheFooter.vue';
import LeafletMap from 'components/leaflet/LeafletMap.vue';
</script>
<style scoped lang="sass">
.leafletLayout
height: 600px
max-width: 800px
width: 100vw
</style>
+13 -10
View File
@@ -3,19 +3,23 @@ import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('layouts/FullMapLayout.vue'),
children: [
{
path: '',
name: 'simPI',
component: () => import('pages/FullMapPage.vue'),
},
],
},
{
path: '/old',
component: () => import('layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'home',
redirect: {
name: 'Leaflet',
},
},
{
path: 'leaflet',
name: 'Leaflet',
component: () => import('pages/IndexPage.vue'),
name: 'oldMap',
component: () => import('pages/LeafletPage.vue'),
},
{
path: 'test',
@@ -29,7 +33,6 @@ const routes: RouteRecordRaw[] = [
},
],
},
// Always leave this as last one,
// but you can also remove it
{
+30 -7
View File
@@ -4,23 +4,27 @@ import type { DeviceCommands } from 'components/models';
import { useSocketStore } from 'stores/socket';
type lastUpdatedTime = number | null;
export const useDeviceStore = defineStore('deviceStore', {
state: () => {
return {
deviceConnected: false as boolean,
lastKnownUptime: 0 as number,
lastUpdatedTime: null as lastUpdatedTime,
};
},
getters: {},
actions: {
digestUpdate(data: boolean) {
this.deviceConnected = data;
digestUpdate(data: { uptime: number, connected: boolean }) {
this.deviceConnected = data.connected;
this.lastKnownUptime = data.uptime;
this.lastUpdatedTime = Date.now();
},
deviceControl(command: DeviceCommands, delay: number = 0) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
if (debugLog) {
console.log('deviceStore: got command: ', command, ' with delay: ', delay, ' seconds');
}
@@ -30,14 +34,33 @@ export const useDeviceStore = defineStore('deviceStore', {
(response) => {
console.log(response.command_status, response.command);
if (response.command_status == 'ERROR') {
fnctRtn = { sts: 'error', msg: response.message?.toString() };
functionReturn = { status: 'error', message: response.message?.toString() };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
functionReturn = { status: 'OK', message: response.message?.toString() };
}
},
);
return fnctRtn;
return functionReturn;
},
async deviceControlAsync(command: DeviceCommands, delay: number = 0): Promise<{ status: string; message: string }> {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
if (debugLog) {
console.log('deviceStore: got command: ', command, ' with delay: ', delay, ' seconds');
}
return await new Promise((resolve) => {
socket.emit('device_control', { command: command, delay: delay }, (response) => {
if (debugLog) {
console.log(response.command_status, response.command);
}
resolve({
status: response.command_status === 'ERROR' ? 'error' : 'OK',
message: response.message || '',
});
});
});
}
}
});
+137 -39
View File
@@ -1,26 +1,41 @@
import { defineStore, acceptHMRUpdate } from 'pinia';
import { socket } from 'boot/socketio';
import { useSocketStore } from 'stores/socket';
import type { Favorite } from 'components/models';
import type { Favorite, FavoriteCommands, Category, CategoryCommands } from 'components/models';
interface CatMap extends Category {
favorites: Favorite[];
}
interface CategoryWithFavorites extends Category {
favorites: Favorite[];
}
export const useFavoriteStore = defineStore('favoriteStore', {
state: () => {
return {
favorites: [] as Favorite[],
categories: [] as string[],
favMap: new Map<number, Favorite>(),
categories: [] as Category[],
catMap: new Map<number, CatMap>(),
categoriesWithFavorites: new Map<number, CategoryWithFavorites>(),
catsWithFavs: [] as CategoryWithFavorites[],
};
},
getters: {},
actions: {
favoriteControl({ command, favorite }: { command: string; favorite?: Favorite }) {
getFavorite(id: number) {
return this.favMap.get(id);
},
favoriteControl({ command, favorite }: { command: FavoriteCommands; favorite?: Favorite }) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
switch (command) {
case 'set':
if (!favorite) {
console.log('Favorite not provided, favdata: %s', favorite);
fnctRtn = { sts: 'error', msg: 'Favorite not provided.' };
functionReturn = { status: 'error', message: 'Favorite not provided.' };
throw new Error('Favorite not provided.');
}
socket.emit('favorite_control', { command: 'set', favorite: favorite }, (response) => {
@@ -28,17 +43,12 @@ export const useFavoriteStore = defineStore('favoriteStore', {
throw new Error(response.message);
} else {
if (!response.data?.favorites) {
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
functionReturn = { status: 'error', message: 'No favorites found.' };
throw new Error(response.message);
} else {
console.log('Favorite added, favdata: %s', favorite);
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
functionReturn = { status: 'OK', message: 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);
@@ -50,20 +60,15 @@ export const useFavoriteStore = defineStore('favoriteStore', {
case 'delete':
socket.emit('favorite_control', { command: 'delete', favorite: favorite }, (response) => {
if (response.command_status == 'ERROR') {
fnctRtn = { sts: 'error', msg: response.message };
functionReturn = { status: 'error', message: response.message };
throw new Error(response.message);
} else {
if (!response.data?.favorites) {
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
functionReturn = { status: 'error', message: 'No favorites found.' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
functionReturn = { status: 'OK', message: 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);
@@ -75,20 +80,20 @@ export const useFavoriteStore = defineStore('favoriteStore', {
case 'get':
socket.emit('favorite_control', { command: 'get', favorite: favorite }, (response) => {
if (response.command_status == 'ERROR') {
fnctRtn = { sts: 'error', msg: response.message };
functionReturn = { status: 'error', message: response.message };
throw new Error(response.message);
} else {
if (!response.data?.favorites) {
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
functionReturn = { status: 'error', message: 'No favorites found.' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
functionReturn = { status: 'OK', message: response.message?.toString() };
this.favorites = response.data.favorites;
this.favMap.clear();
response.data.favorites.forEach((fav) => {
if(!this.categories.includes(fav.category)) {
this.categories.push(fav.category);
}
})
this.favMap.set(fav.id, fav);
});
this.mapCategoriesAndFavorites();
}
if (debugLog) {
console.log('response from favorite_control_get: ', response);
@@ -98,23 +103,116 @@ export const useFavoriteStore = defineStore('favoriteStore', {
});
break;
default:
fnctRtn = { sts: 'error', msg: 'Invalid command' + command };
functionReturn = { status: 'error', message: 'Invalid command' + command };
throw new Error('Invalid command' + command);
}
return fnctRtn;
return functionReturn;
},
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);
categoryControl({ command, category }: { command: CategoryCommands; category?: Category }) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
switch (command) {
case 'add':
if (!category) {
console.log('Category not provided, favdata: %s', category);
functionReturn = { status: 'error', message: 'Category not provided.' };
throw new Error('Category not provided.');
}
socket.emit('category_control', { command: 'add', category: category }, (response) => {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.categories) {
functionReturn = { status: 'error', message: 'No categories found.' };
throw new Error(response.message);
} else {
console.log('Category added, favdata: %s', category);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.categories = response.data.categories;
}
if (debugLog) {
console.log('response from category_control_add: ', response);
}
return response.message;
}
});
return categories;
break;
case 'delete':
socket.emit('category_control', { command: 'delete', category: category }, (response) => {
if (response.command_status == 'ERROR') {
functionReturn = { status: 'error', message: response.message };
throw new Error(response.message);
} else {
if (!response.data?.categories) {
functionReturn = { status: 'error', message: 'No categories found.' };
throw new Error(response.message);
} else {
functionReturn = { status: 'OK', message: response.message?.toString() };
this.categories = response.data.categories;
}
if (debugLog) {
console.log('response from category_control_delete: ', response);
}
return response.message;
}
});
break;
case 'get':
socket.emit('category_control', { command: 'get', category: category }, (response) => {
if (response.command_status == 'ERROR') {
functionReturn = { status: 'error', message: response.message };
throw new Error(response.message);
} else {
if (!response.data?.categories) {
functionReturn = { status: 'error', message: 'No categories found.' };
throw new Error(response.message);
} else {
functionReturn = { status: 'OK', message: response.message?.toString() };
this.categories = response.data.categories;
this.catMap.clear();
response.data.categories.forEach((cat) => {
this.catMap.set(cat.id, cat);
});
this.mapCategoriesAndFavorites();
}
if (debugLog) {
console.log('response from category_control_get: ', response);
}
return response.message;
}
});
break;
default:
functionReturn = { status: 'error', message: 'Invalid command' + command };
throw new Error('Invalid command' + command);
}
return functionReturn;
},
mapCategoriesAndFavorites(): void {
const favoritesByCatId = new Map<number, Favorite[]>();
for (const fav of this.favorites) {
const list = favoritesByCatId.get(fav.category_id) || [];
fav.openedFab = null;
list.push(fav);
favoritesByCatId.set(fav.category_id, list);
}
const resultMap = new Map<number, CategoryWithFavorites>();
for (const cat of this.categories) {
resultMap.set(cat.id, {
...cat,
favorites: favoritesByCatId.get(cat.id) || [],
openedFab: null,
});
}
this.categoriesWithFavorites = resultMap;
this.catsWithFavs = Array.from(resultMap.values());
},
initialize(): void {
this.categoryControl({ command: 'get' });
this.favoriteControl({ command: 'get' });
},
},
});
+100 -55
View File
@@ -5,7 +5,7 @@ import { useQuasar } from 'quasar';
import iCloudCodeDialog from 'components/iCloudCodeDialog.vue';
import type { FindMyUpdate, icloudData, iCloudMonitorResponse } from 'components/models';
import type { CommandResponse, FindMyUpdate, icloudStatusUpdate } from 'components/models';
const $q = useQuasar();
@@ -27,6 +27,7 @@ export const useIcloudStore = defineStore('icloudStore', {
bindEvents() {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
/*
// fmf_update
socket.on('fmf_update', (data: FindMyUpdate): void => {
if (debugLog) {
@@ -34,7 +35,7 @@ export const useIcloudStore = defineStore('icloudStore', {
}
this.findMyUpdate = data;
});
*/
// icloud_2fa_request
socket.on('icloud_2fa_request', (callback) => {
if (debugLog) {
@@ -63,14 +64,17 @@ export const useIcloudStore = defineStore('icloudStore', {
icloudMonitorControl(command: string) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
let functionReturn: { status: string; message?: string | undefined } = {
status: '',
message: '',
};
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' };
functionReturn = { status: 'error', message: 'iCloud Monitor is already running' };
throw new Error('iCloud Monitor is already running');
}
if (debugLog) {
@@ -79,23 +83,28 @@ export const useIcloudStore = defineStore('icloudStore', {
socket.emit(
'icloud_monitor_control',
{ command: 'start' },
(response: iCloudMonitorResponse) => {
fnctRtn.sts = response.command_status;
(response: CommandResponse) => {
functionReturn.status = response.command_status;
if (response.command_status == 'ERROR') {
if (response.message) {
if (debugLog) {
console.log(response.message);
}
fnctRtn.msg = response.message;
functionReturn.message = response.message;
} else {
fnctRtn.msg = 'Error';
functionReturn.message = 'Error';
}
throw new Error(fnctRtn.msg);
throw new Error(functionReturn.message);
} else {
fnctRtn = { sts: 'OK', msg: 'iCloud Monitor: ' + response.command_status };
this.icloudMonitor = true;
functionReturn = {
status: 'OK',
message: 'iCloud Monitor: ' + response.command_status,
};
if(response.data?.icloud){
this.digestUpdate(response.data.icloud);
}
}
}
},
);
break;
case 'stop':
@@ -103,7 +112,7 @@ export const useIcloudStore = defineStore('icloudStore', {
console.log('socketStore: got command: icloudMonitor stop');
}
if (!this.icloudMonitor) {
fnctRtn = { sts: 'error', msg: 'iCloud Monitor is not running' };
functionReturn = { status: 'error', message: 'iCloud Monitor is not running' };
throw new Error('iCloud Monitor is not running');
}
if (debugLog) {
@@ -112,21 +121,26 @@ export const useIcloudStore = defineStore('icloudStore', {
socket.emit(
'icloud_monitor_control',
{ command: 'stop' },
(response: iCloudMonitorResponse) => {
fnctRtn.sts = response.command_status;
(response: CommandResponse) => {
functionReturn.status = response.command_status;
if (response.command_status == 'ERROR') {
if (response.message) {
if (debugLog) {
console.log(response.message);
}
fnctRtn.msg = response.message;
functionReturn.message = response.message;
} else {
fnctRtn.msg = 'Error';
functionReturn.message = 'Error';
}
throw new Error(fnctRtn.msg);
throw new Error(functionReturn.message);
} else {
fnctRtn = { sts: 'OK', msg: 'iCloud Monitor: ' + response.command_status };
this.icloudMonitor = false;
functionReturn = {
status: 'OK',
message: 'iCloud Monitor: ' + response.command_status,
};
if (response.data?.icloud) {
this.digestUpdate(response.data.icloud);
}
}
},
);
@@ -137,28 +151,28 @@ export const useIcloudStore = defineStore('icloudStore', {
}
socket.emit(
'icloud_monitor_control',
{ command: 'get' },
(response: iCloudMonitorResponse) => {
fnctRtn.sts = response.command_status;
{ command: 'status' },
(response: CommandResponse) => {
functionReturn.status = response.command_status;
if (response.command_status == 'error') {
if (response.message) {
if (debugLog) {
console.log(response.message);
}
fnctRtn.msg = response.message;
functionReturn.message = response.message;
} else {
fnctRtn.msg = 'Error';
functionReturn.message = 'Error';
}
throw new Error(fnctRtn.msg);
throw new Error(functionReturn.message);
} else {
fnctRtn = {
sts: 'OK',
msg: 'iCloud Location Requested',
functionReturn = {
status: 'OK',
message: 'iCloud Location Requested',
};
}
this.icloudMonitor = !!(
response.icloud_monitor_enabled && response.icloud_monitor_running
);
if (response.data?.icloud) {
this.digestUpdate(response.data.icloud);
}
},
);
break;
@@ -168,49 +182,80 @@ export const useIcloudStore = defineStore('icloudStore', {
}
socket.emit(
'icloud_monitor_control',
{ command: 'status' },
(response: iCloudMonitorResponse) => {
fnctRtn.sts = response.command_status;
{ command: 'refresh' },
(response: CommandResponse) => {
functionReturn.status = response.command_status;
if (response.command_status == 'ERROR') {
if (response.message) {
if (debugLog) {
console.log(response.message);
}
fnctRtn.msg = response.message;
functionReturn.message = response.message;
} else {
fnctRtn.msg = 'Error';
functionReturn.message = 'Error';
}
throw new Error(fnctRtn.msg);
throw new Error(functionReturn.message);
} else {
fnctRtn = {
sts: 'OK',
msg:
functionReturn = {
status: 'OK',
message:
'iCloud Monitor Enabled: ' +
response.icloud_monitor_enabled +
response.data?.icloud?.monitor_enabled +
'iCloud Monitor Running: ' +
response.icloud_monitor_running,
response.data?.icloud?.monitor_running,
};
}
this.icloudMonitor = !!(
response.icloud_monitor_enabled && response.icloud_monitor_running
);
if (response.data?.icloud) {
this.digestUpdate(response.data.icloud);
}
},
);
break;
default:
fnctRtn = { sts: 'error', msg: 'Invalid command' };
functionReturn = { status: 'error', message: 'Invalid command' };
throw new Error('Invalid command');
}
return fnctRtn;
return functionReturn;
},
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;
async icloudMonitorControlAsync(command: string): Promise<{ status: string; message: string }> {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
if (debugLog) {
console.log('socketStore: got command: icloudMonitor ', command);
}
if (command === 'start' && this.icloudMonitor) {
throw new Error('iCloud Monitor is already running');
}
if (command === 'stop' && !this.icloudMonitor) {
throw new Error('iCloud Monitor is not running');
}
return await new Promise((resolve) => {
socket.emit('icloud_monitor_control', { command: command }, (response: CommandResponse) => {
if (response.data?.icloud) {
this.digestUpdate(response.data.icloud);
}
if (debugLog) {
console.log('response from icloud_monitor_control: ', response);
}
resolve({
status: response.command_status === 'ERROR' ? 'error' : 'OK',
message: response.message || '',
});
});
});
},
digestUpdate(data: icloudStatusUpdate) {
if ('monitor_running' in data) this.icloudMonitor = data.monitor_running;
if ('monitor_running' in data) this.icloudMonitor = data.monitor_running;
if ('consumer_queue' in data) this.consumerQueue = data.consumer_queue;
if ('consumer_task' in data) this.consumerTask = data.consumer_task;
if ('monitor_enabled' in data) this.monitorEnabled = data.monitor_enabled;
if ('monitor_task' in data) this.monitorTask = data.monitor_task;
if ('monitor_running' in data) this.monitorRunning = data.monitor_running;
if (data.fmf_location) this.findMyUpdate = data.fmf_location;
},
},
});
+397 -7
View File
@@ -1,36 +1,160 @@
import { defineStore, acceptHMRUpdate } from 'pinia';
import { favorites } from 'constants/favorites'
import { shallowRef, markRaw } from 'vue';
import { favorites } from 'constants/favorites';
import * as L from 'leaflet';
import type { Map, LatLngExpression, LayerGroup, FeatureGroup, Marker } from 'leaflet';
import type { SearchResult } from 'components/models';
import type { IRouter } from 'leaflet-routing-machine';
import { customRouter } from 'functions/serviceURL';
import { startIcon, intermediateIcon, endIcon } from 'constants/icons';
import type {
RoutesSet,
LatLng,
routeSegments,
routeDirections,
routeCoordinates,
ControlCategoryKey,
BaseLayer,
BaseLayerWMS,
} from 'components/models';
import { type AddLocationResult, useAddLocation } from 'composables/useAddLocation';
interface LayerControl {
marker: boolean;
simQueue: boolean;
favorites: boolean;
routing: boolean;
findMy: boolean;
locateControl: boolean;
}
interface LayerGroups {
marker: ReturnType<typeof shallowRef<LayerGroup | null>>;
simQueue: ReturnType<typeof shallowRef<LayerGroup | null>>;
favorites: ReturnType<typeof shallowRef<LayerGroup | null>>;
routing: ReturnType<typeof shallowRef<LayerGroup | null>>;
findMy: ReturnType<typeof shallowRef<LayerGroup | null>>;
locateControl: ReturnType<typeof shallowRef<LayerGroup | null>>;
}
type RoutingWaypoint = {
latLng: L.LatLng;
};
type FabKeys = 'favorites' | 'appControl' | 'statusBar' | 'layerControl';
interface AppControl {
opened: boolean;
openedFab: ControlCategoryKey | null;
}
interface OtherFabs {
opened: boolean;
}
type Fabs = {
appControl: AppControl;
} & {
[K in Exclude<FabKeys, 'appControl'>]: OtherFabs;
};
interface RoutingOptions {
waypoints: L.LatLng[];
router: IRouter;
routeWhileDragging: boolean;
createMarker: (i: number, waypoint: RoutingWaypoint, n: number) => L.Marker | false;
}
interface State {
zoom: number;
center: [number, number] | [null, null] | null;
mapObject: ReturnType<typeof shallowRef<Map | null>>;
markerLatLng: [number, number] | [null, null] | null;
qLocDrawer: boolean;
leafletDrawerOpen: boolean;
locationDrawerSettings: {
opened: boolean;
pinned: boolean;
};
controlDrawerSettings: {
opened: boolean;
pinned: boolean;
};
statusBarSettings: {
opened: boolean;
};
layerControl: LayerControl;
layerGroups: LayerGroups;
routeSet: {
start: LatLng;
end: LatLng;
wayPoints?: LatLng[] | null | undefined;
};
fabs: Fabs;
routesSet: RoutesSet[] | null;
routeSegments: routeSegments[] | null;
routeDirections: routeDirections[] | null;
routeCoordinates: routeCoordinates[] | null;
routingOptions: RoutingOptions;
clickedLatLng: LatLng | null;
baseLayer: {
selected: number;
options: BaseLayer[];
};
baseLayerWMS: BaseLayerWMS;
}
export const useLeafletStore = defineStore('leaflet', {
export const useLeafletStore = defineStore('leafletStore', {
state: (): State => {
return {
zoom: 10,
center: [favorites.home.coords.lat, favorites.home.coords.lng],
clickedLatLng: null,
mapObject: shallowRef(null),
markerLatLng: null,
qLocDrawer: false,
layerControl: {
marker: true,
simQueue: true,
favorites: true,
routing: false,
findMy: false,
locateControl: true,
},
layerGroups: {
marker: shallowRef(null),
simQueue: shallowRef(null),
favorites: shallowRef(null),
routing: shallowRef(null),
findMy: shallowRef(null),
locateControl: shallowRef(null),
},
leafletDrawerOpen: false,
locationDrawerSettings: {
opened: false,
pinned: false,
},
fabs: {
statusBar: {
opened: false,
},
layerControl: {
opened: false,
},
favorites: {
opened: false,
},
appControl: {
opened: false,
openedFab: null,
},
},
controlDrawerSettings: {
opened: false,
pinned: false,
},
statusBarSettings: {
opened: false,
},
routeSet: {
start: { lat: null, lng: null },
end: { lat: null, lng: null },
@@ -40,9 +164,125 @@ export const useLeafletStore = defineStore('leaflet', {
routeSegments: [],
routeDirections: [],
routeCoordinates: [],
baseLayer: {
selected: 1,
options: [
{
value: 1,
label: 'Open Street Map',
layer: shallowRef(
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
maxZoom: 19,
attribution: '© OpenStreetMap',
minZoom: 2,
noWrap: true,
}
))
},
{
value: 2,
label: 'Topography',
layer: shallowRef(L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
minZoom: 2,
maxZoom: 17,
attribution: '© OpenTopoMap',
noWrap: true,
}))
},
{
value: 3,
label: 'Satellite',
layer: shallowRef(L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{
minZoom: 2,
maxZoom: 19,
attribution: '© ArcGIS',
noWrap: true,
}))
}
],
},
baseLayerWMS: {
selected: false,
label: 'OpenStreetMap Transparent',
layer: shallowRef(L.tileLayer.wms(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
minZoom: 2,
maxZoom: 19,
attribution: '© OpenStreetMap',
layers: 'semitransparent',
transparent: true,
format: 'image/png',
opacity: 0.5,
}
)),
},
routingOptions: {
waypoints: [],
router: markRaw(customRouter),
routeWhileDragging: true,
createMarker: markRaw(function (i: number, waypoint: RoutingWaypoint, n: number) {
let icon;
if (i === 0) {
icon = startIcon;
} else if (i === n - 1) {
icon = endIcon;
} else {
icon = intermediateIcon;
}
return L.marker(waypoint.latLng, {
draggable: true,
icon,
contextmenu: false,
contextmenuItems: [],
});
}) as RoutingOptions['createMarker'],
},
};
},
getters: {
getBaseLayer: (state) => state.baseLayer.selected,
getOverlayLayer: (state) => state.baseLayerWMS.selected,
},
actions: {
closeOtherFabs(fabName: 'favorites' | 'appControl' | 'statusBar' | 'layerControl') {
Object.keys(this.fabs).forEach((key) => {
const typedKey = key as keyof Fabs;
if (typedKey !== fabName) {
this.fabs[typedKey].opened = false;
}
});
},
setMap(map: Map) {
this.mapObject = map;
},
/*
setLayerGroup(layerGroup: LayerGroup, name: string)
this.layerGroups[name] = layerGroup;
},
addToLayer(name: string, obj: LayerGroup | FeatureGroup) {
this.layerGroups[name]?.addLayer(obj);
},
*/
flyTo(lat: number, lng: number, zoom: number) {
if (!this.mapObject) return;
this.mapObject.flyTo([lat, lng], zoom);
},
setFavLayerGroup(layerGroup: LayerGroup) {
this.layerGroups.favorites = layerGroup;
},
setLCLayerGroup(layerGroup: LayerGroup) {
this.layerGroups.locateControl = layerGroup;
},
addToFavLayer(fGrp: FeatureGroup) {
this.layerGroups.favorites?.addLayer(fGrp);
},
removeMap() {
this.mapObject = null;
},
clearRouteSegments() {
this.routeSegments = [];
},
@@ -52,17 +292,167 @@ export const useLeafletStore = defineStore('leaflet', {
setZoom(zoom: number) {
this.zoom = zoom;
},
zoomIn() {
this.mapObject?.zoomIn()
},
zoomOut() {
this.mapObject?.zoomOut()
},
setBaseLayer(newVal?: number, oldVal?: number) {
if (newVal === undefined && this.baseLayer.selected === undefined) return;
if (oldVal && this.mapObject) {
const oldLayer = this.baseLayer.options.filter(u => u.value === oldVal)[0];
if (oldLayer) {
console.log("OldLayer name: ", oldLayer.label);
oldLayer.layer!.remove();
}
}
const bLayer = this.baseLayer.options.filter(u => u.value === this.baseLayer.selected)[0];
if(this.mapObject && bLayer) {
console.log("NewLayer name: ", bLayer.label);
bLayer.layer!.addTo(this.mapObject);
}
},
toggleWmsLayer() {
const wmsLayer = this.baseLayerWMS.layer;
if (wmsLayer && this.mapObject) {
if (this.baseLayerWMS.selected) {
wmsLayer.addTo(this.mapObject);
} else {
wmsLayer.remove();
}
}
},
setMarkerLatLng(lat: number, lng: number) {
this.markerLatLng = [lat, lng];
},
toggleQLocDrawer() {
this.qLocDrawer = !this.qLocDrawer
toggleleafletDrawerOpen() {
this.leafletDrawerOpen = !this.leafletDrawerOpen;
},
setRouteDirs(data: routeDirections[]) {
this.routeDirections = data;
},
closeAllPopups() {
if (this.mapObject) {
this.mapObject?.closePopup();
}
})
},
getDistance(coord1: LatLngExpression, coord2: LatLngExpression): number | null {
const distance = this.mapObject?.distance(coord1, coord2);
return distance ?? null;
},
onLayerToggle(val: boolean) {
console.log('Layer toggle activated, new val: ', val);
if (val) {
console.log('Layer toggle activated');
this.mapObject?.invalidateSize();
}
},
updateRouteWaypoints() {
this.layerControl.routing = true;
const waypoints: L.LatLng[] = [];
const start = this.routeSet.start;
const end = this.routeSet.end;
if (start && typeof start.lat === 'number' && typeof start.lng === 'number') {
waypoints.push(L.latLng(start.lat, start.lng));
}
if (end && typeof end.lat === 'number' && typeof end.lng === 'number') {
waypoints.push(L.latLng(end.lat, end.lng));
}
console.log('Updating route with waypoints: ', waypoints);
this.routingOptions.waypoints = waypoints;
},
controlsOpenedHandler(state: boolean) {
this.controlDrawerSettings.opened = state;
},
locationsOpenedHandler(state: boolean) {
this.locationDrawerSettings.opened = state;
},
statusBarOpenedHandler(state: boolean) {
this.statusBarSettings.opened = state;
},
async routeAllPointsToQueue() {
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
let result: AddLocationResult = {
notificationType: 'negative',
responseMessage: 'No route points to add',
};
if (this.routeSet.start && this.routeSet.end && this.routeCoordinates) {
if (this.routeCoordinates && this.routeCoordinates.length > 0) {
for (const coord of this.routeCoordinates) {
if (coord) {
const type = coord.instruction ? 'instruction' : 'coord';
result = await useAddLocation(
{ lat: Number(coord.lat), lng: Number(coord.lng) },
false,
coord.timeFromPrev,
undefined,
type,
coord.instruction,
);
}
await delay(1000);
}
}
}
return { type: result.notificationType, message: result.responseMessage };
},
async routeToQueue() {
let result: AddLocationResult = {
notificationType: 'negative',
responseMessage: 'No route points to add',
};
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
if (this.routeSet.start && this.routeSet.end && this.routeDirections) {
if (this.routeDirections && this.routeDirections.length > 0) {
for (const direction of this.routeDirections) {
if (direction.coordinates) {
result = await useAddLocation(
{ lat: Number(direction.coordinates.lat), lng: Number(direction.coordinates.lng) },
false,
direction.time,
undefined,
'instruction',
direction.text,
);
}
await delay(1000);
}
}
}
return { type: result.notificationType, message: result.responseMessage };
},
clearRoute() {
this.layerControl.routing = false;
this.clearRouteSegments();
this.routeSet.start = {lat: null, lng: null};
this.routeSet.end = {lat: null, lng: null};
return {type: 'positive', message: 'Route cleared successfully'};
},
moveToLocation(location: SearchResult) {
if (!location) return;
const { y: lat, x: lng, bounds } = location;
if (bounds && bounds.length) {
// Fit bounds if available (preferred for bounding boxes)
const southWest = L.latLng(bounds[0][0], bounds[0][1]);
const northEast = L.latLng(bounds[1][0], bounds[1][1]);
this.mapObject?.fitBounds(L.latLngBounds(southWest, northEast));
} else {
// Otherwise, pan to center
this.mapObject?.setView([lat, lng], 14);
}
let searchMarker: Marker = new L.Marker([0,0]);
// Update/create a marker at the selected location
if (searchMarker) {
searchMarker.setLatLng([lat, lng]);
} else {
searchMarker = L.marker([lat, lng]).addTo(this.mapObject!);
}
},
},
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useLeafletStore, import.meta.hot));
+55
View File
@@ -0,0 +1,55 @@
import { acceptHMRUpdate, defineStore } from 'pinia';
import type { LatLngExpression } from 'leaflet';
import { latLng } from 'leaflet';
import type { GeoCacheResponse } from 'components/models';
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
interface LocationMap {
[key: string]: GeoCacheResponse;
}
interface State {
locations: LocationMap[];
}
export const useLocationStore = defineStore('locationStore', {
state: (): State => {
return {
locations: [],
};
},
getters: {},
actions: {
async getCachedLocation(latlng: LatLngExpression) {
const lat: number = latLng(latlng).lat;
const lng: number = latLng(latlng).lng;
const key = `${lat},${lng}`;
const reslt = this.locations.find((location) => location[key]);
if (reslt) {
console.log('Location found in local cache:', reslt[key]);
return reslt[key];
} else {
try {
const resp = await reverseGeocodeRateLimited(latLng(latlng).lat, latLng(latlng).lng);
if (!resp) {
throw new Error('Reverse geocoding failed')
}
const lMap: LocationMap = {[key]: resp};
this.locations.push(lMap);
return resp;
} catch (error) {
if (error instanceof Error) {
console.error('Error fetching location:', error.message);
}
throw error;
}
}
},
},
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useLocationStore, import.meta.hot));
}
+264 -105
View File
@@ -3,23 +3,24 @@ import { socket } from 'boot/socketio';
import { useSocketStore } from 'stores/socket';
import type {
CurrentLocation,
LocationItemUpdate,
LocationMarkUpdateResponse,
CommandResponse,
LocationQueue,
NominatimResponse,
QueueData,
SimulationControlResponse,
SimulationStatus,
ReturnedStatusMessage,
SimulationCommands,
SimulationStatusUpdate,
SimulationState,
} from 'components/models';
export const useSimulationStore = defineStore('simulationStore', {
state: () => {
state: (): SimulationState => {
return {
currentLocation: null as CurrentLocation | null | undefined,
gpsNoise: null as boolean | undefined | null,
currentLocation: {
loc_id: null,
latitude: null,
longitude: null,
},
gpsNoise: null,
locationQueueData: {} as LocationQueue,
locationQueueDeletedItems: [] as string[],
locationQueueOrder: [] as string[],
@@ -27,24 +28,58 @@ export const useSimulationStore = defineStore('simulationStore', {
simulationRunning: false as boolean | undefined | null,
simulationState: null as string | null | undefined,
testMode: null as boolean | undefined | null,
workerTask: null as string | undefined | null,
};
},
getters: {},
getters: {
locationQueueFilteredActive: (state): string[] => {
let filteredQueue: string[] = [];
if (state.simulationQueueLength) {
if (state.simulationQueueLength > 0) {
filteredQueue = state.locationQueueOrder.filter(
(loc_id: string) =>
state.locationQueueData?.[loc_id]?.status === 'set' ||
state.locationQueueData?.[loc_id]?.status === 'queued',
);
}
}
return filteredQueue;
},
locationQueueFilteredDone: (state): string[] => {
let filteredQueue: string[] = [];
if (state.simulationQueueLength) {
if (state.simulationQueueLength > 0) {
filteredQueue = state.locationQueueOrder.filter(
(loc_id: string) => state.locationQueueData?.[loc_id]?.status === 'done',
);
}
}
return filteredQueue;
},
isLastLocationItem: (state) => {
if (state.simulationQueueLength && state.currentLocation.loc_id) {
if (state.simulationQueueLength > 0 && state.currentLocation) {
const currentIndex = state.locationQueueOrder.indexOf(state.currentLocation.loc_id);
return state.simulationQueueLength - 1 === currentIndex;
} else {
return false;
}
} else {
return false;
}
},
},
actions: {
bindEvents() {
/*
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
// simulation_status
socket.on('simulation_status', (data: SimulationStatus): void => {
socket.on('simulation_status', (data: SimulationStatusUpdate): 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,
};
this.digestUpdate(data);
});
// queue_data_update
@@ -52,13 +87,15 @@ export const useSimulationStore = defineStore('simulationStore', {
if (debugLog) {
console.log('QueueUpdate received: ', inData);
}
this.digestQueueUpdate(inData);
this.digestUpdate(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,
@@ -67,6 +104,8 @@ export const useSimulationStore = defineStore('simulationStore', {
loc_id,
delay,
address,
type,
instruction,
}: {
command: string;
latitude?: number | null | undefined;
@@ -74,10 +113,15 @@ export const useSimulationStore = defineStore('simulationStore', {
loc_id?: string | null | undefined;
delay?: number | null | undefined;
address?: NominatimResponse | string | null | undefined;
type?: 'instruction' | 'coord' | null | undefined;
instruction?: string | undefined;
}) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
let functionReturn: { status: string; message?: string | undefined } = {
status: '',
message: '',
};
switch (command) {
case 'start':
if (debugLog) {
@@ -88,7 +132,7 @@ export const useSimulationStore = defineStore('simulationStore', {
this.simulationState == 'RUNNING' ||
this.simulationState == 'PAUSED'
) {
fnctRtn = { sts: 'error', msg: 'Simulation is already running' };
functionReturn = { status: 'error', message: 'Simulation is already running' };
throw new Error('Simulation is already running' + this.simulationState);
}
if (debugLog) {
@@ -97,17 +141,17 @@ export const useSimulationStore = defineStore('simulationStore', {
socket.emit(
'simulation_control',
{ command: 'start' },
(response: SimulationControlResponse) => {
(response: CommandResponse) => {
if (response.command_status == 'ERROR') {
fnctRtn = { sts: 'error', msg: response.message?.toString() };
functionReturn = { status: 'error', message: response.message?.toString() };
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_start: ', response);
@@ -121,16 +165,17 @@ export const useSimulationStore = defineStore('simulationStore', {
socket.emit(
'simulation_control',
{ command: 'test-mode' },
(response: SimulationControlResponse) => {
(response: CommandResponse) => {
console.log(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' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_test-mode: ', response);
@@ -144,16 +189,16 @@ export const useSimulationStore = defineStore('simulationStore', {
socket.emit(
'simulation_control',
{ command: 'gps-noise' },
(response: SimulationControlResponse) => {
(response: CommandResponse) => {
if (response.command_status === 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_gps-noise: ', response);
@@ -170,16 +215,16 @@ export const useSimulationStore = defineStore('simulationStore', {
socket.emit(
'simulation_control',
{ command: 'pause' },
(response: SimulationControlResponse) => {
(response: CommandResponse) => {
if (response.command_status === 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_pause: ', response);
@@ -197,12 +242,12 @@ export const useSimulationStore = defineStore('simulationStore', {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_resume: ', response);
@@ -219,12 +264,12 @@ export const useSimulationStore = defineStore('simulationStore', {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_clear: ', response);
@@ -241,12 +286,12 @@ export const useSimulationStore = defineStore('simulationStore', {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_reset: ', response);
@@ -263,12 +308,12 @@ export const useSimulationStore = defineStore('simulationStore', {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_end: ', response);
@@ -292,17 +337,19 @@ export const useSimulationStore = defineStore('simulationStore', {
longitude: longitude,
delay: delay,
address: address,
type: type,
instruction: instruction,
},
(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' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_add: ', response);
@@ -320,12 +367,12 @@ export const useSimulationStore = defineStore('simulationStore', {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
if (!response.data?.simulation_queue) {
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
this.digestQueueUpdate(response.data.simulation_queue);
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_delete: ', response);
@@ -333,14 +380,20 @@ export const useSimulationStore = defineStore('simulationStore', {
return response.message;
}
});
this.updateLocationMark(loc_id, 'status', 'deleted');
break;
case 'next':
socket.emit('simulation_control', { command: 'next' }, (response) => {
socket.emit('simulation_control', { command: 'next' },
(response: CommandResponse) => {
if (response.command_status == 'ERROR') {
throw new Error(response.message);
} else {
this.simulationState = response.data?.simulation_queue?.state;
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
functionReturn = { status: 'OK', message: response.message?.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulate_control_next: ', response);
}
@@ -349,15 +402,94 @@ export const useSimulationStore = defineStore('simulationStore', {
});
break;
default:
fnctRtn = { sts: 'error', msg: 'Invalid command' };
functionReturn = { status: 'error', message: 'Invalid command' };
throw new Error('Invalid command');
}
return fnctRtn;
return functionReturn;
},
async simulationControlAsync({
command,
latitude,
longitude,
loc_id,
delay,
address,
type,
instruction,
}: {
command: string;
latitude?: number | null | undefined;
longitude?: number | null | undefined;
loc_id?: string | null | undefined;
delay?: number | null | undefined;
address?: NominatimResponse | string | null | undefined;
type?: 'instruction' | 'coord' | null | undefined;
instruction?: string | undefined;
}): Promise<ReturnedStatusMessage> {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
if (
command === 'start' &&
(this.simulationRunning ||
this.simulationState === 'RUNNING' ||
this.simulationState === 'PAUSED')
) {
throw new Error('Simulation is already running' + this.simulationState);
}
if (command === 'pause' && this.simulationState !== 'RUNNING') {
throw new Error('Simulation is not running');
}
if (command === 'resume' && this.simulationState !== 'PAUSED') {
throw new Error('Simulation is not paused');
}
if ((command === 'clear' || command === 'reset') && this.simulationQueueLength === 0) {
throw new Error('Simulation queue is empty');
}
if (command === 'end' && (this.simulationState === 'ENDED' || !this.simulationRunning)) {
throw new Error('Simulation has already ended');
}
if (command === 'add' && (!latitude || !longitude)) {
throw new Error('latitude or longitude not set');
}
if (command === 'delete' && !loc_id) {
throw new Error('loc_id not set.');
}
const args = {
command: command as SimulationCommands,
latitude,
longitude,
delay,
loc_id,
address: address ?? '',
type,
instruction,
};
if (debugLog) {
console.log('Emitting simulation_control: ', args);
}
return await new Promise<ReturnedStatusMessage>((resolve) => {
socket.emit('simulation_control', args, (response: CommandResponse) => {
if (response.data?.simulation) {
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from simulation_control: ', response);
}
resolve({
status: response.command_status === 'ERROR' ? 'error' : 'OK',
message: response.message || '',
});
});
});
},
updateLocationMark(loc_id: string, key: string, value: string | number) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { command_status: string; message: string };
let functionReturn: ReturnedStatusMessage = { status: '', message: '' };
if (debugLog) {
console.log(
'socketStore: Update LocationMark request, loc_id: %s, key: %s, new value: %s',
@@ -367,8 +499,8 @@ export const useSimulationStore = defineStore('simulationStore', {
);
}
if (!this.locationQueueData[loc_id]) {
fnctRtn = {
command_status: 'error',
functionReturn = {
status: 'error',
message: 'Location Id: ' + loc_id + ' is not in the simulation queue',
};
throw new Error('loc_id ' + loc_id + 'not found');
@@ -379,62 +511,89 @@ export const useSimulationStore = defineStore('simulationStore', {
socket.emit(
'location_item_update',
{ loc_id: loc_id, key: key, value: value },
(response: LocationMarkUpdateResponse) => {
(response: CommandResponse) => {
console.log('response from location_item_update: ', response);
if (response.command_status == 'ERROR') {
fnctRtn = { command_status: 'error', message: response.message?.toString() };
functionReturn = { status: 'error', message: response.message?.toString() };
throw new Error(response.message);
} else {
fnctRtn = { command_status: 'OK', message: response.message?.toString() };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
functionReturn = { status: 'OK', message: response.message.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from backend: ', response);
console.log('response from simulate_control_test-mode: ', response);
}
this.locationQueueData[loc_id] = response.data;
}
return fnctRtn;
return functionReturn;
},
);
},
updateLocationQueueCombined(newOrder: string[]) {
const combinedNewOrder = [...this.locationQueueFilteredDone, ...newOrder];
this.updateLocationQueueOrder(combinedNewOrder);
},
updateLocationQueueOrder(newOrder: string[]) {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { command_status: string; message: string };
let functionReturn: { 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() };
functionReturn = { status: 'error', message: response.message?.toString() };
throw new Error(response.message);
} else {
fnctRtn = { command_status: 'OK', message: response.message?.toString() };
if (!response.data?.simulation) {
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
throw new Error(response.message);
} else {
functionReturn = { status: 'OK', message: response.message.toString() };
this.digestUpdate(response.data.simulation);
}
if (debugLog) {
console.log('response from queue_order_update: ', response);
console.log('response from simulate_control_test-mode: ', response);
}
this.locationQueueOrder = response.data;
}
return fnctRtn;
return functionReturn;
});
},
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;
getLocationData(loc_id: strong): LocationMark {
return this.locationQueueData[loc_id]
},
digestCurrentLocation(data: CurrentLocation): void {
this.currentLocation = {
loc_id: data.loc_id,
latitude: data.latitude,
longitude: data.longitude,
next_move: data.next_move,
};
digestUpdate(data: SimulationStatusUpdate): void {
if (data.current_location) this.updateCurrentLocation(data.current_location);
if ('test_mode' in data) this.testMode = data.test_mode;
if ('gps_noise' in data) this.gpsNoise = data.gps_noise;
if ('active' in data) this.simulationRunning = data.active;
if ('state' in data) this.simulationState = data.state;
if (data.data) this.updateData(data.data);
if (data.order) {
this.locationQueueOrder = data.order;
this.simulationQueueLength = data.order.length;
}
if ('deleted_items' in data) this.locationQueueDeletedItems = data.deleted_items ?? [];
if ('worker_task' in data) this.workerTask = data.worker_task;
},
updateCurrentLocation(data: { loc_id?: string; latitude?: number; longitude?: number }): void {
if ('loc_id' in data) this.currentLocation.loc_id = data.loc_id ?? null;
if ('latitude' in data) this.currentLocation.latitude = data.latitude ?? null;
if ('longitude' in data) this.currentLocation.longitude = data.longitude ?? null;
},
updateData(data: LocationQueue): void {
const incomingKeyCount = Object.keys(data).length;
if (incomingKeyCount === 0) this.locationQueueData = {};
if (incomingKeyCount === 1) {
this.locationQueueData = { ...this.locationQueueData, ...data };
} else {
this.locationQueueData = data;
}
},
},
});
+46 -12
View File
@@ -1,14 +1,14 @@
import { defineStore, acceptHMRUpdate } from 'pinia';
import { socket } from 'boot/socketio';
import { Dialog } from 'quasar';
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';
import type { AppError, ErrorFull, StatusUpdate, SendTestPromptResp } from 'components/models';
export const useSocketStore = defineStore('socketStore', {
state: () => {
@@ -88,6 +88,28 @@ export const useSocketStore = defineStore('socketStore', {
console.error('Error Received: ', data);
});
// testPrompt
socket.on('test_prompt', (data, callback) => {
Dialog.create({
title: 'Confirm Action',
message: data.prompt_name,
cancel: true,
persistent: true,
})
.onOk(() => {
// Execute callback to send 'YES' state back to Python
callback({ confirmed: true });
})
.onCancel(() => {
// Execute callback to send 'NO' state back to Python
callback({ confirmed: false });
})
.onDismiss(() => {
// Handle backdrop clicks if persistent is false
callback({ confirmed: false });
});
});
// status
socket.on('status', (data: StatusUpdate): void => {
if (this.debugLog) {
@@ -98,6 +120,9 @@ export const useSocketStore = defineStore('socketStore', {
},
requestUpdate(): void {
socket.emit('request_update', (response: StatusUpdate) => {
if (this.debugLog) {
console.debug('Received status update: ', response);
}
this.digestUpdate(response);
});
},
@@ -106,20 +131,29 @@ export const useSocketStore = defineStore('socketStore', {
const icloudStore = useIcloudStore();
const simulationStore = useSimulationStore();
const tunneldStore = useTunneldStore();
if (data.simulation_queue) {
simulationStore.digestQueueUpdate(data.simulation_queue);
if (data.simulation) {
simulationStore.digestUpdate(data.simulation);
}
deviceStore.deviceConnected = !!(data.udid && data.tunnel);
if (data.current_location) {
simulationStore.digestCurrentLocation(data.current_location);
if (data.device) {
const devConnected: boolean = !!data.device.udid;
deviceStore.digestUpdate({ connected: devConnected, uptime: data.device.uptime ?? 0 });
}
if (data.tunneld) {
tunneldStore.tunnelConnected = !!data.tunneld.tunnel;
tunneldStore.tunneldWatcher = data.tunneld.tunnel_watcher_running;
}
tunneldStore.tunnelConnected = !!data.tunnel;
tunneldStore.tunneldWatcher = data.tunnel_watcher_running;
if (data.icloud) {
icloudStore.digestUpdate(data.icloud);
}
icloudStore.findMyUpdate = data.fmf_location;
},
requestSendTestPrompt(promptName: string) {
socket.emit(
'send_test_prompt',
{ prompt_name: promptName },
(response: SendTestPromptResp) => {
console.log('Response from requestSendTestPrompt:', response);
},
);
},
},
});
+27 -4
View File
@@ -18,7 +18,7 @@ export const useTunneldStore = defineStore('tunneldStore', {
tunneldControl(command: TunneldCommands, udid: string = '') {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
if (debugLog) {
console.log('tunneldStore: got command: ', command);
}
@@ -30,14 +30,37 @@ export const useTunneldStore = defineStore('tunneldStore', {
'tunneld_control', args, (response) => {
console.log(response.command_status, response.command);
if (response.command_status == 'ERROR') {
fnctRtn = { sts: 'error', msg: response.message?.toString() };
functionReturn = { status: 'error', message: response.message?.toString() };
throw new Error(response.message);
} else {
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
functionReturn = { status: 'OK', message: response.message?.toString() };
}
},
);
return fnctRtn;
return functionReturn;
},
async tunneldControlAsync(command: TunneldCommands, udid: string = ''): Promise<{ status: string; message: string }> {
const socketStore = useSocketStore();
const debugLog = socketStore.debugLog;
if (debugLog) {
console.log('tunneldStore: got command: ', command);
}
let args: { command: TunneldCommands; udid?: string } = { command: command };
if (udid !== '') {
args = { command: command, udid: udid };
}
return await new Promise((resolve) => {
socket.emit('tunneld_control', args, (response) => {
if (debugLog) {
console.log(response.command_status, response.command);
}
resolve({
status: response.command_status === 'ERROR' ? 'error' : 'OK',
message: response.message || '',
});
});
});
}
}
});