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>
|
<template>
|
||||||
<q-dialog ref="dlgRef" persistent>
|
<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-toolbar>
|
||||||
<q-avatar icon="mdi-star" color="secondary" text-color="yellow" />
|
<q-avatar icon="mdi-star" color="secondary" text-color="yellow" />
|
||||||
<q-toolbar-title>Clear Favorite</q-toolbar-title>
|
<q-toolbar-title>Clear Favorite</q-toolbar-title>
|
||||||
@@ -24,78 +24,90 @@
|
|||||||
>Edit Favorite</q-toolbar-title
|
>Edit Favorite</q-toolbar-title
|
||||||
>
|
>
|
||||||
</q-toolbar>
|
</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
|
<q-input
|
||||||
color="secondary"
|
color="secondary"
|
||||||
v-model="formData.name"
|
v-model="formData.name"
|
||||||
label="Favorite Name"
|
label="Favorite Name"
|
||||||
autofocus
|
autofocus
|
||||||
@keyup.enter="onOkClick"
|
@keyup.enter="onOkClick"
|
||||||
style="max-width: 250px"
|
style="max-width: 360px"
|
||||||
/>
|
/>
|
||||||
<div class="flex col q-col-gutter-md">
|
<div class="flex col q-col-gutter-md">
|
||||||
<q-select
|
<q-select
|
||||||
|
dense
|
||||||
|
options-dense
|
||||||
color="accent"
|
color="accent"
|
||||||
v-model="formData.category"
|
v-model="formData.category_id"
|
||||||
:options="categories"
|
:options="categories"
|
||||||
label="Category"
|
label="Select Category"
|
||||||
use-input
|
options-value="id"
|
||||||
new-value-mode="add-unique"
|
|
||||||
style="max-width: 150px"
|
|
||||||
>
|
|
||||||
<template v-slot:no-option>
|
|
||||||
<q-item>
|
|
||||||
<q-item-section class="text-grey"> No results or loading... </q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
<q-select
|
|
||||||
color="accent"
|
|
||||||
v-model="formData.icon"
|
|
||||||
:options="filteredIcons"
|
|
||||||
option-label="name"
|
option-label="name"
|
||||||
option-value="icon"
|
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
use-input
|
use-input
|
||||||
clearable
|
style="max-width: 175px"
|
||||||
label="Select Icon"
|
|
||||||
@filter="iconFilterFcn"
|
|
||||||
style="max-width: 150px"
|
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:selected-item="scope: any">
|
||||||
<q-icon
|
<q-item>
|
||||||
:name="formData.icon ? formData.icon : 'mdi-library'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-slot:option="scope">
|
|
||||||
<q-item v-bind="scope.itemProps">
|
|
||||||
<q-item-section avatar>
|
<q-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-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
<q-item-label>{{ scope.opt.name }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</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>
|
</q-select>
|
||||||
|
<IconPicker v-model="formData.icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex col q-col-gutter-md">
|
<div class="flex">
|
||||||
<q-input
|
<div class="row q-col-gutter-md">
|
||||||
v-model="formData.longitude"
|
<q-input
|
||||||
label="Longitude"
|
v-model="formData.longitude"
|
||||||
style="max-width: 150px"
|
label="Longitude"
|
||||||
color="secondary"
|
style="max-width: 118px"
|
||||||
readonly
|
color="secondary"
|
||||||
/>
|
readonly
|
||||||
<q-input
|
/>
|
||||||
v-model="formData.latitude"
|
<q-input
|
||||||
label="Latitude"
|
v-model="formData.latitude"
|
||||||
style="max-width: 150px"
|
label="Latitude"
|
||||||
color="secondary"
|
style="max-width: 118px"
|
||||||
readonly
|
color="secondary"
|
||||||
/>
|
readonly
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="formData.radius"
|
||||||
|
label="Radius"
|
||||||
|
style="max-width: 118px"
|
||||||
|
color="secondary"
|
||||||
|
suffix="m"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
@@ -110,16 +122,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useFavoriteStore } from 'stores/favorite';
|
import { useFavoriteStore } from 'stores/favorite';
|
||||||
import { useDialogPluginComponent } from 'quasar';
|
import { useDialogPluginComponent } from 'quasar';
|
||||||
import type { Favorite } from 'components/models';
|
import type { NewFavorite, FavoriteCommands, GeoCacheResponse } from 'components/models';
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
|
import FormattedAddress from 'components/FormattedAddress.vue';
|
||||||
import iconList from '@quasar/extras/mdi-v7/icons.json';
|
import IconPicker from 'components/IconPicker.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
id?: number | undefined | null;
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
isFavorite: boolean;
|
radius: number;
|
||||||
operation: string;
|
geoCacheResp: GeoCacheResponse;
|
||||||
|
operation: FavoriteCommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteStore = useFavoriteStore();
|
const favoriteStore = useFavoriteStore();
|
||||||
@@ -130,86 +144,37 @@ defineEmits([...useDialogPluginComponent.emits]);
|
|||||||
|
|
||||||
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
||||||
|
|
||||||
const iconsList = ref(iconList);
|
const categories = computed(() => favoriteStore.categories);
|
||||||
|
|
||||||
interface FormattedIcon {
|
const address = props.geoCacheResp.address;
|
||||||
name: string;
|
const isFavorite = !!props.geoCacheResp.favorite;
|
||||||
icon: string;
|
const icon = ref(props.geoCacheResp.favorite?.icon as string);
|
||||||
}
|
const name = ref(props.geoCacheResp.favorite?.name);
|
||||||
const iconsListFormatted: FormattedIcon[] = iconsList.value.map((str) => {
|
const categoryId = ref(props.geoCacheResp.favorite?.category_id);
|
||||||
const name = str
|
|
||||||
.replace(/^[a-z]+/, '')
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.trim();
|
|
||||||
const icon = str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
||||||
return { name, icon };
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredIcons = ref(iconsListFormatted.slice(0, 100));
|
const formData: NewFavorite = reactive({
|
||||||
const icon = ref();
|
|
||||||
const name = ref();
|
|
||||||
const address = ref();
|
|
||||||
const category = ref();
|
|
||||||
const categories = computed({
|
|
||||||
get: () => favoriteStore.categories,
|
|
||||||
set: (value) => {
|
|
||||||
favoriteStore.categories = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const formData: Favorite = reactive({
|
|
||||||
name: name.value,
|
name: name.value,
|
||||||
longitude: props.lng,
|
longitude: props.lng,
|
||||||
latitude: props.lat,
|
latitude: props.lat,
|
||||||
category: category.value,
|
category_id: categoryId.value,
|
||||||
icon: icon.value,
|
icon: icon.value,
|
||||||
}) as Favorite;
|
radius: props.radius,
|
||||||
const loading = ref(true);
|
address: address,
|
||||||
|
}) as NewFavorite;
|
||||||
function iconFilterFcn(val: string, update: (callback: () => void) => void) {
|
|
||||||
update(() => {
|
|
||||||
if (val === '') {
|
|
||||||
filteredIcons.value = iconsListFormatted.slice(0, 100);
|
|
||||||
} else {
|
|
||||||
const needle = val.toLowerCase();
|
|
||||||
filteredIcons.value = iconsListFormatted
|
|
||||||
.filter(v =>
|
|
||||||
v.name.toLowerCase().includes(needle) ||
|
|
||||||
v.icon.toLowerCase().includes(needle)
|
|
||||||
)
|
|
||||||
.slice(0, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOkClick() {
|
function onOkClick() {
|
||||||
if (props.isFavorite && props.operation === 'clear') {
|
const response = { operation: props.operation, data: formData };
|
||||||
favoriteStore.favoriteControl({ command: 'delete', favorite: formData });
|
onDialogOK(response);
|
||||||
}
|
|
||||||
favoriteStore.favoriteControl({ command: 'set', favorite: formData });
|
|
||||||
onDialogOK({ operation: props.operation, data: formData });
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
onMounted(() => {
|
||||||
|
if (props.geoCacheResp.favorite) {
|
||||||
onMounted(async () => {
|
formData.name = props.geoCacheResp.favorite.name;
|
||||||
try {
|
formData.icon = props.geoCacheResp.favorite.icon;
|
||||||
loading.value = true;
|
formData.category_id = props.geoCacheResp.favorite.category_id;
|
||||||
const response = await reverseGeocodeRateLimited(props.lat, props.lng);
|
|
||||||
console.log('reverse geocode response: ', response);
|
|
||||||
address.value = response.address;
|
|
||||||
if (response.favorite) {
|
|
||||||
formData.name = response.favorite.name;
|
|
||||||
formData.category = response.favorite.category;
|
|
||||||
formData.icon = response.favorite.icon;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching reverse geocode:', error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await sleep(500);
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
</script>
|
</script>
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
.add-loc-card
|
.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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div style="white-space: pre-line">
|
<div>
|
||||||
{{ formattedAddressLine1 }}
|
{{ formattedAddressLine1 }}
|
||||||
</div>
|
</div>
|
||||||
<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 { useSimulationStore } from 'stores/simulation';
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import type { NominatimResponse } from 'components/models';
|
import type { NominatimResponse } from 'components/models';
|
||||||
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
|
import {
|
||||||
|
Icon,
|
||||||
|
PinCirclePanel,
|
||||||
|
PinStarPanel,
|
||||||
|
PointCircle,
|
||||||
|
TackDiamondBorder,
|
||||||
|
} from 'leaflet-extra-markers';
|
||||||
|
|
||||||
import FormattedAddress from 'components/FormattedAddress.vue';
|
import FormattedAddress from 'components/FormattedAddress.vue';
|
||||||
//import NestedKnob from 'components/NestedKnob.vue';
|
|
||||||
|
|
||||||
const simulationStore = useSimulationStore();
|
const simulationStore = useSimulationStore();
|
||||||
const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } =
|
const {
|
||||||
storeToRefs(simulationStore);
|
currentLocation,
|
||||||
|
locationQueueOrder,
|
||||||
|
locationQueueData,
|
||||||
|
locationQueueDeletedItems,
|
||||||
|
simulationRunning,
|
||||||
|
} = storeToRefs(simulationStore);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps<{
|
||||||
address: {
|
address?: NominatimResponse | undefined;
|
||||||
type: Object as () => NominatimResponse,
|
latitude: number;
|
||||||
required: false,
|
longitude: number;
|
||||||
},
|
icon?: string;
|
||||||
latitude: {
|
start: string;
|
||||||
type: Number,
|
end?: string | undefined;
|
||||||
required: true,
|
delay: number;
|
||||||
},
|
active: boolean;
|
||||||
longitude: {
|
isCurrentDelay: boolean;
|
||||||
type: Number,
|
loc_id: string;
|
||||||
required: true,
|
isLast: boolean;
|
||||||
},
|
index: number;
|
||||||
icon: {
|
status?: string | undefined;
|
||||||
type: String,
|
type?: string | undefined;
|
||||||
default: 'mdi-map-marker',
|
instruction?: string | undefined;
|
||||||
},
|
}>();
|
||||||
start: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
delay: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isCurrentDelay: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
loc_id: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isLast: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
index: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
default: 'queued',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define custom events that this component can emit
|
// Define custom events that this component can emit
|
||||||
const emit = defineEmits(['item-clicked']);
|
const emit = defineEmits(['item-clicked']);
|
||||||
@@ -75,6 +48,14 @@ function itemClicked() {
|
|||||||
emit('item-clicked', param1);
|
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());
|
const currentTime = ref(new Date());
|
||||||
let timerId: number;
|
let timerId: number;
|
||||||
|
|
||||||
@@ -103,7 +84,24 @@ function formatInAgo(seconds: number) {
|
|||||||
const calculateDeltaTime = computed(() => {
|
const calculateDeltaTime = computed(() => {
|
||||||
let delta;
|
let delta;
|
||||||
if (props.active) {
|
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 !== '') {
|
} else if (props.end && props.end !== '') {
|
||||||
const pastTime: Date = new Date(props.end);
|
const pastTime: Date = new Date(props.end);
|
||||||
const nowMs: number = currentTime.value.getTime();
|
const nowMs: number = currentTime.value.getTime();
|
||||||
@@ -124,7 +122,7 @@ const calculateDeltaTime = computed(() => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let startSlice;
|
let startSlice;
|
||||||
if (currentIndex.value > 0) {
|
if (currentIndex.value && currentIndex.value > 0) {
|
||||||
startSlice = currentIndex.value;
|
startSlice = currentIndex.value;
|
||||||
} else {
|
} else {
|
||||||
startSlice = 0;
|
startSlice = 0;
|
||||||
@@ -175,15 +173,26 @@ const humanReadableDateTime = (iso: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentIndex = computed(() => {
|
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(() => {
|
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(() => {
|
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(() => {
|
const itemClass = computed(() => {
|
||||||
@@ -193,33 +202,66 @@ const itemClass = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const customIcon = computed(() => {
|
const customIcon = computed(() => {
|
||||||
if (myUpdatedIndex.value > 0) {
|
if (myData.value.status === 'deleted') {
|
||||||
return new Icon({
|
|
||||||
color: 'blue',
|
|
||||||
accentColor: 'firebrick',
|
|
||||||
content: myUpdatedIndex.value.toString(),
|
|
||||||
contentColor: 'white',
|
|
||||||
scale: 1,
|
|
||||||
svg: PinCirclePanel,
|
|
||||||
});
|
|
||||||
} else if (myUpdatedIndex.value < 0) {
|
|
||||||
return new Icon({
|
return new Icon({
|
||||||
color: 'black',
|
color: 'black',
|
||||||
accentColor: 'grey',
|
accentColor: 'black',
|
||||||
content: myUpdatedIndex.value.toString(),
|
content: myUpdatedIndex.value.toString(),
|
||||||
contentColor: 'white',
|
contentColor: 'white',
|
||||||
scale: 1,
|
scale: 1,
|
||||||
svg: PinCirclePanel,
|
svg: TackDiamondBorder,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new Icon({
|
if (myUpdatedIndex.value > 0) {
|
||||||
color: 'pink',
|
if (props.type === 'coord') {
|
||||||
accentColor: 'black',
|
return new Icon({
|
||||||
content: '*',
|
color: 'blue',
|
||||||
contentColor: 'black',
|
accentColor: 'firebrick',
|
||||||
scale: 1,
|
content: myUpdatedIndex.value.toString(),
|
||||||
svg: PinStarPanel,
|
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(() => {
|
const displayDelay = computed(() => {
|
||||||
|
if (currentIndex.value === null) return true;
|
||||||
return props.index > currentIndex.value;
|
return props.index > currentIndex.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const delayActive = computed(() => {
|
const delayActive = computed(() => {
|
||||||
return props.index === currentIndex.value + 1;
|
return props.index === currentIndex.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const canDrag = computed(() => {
|
const canDrag = computed(() => {
|
||||||
|
if (currentIndex.value === null) return true;
|
||||||
return props.index > currentIndex.value + 1;
|
return props.index > currentIndex.value + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -330,8 +374,8 @@ onUnmounted(() => {
|
|||||||
<q-item
|
<q-item
|
||||||
v-if="displayDelay"
|
v-if="displayDelay"
|
||||||
:active="delayActive"
|
:active="delayActive"
|
||||||
:class="delayActive ? 'text-orange bg-dark' : ''"
|
:class="delayActive ? 'text-orange' : ''"
|
||||||
active-class="text-orange bg-dark"
|
active-class="text-orange"
|
||||||
>
|
>
|
||||||
<q-item-section class="flex flex-center">
|
<q-item-section class="flex flex-center">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -368,18 +412,19 @@ onUnmounted(() => {
|
|||||||
:active="active"
|
:active="active"
|
||||||
@click="itemClicked"
|
@click="itemClicked"
|
||||||
:class="itemClass"
|
:class="itemClass"
|
||||||
active-class="text-orange bg-dark"
|
active-class="text-orange"
|
||||||
>
|
>
|
||||||
<q-item-section style="width: 70%">
|
<q-item-section style="width: 70%">
|
||||||
<q-item-label v-if="address">
|
<q-item-label v-if="address">
|
||||||
<FormattedAddress :address="props.address" />
|
<FormattedAddress :address="props.address" />
|
||||||
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
|
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
<q-item-label v-else-if="instruction" lines="2">{{ instruction }}</q-item-label>
|
||||||
<q-item-label v-else> {{ latitude }}, {{ longitude }} </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) }}
|
start: {{ humanReadableDateTime(start) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption lines="1" v-if="end && simulationRunning">
|
<q-item-label caption lines="1" v-if="end && simulationRunning && !isDeleted">
|
||||||
end: {{ humanReadableDateTime(end) }}
|
end: {{ humanReadableDateTime(end) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label caption lines="1" v-if="!active && !end">
|
<q-item-label caption lines="1" v-if="!active && !end">
|
||||||
@@ -389,7 +434,7 @@ onUnmounted(() => {
|
|||||||
<q-item-section
|
<q-item-section
|
||||||
side
|
side
|
||||||
style="
|
style="
|
||||||
padding: 30px 0 0 10px;
|
padding: 30px 0 0 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 35%;
|
width: 35%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -398,7 +443,7 @@ onUnmounted(() => {
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<q-icon :class="{ 'drag-handle': canDrag }" v-html="iconElement.outerHTML" />
|
<q-icon :class="{ 'drag-handle': canDrag }" v-html="iconElement.outerHTML" />
|
||||||
<q-item-label caption lines="1" v-if="simulationRunning">
|
<q-item-label caption lines="1" v-if="simulationRunning && !isDeleted">
|
||||||
{{ calculateDeltaTime }}
|
{{ calculateDeltaTime }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|||||||
+146
-158
@@ -1,15 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
|
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
|
||||||
|
|
||||||
import { favorites } from 'constants/favorites';
|
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 { useDeviceStore } from 'stores/device';
|
||||||
import { useLeafletStore } from 'stores/leaflet';
|
import { useLeafletStore } from 'stores/leaflet';
|
||||||
@@ -17,7 +16,6 @@ import { useSimulationStore } from 'stores/simulation';
|
|||||||
import { useIcloudStore } from 'stores/icloud';
|
import { useIcloudStore } from 'stores/icloud';
|
||||||
import { useTunneldStore } from 'stores/tunneld';
|
import { useTunneldStore } from 'stores/tunneld';
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
const deviceStore = useDeviceStore();
|
const deviceStore = useDeviceStore();
|
||||||
@@ -25,6 +23,7 @@ const leafletStore = useLeafletStore();
|
|||||||
const simulationStore = useSimulationStore();
|
const simulationStore = useSimulationStore();
|
||||||
const icloudStore = useIcloudStore();
|
const icloudStore = useIcloudStore();
|
||||||
const tunneldStore = useTunneldStore();
|
const tunneldStore = useTunneldStore();
|
||||||
|
const { actions: controls } = useControls();
|
||||||
const { center, markerLatLng, zoom } = storeToRefs(leafletStore);
|
const { center, markerLatLng, zoom } = storeToRefs(leafletStore);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -43,7 +42,7 @@ const menuOpen = ref(false);
|
|||||||
const favoritesMap = favorites as Record<string, unknown>;
|
const favoritesMap = favorites as Record<string, unknown>;
|
||||||
|
|
||||||
const isLast = computed(() => {
|
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);
|
const currentIndex = locationQueueOrder.value.indexOf(currentLocation.value.loc_id);
|
||||||
return locationQueueOrder.value.length - 1 === currentIndex;
|
return locationQueueOrder.value.length - 1 === currentIndex;
|
||||||
} else {
|
} else {
|
||||||
@@ -51,15 +50,6 @@ const isLast = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type ControlAction = {
|
|
||||||
name: string;
|
|
||||||
cmd: string;
|
|
||||||
cmdClass: string;
|
|
||||||
icon: string;
|
|
||||||
cnfrm: boolean;
|
|
||||||
delay: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FavoriteWithCoords = {
|
type FavoriteWithCoords = {
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -95,8 +85,8 @@ function handleTestToggle() {
|
|||||||
delay: 0,
|
delay: 0,
|
||||||
address: null,
|
address: null,
|
||||||
});
|
});
|
||||||
if (response.sts === 'error') {
|
if (response.status === 'error') {
|
||||||
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
|
$q.notify({ type: 'negative', message: response.message ?? 'Failed to toggle test mode' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +99,8 @@ function handleGpsNoiseToggle() {
|
|||||||
delay: 0,
|
delay: 0,
|
||||||
address: null,
|
address: null,
|
||||||
});
|
});
|
||||||
if (response.sts === 'error') {
|
if (response.status === 'error') {
|
||||||
$q.notify({ type: 'negative', message: response.msg ?? 'Failed to toggle test mode' });
|
$q.notify({ type: 'negative', message: response.message ?? 'Failed to toggle test mode' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +115,8 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
})
|
})
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
|
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = simulationStore.simulationControl({
|
const ack = simulationStore.simulationControl({
|
||||||
command: cmdAttr['cmd'],
|
command: cmdAttr['cmd'],
|
||||||
@@ -136,71 +126,71 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
delay: cmdAttr.delay,
|
delay: cmdAttr.delay,
|
||||||
address: null,
|
address: null,
|
||||||
});
|
});
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Simulation Command ERROR: ', error.message);
|
console.error('Simulation Command ERROR: ', error.message);
|
||||||
notMsg = `Simulation Command Error: ${error.message}`;
|
notificationMessage = `Simulation Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Simulation Command Error: ', error);
|
console.error('Simulation Command Error: ', error);
|
||||||
notMsg = 'Simulation Command Error: Unknow error';
|
notificationMessage = 'Simulation Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
|
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
|
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Device Command ERROR: ', error.message);
|
console.error('Device Command ERROR: ', error.message);
|
||||||
notMsg = `Device Command Error: ${error.message}`;
|
notificationMessage = `Device Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Device Command Error: ', error);
|
console.error('Device Command Error: ', error);
|
||||||
notMsg = 'Device Command Error: Unknow error';
|
notificationMessage = 'Device Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
|
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
|
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Tunneld Command ERROR: ', error.message);
|
console.error('Tunneld Command ERROR: ', error.message);
|
||||||
notMsg = `Tunneld Command Error: ${error.message}`;
|
notificationMessage = `Tunneld Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Tunneld Command Error: ', error);
|
console.error('Tunneld Command Error: ', error);
|
||||||
notMsg = 'Tunneld Command Error: Unknow error';
|
notificationMessage = 'Tunneld Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -212,8 +202,8 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
|
if (cmdAttr.cmdClass === 'sim_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = simulationStore.simulationControl({
|
const ack = simulationStore.simulationControl({
|
||||||
command: cmdAttr.cmd,
|
command: cmdAttr.cmd,
|
||||||
@@ -223,96 +213,96 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
delay: cmdAttr.delay,
|
delay: cmdAttr.delay,
|
||||||
address: null,
|
address: null,
|
||||||
});
|
});
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Simulation Command ERROR: ', error.message);
|
console.error('Simulation Command ERROR: ', error.message);
|
||||||
notMsg = `Simulation Command Error: ${error.message}`;
|
notificationMessage = `Simulation Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Simulation Command Error: ', error);
|
console.error('Simulation Command Error: ', error);
|
||||||
notMsg = 'Simulation Command Error: Unknow error';
|
notificationMessage = 'Simulation Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cmdAttr.cmdClass === 'icloud-monitor_cntrl_class') {
|
if (cmdAttr.cmdClass === 'icloud-monitor_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = icloudStore.icloudMonitorControl(cmdAttr.cmd);
|
const ack = icloudStore.icloudMonitorControl(cmdAttr.cmd);
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Simulation Command ERROR: ', error.message);
|
console.error('Simulation Command ERROR: ', error.message);
|
||||||
notMsg = `Simulation Command Error: ${error.message}`;
|
notificationMessage = `Simulation Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Simulation Command Error: ', error);
|
console.error('Simulation Command Error: ', error);
|
||||||
notMsg = 'Simulation Command Error: Unknow error';
|
notificationMessage = 'Simulation Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
|
if (cmdAttr.cmdClass === 'dev_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
|
const ack = deviceStore.deviceControl(cmdAttr.cmd as DeviceCommands, cmdAttr.delay);
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Device Command ERROR: ', error.message);
|
console.error('Device Command ERROR: ', error.message);
|
||||||
notMsg = `Device Command Error: ${error.message}`;
|
notificationMessage = `Device Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Device Command Error: ', error);
|
console.error('Device Command Error: ', error);
|
||||||
notMsg = 'Device Command Error: Unknow error';
|
notificationMessage = 'Device Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
|
if (cmdAttr.cmdClass === 'tunneld_cntrl_class') {
|
||||||
let notType: string = 'positive';
|
let notificationType: string = 'positive';
|
||||||
let notMsg: string = '';
|
let notificationMessage: string = '';
|
||||||
try {
|
try {
|
||||||
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
|
const ack = tunneldStore.tunneldControl(cmdAttr.cmd as TunneldCommands);
|
||||||
if (ack.sts === 'error') {
|
if (ack.status === 'error') {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
}
|
}
|
||||||
if (ack.msg) {
|
if (ack.message) {
|
||||||
notMsg = ack.msg;
|
notificationMessage = ack.message;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notType = 'negative';
|
notificationType = 'negative';
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error('Tunneld Command ERROR: ', error.message);
|
console.error('Tunneld Command ERROR: ', error.message);
|
||||||
notMsg = `Tunneld Command Error: ${error.message}`;
|
notificationMessage = `Tunneld Command Error: ${error.message}`;
|
||||||
} else {
|
} else {
|
||||||
console.error('Tunneld Command Error: ', error);
|
console.error('Tunneld Command Error: ', error);
|
||||||
notMsg = 'Tunneld Command Error: Unknow error';
|
notificationMessage = 'Tunneld Command Error: Unknow error';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
$q.notify({ type: notType, message: notMsg });
|
$q.notify({ type: notificationType, message: notificationMessage });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +333,6 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
stretch
|
stretch
|
||||||
flat
|
flat
|
||||||
label="Favorites"
|
label="Favorites"
|
||||||
v-if="route.name === 'Leaflet'"
|
|
||||||
class="text-weight-bold"
|
class="text-weight-bold"
|
||||||
>
|
>
|
||||||
<q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
|
<q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
|
||||||
@@ -404,7 +393,7 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
</q-list>
|
</q-list>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
</q-btn>
|
</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-list dark>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="controls"
|
group="controls"
|
||||||
@@ -451,18 +440,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.simulation.start)"
|
@click="handleControlClick(controls.simulation.commands.start)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.simulation.start.icon"
|
:icon="controls.simulation.commands.start.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -471,18 +460,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.simulation.pause)"
|
@click="handleControlClick(controls.simulation.commands.pause)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.simulation.pause.icon"
|
:icon="controls.simulation.commands.pause.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -491,18 +480,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.simulation.resume)"
|
@click="handleControlClick(controls.simulation.commands.resume)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.simulation.resume.icon"
|
:icon="controls.simulation.commands.resume.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -511,18 +500,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.simulation.clear)"
|
@click="handleControlClick(controls.simulation.commands.clear)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.simulation.clear.icon"
|
:icon="controls.simulation.commands.clear.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -531,18 +520,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.simulation.reset)"
|
@click="handleControlClick(controls.simulation.commands.reset)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.simulation.reset.icon"
|
:icon="controls.simulation.commands.reset.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -551,18 +540,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.simulation.end)"
|
@click="handleControlClick(controls.simulation.commands.end)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.simulation.end.icon"
|
:icon="controls.simulation.commands.end.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
@@ -579,18 +568,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.icloudmonitor.start)"
|
@click="handleControlClick(controls.icloudmonitor.commands.start)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.icloudmonitor.start.icon"
|
:icon="controls.icloudmonitor.commands.start.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -599,19 +588,19 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.icloudmonitor.stop)"
|
@click="handleControlClick(controls.icloudmonitor.commands.stop)"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.icloudmonitor.stop.icon"
|
:icon="controls.icloudmonitor.commands.stop.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
@@ -622,18 +611,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.tunneld.start)"
|
@click="handleControlClick(controls.tunneld.commands.start)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.tunneld.start.icon"
|
:icon="controls.tunneld.commands.start.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -642,18 +631,37 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
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-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.tunneld.start_watcher.icon"
|
:icon="controls.tunneld.commands.start_watcher.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -662,18 +670,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.tunneld.restart)"
|
@click="handleControlClick(controls.tunneld.commands.clear)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.tunneld.restart.icon"
|
:icon="controls.tunneld.commands.clear.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -682,38 +690,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.tunneld.clear)"
|
@click="handleControlClick(controls.tunneld.commands.cancel)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.tunneld.clear.icon"
|
:icon="controls.tunneld.commands.cancel.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label> {{ controls.tunneld.clear.name }} </q-item-label>
|
<q-item-label> {{ controls.tunneld.commands.cancel.name }} </q-item-label>
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item
|
|
||||||
v-if="tunneldStore.tunnelConnected"
|
|
||||||
dark
|
|
||||||
clickable
|
|
||||||
v-ripple
|
|
||||||
v-close-popup
|
|
||||||
@click="handleControlClick(controls.tunneld.cancel)"
|
|
||||||
>
|
|
||||||
<q-item-section avatar class="q-pl-lg">
|
|
||||||
<q-avatar
|
|
||||||
:icon="controls.tunneld.cancel.icon"
|
|
||||||
color="secondary"
|
|
||||||
text-color="black"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label> {{ controls.tunneld.cancel.name }} </q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -722,18 +710,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
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-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.tunneld.end_watcher.icon"
|
:icon="controls.tunneld.commands.end_watcher.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -742,18 +730,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.tunneld.shutdown)"
|
@click="handleControlClick(controls.tunneld.commands.shutdown)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.tunneld.shutdown.icon"
|
:icon="controls.tunneld.commands.shutdown.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
@@ -768,18 +756,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.device.reboot)"
|
@click="handleControlClick(controls.device.commands.reboot)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.device.reboot.icon"
|
:icon="controls.device.commands.reboot.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -787,18 +775,18 @@ function handleControlClick(cmdAttr: ControlAction) {
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="handleControlClick(controls.device.shutdown)"
|
@click="handleControlClick(controls.device.commands.shutdown)"
|
||||||
>
|
>
|
||||||
<q-item-section avatar class="q-pl-lg">
|
<q-item-section avatar class="q-pl-lg">
|
||||||
<q-avatar
|
<q-avatar
|
||||||
:icon="controls.device.shutdown.icon"
|
:icon="controls.device.commands.shutdown.icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
text-color="black"
|
text-color="black"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-expansion-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>
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<q-btn flat label="No Delay" @click="onNoDelayClick" />
|
||||||
<q-btn flat label="OK" @click="onOkClick" />
|
<q-btn flat label="OK" @click="onOkClick" />
|
||||||
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
||||||
</div>
|
</div>
|
||||||
@@ -62,13 +63,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDialogPluginComponent } from 'quasar';
|
import { useDialogPluginComponent } from 'quasar';
|
||||||
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
|
import { useLocationStore } from 'stores/location';
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import FormattedAddress from 'components/FormattedAddress.vue';
|
import FormattedAddress from 'components/FormattedAddress.vue';
|
||||||
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
|
import EditFavoriteDialog from 'components/EditFavoriteDialog.vue';
|
||||||
import type { NominatimResponse } from 'components/models';
|
import type { NominatimResponse } from 'components/models';
|
||||||
|
|
||||||
|
const locationStore = useLocationStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
lat: { type: Number, required: true },
|
lat: { type: Number, required: true },
|
||||||
lng: { type: Number, required: true },
|
lng: { type: Number, required: true },
|
||||||
@@ -92,11 +95,11 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const response = await reverseGeocodeRateLimited(props.lat, props.lng);
|
const response = await locationStore.getCachedLocation({ lat: props.lat, lng: props.lng});
|
||||||
console.log('reverse geocode response: ', response);
|
console.log('reverse geocode response: ', response);
|
||||||
address.value = response.address;
|
address.value = response?.address;
|
||||||
if (response.favorite) {
|
if (response?.favorite) {
|
||||||
favorite.value = response.favorite;
|
favorite.value = response?.favorite;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching reverse geocode:', error);
|
console.error('Error fetching reverse geocode:', error);
|
||||||
@@ -141,6 +144,10 @@ function handleFavoriteClick() {
|
|||||||
function onOkClick() {
|
function onOkClick() {
|
||||||
onDialogOK({ delay: delay.value, address: address.value });
|
onDialogOK({ delay: delay.value, address: address.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onNoDelayClick() {
|
||||||
|
onDialogOK({ delay: 0, address: address.value });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="sass" scoped>
|
<style lang="sass" scoped>
|
||||||
.add-loc-card
|
.add-loc-card
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@@ -30,7 +30,7 @@ const timeWithSeconds = ref('00:00:45');
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
<div class="flex flex-center col q-ma-lg q-gutter-md">
|
||||||
<q-btn icom="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
|
<q-btn icon="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
|
||||||
<q-btn label="Connect" @click="socketStore.connect()" />
|
<q-btn label="Connect" @click="socketStore.connect()" />
|
||||||
<q-btn label="Disconnect" @click="socketStore.disconnect()" />
|
<q-btn label="Disconnect" @click="socketStore.disconnect()" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+156
-47
@@ -5,6 +5,7 @@ import { useIcloudStore } from 'stores/icloud';
|
|||||||
import { useDeviceStore } from 'stores/device';
|
import { useDeviceStore } from 'stores/device';
|
||||||
import { useTunneldStore } from 'stores/tunneld';
|
import { useTunneldStore } from 'stores/tunneld';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const simulationStore = useSimulationStore();
|
const simulationStore = useSimulationStore();
|
||||||
@@ -15,9 +16,23 @@ const tunneldStore = useTunneldStore();
|
|||||||
const { sockConnected } = storeToRefs(socketStore);
|
const { sockConnected } = storeToRefs(socketStore);
|
||||||
const { deviceConnected } = storeToRefs(deviceStore);
|
const { deviceConnected } = storeToRefs(deviceStore);
|
||||||
const { tunnelConnected } = storeToRefs(tunneldStore);
|
const { tunnelConnected } = storeToRefs(tunneldStore);
|
||||||
const { simulationState, testMode } = storeToRefs(simulationStore);
|
const { simulationState } = storeToRefs(simulationStore);
|
||||||
const { icloudMonitor } = storeToRefs(icloudStore);
|
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 {
|
function statusDevColor(state: string | boolean | null | undefined): string {
|
||||||
if (state === null || state === undefined) {
|
if (state === null || state === undefined) {
|
||||||
return 'grey';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-toolbar :class="testMode ? 'bg-warning text-black' : 'bg-primary text-white'">
|
<!--
|
||||||
<div class="flex col">
|
<q-img src="~assets/simloc_logo2.png" class="logo" @click="showStatus = !showStatus" />
|
||||||
<q-space />
|
<span class="text-grey-9">SimPi</span>
|
||||||
<!--
|
<q-space />
|
||||||
<div style="width: 80vw" class="flex justify-end">
|
-->
|
||||||
-->
|
<div class="flex status-bar-container">
|
||||||
<div class="q-gutter-x-sm">
|
<div class="row justify-center q-gutter-x-xs">
|
||||||
<q-btn dense rounded push size="sm" icon="mdi-cog" @click="socketStore.toggleSock()">
|
<q-btn
|
||||||
<q-badge :color="statusDevColor(sockConnected)" rounded floating />
|
round
|
||||||
</q-btn>
|
flat
|
||||||
<q-btn
|
color="bg-grey-9"
|
||||||
dense
|
:class="bgClass"
|
||||||
rounded
|
class="text-grey-9"
|
||||||
push
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="mdi-cellphone"
|
icon="mdi-cog"
|
||||||
@click="socketStore.requestUpdate()"
|
aria-label="Uptime"
|
||||||
>
|
>
|
||||||
<q-badge :color="statusDevColor(deviceConnected)" rounded floating />
|
<q-badge color="positive" rounded floating />
|
||||||
</q-btn>
|
<q-tooltip>{{ formattedUptime }}</q-tooltip>
|
||||||
<q-btn dense rounded push size="sm" icon="mdi-subway" @click="socketStore.requestUpdate()">
|
</q-btn>
|
||||||
<q-badge :color="statusDevColor(tunnelConnected)" rounded floating />
|
<q-btn
|
||||||
</q-btn>
|
round
|
||||||
<q-btn
|
flat
|
||||||
dense
|
color="bg-grey-9"
|
||||||
rounded
|
:class="bgClass"
|
||||||
push
|
class="text-grey-9"
|
||||||
size="sm"
|
dense
|
||||||
icon="mdi-map-marker"
|
size="sm"
|
||||||
@click="socketStore.requestUpdate()"
|
icon="mdi-cog"
|
||||||
>
|
@click="socketStore.toggleSock()"
|
||||||
<q-badge :color="statusDevColor(simulationState)" rounded floating />
|
aria-label="socketIO Status"
|
||||||
</q-btn>
|
>
|
||||||
<q-btn
|
<q-badge :color="statusDevColor(sockConnected)" rounded floating />
|
||||||
dense
|
</q-btn>
|
||||||
rounded
|
<q-btn
|
||||||
push
|
round
|
||||||
size="sm"
|
flat
|
||||||
icon="mdi-cloud"
|
color="bg-grey-9"
|
||||||
@click="icloudStore.icloudMonitorControl('refresh')"
|
:class="bgClass"
|
||||||
>
|
class="text-grey-9"
|
||||||
<q-badge :color="statusDevColor(icloudMonitor)" rounded floating />
|
dense
|
||||||
</q-btn>
|
size="sm"
|
||||||
</div>
|
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>
|
</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>
|
</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"
|
@keyup.enter="onOkClick"
|
||||||
label="Delay (seconds)"
|
label="Delay (seconds)"
|
||||||
type="number"
|
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-section>
|
||||||
<q-card-actions align="right">
|
<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 @@
|
|||||||
/*
|
import type { ComputedRef, shallowRef } from 'vue';
|
||||||
export interface Meta {
|
import type * as L from 'leaflet';
|
||||||
totalCount: number;
|
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
|
// 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 {
|
export interface AppError {
|
||||||
type: string;
|
type: string;
|
||||||
message: 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 {
|
export interface ErrorFull {
|
||||||
type: string;
|
type: string;
|
||||||
error: 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 {
|
export interface FindMyUpdate {
|
||||||
altitude: number;
|
altitude: number;
|
||||||
batteryLevel: number;
|
batteryLevel: number;
|
||||||
@@ -217,15 +154,16 @@ export interface FindMyUpdate {
|
|||||||
verticalAccuracy: number;
|
verticalAccuracy: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// END SERVER TO CLIENT
|
|
||||||
|
|
||||||
// CLIENT TO SERVER
|
// CLIENT TO SERVER
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
export interface ClientToServerEvents {
|
||||||
// built-in
|
// built-in
|
||||||
message: (e: string, callback: (b: boolean, r: string) => void) => void;
|
message: (e: string, callback: (b: boolean, r: string) => void) => void;
|
||||||
// testing
|
// testing
|
||||||
send_test_prompt: (p: string, callback: (response: string) => void) => void;
|
send_test_prompt: (
|
||||||
|
args: { prompt_name: string },
|
||||||
|
callback: (response: SendTestPromptResp) => void,
|
||||||
|
) => void;
|
||||||
// status
|
// status
|
||||||
request_update: (callback: (response: StatusUpdate) => void) => void;
|
request_update: (callback: (response: StatusUpdate) => void) => void;
|
||||||
// control
|
// control
|
||||||
@@ -234,15 +172,22 @@ export interface ClientToServerEvents {
|
|||||||
command: TunneldCommands;
|
command: TunneldCommands;
|
||||||
udid?: string;
|
udid?: string;
|
||||||
},
|
},
|
||||||
callback?: (response: TunneldControlResponse) => void,
|
callback?: (response: CommandResponse) => void,
|
||||||
) => void;
|
) => void;
|
||||||
favorite_control: (
|
favorite_control: (
|
||||||
args: {
|
args: {
|
||||||
command: FavoriteCommands;
|
command: FavoriteCommands;
|
||||||
favorite?: Favorite | undefined | null;
|
favorite?: Favorite | undefined | null;
|
||||||
},
|
},
|
||||||
callback: (response: FavoriteControlResponse) => void,
|
callback: (response: FavControlResponse) => void,
|
||||||
) => void;
|
) => void;
|
||||||
|
category_control: (
|
||||||
|
args: {
|
||||||
|
command: CategoryCommands;
|
||||||
|
category?: Category | undefined | null;
|
||||||
|
},
|
||||||
|
callback: (response: FavControlResponse) => void,
|
||||||
|
) => void;
|
||||||
simulation_control: (
|
simulation_control: (
|
||||||
args: {
|
args: {
|
||||||
command: SimulationCommands;
|
command: SimulationCommands;
|
||||||
@@ -251,21 +196,23 @@ export interface ClientToServerEvents {
|
|||||||
delay?: number | null | undefined;
|
delay?: number | null | undefined;
|
||||||
loc_id?: string | null | undefined;
|
loc_id?: string | null | undefined;
|
||||||
address?: NominatimResponse | 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;
|
) => void;
|
||||||
device_control: (
|
device_control: (
|
||||||
args: {
|
args: {
|
||||||
command: DeviceCommands;
|
command: DeviceCommands;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
},
|
},
|
||||||
callback?: (response: DeviceControlResponse) => void,
|
callback?: (response: CommandResponse) => void,
|
||||||
) => void;
|
) => void;
|
||||||
icloud_monitor_control: (
|
icloud_monitor_control: (
|
||||||
args: {
|
args: {
|
||||||
command: string;
|
command: string;
|
||||||
},
|
},
|
||||||
callback?: (response: iCloudMonitorResponse) => void,
|
callback?: (response: CommandResponse) => void,
|
||||||
) => void;
|
) => void;
|
||||||
// data updates
|
// data updates
|
||||||
location_item_update: (
|
location_item_update: (
|
||||||
@@ -274,13 +221,13 @@ export interface ClientToServerEvents {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
},
|
},
|
||||||
callback?: (response: LocationMarkUpdateResponse) => void,
|
callback?: (response: CommandResponse) => void,
|
||||||
) => void;
|
) => void;
|
||||||
queue_order_update: (
|
queue_order_update: (
|
||||||
args: {
|
args: {
|
||||||
newOrder: string[];
|
newOrder: string[];
|
||||||
},
|
},
|
||||||
callback?: (response: QueueOrderUpdateResponse) => void,
|
callback?: (response: CommandResponse) => void,
|
||||||
) => void;
|
) => void;
|
||||||
// data requests
|
// data requests
|
||||||
reverse_geocode: (
|
reverse_geocode: (
|
||||||
@@ -292,126 +239,52 @@ export interface ClientToServerEvents {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FavoriteControlResponse {
|
// Commands
|
||||||
command_status: string;
|
export type FavoriteCommands = 'set' | 'delete' | 'get' | 'edit';
|
||||||
command: FavoriteCommands;
|
export type CategoryCommands = 'get' | 'add' | 'delete' | 'edit';
|
||||||
command_class: string;
|
export type SimulationCommands =
|
||||||
data?: {
|
| 'restart'
|
||||||
favorites: Favorite[] | undefined | null;
|
| 'start'
|
||||||
};
|
| 'pause'
|
||||||
message?: string | undefined;
|
| '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 {
|
export interface CommandResponse {
|
||||||
command_status: string;
|
|
||||||
command: SimulationCommands;
|
|
||||||
command_class: string;
|
|
||||||
data?: SimulationControlResponseData | undefined | null;
|
|
||||||
message?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SimulationControlResponseData {
|
|
||||||
simulation_queue?: QueueData;
|
|
||||||
location_item?: LocationMark;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeviceControlResponse {
|
|
||||||
command_status: string;
|
|
||||||
command_class: string;
|
|
||||||
command: DeviceCommands;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface TunneldControlResponse {
|
|
||||||
command_status: string;
|
|
||||||
command_class: string;
|
|
||||||
command: TunneldCommands;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface iCloudMonitorResponse {
|
|
||||||
command_status: string;
|
command_status: string;
|
||||||
command: string;
|
command: string;
|
||||||
command_class: string;
|
command_class: string;
|
||||||
icloud_monitor_enabled?: boolean | undefined | null;
|
message: string;
|
||||||
icloud_monitor_running?: boolean | undefined | null;
|
data?: StatusUpdate;
|
||||||
message?: string | undefined | null;
|
|
||||||
error?: string | undefined | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
export interface routeSegments {
|
||||||
fromWaypoint: number;
|
fromWaypoint: number;
|
||||||
toWaypoint: number;
|
toWaypoint: number;
|
||||||
@@ -453,12 +326,51 @@ export interface RouteSet {
|
|||||||
wayPoints?: [number, number][] | [null, null] | [undefined, undefined] | null | undefined;
|
wayPoints?: [number, number][] | [null, null] | [undefined, undefined] | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Favorite {
|
export interface NewFavorite {
|
||||||
name: string | null | undefined;
|
id?: number;
|
||||||
|
name?: string;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
icon: string | null;
|
icon?: string;
|
||||||
category: 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 {
|
export interface GeoCacheResponse {
|
||||||
@@ -475,7 +387,7 @@ export interface NominatimResponse {
|
|||||||
neighbourhood?: string | null | undefined;
|
neighbourhood?: string | null | undefined;
|
||||||
suburb?: string | null | undefined;
|
suburb?: string | null | undefined;
|
||||||
county: string;
|
county: string;
|
||||||
town? : string | null | undefined;
|
town?: string | null | undefined;
|
||||||
city?: string | null | undefined;
|
city?: string | null | undefined;
|
||||||
village?: string | null | undefined;
|
village?: string | null | undefined;
|
||||||
municipality?: string | null | undefined;
|
municipality?: string | null | undefined;
|
||||||
@@ -490,3 +402,57 @@ export interface NominatimRequest {
|
|||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: 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 { ref, type Ref } from 'vue';
|
||||||
import * as LeafLet from 'leaflet';
|
import * as L from 'leaflet';
|
||||||
import type { LeafletMouseEvent } from 'leaflet';
|
import type { LeafletMouseEvent } from 'leaflet';
|
||||||
import { reverseGeocodeRateLimited } from 'functions/reverseGeocodeSocket';
|
import { useLocationStore } from 'stores/location';
|
||||||
|
import { useLeafletStore } from 'stores/leaflet';
|
||||||
|
|
||||||
|
|
||||||
type RouteSet = {
|
type RouteSet = {
|
||||||
start?: LeafLet.LatLng | null | undefined;
|
start?: L.LatLng | null | undefined;
|
||||||
end?: LeafLet.LatLng | null | undefined;
|
end?: L.LatLng | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RouteSetLike = {
|
type RouteSetLike = {
|
||||||
@@ -14,44 +16,75 @@ type RouteSetLike = {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const locationStore = useLocationStore();
|
||||||
|
const leafletStore = useLeafletStore();
|
||||||
|
|
||||||
export function useMarkerContextMenu(
|
export function useMarkerContextMenu(
|
||||||
routeSet: Ref<RouteSetLike>,
|
routeSet: Ref<RouteSetLike>,
|
||||||
updateRoute: () => void,
|
updateRoute: () => void,
|
||||||
closeAllPopups: () => void,
|
closeAllPopups: () => void,
|
||||||
) {
|
) {
|
||||||
const clickedLatLng = ref<LeafLet.LatLng | null>(null);
|
const clickedLatLng = ref<L.LatLng | null>(null);
|
||||||
const isFavorite = ref(false);
|
const isFavorite = ref(false);
|
||||||
|
|
||||||
const handleMarkerClick = async (event: LeafletMouseEvent) => {
|
const handleMarkerClick = async (event: LeafletMouseEvent) => {
|
||||||
console.log('marker clicked', event);
|
console.log('marker clicked', event);
|
||||||
closeAllPopups();
|
closeAllPopups();
|
||||||
clickedLatLng.value = event.latlng;
|
clickedLatLng.value = event.latlng;
|
||||||
const resp = await reverseGeocodeRateLimited(Number(event.latlng.lat), Number(event.latlng.lng));
|
const resp = await locationStore.getCachedLocation(
|
||||||
isFavorite.value = !!resp.favorite;
|
L.latLng(Number(event.latlng.lat), Number(event.latlng.lng)),
|
||||||
LeafLet.DomEvent.stopPropagation(event.originalEvent);
|
);
|
||||||
|
/* 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 = () => {
|
const handleMarkerDrag = (latlng: L.LatLng) => {
|
||||||
if (!clickedLatLng.value) return;
|
clickedLatLng.value = latlng;
|
||||||
closeAllPopups();
|
|
||||||
(routeSet.value as RouteSet).start = clickedLatLng.value;
|
|
||||||
console.log('setStartRoute: ', routeSet.value.start);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setEndRoute = () => {
|
const handleMarkerDragEnd = async (latlng: L.LatLng) => {
|
||||||
if (!clickedLatLng.value) return;
|
clickedLatLng.value = latlng;
|
||||||
(routeSet.value as RouteSet).end = clickedLatLng.value;
|
const resp = await locationStore.getCachedLocation(latlng);
|
||||||
closeAllPopups();
|
isFavorite.value = !!resp?.favorite;
|
||||||
console.log('setEndRoute: ', routeSet.value.end);
|
|
||||||
updateRoute();
|
/*
|
||||||
console.log('updating Route');
|
const resp = await reverseGeocodeRateLimited(Number(latlng.lat), Number(latlng.lng));
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
|
||||||
isFavorite,
|
const setStartRoute = () => {
|
||||||
clickedLatLng,
|
leafletStore.layerControl.routing = true;
|
||||||
handleMarkerClick,
|
if (!clickedLatLng.value) return;
|
||||||
setStartRoute,
|
closeAllPopups();
|
||||||
setEndRoute,
|
(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: {
|
simulation: {
|
||||||
start: {
|
key: 'simulation',
|
||||||
name: 'Start Location Sim',
|
name: 'Simulation',
|
||||||
cmd: 'start',
|
icon: 'mdi-map-marker-multiple',
|
||||||
cmdClass: 'sim_cntrl_class',
|
commands: {
|
||||||
icon: 'mdi-play',
|
testMode: {
|
||||||
cnfrm: false,
|
name: 'Test Mode',
|
||||||
delay: 0,
|
cmd: 'test-mode',
|
||||||
},
|
cmdClass: 'sim_cntrl_class',
|
||||||
pause: {
|
icon: 'mdi-test-tube-off',
|
||||||
name: 'Pause Location Sim',
|
cnfrm: false,
|
||||||
cmd: 'pause',
|
disabled: false,
|
||||||
cmdClass: 'sim_cntrl_class',
|
delay: 0,
|
||||||
icon: 'mdi-pause',
|
iconColor: 'grey-10',
|
||||||
cnfrm: false,
|
isVisible: true,
|
||||||
delay: 0,
|
},
|
||||||
},
|
gpsNoise: {
|
||||||
resume: {
|
name: 'GPS Noise',
|
||||||
name: 'Resume Location Simulation',
|
cmd: 'gps-noise',
|
||||||
cmd: 'resume',
|
cmdClass: 'sim_cntrl_class',
|
||||||
cmdClass: 'sim_cntrl_class',
|
icon: 'mdi-satellite-variant',
|
||||||
icon: 'mdi-play-pause',
|
cnfrm: false,
|
||||||
cnfrm: false,
|
disabled: false,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
},
|
iconColor: 'grey-10',
|
||||||
clear: {
|
isVisible: true,
|
||||||
name: 'Clear Future Items',
|
},
|
||||||
cmd: 'clear',
|
start: {
|
||||||
cmdClass: 'sim_cntrl_class',
|
name: 'Start',
|
||||||
icon: 'msi-map-marker-remove',
|
cmd: 'start',
|
||||||
cnfrm: false,
|
cmdClass: 'sim_cntrl_class',
|
||||||
delay: 0,
|
icon: 'mdi-play',
|
||||||
},
|
cnfrm: false,
|
||||||
reset: {
|
disabled: false,
|
||||||
name: 'Reset Location Queue',
|
delay: 0,
|
||||||
cmd: 'reset',
|
iconColor: 'grey-10',
|
||||||
cmdClass: 'sim_cntrl_class',
|
isVisible: true,
|
||||||
icon: 'mdi-restart',
|
},
|
||||||
cnfrm: true,
|
pause: {
|
||||||
delay: 0,
|
name: 'Pause Location Sim',
|
||||||
},
|
cmd: 'pause',
|
||||||
end: {
|
cmdClass: 'sim_cntrl_class',
|
||||||
name: 'End Location Sim',
|
icon: 'mdi-pause',
|
||||||
cmd: 'end',
|
cnfrm: false,
|
||||||
cmdClass: 'sim_cntrl_class',
|
disabled: false,
|
||||||
icon: 'mdi-stop',
|
delay: 0,
|
||||||
cnfrm: true,
|
iconColor: 'grey-10',
|
||||||
delay: 0,
|
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: {
|
device: {
|
||||||
shutdown: {
|
key: 'device',
|
||||||
name: 'Shutdown',
|
name: 'Device',
|
||||||
cmd: 'shutdown',
|
icon: 'mdi-raspberry-pi',
|
||||||
cmdClass: 'dev_cntrl_class',
|
commands: {
|
||||||
icon: 'mdi-power',
|
shutdown: {
|
||||||
cnfrm: true,
|
name: 'Shutdown',
|
||||||
delay: 5,
|
cmd: 'shutdown',
|
||||||
},
|
cmdClass: 'dev_cntrl_class',
|
||||||
reboot: {
|
icon: 'mdi-power',
|
||||||
name: 'Reboot',
|
cnfrm: true,
|
||||||
cmd: 'reboot',
|
disabled: false,
|
||||||
cmdClass: 'dev_cntrl_class',
|
delay: 5,
|
||||||
icon: 'mdi-restart',
|
iconColor: 'grey-10',
|
||||||
cnfrm: true,
|
isVisible: true,
|
||||||
delay: 5,
|
},
|
||||||
|
reboot: {
|
||||||
|
name: 'Reboot',
|
||||||
|
cmd: 'reboot',
|
||||||
|
cmdClass: 'dev_cntrl_class',
|
||||||
|
icon: 'mdi-restart',
|
||||||
|
cnfrm: true,
|
||||||
|
disabled: false,
|
||||||
|
delay: 5,
|
||||||
|
iconColor: 'grey-10',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
icloudmonitor: {
|
icloudmonitor: {
|
||||||
start: {
|
key: 'icloudmonitor',
|
||||||
name: 'Start iCloud Monitor',
|
icon: 'mdi-cloud',
|
||||||
cmd: 'start',
|
name: 'iCloud Monitor',
|
||||||
cmdClass: 'icloud-monitor_cntrl_class',
|
commands: {
|
||||||
icon: 'mdi-play',
|
start: {
|
||||||
cnfrm: false,
|
name: 'Start',
|
||||||
delay: 0,
|
cmd: 'start',
|
||||||
},
|
cmdClass: 'icloud-monitor_cntrl_class',
|
||||||
stop: {
|
icon: 'mdi-play',
|
||||||
name: 'Stop iCloud Monitor',
|
cnfrm: false,
|
||||||
cmd: 'stop',
|
disabled: false,
|
||||||
cmdClass: 'icloud-monitor_cntrl_class',
|
delay: 0,
|
||||||
icon: 'mdi-stop',
|
iconColor: 'grey-10',
|
||||||
cnfrm: false,
|
isVisible: true,
|
||||||
delay: 0,
|
},
|
||||||
|
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: {
|
tunneld: {
|
||||||
start: {
|
key: 'tunneld',
|
||||||
name: 'Start Tunneld',
|
name: 'tunneld',
|
||||||
cmd: 'start',
|
icon: 'mdi-subway',
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
commands: {
|
||||||
icon: 'mdi-play',
|
start: {
|
||||||
cnfrm: false,
|
name: 'Start',
|
||||||
delay: 0,
|
cmd: 'start',
|
||||||
},
|
cmdClass: 'tunneld_cntrl_class',
|
||||||
start_watcher: {
|
icon: 'mdi-play',
|
||||||
name: 'Start TunneldWatcher',
|
cnfrm: false,
|
||||||
cmd: 'start-watcher',
|
disabled: false,
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
delay: 0,
|
||||||
icon: 'mdi-play',
|
iconColor: 'grey-10',
|
||||||
cnfrm: false,
|
isVisible: true,
|
||||||
delay: 0,
|
},
|
||||||
},
|
start_watcher: {
|
||||||
end_watcher: {
|
name: 'Start TunneldWatcher',
|
||||||
name: 'End TunneldWatcher',
|
cmd: 'start-watcher',
|
||||||
cmd: 'end-watcher',
|
cmdClass: 'tunneld_cntrl_class',
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
icon: 'mdi-play',
|
||||||
icon: 'mdi-stop',
|
cnfrm: false,
|
||||||
cnfrm: false,
|
disabled: false,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
},
|
iconColor: 'grey-10',
|
||||||
clear: {
|
isVisible: true,
|
||||||
name: 'Clear Tunneld',
|
},
|
||||||
cmd: 'clear',
|
end_watcher: {
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
name: 'End TunneldWatcher',
|
||||||
icon: 'mdi-backspace',
|
cmd: 'end-watcher',
|
||||||
cnfrm: false,
|
cmdClass: 'tunneld_cntrl_class',
|
||||||
delay: 0,
|
icon: 'mdi-stop',
|
||||||
},
|
cnfrm: false,
|
||||||
cancel: {
|
disabled: false,
|
||||||
name: 'Cancel Tunneld',
|
delay: 0,
|
||||||
cmd: 'cancel',
|
iconColor: 'grey-10',
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
isVisible: true,
|
||||||
icon: 'mdi-cancel',
|
},
|
||||||
cnfrm: false,
|
clear: {
|
||||||
delay: 0,
|
name: 'Clear',
|
||||||
},
|
cmd: 'clear',
|
||||||
restart: {
|
cmdClass: 'tunneld_cntrl_class',
|
||||||
name: 'Restart Tunneld',
|
icon: 'mdi-backspace',
|
||||||
cmd: 'restart',
|
cnfrm: false,
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
disabled: false,
|
||||||
icon: 'mdi-restart',
|
delay: 0,
|
||||||
cnfrm: false,
|
iconColor: 'grey-10',
|
||||||
delay: 0,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
shutdown: {
|
cancel: {
|
||||||
name: 'Shutdown Tunneld',
|
name: 'Cancel',
|
||||||
cmd: 'shutdown',
|
cmd: 'cancel',
|
||||||
cmdClass: 'tunneld_cntrl_class',
|
cmdClass: 'tunneld_cntrl_class',
|
||||||
icon: 'mdi-stop',
|
icon: 'mdi-cancel',
|
||||||
cnfrm: false,
|
cnfrm: false,
|
||||||
delay: 0,
|
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>
|
<template>
|
||||||
<q-layout view="hhh lpr fff">
|
<q-layout view="hhh lpr fff">
|
||||||
<q-header>
|
<q-header>
|
||||||
<MenuBar @drawer="drawer = !drawer" />
|
<MenuBar />
|
||||||
</q-header>
|
</q-header>
|
||||||
<q-footer>
|
<q-footer>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
</q-footer>
|
</q-footer>
|
||||||
<!--
|
|
||||||
<q-drawer
|
|
||||||
v-model="drawer"
|
|
||||||
:width="200"
|
|
||||||
:breakpoint="500"
|
|
||||||
overlay
|
|
||||||
:class="$q.dark.isActive ? 'bg-grey-8' : 'bg-grey-3'"
|
|
||||||
>
|
|
||||||
<q-scroll-area class="fit">
|
|
||||||
<q-list>
|
|
||||||
<template v-for="(menuItem, index) in menuList" :key="index">
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
:active="menuItem.route === route.name"
|
|
||||||
v-ripple
|
|
||||||
@click="$router.push({ name: menuItem.route })"
|
|
||||||
>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon :name="menuItem.icon" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
{{ menuItem.label }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-separator :key="'sep' + index" v-if="menuItem.separator" />
|
|
||||||
</template>
|
|
||||||
</q-list>
|
|
||||||
</q-scroll-area>
|
|
||||||
</q-drawer>
|
|
||||||
-->
|
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view />
|
<router-view />
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
@@ -43,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
import { useSimulationStore } from 'stores/simulation';
|
import { useSimulationStore } from 'stores/simulation';
|
||||||
import { useSocketStore } from 'stores/socket';
|
import { useSocketStore } from 'stores/socket';
|
||||||
@@ -51,15 +21,13 @@ import { useIcloudStore } from 'stores/icloud';
|
|||||||
import { useFavoriteStore } from 'stores/favorite';
|
import { useFavoriteStore } from 'stores/favorite';
|
||||||
|
|
||||||
import MenuBar from 'components/MenuBar.vue';
|
import MenuBar from 'components/MenuBar.vue';
|
||||||
import StatusBar from 'components/StatusBar.vue';
|
import StatusBar from '../components/StatusBar.vue';
|
||||||
|
|
||||||
const simulationStore = useSimulationStore();
|
const simulationStore = useSimulationStore();
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const icloudStore = useIcloudStore();
|
const icloudStore = useIcloudStore();
|
||||||
const favoriteStore = useFavoriteStore();
|
const favoriteStore = useFavoriteStore();
|
||||||
|
|
||||||
const drawer = ref(false);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
socketStore.bindEvents();
|
socketStore.bindEvents();
|
||||||
simulationStore.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>
|
<template>
|
||||||
<q-page class="row items-center justify-evenly">
|
<q-page class="row items-center justify-evenly">
|
||||||
<LeafletTest />
|
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LeafletTest from 'components/LeafletTest.vue'
|
|
||||||
|
|
||||||
</script>
|
</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[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
component: () => import('layouts/FullMapLayout.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'simPI',
|
||||||
|
component: () => import('pages/FullMapPage.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/old',
|
||||||
component: () => import('layouts/MainLayout.vue'),
|
component: () => import('layouts/MainLayout.vue'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'home',
|
name: 'oldMap',
|
||||||
redirect: {
|
component: () => import('pages/LeafletPage.vue'),
|
||||||
name: 'Leaflet',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'leaflet',
|
|
||||||
name: 'Leaflet',
|
|
||||||
component: () => import('pages/IndexPage.vue'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'test',
|
path: 'test',
|
||||||
@@ -29,7 +33,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
{
|
{
|
||||||
|
|||||||
+30
-7
@@ -4,23 +4,27 @@ import type { DeviceCommands } from 'components/models';
|
|||||||
|
|
||||||
import { useSocketStore } from 'stores/socket';
|
import { useSocketStore } from 'stores/socket';
|
||||||
|
|
||||||
|
type lastUpdatedTime = number | null;
|
||||||
|
|
||||||
export const useDeviceStore = defineStore('deviceStore', {
|
export const useDeviceStore = defineStore('deviceStore', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
deviceConnected: false as boolean,
|
deviceConnected: false as boolean,
|
||||||
|
lastKnownUptime: 0 as number,
|
||||||
|
lastUpdatedTime: null as lastUpdatedTime,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
digestUpdate(data: boolean) {
|
digestUpdate(data: { uptime: number, connected: boolean }) {
|
||||||
this.deviceConnected = data;
|
this.deviceConnected = data.connected;
|
||||||
|
this.lastKnownUptime = data.uptime;
|
||||||
|
this.lastUpdatedTime = Date.now();
|
||||||
},
|
},
|
||||||
deviceControl(command: DeviceCommands, delay: number = 0) {
|
deviceControl(command: DeviceCommands, delay: number = 0) {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('deviceStore: got command: ', command, ' with delay: ', delay, ' seconds');
|
console.log('deviceStore: got command: ', command, ' with delay: ', delay, ' seconds');
|
||||||
}
|
}
|
||||||
@@ -30,14 +34,33 @@ export const useDeviceStore = defineStore('deviceStore', {
|
|||||||
(response) => {
|
(response) => {
|
||||||
console.log(response.command_status, response.command);
|
console.log(response.command_status, response.command);
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
fnctRtn = { sts: 'error', msg: response.message?.toString() };
|
functionReturn = { status: 'error', message: response.message?.toString() };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} 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 { defineStore, acceptHMRUpdate } from 'pinia';
|
||||||
import { socket } from 'boot/socketio';
|
import { socket } from 'boot/socketio';
|
||||||
import { useSocketStore } from 'stores/socket';
|
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', {
|
export const useFavoriteStore = defineStore('favoriteStore', {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
favorites: [] as Favorite[],
|
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: {},
|
getters: {},
|
||||||
actions: {
|
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 socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'set':
|
case 'set':
|
||||||
if (!favorite) {
|
if (!favorite) {
|
||||||
console.log('Favorite not provided, favdata: %s', 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.');
|
throw new Error('Favorite not provided.');
|
||||||
}
|
}
|
||||||
socket.emit('favorite_control', { command: 'set', favorite: favorite }, (response) => {
|
socket.emit('favorite_control', { command: 'set', favorite: favorite }, (response) => {
|
||||||
@@ -28,17 +43,12 @@ export const useFavoriteStore = defineStore('favoriteStore', {
|
|||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.favorites) {
|
if (!response.data?.favorites) {
|
||||||
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
|
functionReturn = { status: 'error', message: 'No favorites found.' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
console.log('Favorite added, favdata: %s', favorite);
|
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;
|
this.favorites = response.data.favorites;
|
||||||
response.data.favorites.forEach((fav) => {
|
|
||||||
if (!this.categories.includes(fav.category)) {
|
|
||||||
this.categories.push(fav.category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from favorite_control_add: ', response);
|
console.log('response from favorite_control_add: ', response);
|
||||||
@@ -50,20 +60,15 @@ export const useFavoriteStore = defineStore('favoriteStore', {
|
|||||||
case 'delete':
|
case 'delete':
|
||||||
socket.emit('favorite_control', { command: 'delete', favorite: favorite }, (response) => {
|
socket.emit('favorite_control', { command: 'delete', favorite: favorite }, (response) => {
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
fnctRtn = { sts: 'error', msg: response.message };
|
functionReturn = { status: 'error', message: response.message };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.favorites) {
|
if (!response.data?.favorites) {
|
||||||
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
|
functionReturn = { status: 'error', message: 'No favorites found.' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.favorites = response.data.favorites;
|
this.favorites = response.data.favorites;
|
||||||
response.data.favorites.forEach((fav) => {
|
|
||||||
if (!this.categories.includes(fav.category)) {
|
|
||||||
this.categories.push(fav.category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from favorite_control_delete: ', response);
|
console.log('response from favorite_control_delete: ', response);
|
||||||
@@ -75,20 +80,20 @@ export const useFavoriteStore = defineStore('favoriteStore', {
|
|||||||
case 'get':
|
case 'get':
|
||||||
socket.emit('favorite_control', { command: 'get', favorite: favorite }, (response) => {
|
socket.emit('favorite_control', { command: 'get', favorite: favorite }, (response) => {
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
fnctRtn = { sts: 'error', msg: response.message };
|
functionReturn = { status: 'error', message: response.message };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.favorites) {
|
if (!response.data?.favorites) {
|
||||||
fnctRtn = { sts: 'error', msg: 'No favorites found.' };
|
functionReturn = { status: 'error', message: 'No favorites found.' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.favorites = response.data.favorites;
|
this.favorites = response.data.favorites;
|
||||||
|
this.favMap.clear();
|
||||||
response.data.favorites.forEach((fav) => {
|
response.data.favorites.forEach((fav) => {
|
||||||
if(!this.categories.includes(fav.category)) {
|
this.favMap.set(fav.id, fav);
|
||||||
this.categories.push(fav.category);
|
});
|
||||||
}
|
this.mapCategoriesAndFavorites();
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from favorite_control_get: ', response);
|
console.log('response from favorite_control_get: ', response);
|
||||||
@@ -98,24 +103,117 @@ export const useFavoriteStore = defineStore('favoriteStore', {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fnctRtn = { sts: 'error', msg: 'Invalid command' + command };
|
functionReturn = { status: 'error', message: 'Invalid command' + command };
|
||||||
throw new Error('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 {
|
initialize(): void {
|
||||||
|
this.categoryControl({ command: 'get' });
|
||||||
this.favoriteControl({ 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 iCloudCodeDialog from 'components/iCloudCodeDialog.vue';
|
||||||
|
|
||||||
import type { FindMyUpdate, icloudData, iCloudMonitorResponse } from 'components/models';
|
import type { CommandResponse, FindMyUpdate, icloudStatusUpdate } from 'components/models';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
bindEvents() {
|
bindEvents() {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
|
/*
|
||||||
// fmf_update
|
// fmf_update
|
||||||
socket.on('fmf_update', (data: FindMyUpdate): void => {
|
socket.on('fmf_update', (data: FindMyUpdate): void => {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
@@ -34,7 +35,7 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
}
|
}
|
||||||
this.findMyUpdate = data;
|
this.findMyUpdate = data;
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
// icloud_2fa_request
|
// icloud_2fa_request
|
||||||
socket.on('icloud_2fa_request', (callback) => {
|
socket.on('icloud_2fa_request', (callback) => {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
@@ -63,14 +64,17 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
icloudMonitorControl(command: string) {
|
icloudMonitorControl(command: string) {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
let functionReturn: { status: string; message?: string | undefined } = {
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'start':
|
case 'start':
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('socketStore: got command: icloudMonitor start');
|
console.log('socketStore: got command: icloudMonitor start');
|
||||||
}
|
}
|
||||||
if (this.icloudMonitor) {
|
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');
|
throw new Error('iCloud Monitor is already running');
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
@@ -79,23 +83,28 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'icloud_monitor_control',
|
'icloud_monitor_control',
|
||||||
{ command: 'start' },
|
{ command: 'start' },
|
||||||
(response: iCloudMonitorResponse) => {
|
(response: CommandResponse) => {
|
||||||
fnctRtn.sts = response.command_status;
|
functionReturn.status = response.command_status;
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
if (response.message) {
|
if (response.message) {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log(response.message);
|
console.log(response.message);
|
||||||
}
|
}
|
||||||
fnctRtn.msg = response.message;
|
functionReturn.message = response.message;
|
||||||
} else {
|
} else {
|
||||||
fnctRtn.msg = 'Error';
|
functionReturn.message = 'Error';
|
||||||
}
|
}
|
||||||
throw new Error(fnctRtn.msg);
|
throw new Error(functionReturn.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: 'iCloud Monitor: ' + response.command_status };
|
functionReturn = {
|
||||||
this.icloudMonitor = true;
|
status: 'OK',
|
||||||
|
message: 'iCloud Monitor: ' + response.command_status,
|
||||||
|
};
|
||||||
|
if(response.data?.icloud){
|
||||||
|
this.digestUpdate(response.data.icloud);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'stop':
|
case 'stop':
|
||||||
@@ -103,7 +112,7 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
console.log('socketStore: got command: icloudMonitor stop');
|
console.log('socketStore: got command: icloudMonitor stop');
|
||||||
}
|
}
|
||||||
if (!this.icloudMonitor) {
|
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');
|
throw new Error('iCloud Monitor is not running');
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
@@ -112,21 +121,26 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'icloud_monitor_control',
|
'icloud_monitor_control',
|
||||||
{ command: 'stop' },
|
{ command: 'stop' },
|
||||||
(response: iCloudMonitorResponse) => {
|
(response: CommandResponse) => {
|
||||||
fnctRtn.sts = response.command_status;
|
functionReturn.status = response.command_status;
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
if (response.message) {
|
if (response.message) {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log(response.message);
|
console.log(response.message);
|
||||||
}
|
}
|
||||||
fnctRtn.msg = response.message;
|
functionReturn.message = response.message;
|
||||||
} else {
|
} else {
|
||||||
fnctRtn.msg = 'Error';
|
functionReturn.message = 'Error';
|
||||||
}
|
}
|
||||||
throw new Error(fnctRtn.msg);
|
throw new Error(functionReturn.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: 'iCloud Monitor: ' + response.command_status };
|
functionReturn = {
|
||||||
this.icloudMonitor = false;
|
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(
|
socket.emit(
|
||||||
'icloud_monitor_control',
|
'icloud_monitor_control',
|
||||||
{ command: 'get' },
|
{ command: 'status' },
|
||||||
(response: iCloudMonitorResponse) => {
|
(response: CommandResponse) => {
|
||||||
fnctRtn.sts = response.command_status;
|
functionReturn.status = response.command_status;
|
||||||
if (response.command_status == 'error') {
|
if (response.command_status == 'error') {
|
||||||
if (response.message) {
|
if (response.message) {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log(response.message);
|
console.log(response.message);
|
||||||
}
|
}
|
||||||
fnctRtn.msg = response.message;
|
functionReturn.message = response.message;
|
||||||
} else {
|
} else {
|
||||||
fnctRtn.msg = 'Error';
|
functionReturn.message = 'Error';
|
||||||
}
|
}
|
||||||
throw new Error(fnctRtn.msg);
|
throw new Error(functionReturn.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = {
|
functionReturn = {
|
||||||
sts: 'OK',
|
status: 'OK',
|
||||||
msg: 'iCloud Location Requested',
|
message: 'iCloud Location Requested',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.icloudMonitor = !!(
|
if (response.data?.icloud) {
|
||||||
response.icloud_monitor_enabled && response.icloud_monitor_running
|
this.digestUpdate(response.data.icloud);
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -168,49 +182,80 @@ export const useIcloudStore = defineStore('icloudStore', {
|
|||||||
}
|
}
|
||||||
socket.emit(
|
socket.emit(
|
||||||
'icloud_monitor_control',
|
'icloud_monitor_control',
|
||||||
{ command: 'status' },
|
{ command: 'refresh' },
|
||||||
(response: iCloudMonitorResponse) => {
|
(response: CommandResponse) => {
|
||||||
fnctRtn.sts = response.command_status;
|
functionReturn.status = response.command_status;
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
if (response.message) {
|
if (response.message) {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log(response.message);
|
console.log(response.message);
|
||||||
}
|
}
|
||||||
fnctRtn.msg = response.message;
|
functionReturn.message = response.message;
|
||||||
} else {
|
} else {
|
||||||
fnctRtn.msg = 'Error';
|
functionReturn.message = 'Error';
|
||||||
}
|
}
|
||||||
throw new Error(fnctRtn.msg);
|
throw new Error(functionReturn.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = {
|
functionReturn = {
|
||||||
sts: 'OK',
|
status: 'OK',
|
||||||
msg:
|
message:
|
||||||
'iCloud Monitor Enabled: ' +
|
'iCloud Monitor Enabled: ' +
|
||||||
response.icloud_monitor_enabled +
|
response.data?.icloud?.monitor_enabled +
|
||||||
'iCloud Monitor Running: ' +
|
'iCloud Monitor Running: ' +
|
||||||
response.icloud_monitor_running,
|
response.data?.icloud?.monitor_running,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.icloudMonitor = !!(
|
if (response.data?.icloud) {
|
||||||
response.icloud_monitor_enabled && response.icloud_monitor_running
|
this.digestUpdate(response.data.icloud);
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fnctRtn = { sts: 'error', msg: 'Invalid command' };
|
functionReturn = { status: 'error', message: 'Invalid command' };
|
||||||
throw new Error('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 { 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 {
|
import type {
|
||||||
RoutesSet,
|
RoutesSet,
|
||||||
LatLng,
|
LatLng,
|
||||||
routeSegments,
|
routeSegments,
|
||||||
routeDirections,
|
routeDirections,
|
||||||
routeCoordinates,
|
routeCoordinates,
|
||||||
|
ControlCategoryKey,
|
||||||
|
BaseLayer,
|
||||||
|
BaseLayerWMS,
|
||||||
} from 'components/models';
|
} 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 {
|
interface State {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
center: [number, number] | [null, null] | null;
|
center: [number, number] | [null, null] | null;
|
||||||
|
mapObject: ReturnType<typeof shallowRef<Map | null>>;
|
||||||
markerLatLng: [number, number] | [null, null] | 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: {
|
routeSet: {
|
||||||
start: LatLng;
|
start: LatLng;
|
||||||
end: LatLng;
|
end: LatLng;
|
||||||
wayPoints?: LatLng[] | null | undefined;
|
wayPoints?: LatLng[] | null | undefined;
|
||||||
};
|
};
|
||||||
|
fabs: Fabs;
|
||||||
routesSet: RoutesSet[] | null;
|
routesSet: RoutesSet[] | null;
|
||||||
routeSegments: routeSegments[] | null;
|
routeSegments: routeSegments[] | null;
|
||||||
routeDirections: routeDirections[] | null;
|
routeDirections: routeDirections[] | null;
|
||||||
routeCoordinates: routeCoordinates[] | 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 => {
|
state: (): State => {
|
||||||
return {
|
return {
|
||||||
zoom: 10,
|
zoom: 10,
|
||||||
center: [favorites.home.coords.lat, favorites.home.coords.lng],
|
center: [favorites.home.coords.lat, favorites.home.coords.lng],
|
||||||
|
clickedLatLng: null,
|
||||||
|
mapObject: shallowRef(null),
|
||||||
markerLatLng: 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: {
|
routeSet: {
|
||||||
start: { lat: null, lng: null },
|
start: { lat: null, lng: null },
|
||||||
end: { lat: null, lng: null },
|
end: { lat: null, lng: null },
|
||||||
@@ -40,9 +164,125 @@ export const useLeafletStore = defineStore('leaflet', {
|
|||||||
routeSegments: [],
|
routeSegments: [],
|
||||||
routeDirections: [],
|
routeDirections: [],
|
||||||
routeCoordinates: [],
|
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: {
|
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() {
|
clearRouteSegments() {
|
||||||
this.routeSegments = [];
|
this.routeSegments = [];
|
||||||
},
|
},
|
||||||
@@ -52,17 +292,167 @@ export const useLeafletStore = defineStore('leaflet', {
|
|||||||
setZoom(zoom: number) {
|
setZoom(zoom: number) {
|
||||||
this.zoom = zoom;
|
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) {
|
setMarkerLatLng(lat: number, lng: number) {
|
||||||
this.markerLatLng = [lat, lng];
|
this.markerLatLng = [lat, lng];
|
||||||
},
|
},
|
||||||
toggleQLocDrawer() {
|
toggleleafletDrawerOpen() {
|
||||||
this.qLocDrawer = !this.qLocDrawer
|
this.leafletDrawerOpen = !this.leafletDrawerOpen;
|
||||||
},
|
},
|
||||||
setRouteDirs(data: routeDirections[]) {
|
setRouteDirs(data: routeDirections[]) {
|
||||||
this.routeDirections = data;
|
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) {
|
if (import.meta.hot) {
|
||||||
import.meta.hot.accept(acceptHMRUpdate(useLeafletStore, 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 { useSocketStore } from 'stores/socket';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CurrentLocation,
|
CommandResponse,
|
||||||
LocationItemUpdate,
|
|
||||||
LocationMarkUpdateResponse,
|
|
||||||
LocationQueue,
|
LocationQueue,
|
||||||
NominatimResponse,
|
NominatimResponse,
|
||||||
QueueData,
|
ReturnedStatusMessage,
|
||||||
SimulationControlResponse,
|
SimulationCommands,
|
||||||
SimulationStatus,
|
SimulationStatusUpdate,
|
||||||
|
SimulationState,
|
||||||
} from 'components/models';
|
} from 'components/models';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useSimulationStore = defineStore('simulationStore', {
|
export const useSimulationStore = defineStore('simulationStore', {
|
||||||
state: () => {
|
state: (): SimulationState => {
|
||||||
return {
|
return {
|
||||||
currentLocation: null as CurrentLocation | null | undefined,
|
currentLocation: {
|
||||||
gpsNoise: null as boolean | undefined | null,
|
loc_id: null,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
},
|
||||||
|
gpsNoise: null,
|
||||||
locationQueueData: {} as LocationQueue,
|
locationQueueData: {} as LocationQueue,
|
||||||
locationQueueDeletedItems: [] as string[],
|
locationQueueDeletedItems: [] as string[],
|
||||||
locationQueueOrder: [] as string[],
|
locationQueueOrder: [] as string[],
|
||||||
@@ -27,24 +28,58 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
simulationRunning: false as boolean | undefined | null,
|
simulationRunning: false as boolean | undefined | null,
|
||||||
simulationState: null as string | null | undefined,
|
simulationState: null as string | null | undefined,
|
||||||
testMode: null as boolean | undefined | null,
|
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: {
|
actions: {
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
|
/*
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
// simulation_status
|
// simulation_status
|
||||||
socket.on('simulation_status', (data: SimulationStatus): void => {
|
socket.on('simulation_status', (data: SimulationStatusUpdate): void => {
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('event: simulation_status received: ', data);
|
console.log('event: simulation_status received: ', data);
|
||||||
}
|
}
|
||||||
console.log('updating currentLocation', data);
|
this.digestUpdate(data);
|
||||||
this.currentLocation = {
|
|
||||||
loc_id: data.loc_id,
|
|
||||||
latitude: data.latitude,
|
|
||||||
longitude: data.longitude,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// queue_data_update
|
// queue_data_update
|
||||||
@@ -52,13 +87,15 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('QueueUpdate received: ', inData);
|
console.log('QueueUpdate received: ', inData);
|
||||||
}
|
}
|
||||||
this.digestQueueUpdate(inData);
|
this.digestUpdate(inData);
|
||||||
});
|
});
|
||||||
// location_item_update
|
// location_item_update
|
||||||
socket.on('location_item_update', (inData: LocationItemUpdate): void => {
|
socket.on('location_item_update', (inData: LocationItemUpdate): void => {
|
||||||
console.log('Location item update received, data: ', inData);
|
console.log('Location item update received, data: ', inData);
|
||||||
this.locationQueueData[inData.loc_id] = inData.data;
|
this.locationQueueData[inData.loc_id] = inData.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
simulationControl({
|
simulationControl({
|
||||||
command,
|
command,
|
||||||
@@ -67,6 +104,8 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
loc_id,
|
loc_id,
|
||||||
delay,
|
delay,
|
||||||
address,
|
address,
|
||||||
|
type,
|
||||||
|
instruction,
|
||||||
}: {
|
}: {
|
||||||
command: string;
|
command: string;
|
||||||
latitude?: number | null | undefined;
|
latitude?: number | null | undefined;
|
||||||
@@ -74,10 +113,15 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
loc_id?: string | null | undefined;
|
loc_id?: string | null | undefined;
|
||||||
delay?: number | null | undefined;
|
delay?: number | null | undefined;
|
||||||
address?: NominatimResponse | string | null | undefined;
|
address?: NominatimResponse | string | null | undefined;
|
||||||
|
type?: 'instruction' | 'coord' | null | undefined;
|
||||||
|
instruction?: string | undefined;
|
||||||
}) {
|
}) {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
let functionReturn: { status: string; message?: string | undefined } = {
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'start':
|
case 'start':
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
@@ -88,7 +132,7 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
this.simulationState == 'RUNNING' ||
|
this.simulationState == 'RUNNING' ||
|
||||||
this.simulationState == 'PAUSED'
|
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);
|
throw new Error('Simulation is already running' + this.simulationState);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
@@ -97,17 +141,17 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'simulation_control',
|
'simulation_control',
|
||||||
{ command: 'start' },
|
{ command: 'start' },
|
||||||
(response: SimulationControlResponse) => {
|
(response: CommandResponse) => {
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
fnctRtn = { sts: 'error', msg: response.message?.toString() };
|
functionReturn = { status: 'error', message: response.message?.toString() };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_start: ', response);
|
console.log('response from simulate_control_start: ', response);
|
||||||
@@ -121,16 +165,17 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'simulation_control',
|
'simulation_control',
|
||||||
{ command: 'test-mode' },
|
{ command: 'test-mode' },
|
||||||
(response: SimulationControlResponse) => {
|
(response: CommandResponse) => {
|
||||||
|
console.log(response);
|
||||||
if (response.command_status === 'ERROR') {
|
if (response.command_status === 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_test-mode: ', response);
|
console.log('response from simulate_control_test-mode: ', response);
|
||||||
@@ -144,16 +189,16 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'simulation_control',
|
'simulation_control',
|
||||||
{ command: 'gps-noise' },
|
{ command: 'gps-noise' },
|
||||||
(response: SimulationControlResponse) => {
|
(response: CommandResponse) => {
|
||||||
if (response.command_status === 'ERROR') {
|
if (response.command_status === 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_gps-noise: ', response);
|
console.log('response from simulate_control_gps-noise: ', response);
|
||||||
@@ -170,16 +215,16 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'simulation_control',
|
'simulation_control',
|
||||||
{ command: 'pause' },
|
{ command: 'pause' },
|
||||||
(response: SimulationControlResponse) => {
|
(response: CommandResponse) => {
|
||||||
if (response.command_status === 'ERROR') {
|
if (response.command_status === 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_pause: ', response);
|
console.log('response from simulate_control_pause: ', response);
|
||||||
@@ -197,12 +242,12 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_resume: ', response);
|
console.log('response from simulate_control_resume: ', response);
|
||||||
@@ -219,12 +264,12 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_clear: ', response);
|
console.log('response from simulate_control_clear: ', response);
|
||||||
@@ -241,12 +286,12 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_reset: ', response);
|
console.log('response from simulate_control_reset: ', response);
|
||||||
@@ -263,12 +308,12 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_end: ', response);
|
console.log('response from simulate_control_end: ', response);
|
||||||
@@ -292,17 +337,19 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
delay: delay,
|
delay: delay,
|
||||||
address: address,
|
address: address,
|
||||||
|
type: type,
|
||||||
|
instruction: instruction,
|
||||||
},
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_add: ', response);
|
console.log('response from simulate_control_add: ', response);
|
||||||
@@ -320,12 +367,12 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
if (!response.data?.simulation_queue) {
|
if (!response.data?.simulation) {
|
||||||
fnctRtn = { sts: 'error', msg: 'Simulation queue data missing' };
|
functionReturn = { status: 'error', message: 'Simulation queue data missing' };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
fnctRtn = { sts: 'OK', msg: response.message?.toString() };
|
functionReturn = { status: 'OK', message: response.message?.toString() };
|
||||||
this.digestQueueUpdate(response.data.simulation_queue);
|
this.digestUpdate(response.data.simulation);
|
||||||
}
|
}
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_delete: ', response);
|
console.log('response from simulate_control_delete: ', response);
|
||||||
@@ -333,14 +380,20 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
return response.message;
|
return response.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.updateLocationMark(loc_id, 'status', 'deleted');
|
|
||||||
break;
|
break;
|
||||||
case 'next':
|
case 'next':
|
||||||
socket.emit('simulation_control', { command: 'next' }, (response) => {
|
socket.emit('simulation_control', { command: 'next' },
|
||||||
|
(response: CommandResponse) => {
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} 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) {
|
if (debugLog) {
|
||||||
console.log('response from simulate_control_next: ', response);
|
console.log('response from simulate_control_next: ', response);
|
||||||
}
|
}
|
||||||
@@ -349,15 +402,94 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fnctRtn = { sts: 'error', msg: 'Invalid command' };
|
functionReturn = { status: 'error', message: 'Invalid command' };
|
||||||
throw new Error('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) {
|
updateLocationMark(loc_id: string, key: string, value: string | number) {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { command_status: string; message: string };
|
let functionReturn: ReturnedStatusMessage = { status: '', message: '' };
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log(
|
console.log(
|
||||||
'socketStore: Update LocationMark request, loc_id: %s, key: %s, new value: %s',
|
'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]) {
|
if (!this.locationQueueData[loc_id]) {
|
||||||
fnctRtn = {
|
functionReturn = {
|
||||||
command_status: 'error',
|
status: 'error',
|
||||||
message: 'Location Id: ' + loc_id + ' is not in the simulation queue',
|
message: 'Location Id: ' + loc_id + ' is not in the simulation queue',
|
||||||
};
|
};
|
||||||
throw new Error('loc_id ' + loc_id + 'not found');
|
throw new Error('loc_id ' + loc_id + 'not found');
|
||||||
@@ -379,62 +511,89 @@ export const useSimulationStore = defineStore('simulationStore', {
|
|||||||
socket.emit(
|
socket.emit(
|
||||||
'location_item_update',
|
'location_item_update',
|
||||||
{ loc_id: loc_id, key: key, value: value },
|
{ 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') {
|
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);
|
throw new Error(response.message);
|
||||||
} else {
|
} 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) {
|
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[]) {
|
updateLocationQueueOrder(newOrder: string[]) {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { command_status: string; message: string };
|
let functionReturn: { status: string; message: string };
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('socketStore: Update Location Queue Order, new order: %s', newOrder);
|
console.log('socketStore: Update Location Queue Order, new order: %s', newOrder);
|
||||||
}
|
}
|
||||||
socket.emit('queue_order_update', { newOrder: newOrder }, (response) => {
|
socket.emit('queue_order_update', { newOrder: newOrder }, (response) => {
|
||||||
if (response.command_status == 'ERROR') {
|
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);
|
throw new Error(response.message);
|
||||||
} else {
|
} 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) {
|
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 {
|
getLocationData(loc_id: strong): LocationMark {
|
||||||
console.log('digesting QueueUpdate: ', data);
|
return this.locationQueueData[loc_id]
|
||||||
this.simulationRunning = data.active;
|
},
|
||||||
console.log('Setting SimulationState to %s', data.state);
|
digestUpdate(data: SimulationStatusUpdate): void {
|
||||||
this.simulationState = data.state;
|
if (data.current_location) this.updateCurrentLocation(data.current_location);
|
||||||
this.simulationQueueLength = data.order.length;
|
if ('test_mode' in data) this.testMode = data.test_mode;
|
||||||
this.locationQueueData = data.data;
|
if ('gps_noise' in data) this.gpsNoise = data.gps_noise;
|
||||||
this.locationQueueOrder = data.order;
|
if ('active' in data) this.simulationRunning = data.active;
|
||||||
this.locationQueueDeletedItems = data.deleted_items;
|
if ('state' in data) this.simulationState = data.state;
|
||||||
this.testMode = data.test_mode;
|
if (data.data) this.updateData(data.data);
|
||||||
this.gpsNoise = data.gps_noise;
|
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 { defineStore, acceptHMRUpdate } from 'pinia';
|
||||||
import { socket } from 'boot/socketio';
|
import { socket } from 'boot/socketio';
|
||||||
|
|
||||||
|
import { Dialog } from 'quasar';
|
||||||
|
|
||||||
import { useDeviceStore } from 'stores/device';
|
import { useDeviceStore } from 'stores/device';
|
||||||
import { useIcloudStore } from 'stores/icloud';
|
import { useIcloudStore } from 'stores/icloud';
|
||||||
import { useSimulationStore } from 'stores/simulation';
|
import { useSimulationStore } from 'stores/simulation';
|
||||||
import { useTunneldStore } from 'stores/tunneld';
|
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', {
|
export const useSocketStore = defineStore('socketStore', {
|
||||||
state: () => {
|
state: () => {
|
||||||
@@ -88,6 +88,28 @@ export const useSocketStore = defineStore('socketStore', {
|
|||||||
console.error('Error Received: ', data);
|
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
|
// status
|
||||||
socket.on('status', (data: StatusUpdate): void => {
|
socket.on('status', (data: StatusUpdate): void => {
|
||||||
if (this.debugLog) {
|
if (this.debugLog) {
|
||||||
@@ -98,6 +120,9 @@ export const useSocketStore = defineStore('socketStore', {
|
|||||||
},
|
},
|
||||||
requestUpdate(): void {
|
requestUpdate(): void {
|
||||||
socket.emit('request_update', (response: StatusUpdate) => {
|
socket.emit('request_update', (response: StatusUpdate) => {
|
||||||
|
if (this.debugLog) {
|
||||||
|
console.debug('Received status update: ', response);
|
||||||
|
}
|
||||||
this.digestUpdate(response);
|
this.digestUpdate(response);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -106,20 +131,29 @@ export const useSocketStore = defineStore('socketStore', {
|
|||||||
const icloudStore = useIcloudStore();
|
const icloudStore = useIcloudStore();
|
||||||
const simulationStore = useSimulationStore();
|
const simulationStore = useSimulationStore();
|
||||||
const tunneldStore = useTunneldStore();
|
const tunneldStore = useTunneldStore();
|
||||||
if (data.simulation_queue) {
|
if (data.simulation) {
|
||||||
simulationStore.digestQueueUpdate(data.simulation_queue);
|
simulationStore.digestUpdate(data.simulation);
|
||||||
}
|
}
|
||||||
deviceStore.deviceConnected = !!(data.udid && data.tunnel);
|
if (data.device) {
|
||||||
if (data.current_location) {
|
const devConnected: boolean = !!data.device.udid;
|
||||||
simulationStore.digestCurrentLocation(data.current_location);
|
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) {
|
if (data.icloud) {
|
||||||
icloudStore.digestUpdate(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 = '') {
|
tunneldControl(command: TunneldCommands, udid: string = '') {
|
||||||
const socketStore = useSocketStore();
|
const socketStore = useSocketStore();
|
||||||
const debugLog = socketStore.debugLog;
|
const debugLog = socketStore.debugLog;
|
||||||
let fnctRtn: { sts: string; msg?: string | undefined } = { sts: '', msg: '' };
|
let functionReturn: { status: string; message?: string | undefined } = { status: '', message: '' };
|
||||||
if (debugLog) {
|
if (debugLog) {
|
||||||
console.log('tunneldStore: got command: ', command);
|
console.log('tunneldStore: got command: ', command);
|
||||||
}
|
}
|
||||||
@@ -30,14 +30,37 @@ export const useTunneldStore = defineStore('tunneldStore', {
|
|||||||
'tunneld_control', args, (response) => {
|
'tunneld_control', args, (response) => {
|
||||||
console.log(response.command_status, response.command);
|
console.log(response.command_status, response.command);
|
||||||
if (response.command_status == 'ERROR') {
|
if (response.command_status == 'ERROR') {
|
||||||
fnctRtn = { sts: 'error', msg: response.message?.toString() };
|
functionReturn = { status: 'error', message: response.message?.toString() };
|
||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} 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