Files
sim-location-frontend/src/components/LocationItem.vue
T
2026-06-10 11:15:15 -04:00

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>