add EditFavoriteDialog component and simloc_logo.svg

split socketio strore into seperate stores
added all routing points to sim
add vuedraggable
built icon picker for favorites
and more
This commit is contained in:
2026-04-29 13:17:49 -04:00
parent 52f05550e0
commit ddaf682d16
35 changed files with 4838 additions and 1486 deletions
+165 -167
View File
@@ -1,20 +1,21 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useSocketioStore } from 'stores/socketio';
import { useSimulationStore } from 'stores/simulation';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import type { NominatimResponse } from 'components/models';
import { Icon, PinCirclePanel, PinStarPanel } from 'leaflet-extra-markers';
import FormattedAddress from 'components/FormattedAddress.vue';
//import NestedKnob from 'components/NestedKnob.vue';
const socketStore = useSocketioStore();
const simulationStore = useSimulationStore();
const { currentLocation, locationQueueOrder, locationQueueData, simulationRunning } =
storeToRefs(socketStore);
storeToRefs(simulationStore);
const props = defineProps({
address: {
type: Object as () => NominatimResponse,
required: true,
required: false,
},
latitude: {
type: Number,
@@ -26,7 +27,7 @@ const props = defineProps({
},
icon: {
type: String,
default: 'location_on',
default: 'mdi-map-marker',
},
start: {
type: String,
@@ -136,19 +137,8 @@ const calculateDeltaTime = computed(() => {
}
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 secondsToHhMmSs = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
@@ -156,7 +146,17 @@ const secondsToTime = (seconds: number) => {
.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];
@@ -173,78 +173,6 @@ const humanReadableDateTime = (iso: string) => {
second: '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;
@@ -258,12 +186,6 @@ 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';
@@ -313,21 +235,80 @@ onMounted(() => {
timerId = requestAnimationFrame(update);
});
const delayValue = computed({
get: () => props.delay,
set: (val) => socketStore.updateLocationMark(props.loc_id, 'delay', val),
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', delay: props.delay, step: 10, max: 300 } as const;
return {
units: 's',
display: Math.floor((props.delay % 3600) / 60),
step: 10,
max: 80,
} as const;
} else if (props.delay <= 3600) {
return { units: 'm', delay: props.delay / 60, step: 60, max: 4500 } as const;
return {
units: 'm',
display: Math.floor((props.delay % 3600) / 60),
step: 1,
max: 80,
} as const;
} else {
return { units: 'h', delay: props.delay / 3600, step: 3600, max: 21600 } as const;
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(() => {
return props.index > currentIndex.value;
});
@@ -336,77 +317,94 @@ const delayActive = computed(() => {
return props.index === currentIndex.value + 1;
});
const canDrag = 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;
"
<div>
<q-item
v-if="displayDelay"
:active="delayActive"
:class="delayActive ? 'text-orange bg-dark' : ''"
active-class="text-orange bg-dark"
>
<q-icon class="drag-handle" 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" />
<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 bg-dark"
>
<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> {{ latitude }}, {{ longitude }} </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-if="!active && !end">
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 :class="{ 'drag-handle': canDrag }" 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" />
</div>
</template>
<style lang="sass" scoped>
.past