462 lines
12 KiB
Vue
462 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { storeToRefs } from 'pinia';
|
|
import { useSimulationStore } from 'stores/simulation';
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
import type { NominatimResponse } from 'components/models';
|
|
import {
|
|
Icon,
|
|
PinCirclePanel,
|
|
PinStarPanel,
|
|
PointCircle,
|
|
TackDiamondBorder,
|
|
} from 'leaflet-extra-markers';
|
|
|
|
import FormattedAddress from 'components/FormattedAddress.vue';
|
|
|
|
const simulationStore = useSimulationStore();
|
|
const {
|
|
currentLocation,
|
|
locationQueueOrder,
|
|
locationQueueData,
|
|
locationQueueDeletedItems,
|
|
simulationRunning,
|
|
} = storeToRefs(simulationStore);
|
|
|
|
const props = defineProps<{
|
|
address?: NominatimResponse | undefined;
|
|
latitude: number;
|
|
longitude: number;
|
|
icon?: string;
|
|
start: string;
|
|
end?: string | undefined;
|
|
delay: number;
|
|
active: boolean;
|
|
isCurrentDelay: boolean;
|
|
loc_id: string;
|
|
isLast: boolean;
|
|
index: number;
|
|
status?: string | undefined;
|
|
type?: string | undefined;
|
|
instruction?: string | undefined;
|
|
}>();
|
|
|
|
// Define custom events that this component can emit
|
|
const emit = defineEmits(['item-clicked']);
|
|
|
|
function itemClicked() {
|
|
const param1 = props.loc_id;
|
|
emit('item-clicked', param1);
|
|
}
|
|
|
|
const myData = computed(() => {
|
|
return simulationStore.getLocationData(props.loc_id);
|
|
});
|
|
|
|
const isDeleted = computed(() => {
|
|
if (myData.value) {
|
|
return myData.value.status === 'deleted' ? true : false;
|
|
} else {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
const currentTime = ref(new Date());
|
|
let timerId: number;
|
|
|
|
function formatInAgo(seconds: number) {
|
|
let delta;
|
|
if (seconds === 0) {
|
|
delta = 'now';
|
|
}
|
|
if (seconds > 0) {
|
|
const minutes: number = Math.floor(seconds / 60);
|
|
const hours: number = Math.floor(minutes / 60);
|
|
const days: number = Math.floor(hours / 24);
|
|
if (days > 0) {
|
|
delta = `in ${days} day${days > 1 ? 's' : ''}`;
|
|
} else if (hours > 0) {
|
|
delta = `in ${hours} hour${hours > 1 ? 's' : ''}`;
|
|
} else if (minutes > 0) {
|
|
delta = `in ${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
} else {
|
|
delta = `in ${seconds} second${seconds > 1 ? 's' : ''}`;
|
|
}
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
const calculateDeltaTime = computed(() => {
|
|
let delta;
|
|
if (props.active) {
|
|
const startTime = new Date(props.start);
|
|
const nowMs: number = currentTime.value.getTime();
|
|
const pastMs: number = startTime.getTime();
|
|
const diffMs: number = Math.abs(nowMs - pastMs);
|
|
const seconds: number = Math.floor(diffMs / 1000);
|
|
const minutes: number = Math.floor(seconds / 60);
|
|
const remainderSeconds: number = seconds % 60;
|
|
const hours: number = Math.floor(minutes / 60);
|
|
const days: number = Math.floor(hours / 24);
|
|
if (days > 0) {
|
|
delta = `for ${days} d${days > 1 ? 's' : ''}`;
|
|
} else if (hours > 0) {
|
|
delta = `for ${hours} h${hours > 1 ? 's' : ''}`;
|
|
} else if (minutes > 0) {
|
|
delta = `for ${minutes}:${remainderSeconds}`;
|
|
} else {
|
|
delta = `for ${seconds} s${seconds > 1 ? 's' : ''}`;
|
|
}
|
|
} else if (props.end && props.end !== '') {
|
|
const pastTime: Date = new Date(props.end);
|
|
const nowMs: number = currentTime.value.getTime();
|
|
const pastMs: number = pastTime.getTime();
|
|
const diffMs: number = Math.abs(nowMs - pastMs);
|
|
const seconds: number = Math.floor(diffMs / 1000);
|
|
const minutes: number = Math.floor(seconds / 60);
|
|
const hours: number = Math.floor(minutes / 60);
|
|
const days: number = Math.floor(hours / 24);
|
|
if (days > 0) {
|
|
delta = `${days} day${days > 1 ? 's' : ''} ago`;
|
|
} else if (hours > 0) {
|
|
delta = `${hours} hr${hours > 1 ? 's' : ''} ago`;
|
|
} else if (minutes > 0) {
|
|
delta = `${minutes} min${minutes > 1 ? 's' : ''} ago`;
|
|
} else {
|
|
delta = `${seconds} sec${seconds > 1 ? 's' : ''} ago`;
|
|
}
|
|
} else {
|
|
let startSlice;
|
|
if (currentIndex.value && currentIndex.value > 0) {
|
|
startSlice = currentIndex.value;
|
|
} else {
|
|
startSlice = 0;
|
|
}
|
|
const waitingFor = locationQueueOrder.value.slice(startSlice, myIndex.value + 1);
|
|
const willWait = waitingFor.reduce((sum, loc_id) => {
|
|
return sum + (locationQueueData.value[loc_id]?.delay || 0);
|
|
}, 0);
|
|
delta = formatInAgo(willWait);
|
|
}
|
|
return delta;
|
|
});
|
|
|
|
const secondsToHhMmSs = (seconds: number) => {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds
|
|
.toString()
|
|
.padStart(2, '0')}`;
|
|
};
|
|
/*
|
|
const secondsToTimeShort = (seconds: number) => {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
if (hours > 2) {
|
|
return hours.toString();
|
|
}
|
|
if (minutes > 5) {
|
|
return minutes.toString();
|
|
} else return seconds.toString();
|
|
};
|
|
const timeToSeconds = (timeIn: string) => {
|
|
const a = timeIn.split(':');
|
|
const seconds = +a[0] * 60 * 60 + +a[1] * 60 + +a[2];
|
|
return seconds;
|
|
};
|
|
*/
|
|
const humanReadableDateTime = (iso: string) => {
|
|
return new Date(iso).toLocaleDateString('en-US', {
|
|
// year: 'numeric',
|
|
// month: 'long',
|
|
// day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
});
|
|
};
|
|
|
|
const currentIndex = computed(() => {
|
|
return currentLocation.value.loc_id
|
|
? locationQueueOrder.value.indexOf(currentLocation.value.loc_id)
|
|
: null;
|
|
});
|
|
|
|
const myIndex = computed(() => {
|
|
if (isDeleted.value) {
|
|
return locationQueueDeletedItems.value.indexOf(props.loc_id);
|
|
} else {
|
|
return locationQueueOrder.value.indexOf(props.loc_id);
|
|
}
|
|
});
|
|
|
|
const myUpdatedIndex = computed(() => {
|
|
if (isDeleted.value) {
|
|
return myIndex.value + 1;
|
|
} else {
|
|
if (currentIndex.value === null) return myIndex.value + 1;
|
|
return myIndex.value - currentIndex.value;
|
|
}
|
|
});
|
|
|
|
const itemClass = computed(() => {
|
|
if (myUpdatedIndex.value > 0) return 'future';
|
|
else if (myUpdatedIndex.value < 0) return 'past';
|
|
else return 'active';
|
|
});
|
|
|
|
const customIcon = computed(() => {
|
|
if (isDeleted.value) {
|
|
return new Icon({
|
|
color: 'black',
|
|
accentColor: 'black',
|
|
content: myUpdatedIndex.value.toString(),
|
|
contentColor: 'white',
|
|
scale: 1,
|
|
svg: TackDiamondBorder,
|
|
});
|
|
} else {
|
|
if (myUpdatedIndex.value > 0) {
|
|
if (props.type === 'coord') {
|
|
return new Icon({
|
|
color: 'blue',
|
|
accentColor: 'firebrick',
|
|
content: myUpdatedIndex.value.toString(),
|
|
contentColor: 'white',
|
|
scale: 0.25,
|
|
svg: PointCircle,
|
|
});
|
|
} else {
|
|
return new Icon({
|
|
color: 'blue',
|
|
accentColor: 'firebrick',
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const iconElement = computed(() => {
|
|
return customIcon.value.createIcon();
|
|
});
|
|
|
|
onMounted(() => {
|
|
const update = () => {
|
|
currentTime.value = new Date();
|
|
timerId = requestAnimationFrame(update);
|
|
};
|
|
timerId = requestAnimationFrame(update);
|
|
});
|
|
|
|
const delayConverted = computed({
|
|
get: () => {
|
|
if (props.delay <= 260) {
|
|
return props.delay;
|
|
} else if (props.delay <= 3600) {
|
|
return Math.floor(props.delay / 60);
|
|
} else {
|
|
return Math.floor(props.delay / 3600);
|
|
}
|
|
},
|
|
set: (val) => {
|
|
if (getDelayAttrs.value.units == 's') {
|
|
simulationStore.updateLocationMark(props.loc_id, 'delay', val);
|
|
} else if (getDelayAttrs.value.units == 'm') {
|
|
simulationStore.updateLocationMark(props.loc_id, 'delay', val * 60);
|
|
} else if (getDelayAttrs.value.units == 'h') {
|
|
simulationStore.updateLocationMark(props.loc_id, 'delay', val * 3600);
|
|
}
|
|
},
|
|
});
|
|
|
|
const delayHhMmSs = computed({
|
|
get: () => secondsToHhMmSs(props.delay),
|
|
set: (val) => {
|
|
const totalSeconds = val.split(':').reduce((acc, time) => 60 * acc + +time, 0);
|
|
simulationStore.updateLocationMark(props.loc_id, 'delay', totalSeconds);
|
|
},
|
|
});
|
|
|
|
const getDelayAttrs = computed(() => {
|
|
if (props.delay <= 260) {
|
|
return {
|
|
units: 's',
|
|
display: Math.floor((props.delay % 3600) / 60),
|
|
step: 10,
|
|
max: 80,
|
|
} as const;
|
|
} else if (props.delay <= 3600) {
|
|
return {
|
|
units: 'm',
|
|
display: Math.floor((props.delay % 3600) / 60),
|
|
step: 1,
|
|
max: 80,
|
|
} as const;
|
|
} else {
|
|
return {
|
|
units: 'h',
|
|
display: Math.floor(props.delay / 3600),
|
|
step: 1,
|
|
max: 12,
|
|
} as const;
|
|
}
|
|
});
|
|
|
|
/*
|
|
const delayMask = computed(() => {
|
|
if (props.delay < 60) {
|
|
return 'ss';
|
|
}
|
|
if (props.delay < 3600) {
|
|
return 'mm:ss';
|
|
} else return 'HH:mm:ss';
|
|
});
|
|
|
|
const delayInputMask = computed(() => {
|
|
if (props.delay < 60) {
|
|
return '##';
|
|
}
|
|
if (props.delay < 3600) {
|
|
return '##:##';
|
|
} else return '##:##:##';
|
|
});
|
|
*/
|
|
|
|
const displayDelay = computed(() => {
|
|
if (currentIndex.value === null) return true;
|
|
return props.index > currentIndex.value;
|
|
});
|
|
|
|
const delayActive = computed(() => {
|
|
return props.index === currentIndex.value;
|
|
});
|
|
|
|
const canDrag = computed(() => {
|
|
if (currentIndex.value === null) return true;
|
|
return props.index > currentIndex.value + 1;
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
cancelAnimationFrame(timerId);
|
|
});
|
|
</script>
|
|
<template>
|
|
<div>
|
|
<q-item
|
|
v-if="displayDelay"
|
|
:active="delayActive"
|
|
:class="delayActive ? 'text-orange' : ''"
|
|
active-class="text-orange"
|
|
>
|
|
<q-item-section class="flex flex-center">
|
|
<q-input
|
|
class="q-pt-md"
|
|
filled
|
|
v-model="delayHhMmSs"
|
|
mask="fulltime"
|
|
stack-label
|
|
label="Delay"
|
|
dense
|
|
style="max-width: 150px"
|
|
>
|
|
<template v-slot:append> </template>
|
|
</q-input>
|
|
<q-slider
|
|
v-model="delayConverted"
|
|
:min="0"
|
|
:step="getDelayAttrs.step"
|
|
:max="getDelayAttrs.max"
|
|
label
|
|
switch-label-side
|
|
:label-value="delayConverted + ' ' + getDelayAttrs.units"
|
|
markers
|
|
snap
|
|
color="primary"
|
|
style="max-width: 75%"
|
|
/>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-separator inset spaced v-if="displayDelay" />
|
|
<q-item
|
|
v-ripple
|
|
clickable
|
|
:active="active"
|
|
@click="itemClicked"
|
|
:class="itemClass"
|
|
active-class="text-orange"
|
|
>
|
|
<q-item-section style="width: 70%">
|
|
<q-item-label v-if="address">
|
|
<FormattedAddress :address="props.address" />
|
|
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
|
|
</q-item-label>
|
|
<q-item-label v-else-if="instruction" lines="2">{{ instruction }}</q-item-label>
|
|
<q-item-label v-else> {{ latitude }}, {{ longitude }} </q-item-label>
|
|
<q-item-label caption lines="1" v-if="start && simulationRunning && !isDeleted">
|
|
start: {{ humanReadableDateTime(start) }}
|
|
</q-item-label>
|
|
<q-item-label caption lines="1" v-if="end && simulationRunning && !isDeleted">
|
|
end: {{ humanReadableDateTime(end) }}
|
|
</q-item-label>
|
|
<q-item-label caption lines="1" v-if="!active && !end">
|
|
delay: {{ delay }} seconds
|
|
</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section
|
|
side
|
|
style="
|
|
padding: 30px 0 0 0;
|
|
height: 100%;
|
|
width: 35%;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
"
|
|
>
|
|
<q-icon :class="{ 'drag-handle': canDrag }" v-html="iconElement.outerHTML" />
|
|
<q-item-label caption lines="1" v-if="simulationRunning && !isDeleted">
|
|
{{ calculateDeltaTime }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-separator spaced inset v-if="!isLast" />
|
|
</div>
|
|
</template>
|
|
<style lang="sass" scoped>
|
|
.past
|
|
color: gray
|
|
</style>
|