lots of changes
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useSocketioStore } from 'stores/socketio';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import type { NominatimResponse } from 'components/models';
|
||||
import { Icon, PinCirclePanel } from 'leaflet-extra-markers';
|
||||
|
||||
import FormattedAddress from 'components/FormattedAddress.vue';
|
||||
|
||||
const socketStore = useSocketioStore();
|
||||
const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } =
|
||||
storeToRefs(socketStore);
|
||||
|
||||
const props = defineProps({
|
||||
address: {
|
||||
type: Object as () => NominatimResponse,
|
||||
required: true,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'location_on',
|
||||
},
|
||||
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
|
||||
const emit = defineEmits(['item-clicked']);
|
||||
|
||||
function itemClicked() {
|
||||
const param1 = props.loc_id;
|
||||
emit('item-clicked', param1);
|
||||
}
|
||||
|
||||
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) {
|
||||
delta = 'now';
|
||||
} 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 > 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 secondsToTime = computed(() => {
|
||||
const seconds = props.delay;
|
||||
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 secondsToTime = (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 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',
|
||||
});
|
||||
};
|
||||
/*
|
||||
const stateAbbrevMap: Record<string, string> = {
|
||||
Alabama: 'AL',
|
||||
Alaska: 'AK',
|
||||
Arizona: 'AZ',
|
||||
Arkansas: 'AR',
|
||||
California: 'CA',
|
||||
Colorado: 'CO',
|
||||
Connecticut: 'CT',
|
||||
Delaware: 'DE',
|
||||
Florida: 'FL',
|
||||
Georgia: 'GA',
|
||||
Hawaii: 'HI',
|
||||
Idaho: 'ID',
|
||||
Illinois: 'IL',
|
||||
Indiana: 'IN',
|
||||
Iowa: 'IA',
|
||||
Kansas: 'KS',
|
||||
Kentucky: 'KY',
|
||||
Louisiana: 'LA',
|
||||
Maine: 'ME',
|
||||
Maryland: 'MD',
|
||||
Massachusetts: 'MA',
|
||||
Michigan: 'MI',
|
||||
Minnesota: 'MN',
|
||||
Mississippi: 'MS',
|
||||
Missouri: 'MO',
|
||||
Montana: 'MT',
|
||||
Nebraska: 'NE',
|
||||
Nevada: 'NV',
|
||||
'New Hampshire': 'NH',
|
||||
'New Jersey': 'NJ',
|
||||
'New Mexico': 'NM',
|
||||
'New York': 'NY',
|
||||
'North Carolina': 'NC',
|
||||
'North Dakota': 'ND',
|
||||
Ohio: 'OH',
|
||||
Oklahoma: 'OK',
|
||||
Oregon: 'OR',
|
||||
Pennsylvania: 'PA',
|
||||
'Rhode Island': 'RI',
|
||||
'South Carolina': 'SC',
|
||||
'South Dakota': 'SD',
|
||||
Tennessee: 'TN',
|
||||
Texas: 'TX',
|
||||
Utah: 'UT',
|
||||
Vermont: 'VT',
|
||||
Virginia: 'VA',
|
||||
Washington: 'WA',
|
||||
'West Virginia': 'WV',
|
||||
Wisconsin: 'WI',
|
||||
Wyoming: 'WY',
|
||||
};
|
||||
|
||||
function formatAddress(input: string): string {
|
||||
const parts = input.split(',').map((p) => p.trim());
|
||||
|
||||
if (parts.length < 5) return input; // fallback safety
|
||||
|
||||
const streetNumber = parts[0];
|
||||
const streetName = parts[1];
|
||||
|
||||
const zip = parts.at(-2) ?? '';
|
||||
const stateFull = parts.at(-3) ?? '';
|
||||
const cityRaw = parts.at(-5) ?? '';
|
||||
|
||||
const city = cityRaw.replace(/^City of\s+/i, '');
|
||||
const state = stateAbbrevMap[stateFull] ?? stateFull;
|
||||
|
||||
return `${streetNumber} ${streetName}, ${city}, ${state} ${zip}`;
|
||||
}
|
||||
*/
|
||||
|
||||
const currentIndex = computed(() => {
|
||||
return currentLocation.value ? locationQueueOrder.value.indexOf(currentLocation.value.loc_id) : 0;
|
||||
});
|
||||
|
||||
const myIndex = computed(() => {
|
||||
return locationQueueOrder.value.indexOf(props.loc_id);
|
||||
});
|
||||
|
||||
const myUpdatedIndex = computed(() => {
|
||||
return myIndex.value - currentIndex.value;
|
||||
});
|
||||
|
||||
/*
|
||||
const markerIndex = computed(() => {
|
||||
return props.active ? '*' : myUpdatedIndex.value.toString();
|
||||
});
|
||||
*/
|
||||
|
||||
const itemClass = computed(() => {
|
||||
if (myUpdatedIndex.value > 0) return 'future';
|
||||
else if (myUpdatedIndex.value < 0) return 'past';
|
||||
else return 'active';
|
||||
});
|
||||
|
||||
const customIcon = computed(() => {
|
||||
if (myUpdatedIndex.value > 0) {
|
||||
return new Icon({
|
||||
color: 'blue',
|
||||
accentColor: 'firebrick',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
});
|
||||
} else {
|
||||
return new Icon({
|
||||
color: 'black',
|
||||
accentColor: 'grey',
|
||||
content: myUpdatedIndex.value.toString(),
|
||||
contentColor: 'white',
|
||||
scale: 1,
|
||||
svg: PinCirclePanel,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const iconElement = computed(() => {
|
||||
return customIcon.value.createIcon();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const update = () => {
|
||||
currentTime.value = new Date();
|
||||
timerId = requestAnimationFrame(update);
|
||||
};
|
||||
timerId = requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
const delayValue = computed({
|
||||
get: () => props.delay,
|
||||
set: (val) => socketStore.updateLocationMark(props.loc_id, 'delay', val),
|
||||
});
|
||||
|
||||
const getDelayAttrs = computed(() => {
|
||||
if (props.delay <= 260) {
|
||||
return { units: 's', delay: props.delay, step: 10, max: 300 } as const;
|
||||
} else if (props.delay <= 3600) {
|
||||
return { units: 'm', delay: props.delay / 60, step: 60, max: 4500 } as const;
|
||||
} else {
|
||||
return { units: 'h', delay: props.delay / 3600, step: 3600, max: 21600 } as const;
|
||||
}
|
||||
});
|
||||
|
||||
const displayDelay = computed(() => {
|
||||
return props.index > currentIndex.value;
|
||||
});
|
||||
|
||||
const delayActive = computed(() => {
|
||||
return props.index === currentIndex.value + 1;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(timerId);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<q-item v-if="displayDelay" :active="delayActive">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="access_time" color="secondary" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-knob
|
||||
show-value
|
||||
v-model="delayValue"
|
||||
size="50px"
|
||||
color="secondary"
|
||||
track-color="dark-page"
|
||||
:step="getDelayAttrs.step"
|
||||
:max="getDelayAttrs.max"
|
||||
>
|
||||
{{ getDelayAttrs.delay }} {{ getDelayAttrs.units }}
|
||||
</q-knob>
|
||||
<!--
|
||||
<q-slider
|
||||
v-model="delayValue"
|
||||
:min="0"
|
||||
:step="getDelayAttrs.step"
|
||||
:max="getDelayAttrs.max"
|
||||
label
|
||||
markers
|
||||
snap
|
||||
label-always
|
||||
:label-value="getDelayAttrs.delay + getDelayAttrs.units"
|
||||
:marker-labels="markerLabel"
|
||||
color="primary"
|
||||
/>
|
||||
-->
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator inset spaced v-if="displayDelay" />
|
||||
<q-item v-ripple clickable :active="active" @click="itemClicked" :class="itemClass">
|
||||
<q-item-section style="width: 70%">
|
||||
<q-item-label>
|
||||
<FormattedAddress :address="props.address" />
|
||||
<q-tooltip> {{ latitude }}, {{ longitude }} </q-tooltip>
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="1" v-if="start && simulationRunning">
|
||||
start: {{ humanReadableDateTime(start) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="1" v-if="end && simulationRunning">
|
||||
end: {{ humanReadableDateTime(end) }}
|
||||
</q-item-label>
|
||||
<q-item-label caption lines="1" v-else> delay: {{ delay }} seconds </q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section
|
||||
side
|
||||
style="
|
||||
padding: 30px 0 0 10px;
|
||||
height: 100%;
|
||||
width: 35%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<q-icon v-html="iconElement.outerHTML" />
|
||||
<q-item-label caption lines="1" v-if="simulationRunning">
|
||||
{{ calculateDeltaTime }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator spaced inset v-if="!isLast" />
|
||||
</template>
|
||||
<style lang="sass" scoped>
|
||||
.past
|
||||
color: gray
|
||||
</style>
|
||||
Reference in New Issue
Block a user