lots of changes

This commit is contained in:
2026-04-14 09:53:12 -04:00
parent 27a2904bab
commit 32c4f2a835
16 changed files with 2461 additions and 1815 deletions
+404
View File
@@ -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>