milestones
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
<q-input
|
||||
v-model="formData.longitude"
|
||||
label="Longitude"
|
||||
style="max-width: 150px"
|
||||
color="secondary"
|
||||
readonly
|
||||
/>
|
||||
<q-input
|
||||
v-model="formData.latitude"
|
||||
label="Latitude"
|
||||
style="max-width: 150px"
|
||||
color="secondary"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<div class="row q-col-gutter-md">
|
||||
<q-input
|
||||
v-model="formData.longitude"
|
||||
label="Longitude"
|
||||
style="max-width: 118px"
|
||||
color="secondary"
|
||||
readonly
|
||||
/>
|
||||
<q-input
|
||||
v-model="formData.latitude"
|
||||
label="Latitude"
|
||||
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 });
|
||||
}
|
||||
favoriteStore.favoriteControl({ command: 'set', favorite: formData });
|
||||
onDialogOK({ operation: props.operation, data: formData });
|
||||
const response = { operation: props.operation, data: formData };
|
||||
onDialogOK(response);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
@@ -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,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="white-space: pre-line">
|
||||
<div>
|
||||
{{ formattedAddressLine1 }}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
+136
-91
@@ -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(() => {
|
||||
return locationQueueOrder.value.indexOf(props.loc_id);
|
||||
if (isDeleted.value) {
|
||||
return locationQueueDeletedItems.value.indexOf(props.loc_id);
|
||||
} else {
|
||||
return locationQueueOrder.value.indexOf(props.loc_id);
|
||||
}
|
||||
});
|
||||
|
||||
const myUpdatedIndex = computed(() => {
|
||||
return myIndex.value - currentIndex.value;
|
||||
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,33 +202,66 @@ const itemClass = computed(() => {
|
||||
});
|
||||
|
||||
const customIcon = computed(() => {
|
||||
if (myUpdatedIndex.value > 0) {
|
||||
return new Icon({
|
||||
color: 'blue',
|
||||
accentColor: 'firebrick',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
});
|
||||
} else if (myUpdatedIndex.value < 0) {
|
||||
if (myData.value.status === 'deleted') {
|
||||
return new Icon({
|
||||
color: 'black',
|
||||
accentColor: 'grey',
|
||||
accentColor: 'black',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
svg: TackDiamondBorder,
|
||||
});
|
||||
} else {
|
||||
return new Icon({
|
||||
color: 'pink',
|
||||
accentColor: 'black',
|
||||
content: '*',
|
||||
contentColor: 'black',
|
||||
scale: 1,
|
||||
svg: PinStarPanel,
|
||||
});
|
||||
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',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
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',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return new Icon({
|
||||
color: 'pink',
|
||||
accentColor: 'black',
|
||||
content: '*',
|
||||
contentColor: 'black',
|
||||
scale: 1,
|
||||
svg: PinStarPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
+146
-158
@@ -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">
|
||||
@@ -404,7 +393,7 @@ function handleControlClick(cmdAttr: ControlAction) {
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<q-btn-dropdown class="text-weight-bold" stretch flat label="Controls">
|
||||
<q-btn-dropdown class="text-weight-bold" stretch flat label="Controls">
|
||||
<q-list dark>
|
||||
<q-expansion-item
|
||||
group="controls"
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 +0,0 @@
|
||||
export {};
|
||||
@@ -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>
|
||||
|
||||
+156
-47
@@ -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">
|
||||
-->
|
||||
<div class="q-gutter-x-sm">
|
||||
<q-btn dense rounded push size="sm" icon="mdi-cog" @click="socketStore.toggleSock()">
|
||||
<q-badge :color="statusDevColor(sockConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
push
|
||||
size="sm"
|
||||
icon="mdi-cellphone"
|
||||
@click="socketStore.requestUpdate()"
|
||||
>
|
||||
<q-badge :color="statusDevColor(deviceConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn dense rounded push size="sm" icon="mdi-subway" @click="socketStore.requestUpdate()">
|
||||
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
push
|
||||
size="sm"
|
||||
icon="mdi-map-marker"
|
||||
@click="socketStore.requestUpdate()"
|
||||
>
|
||||
<q-badge :color="statusDevColor(simulationState)" rounded floating />
|
||||
</q-btn>
|
||||
<q-btn
|
||||
dense
|
||||
rounded
|
||||
push
|
||||
size="sm"
|
||||
icon="mdi-cloud"
|
||||
@click="icloudStore.icloudMonitorControl('refresh')"
|
||||
>
|
||||
<q-badge :color="statusDevColor(icloudMonitor)" rounded floating />
|
||||
</q-btn>
|
||||
</div>
|
||||
<!--
|
||||
<q-img src="~assets/simloc_logo2.png" class="logo" @click="showStatus = !showStatus" />
|
||||
<span class="text-grey-9">SimPi</span>
|
||||
<q-space />
|
||||
-->
|
||||
<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
|
||||
size="sm"
|
||||
icon="mdi-cellphone"
|
||||
@click="socketStore.requestUpdate()"
|
||||
aria-label="Device Status"
|
||||
>
|
||||
<q-badge :color="statusDevColor(deviceConnected)" rounded floating />
|
||||
</q-btn>
|
||||
<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
|
||||
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
|
||||
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-toolbar>
|
||||
<!--
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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="© 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
+277
-311
@@ -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,15 +172,22 @@ 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,
|
||||
) => void;
|
||||
callback: (response: FavControlResponse) => void,
|
||||
) => void;
|
||||
category_control: (
|
||||
args: {
|
||||
command: CategoryCommands;
|
||||
category?: Category | undefined | null;
|
||||
},
|
||||
callback: (response: FavControlResponse) => void,
|
||||
) => void;
|
||||
simulation_control: (
|
||||
args: {
|
||||
command: SimulationCommands;
|
||||
@@ -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 FavControlResponse {
|
||||
command_status: string;
|
||||
command: string;
|
||||
command_class: string;
|
||||
message?: string;
|
||||
data?: {
|
||||
categories?: Category[];
|
||||
favorites?: Favorite[];
|
||||
}
|
||||
|
||||
export interface coords {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
|
||||
export interface SearchControlProps {
|
||||
provider: OpenStreetMapProvider;
|
||||
showMarker: boolean;
|
||||
autoClose: boolean;
|
||||
updateMap: boolean;
|
||||
showPopup: boolean;
|
||||
style: 'button' | 'bar';
|
||||
acceptAutoLoad: boolean;
|
||||
autoComplete: boolean;
|
||||
autoCompleteDelay: number;
|
||||
retainZoomLevel: boolean;
|
||||
animateZoom: boolean;
|
||||
keepResult: boolean;
|
||||
}
|
||||
|
||||
export interface CurrentLocation {
|
||||
loc_id: string;
|
||||
latitude: number | null | undefined;
|
||||
longitude: number | null | undefined;
|
||||
// start_time?: string | null | undefined;
|
||||
// end_time?: string | null | undefined
|
||||
next_move?: number | null | undefined;
|
||||
}
|
||||
|
||||
export interface NextLocation {
|
||||
loc_id: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
time_at_location?: number | null;
|
||||
}
|
||||
|
||||
/*
|
||||
export interface NominatimReverseResponse {
|
||||
place_id: number;
|
||||
licence: string;
|
||||
osm_type: string;
|
||||
osm_id: number;
|
||||
lat: string;
|
||||
lon: string;
|
||||
class: string;
|
||||
type: string;
|
||||
place_rank: number;
|
||||
importance: number;
|
||||
addresstype: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
address: NominatimAddress;
|
||||
boundingbox: string[];
|
||||
}
|
||||
|
||||
export interface NominatimAddress {
|
||||
house_number: string;
|
||||
road: string;
|
||||
village?: string;
|
||||
city?: string;
|
||||
county: string;
|
||||
state: string;
|
||||
'ISO3166-2-lvl4': string;
|
||||
postcode: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
}
|
||||
*/
|
||||
|
||||
export interface routeSegments {
|
||||
fromWaypoint: number;
|
||||
toWaypoint: number;
|
||||
@@ -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 {
|
||||
@@ -475,7 +387,7 @@ export interface NominatimResponse {
|
||||
neighbourhood?: string | null | undefined;
|
||||
suburb?: string | null | undefined;
|
||||
county: string;
|
||||
town? : string | null | undefined;
|
||||
town?: string | null | undefined;
|
||||
city?: string | null | undefined;
|
||||
village?: string | null | undefined;
|
||||
municipality?: string | null | undefined;
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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,44 +16,75 @@ 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 setStartRoute = () => {
|
||||
if (!clickedLatLng.value) return;
|
||||
closeAllPopups();
|
||||
(routeSet.value as RouteSet).start = clickedLatLng.value;
|
||||
console.log('setStartRoute: ', routeSet.value.start);
|
||||
const handleMarkerDrag = (latlng: L.LatLng) => {
|
||||
clickedLatLng.value = latlng;
|
||||
};
|
||||
|
||||
const setEndRoute = () => {
|
||||
if (!clickedLatLng.value) return;
|
||||
(routeSet.value as RouteSet).end = clickedLatLng.value;
|
||||
closeAllPopups();
|
||||
console.log('setEndRoute: ', routeSet.value.end);
|
||||
updateRoute();
|
||||
console.log('updating Route');
|
||||
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));
|
||||
*/
|
||||
};
|
||||
|
||||
return {
|
||||
isFavorite,
|
||||
clickedLatLng,
|
||||
handleMarkerClick,
|
||||
setStartRoute,
|
||||
setEndRoute,
|
||||
};
|
||||
}
|
||||
|
||||
const setStartRoute = () => {
|
||||
leafletStore.layerControl.routing = true;
|
||||
if (!clickedLatLng.value) return;
|
||||
closeAllPopups();
|
||||
(leafletStore.routeSet as RouteSet).start = clickedLatLng.value;
|
||||
console.log('setStartRoute: ', routeSet.value.start);
|
||||
};
|
||||
|
||||
const setEndRoute = () => {
|
||||
leafletStore.layerControl.routing = true;
|
||||
if (!clickedLatLng.value) return;
|
||||
(leafletStore.routeSet as RouteSet).end = clickedLatLng.value;
|
||||
closeAllPopups();
|
||||
console.log('setEndRoute: ', leafletStore.routeSet.end);
|
||||
updateRoute();
|
||||
console.log('updating Route');
|
||||
};
|
||||
|
||||
return {
|
||||
isFavorite,
|
||||
clickedLatLng,
|
||||
handleMarkerClick,
|
||||
setStartRoute,
|
||||
setEndRoute,
|
||||
handleMarkerDrag,
|
||||
handleMarkerDragEnd,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+451
-134
@@ -1,146 +1,463 @@
|
||||
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: {
|
||||
start: {
|
||||
name: 'Start Location Sim',
|
||||
cmd: 'start',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
pause: {
|
||||
name: 'Pause Location Sim',
|
||||
cmd: 'pause',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-pause',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
resume: {
|
||||
name: 'Resume Location Simulation',
|
||||
cmd: 'resume',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-play-pause',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
clear: {
|
||||
name: 'Clear Future Items',
|
||||
cmd: 'clear',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'msi-map-marker-remove',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
reset: {
|
||||
name: 'Reset Location Queue',
|
||||
cmd: 'reset',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: true,
|
||||
delay: 0,
|
||||
},
|
||||
end: {
|
||||
name: 'End Location Sim',
|
||||
cmd: 'end',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: true,
|
||||
delay: 0,
|
||||
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',
|
||||
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',
|
||||
cmd: 'pause',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-pause',
|
||||
cnfrm: false,
|
||||
disabled: false,
|
||||
delay: 0,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
resume: {
|
||||
name: 'Resume Location Simulation',
|
||||
cmd: 'resume',
|
||||
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: '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: false,
|
||||
disabled: false,
|
||||
delay: 0,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
end: {
|
||||
name: 'End Location Sim',
|
||||
cmd: 'end',
|
||||
cmdClass: 'sim_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: true,
|
||||
disabled: false,
|
||||
delay: 0,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
device: {
|
||||
shutdown: {
|
||||
name: 'Shutdown',
|
||||
cmd: 'shutdown',
|
||||
cmdClass: 'dev_cntrl_class',
|
||||
icon: 'mdi-power',
|
||||
cnfrm: true,
|
||||
delay: 5,
|
||||
},
|
||||
reboot: {
|
||||
name: 'Reboot',
|
||||
cmd: 'reboot',
|
||||
cmdClass: 'dev_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: true,
|
||||
delay: 5,
|
||||
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',
|
||||
cmd: 'reboot',
|
||||
cmdClass: 'dev_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: true,
|
||||
disabled: false,
|
||||
delay: 5,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
icloudmonitor: {
|
||||
start: {
|
||||
name: 'Start iCloud Monitor',
|
||||
cmd: 'start',
|
||||
cmdClass: 'icloud-monitor_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
stop: {
|
||||
name: 'Stop iCloud Monitor',
|
||||
cmd: 'stop',
|
||||
cmdClass: 'icloud-monitor_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
key: 'icloudmonitor',
|
||||
icon: 'mdi-cloud',
|
||||
name: 'iCloud Monitor',
|
||||
commands: {
|
||||
start: {
|
||||
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',
|
||||
cmd: 'stop',
|
||||
cmdClass: 'icloud-monitor_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
disabled: false,
|
||||
delay: 5,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
tunneld: {
|
||||
start: {
|
||||
name: 'Start Tunneld',
|
||||
cmd: 'start',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
start_watcher: {
|
||||
name: 'Start TunneldWatcher',
|
||||
cmd: 'start-watcher',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
end_watcher: {
|
||||
name: 'End TunneldWatcher',
|
||||
cmd: 'end-watcher',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
clear: {
|
||||
name: 'Clear Tunneld',
|
||||
cmd: 'clear',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-backspace',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
cancel: {
|
||||
name: 'Cancel Tunneld',
|
||||
cmd: 'cancel',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-cancel',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
restart: {
|
||||
name: 'Restart Tunneld',
|
||||
cmd: 'restart',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-restart',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
},
|
||||
shutdown: {
|
||||
name: 'Shutdown Tunneld',
|
||||
cmd: 'shutdown',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
delay: 0,
|
||||
key: 'tunneld',
|
||||
name: 'tunneld',
|
||||
icon: 'mdi-subway',
|
||||
commands: {
|
||||
start: {
|
||||
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',
|
||||
cmd: 'start-watcher',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-play',
|
||||
cnfrm: false,
|
||||
disabled: false,
|
||||
delay: 0,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
end_watcher: {
|
||||
name: 'End TunneldWatcher',
|
||||
cmd: 'end-watcher',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-stop',
|
||||
cnfrm: false,
|
||||
disabled: false,
|
||||
delay: 0,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
clear: {
|
||||
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',
|
||||
cmd: 'cancel',
|
||||
cmdClass: 'tunneld_cntrl_class',
|
||||
icon: 'mdi-cancel',
|
||||
cnfrm: false,
|
||||
disabled: false,
|
||||
delay: 0,
|
||||
iconColor: 'grey-10',
|
||||
isVisible: true,
|
||||
},
|
||||
restart: {
|
||||
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',
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,24 +103,117 @@ 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;
|
||||
},
|
||||
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;
|
||||
}
|
||||
});
|
||||
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' });
|
||||
},
|
||||
getCategories(): string[] {
|
||||
this.favoriteControl({ command: 'get' });
|
||||
const categories: string[] = [];
|
||||
this.favorites.forEach((favorite) => {
|
||||
if (!categories.includes(favorite.category)) {
|
||||
categories.push(favorite.category);
|
||||
}
|
||||
});
|
||||
return categories;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+101
-56
@@ -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;
|
||||
},
|
||||
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;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+398
-8
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
+265
-106
@@ -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]
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
},
|
||||
digestCurrentLocation(data: CurrentLocation): void {
|
||||
this.currentLocation = {
|
||||
loc_id: data.loc_id,
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
next_move: data.next_move,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+46
-12
@@ -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
@@ -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 || '',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user