Compare commits

...

3 Commits

Author SHA1 Message Date
3f3a5136eb rework Menu Bar / Rework socketioStore 2026-03-21 07:33:12 -04:00
8214f0543a layout changes, status bar addition, drawer, and leaflet store 2026-03-13 14:08:19 -04:00
d1cb31b2c8 changes
# Conflicts:
#	src/components/LeafletTest.vue
2026-03-13 12:11:05 -04:00
29 changed files with 1558 additions and 534 deletions

2
mise.toml Normal file
View File

@@ -0,0 +1,2 @@
[tools]
node = "24.14.0"

44
package-lock.json generated
View File

@@ -13,7 +13,9 @@
"@vue-leaflet/vue-leaflet": "^0.10.1",
"axios": "^1.2.1",
"leaflet": "^1.9.4",
"leaflet-extra-markers": "^2.0.1",
"leaflet-geosearch": "^4.2.2",
"leaflet-routing-machine": "^3.2.12",
"pinia": "^3.0.4",
"quasar": "^2.16.0",
"socket.io-client": "^4.8.3",
@@ -1043,6 +1045,23 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mapbox/corslite": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@mapbox/corslite/-/corslite-0.0.7.tgz",
"integrity": "sha512-w/uS474VFjmqQ7fFWIMZINQM1BAQxDLuoJaZZIPES1BmeYpCtlh9MtbFxKGGDAsfvut8/HircIsVvEYRjQ+iMg==",
"license": "BSD"
},
"node_modules/@mapbox/polyline": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-0.2.0.tgz",
"integrity": "sha512-GCddO0iw6AzOQqZgBmjEQI9Pgo40/yRgkTkikGctE01kNBN0ThWYuAnTD+hRWrAWMV6QJ0rNm4m8DAsaAXE7Pg==",
"bin": {
"polyline": "bin/polyline.bin.js"
},
"engines": {
"node": "*"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5729,6 +5748,14 @@
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/leaflet-extra-markers": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/leaflet-extra-markers/-/leaflet-extra-markers-2.0.1.tgz",
"integrity": "sha512-cbCo+YSgnN+tomDJRxv/vf+ZXfLymSKjDDuv7omYaOO+8oj9q9uvwytL+eViyClPuscAT3lNh339R+gr8w20qg==",
"peerDependencies": {
"leaflet": "^1.9.4"
}
},
"node_modules/leaflet-geosearch": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/leaflet-geosearch/-/leaflet-geosearch-4.2.2.tgz",
@@ -5739,6 +5766,17 @@
"leaflet": "^1.6.0"
}
},
"node_modules/leaflet-routing-machine": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/leaflet-routing-machine/-/leaflet-routing-machine-3.2.12.tgz",
"integrity": "sha512-HLde58G1YtD9xSIzZavJ6BPABZaV1hHeGst8ouhzuxmSC3s32NVtADT+njbIUMW1maHRCrsgTk/E4hz5QH7FrA==",
"license": "ISC",
"dependencies": {
"@mapbox/corslite": "0.0.7",
"@mapbox/polyline": "^0.2.0",
"osrm-text-instructions": "^0.13.2"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -6305,6 +6343,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/osrm-text-instructions": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/osrm-text-instructions/-/osrm-text-instructions-0.13.4.tgz",
"integrity": "sha512-ge4ZTIetMQKAHKq2MwWf83ntzdJN20ndRKRaVNoZ3SkDkBNO99Qddz7r6+hrVx38I+ih6Rk5T1yslczAB6Q9Pg==",
"license": "BSD-2-Clause"
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",

View File

@@ -19,7 +19,9 @@
"@vue-leaflet/vue-leaflet": "^0.10.1",
"axios": "^1.2.1",
"leaflet": "^1.9.4",
"leaflet-extra-markers": "^2.0.1",
"leaflet-geosearch": "^4.2.2",
"leaflet-routing-machine": "^3.2.12",
"pinia": "^3.0.4",
"quasar": "^2.16.0",
"socket.io-client": "^4.8.3",

View File

@@ -2,6 +2,7 @@
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file
import { defineConfig } from '#q-app/wrappers';
import { fileURLToPath } from 'node:url';
export default defineConfig((/* ctx */) => {
return {
@@ -11,7 +12,7 @@ export default defineConfig((/* ctx */) => {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ['axios', 'socket'],
boot: ['axios', 'socketio'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
css: ['app.scss'],
@@ -32,6 +33,11 @@ export default defineConfig((/* ctx */) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
build: {
alias: {
constants: fileURLToPath(new URL('./src/constants', import.meta.url)),
functions: fileURLToPath(new URL('./src/functions', import.meta.url)),
types: fileURLToPath(new URL('./src/types', import.meta.url)),
},
target: {
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
node: 'node20',
@@ -43,7 +49,7 @@ export default defineConfig((/* ctx */) => {
// extendTsConfig (tsConfig) {}
},
vueRouterMode: 'hash', // available values: 'hash', 'history'
vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
@@ -65,7 +71,13 @@ export default defineConfig((/* ctx */) => {
extendViteConf() {
return {
server: {
allowedHosts: ['localhost'],
hmr: {
// overlay: false,
},
allowedHosts: [
'localhost',
'strixx.famor.org'
],
},
};
},
@@ -79,6 +91,7 @@ export default defineConfig((/* ctx */) => {
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{ts,js,mjs,cjs,vue}"',
useFlatConfig: true,
},
overlay: false,
},
{ server: false },
],
@@ -95,6 +108,7 @@ export default defineConfig((/* ctx */) => {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
'/socket.io': {
target: 'http://localhost:8000', // Your backend WebSocket server
@@ -112,7 +126,7 @@ export default defineConfig((/* ctx */) => {
config: {
dark: true,
},
// iconSet: 'material-icons', // Quasar icon set
iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact

194
src/]
View File

@@ -1,194 +0,0 @@
<template>
<div style="height:600px; width:800px">
<q-toolbar class="bg-primary text-white q-my-md shadow-2">
<q-btn flat round dense icon="menu" class="q-mr-sm" />
<q-separator dark vertical inset />
<q-btn stretch flat :icon="home.icon" @click="handleFavClick(home.coords)" />
<q-space />
<q-btn-dropdown stretch flat label="Favorites">
<q-list>
<q-item
v-for="fav in favorites"
:key="fav.id"
clickable
v-ripple
v-close-popup
@click="handleFavClick(fav.coords)"
>
<q-item-section avatar>
<q-avatar :icon="fav.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>{{ fav.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar>
<l-map @ready="onMapReady" ref="mapRef" :zoom="zoom" :center="center" style="height: 500px; width= 100%;" @click="updateMarker">
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
@click="updateMarker($event.latlng)"
></l-tile-layer>
<l-marker :lat-lng="markerLatLng" @click="handleMarkerClick"></l-marker>
</l-map>
</div>
</template>
<script setup lang="ts">
import { useQuasar } from "quasar";
import { Ref, ref } from "vue";
import "leaflet/dist/leaflet.css";
import { LMap, LTileLayer, LMarker } from "@vue-leaflet/vue-leaflet";
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
import "leaflet-geosearch/dist/geosearch.css";
import "leaflet/dist/leaflet.css";
import type { Map, LeafletMouseEvent } from 'leaflet';
import SetLocationnDialog from "components/SetLocationDialog.vue";
import { api } from "src/boot/axios";
const $q = useQuasar();
const mapRef = ref(null);
const zoom = ref(10);
const center: Ref<[number, number]> = ref([40.71278, -74.00594]);
const markerLatLng: Ref<[number, number]> = ref([40.71278, -74.00594]);
const responseMessage = ref("");
interface coords {
lat: number;
lng: number;
}
interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
}
const onMapReady = (map: Map) => {
const provider = new OpenStreetMapProvider();
const searchOptions: SearchControlProps = {
provider: provider,
showMarker: false,
autoClose: true,
updateMap: true,
showPopup: true,
style: 'bar',
acceptAutoLoad: true,
autoComplete: true,
autoCompleteDelay: 250,
retainZoomLevel: false,
animateZoom: true,
keepResult: true
};
const searchControl = new GeoSearchControl(searchOptions);
map.addControl(searchControl);
};
function updateMarker(event: LeafletMouseEvent) {
markerLatLng.value = [event.latlng.lat, event.latlng.lng];
center.value = [event.latlng.lat, event.latlng.lng];
}
function handleMarkerClick(event: LeafletMouseEvent) {
$q.dialog({
component: SetLocationnDialog,
componentProps: {
lat: event.latlng.lat,
lng: event.latlng.lng
}
}).onOk(() => {
console.log("Dialog confirmed");
setLocation({ lat: event.latlng.lat, lng: event.latlng.lng });
}).onCancel(() => {
console.log("Dialog cancelled");
}).onDismiss(() => {
console.log("Dialog dismissed");
});
}
function handleFavClick(coords: coords) {
center.value = [coords.lat, coords.lng];
markerLatLng.value = [coords.lat, coords.lng];
}
async function setLocation(coords: coords) {
try {
const payLoadData = {
lat: coords.lat,
lng: coords.lng
};
const { data } = await api({
method: "post",
url: "/set",
data: payLoadData
});
console.log("Location set successfully:", data.data);
responseMessage.value = `Location successfully set! New location: ${data.lat}, ${data.lng}`;
$q.notify({ type: 'positive', message: responseMessage.value });
} catch (error) {
console.error("Error setting location:", error);
responseMessage.value = `Failed to set location: ${error}`;
$q.notify({ type: 'negative', message: responseMessage.value });
}
}
const home = {
name: "Home",
coords: {
lat: 40.71278,
lng: -74.00594
},
icon: "home"
};
const favorites = [
{
id: 1,
name: "Central Park",
coords: {
lat: 40.785091,
lng: -73.968285
},
icon: "park"
},
{
id: 2,
name: "Times Square",
coords: {
lat: 40.758896,
lng: -73.985130,
},
icon: "star"
},
{
id: 3,
name: "Empire State Building",
coords: {
lat: 40.748817,
lng: -73.985428
},
icon: "building"
}
];
</script>
<style>
.l-map {
height: 100vh;
width: 100vw;
}
</style>

View File

@@ -1,58 +0,0 @@
import { defineBoot } from '#q-app/wrappers';
import type { Socket } from 'socket.io-client';
import { io } from 'socket.io-client';
import type { StatusUpdate } from 'src/types';
interface SimulationStatus {
status: boolean;
data: {
latitude: number;
longitude: number;
start: string;
end?: string;
next_move?: number;
};
}
interface SimulationRequest {
latitude: number;
longitude: number;
delay?: number;
start?: string;
end?: string;
}
interface ServerToClientEvents {
noArg: () => void;
withAck: (d: string, callback: (e: number) => void) => void;
status_update: (d: StatusUpdate) => void;
simulation: (d: SimulationStatus) => void;
}
interface ClientToServerEvents {
message: (data: string) => void;
connect: () => void;
disconnect: () => void;
set_location: (data: SimulationRequest, callback: (response: SimulationStatus) => void) => void;
request_update: (callback: (response: { statusUpdate: StatusUpdate }) => void) => void;
command: (
command: string,
callback: (response: { success: boolean; message?: string }) => void,
) => void;
shutdown: (
delay: number,
callback: (response: { success: boolean; message?: string }) => void,
) => void;
// ... other events
}
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('/', {
autoConnect: true,
transports: ['websocket'],
});
export default defineBoot(({ app }) => {
app.config.globalProperties.$socket = socket;
});
export { socket };

16
src/boot/socketio.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineBoot } from '#q-app/wrappers';
import type { Socket } from 'socket.io-client';
import { io } from 'socket.io-client';
import type { ServerToClientEvents, ClientToServerEvents } from 'components/models';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('/', {
autoConnect: true,
transports: ['websocket', 'webtransport', 'polling'],
});
export default defineBoot(({ app }) => {
app.config.globalProperties.$socket = socket;
});
export { socket };

View File

@@ -0,0 +1,32 @@
<template>
<q-dialog ref="dlgRef" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="add_location" color="primary" text-color="white" />
<span class="q-ml-sm">
Are you sure you want to {{ name }} ?
</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="OK" color="primary" @click="onOkClick" />
<q-btn flat label="Cancel" color="primary" @click="onDialogCancel" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { useDialogPluginComponent } from 'quasar';
const props = defineProps({
name: { type: String, required: true },
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
function onOkClick() {
onDialogOK();
}
</script>

View File

@@ -1,37 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { Todo, Meta } from './models';
interface Props {
title: string;
todos?: Todo[];
meta: Meta;
active: boolean;
}
const props = withDefaults(defineProps<Props>(), {
todos: () => [],
});
const clickCount = ref(0);
function increment() {
clickCount.value += 1;
return clickCount.value;
}
const todoCount = computed(() => props.todos.length);
</script>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import L from 'leaflet';
import 'leaflet-routing-machine';
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css';
import type { IRouter, IGeocoder, LineOptions } from 'leaflet-routing-machine';
// Props
const props = defineProps<{
mapObject?: any;
visible?: boolean;
waypoints: any[];
router?: IRouter;
plan?: L.Routing.Plan;
geocoder?: IGeocoder;
fitSelectedRoutes?: string | boolean;
lineOptions?: LineOptions;
routeLine?: Function;
autoRoute?: boolean;
routeWhileDragging?: boolean;
routeDragInterval?: number;
waypointMode?: string;
useZoomParameter?: boolean;
showAlternatives?: boolean;
altLineOptions?: LineOptions;
}>();
// Defaults
const visible = props.visible ?? true;
const fitSelectedRoutes = props.fitSelectedRoutes ?? 'smart';
const autoRoute = props.autoRoute ?? true;
const routeWhileDragging = props.routeWhileDragging ?? false;
const routeDragInterval = props.routeDragInterval ?? 500;
const waypointMode = props.waypointMode ?? 'connect';
const useZoomParameter = props.useZoomParameter ?? false;
const showAlternatives = props.showAlternatives ?? false;
// State
const ready = ref(false);
const layer = ref<any>(null);
// Methods
function add() {
if (!props.mapObject) return;
const options = {
waypoints: props.waypoints,
fitSelectedRoutes,
autoRoute,
routeWhileDragging,
routeDragInterval,
waypointMode,
useZoomParameter,
showAlternatives,
};
console.log(L.Routing);
const routingLayer = L.Routing.control(options);
routingLayer.addTo(props.mapObject);
layer.value = routingLayer;
ready.value = true;
}
// Watchers
watch(
() => props.mapObject,
(val) => {
if (!val) return;
add();
},
);
// Lifecycle
onMounted(() => {
add();
});
onBeforeUnmount(() => {
if (layer.value) {
layer.value.remove();
}
});
</script>

View File

@@ -1,85 +1,65 @@
<template>
<div style="height:600px; width:800px">
<q-toolbar class="bg-primary text-white q-my-md shadow-2">
<q-btn flat round dense icon="menu" class="q-mr-sm" />
<q-separator dark vertical inset />
<q-btn stretch flat :icon="home.icon" @click="handleFavClick(home.coords)" />
<q-space />
<q-btn-dropdown stretch flat label="Favorites">
<q-list>
<q-item
v-for="fav in favorites"
:key="fav.id"
clickable
v-ripple
v-close-popup
@click="handleFavClick(fav.coords)"
<div style="height: 600px; width: 800px; color: #000000">
<L-Map
@ready="onMapReady"
ref="mapRef"
:zoom="zoom"
:center="center"
style="height: 500px; width: 100%"
@click="updateMarker"
>
<q-item-section avatar>
<q-avatar :icon="fav.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>{{ fav.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar>
<l-map @ready="onMapReady" ref="mapRef" :zoom="zoom" :center="center" style="height: 500px; width: 100%;" @click="updateMarker">
<l-tile-layer
<L-Tile-Layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
@click="updateMarker($event.latlng)"
></l-tile-layer>
<l-marker :lat-lng="markerLatLng" @click="handleMarkerClick"></l-marker>
</l-map>
<StatusBar />
></L-Tile-Layer>
<L-Marker :lat-lng="markerLatLng" @click="handleMarkerClick"></L-Marker>
<L-Routing-Machine
@routingstart="debugRoutingEvent"
@routesfound="debugRoutingEvent"
@routingerror="debugRoutingEvent"
:waypoints="waypoints"
/>
</L-Map>
</div>
</template>
<script setup lang="ts">
import { useQuasar } from "quasar";
import type { Ref} from "vue";
import { ref } from "vue";
import "leaflet/dist/leaflet.css";
import { LMap, LTileLayer, LMarker } from "@vue-leaflet/vue-leaflet";
import { useQuasar } from 'quasar';
import { ref } from 'vue';
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
import "leaflet-geosearch/dist/geosearch.css";
import "leaflet/dist/leaflet.css";
import type { Map, LeafletMouseEvent } from 'leaflet';
import SetLocationDialog from "components/SetLocationDialog.vue";
import StatusBar from "components/StatusBar.vue"
import { api } from "boot/axios";
import LRoutingMachine from 'components/LRoutingMachine.vue';
import 'leaflet-geosearch/dist/geosearch.css';
import 'leaflet/dist/leaflet.css';
import type { coords, SearchControlProps } from 'src/types';
import type { Map, LeafletMouseEvent } from 'leaflet';
import { storeToRefs } from 'pinia';
import { useSocketioStore } from 'stores/socketio';
import { useLeafletStore } from 'stores/leaflet';
import SetLocationDialog from 'components/SetLocationDialog.vue';
import { LMap, LMarker, LTileLayer } from '@vue-leaflet/vue-leaflet';
const waypoints = [
[ 38.7436056, -9.2304153 ],
[ 38.7436056, -0.131281 ],
];
const leafletStore = useLeafletStore();
const socketStore = useSocketioStore();
const { zoom, center, markerLatLng } = storeToRefs(socketStore);
const $q = useQuasar();
const mapRef = ref(null);
const zoom = ref(10);
const center: Ref<[number, number]> = ref([40.71278, -74.00594]);
const markerLatLng: Ref<[number, number]> = ref([40.71278, -74.00594]);
const responseMessage = ref("");
const responseMessage = ref('');
interface coords {
lat: number;
lng: number;
}
const debugRoutingEvent = (event) => {
console.log(`${event.type} event: `, event);
};
interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
}
const onMapReady = (map: Map) => {
const provider = new OpenStreetMapProvider();
const searchOptions: SearchControlProps = {
@@ -94,10 +74,10 @@ const onMapReady = (map: Map) => {
autoCompleteDelay: 250,
retainZoomLevel: false,
animateZoom: true,
keepResult: true
keepResult: true,
};
const searchControl = new GeoSearchControl(searchOptions);
const searchControl = GeoSearchControl(searchOptions);
map.addControl(searchControl);
};
@@ -111,87 +91,43 @@ function handleMarkerClick(event: LeafletMouseEvent) {
component: SetLocationDialog,
componentProps: {
lat: event.latlng.lat,
lng: event.latlng.lng
}
}).onOk(() => {
void setLocation({ lat: event.latlng.lat, lng: event.latlng.lng });
console.log("Dialog confirmed");
}).onCancel(() => {
console.log("Dialog cancelled");
}).onDismiss(() => {
console.log("Dialog dismissed");
lng: event.latlng.lng,
},
})
.onOk((delay: number) => {
void setLocation({ lat: event.latlng.lat, lng: event.latlng.lng }, delay);
console.log(
'Confirmed location add: latitude: ' +
event.latlng.lat +
', longitude: ' +
event.latlng.lng +
', delay: ' +
delay,
);
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
}
function handleFavClick(coords: coords) {
center.value = [coords.lat, coords.lng];
markerLatLng.value = [coords.lat, coords.lng];
}
async function setLocation(coords: coords) {
function setLocation(coords: coords, delay: number) {
try {
const payLoadData = {
latitude: coords.lat,
longitude: coords.lng
};
const { data } = await api({
method: "post",
url: "/set",
data: payLoadData
});
console.log("Location set successfully:", data.data);
responseMessage.value = `Location successfully set! New location: ${data.lat}, ${data.lng}`;
responseMessage.value = socketStore.simulationControl('add', delay, coords.lat, coords.lng);
$q.notify({ type: 'positive', message: responseMessage.value });
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Error setting location:", error.message);
console.error('Error setting location:', error.message);
responseMessage.value = `Failed to set location: ${error.message}`;
} else {
console.error("Error setting location:", error);
console.error('Error setting location:', error);
responseMessage.value = `Failed to set location: Unknown error`;
}
$q.notify({ type: 'negative', message: responseMessage.value });
}
}
const home = {
name: "Home",
coords: {
lat: 40.71278,
lng: -74.00594
},
icon: "home"
};
const favorites = [
{
id: 1,
name: "Central Park",
coords: {
lat: 40.785091,
lng: -73.968285
},
icon: "park"
},
{
id: 2,
name: "Times Square",
coords: {
lat: 40.758896,
lng: -73.985130,
},
icon: "star"
},
{
id: 3,
name: "Empire State Building",
coords: {
lat: 40.748817,
lng: -73.985428
},
icon: "building"
}
];
</script>
<style>

246
src/components/MenuBar.vue Normal file
View File

@@ -0,0 +1,246 @@
<script setup lang="ts">
import { useLeafletStore } from 'stores/leaflet';
import type { Control, coords, CtrlAttr, CtrlAttrs } from 'components/models';
import { storeToRefs } from 'pinia';
import { socket } from 'boot/socketio';
import ConfirmCommandDialog from 'components/ConfirmCommandDiaglog.vue';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import { useSocketioStore } from 'stores/socketio';
import { ref } from 'vue';
import { favorites } from 'constants/favorites';
import { controls } from 'constants/controls';
const route = useRoute();
const $q = useQuasar();
const leafletStore = useLeafletStore();
const socketStore = useSocketioStore();
const { center, markerLatLng } = storeToRefs(leafletStore);
const { simulationRunning, simulationState, simulationQueueLength } = storeToRefs(socketStore);
const menuOpen = ref();
function handleFavClick(coords: coords) {
center.value = [coords.lat, coords.lng];
markerLatLng.value = [coords.lat, coords.lng];
}
function handleControlClick(cmdAttr: CtrlAttr) {
if (cmdAttr.cnfrm) {
$q.dialog({
component: ConfirmCommandDialog,
componentProps: {
name: cmdAttr.name,
},
})
.onOk(() => {
if (cmdAttr.cmdClass === 'simulation_control') {
try {
const ack = socketStore.simulationControl(cmdAttr.cmd, cmdAttr.delay);
$q.notify({ type: 'positive', message: ack });
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Simulation Command ERROR: ', error.message);
const ack = `Simulation Command Error: ${error.message}`;
$q.notify({ type: 'negative', message: ack });
} else {
console.error('Simmulation Command Error: ', error);
const ack = 'Simulation Command Error: Unknow error';
$q.notify({ type: 'negative', message: ack });
}
}
}
if (ctrl.cmdClass === 'device_control') {
socket.emit('device_control', { command: ctrl.cmd, delay: 0 }, (response) => {
console.log(response.status, response.command);
});
}
})
.onCancel(() => {
console.log('Dialog cancelled');
})
.onDismiss(() => {
console.log('Dialog dismissed');
});
} else {
if (ctrl.cmdClass === 'simulation_control') {
try {
const response = socketStore.simulationControl(ctrl.cmd);
$q.notify({ type: 'positive', message: response });
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error: ' + error.message);
$q.notify({ type: 'negative', message: error.toString() });
} else {
console.error('Error setting location:', error);
$q.notify({ type: 'negative', message: error.toString() });
}
}
}
if (ctrl.cmdClass === 'device_control') {
socket.emit('device_control', { command: ctrl.cmd, delay: 0 }, (response) => {
console.log(response.status, response.command);
});
}
}
}
</script>
<template>
<q-toolbar class="bg-primary text-white">
<q-btn @click="$emit('drawer')" flat round dense icon="menu" class="q-mr-sm" />
<q-separator dark inset />
<q-space />
<q-btn :icon-right="menuOpen ? 'arrow_drop_up' : 'arrow_drop_down'" stretch flat label="Favorites" v-if="route.name === 'Leaflet'">
<q-menu @show="menuOpen = true" @hide="menuOpen = false" anchor="bottom end" self="top end">
<q-list>
<template v-for="fav, index) in favorites" :key="index">
<q-item
v-if="fav.coords"
clickable
v-ripple
v-close-popup
@click="handleFavClick(fav.coords)"
>
<q-item-section avatar>
<q-avatar :icon="fav.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>{{ fav.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-else clickable v-ripple>
<q-item-section avatar>
<q-avatar :icon="fav.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>{{ fav.name }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top end" self="top start">
<q-list>
<q-item
v-for="(f, indx) in fav.subitems"
:key="indx"
clickable
v-ripple
v-close-popup
@click="handleFavClick(f.coords)"
>
<q-item-section avatar>
<q-avatar :icon="f.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label>{{ f.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
</template>
</q-list>
</q-menu>
</q-btn>
<q-btn-dropdown stretch flat label="Controls">
<q-list>
<q-item-label header>Simulation Controls</q-item-label>
<q-item
v-if="!simulationRunning"
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.sim_start)">
<q-item-section avatar>
<q-avatar :icon="controls.sim_start.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.sim_start.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="simulationState === 'RUNNING' && simulationRunning"
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.sim_pause)"
>
<q-item-section avatar>
<q-avatar :icon="controls.sim_pause.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.sim_pause.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="simulationState === 'PAUSED'"
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.sim_resume)"
>
<q-item-section avatar>
<q-avatar :icon="controls.sim_resume.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.sim_resume.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="simulationQueueLength > 0"
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.sim_clear)"
>
<q-item-section avatar>
<q-avatar :icon="controls.sim_clear.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.sim_clear.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item
v-if="simulationRunning"
clickable
v-ripple
v-close-popup
@click="handleControlClick(controls.sim_end)"
>
<q-item-section avatar>
<q-avatar :icon="controls.sim_end.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.sim_end.name }} </q-item-label>
</q-item-section>
</q-item>
<q-separator spaced />
<q-item-label header>Device Controls</q-item-label>
<q-item clickable v-ripple v-close-popup @click="handleControlClick(controls.dev_reboot)">
<q-item-section avatar>
<q-avatar :icon="controls.dev_reboot.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.dev_reboot.name }} </q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-ripple v-close-popup @click="handleControlClick(controls.dev_shutdown)">
<q-item-section avatar>
<q-avatar :icon="controls.dev_shutdown.icon" color="primary" text-color="white" />
</q-item-section>
<q-item-section>
<q-item-label> {{ controls.dev_shutdown.name }} </q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-toolbar>
</template>
<style scoped></style>

View File

@@ -3,7 +3,13 @@
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="add_location" color="primary" text-color="white" />
<span class="q-ml-sm"> Are you sure you want to set location to {{ lat }}, {{ lng }} ?</span>
<span class="text-h6"> Add Simulated Location to Queue</span>
</q-card-section>
<q-card-section>
<span class="q-ml-sm">
Are you sure you want to set location to {{ latitude }}, {{ longitude }} ?
</span>
<q-input dense v-model="delay" autofocus @keyup.enter="onOkClick" label="Delay (seconds)" type="number" />
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="OK" color="primary" @click="onOkClick" />
@@ -15,21 +21,22 @@
<script setup lang="ts">
import { useDialogPluginComponent } from 'quasar';
import { ref } from 'vue';
const props = defineProps({
lat: { type: Number, required: true },
lng: { type: Number, required: true }
lng: { type: Number, required: true },
});
defineEmits([
...useDialogPluginComponent.emits
])
const delay = ref(0);
const latitude = props.lat;
const longitude = props.lng;
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef: dlgRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
function onOkClick () {
onDialogOK()
function onOkClick() {
onDialogOK();
}
</script>

View File

@@ -0,0 +1 @@
import { socket } from 'boot/socketio';

View File

@@ -1,23 +1,76 @@
<script setup lang="ts">
import { socket } from 'boot/socket';
import { useStatusStore } from 'stores/status';
import { computed } from 'vue';
import { socket } from 'boot/socketio';
import { useQuasar } from 'quasar';
import { computed, ref, watch } from 'vue';
import { useSocketioStore } from 'stores/socketio';
import { storeToRefs } from 'pinia';
const statusStore = useStatusStore();
const socketioStore = useSocketioStore();
const $q = useQuasar();
const msgInput = ref('');
const sockEvent = ref('');
const eventArgs = ref('');
const socketStatus = computed(() => statusStore.socketConnected ? 'green' : 'red');
const { sockConnected, messageList } = storeToRefs(socketioStore);
socket.off();
statusStore.socketConnect();
statusStore.bindEvents();
const sockStatColor = computed(() => {
return sockConnected.value ? 'green' : 'red';
});
function sendMessage() {
const msgToSend = msgInput.value;
console.log('Sending message... ' + msgToSend);
socket.emit('message', msgToSend, (e, r) => {
console.log('Message: ' + msgToSend + ' delivered: ' + e + ', response: ' + r);
});
msgInput.value = '';
}
function handleEmit() {
const event = sockEvent.value;
const jsonArgs = eventArgs.value;
socket.emit(event, jsonArgs, (resp) => {
console.log('Server Reponse: ' + resp);
});
sockEvent.value = '';
eventArgs.value = '';
};
watch(
messageList,
(newVal: string[], oldVal: string[]) => {
let newMsg: string;
newMsg = newVal[newVal.length - 1];
console.log('New message received: ', newMsg);
console.log('Past List', oldVal);
$q.notify(newMsg);
},
{ deep: true },
);
</script>
<template>
<div class="flex flex-center col q-ma-lg q-gutter-md">
<q-btn round icon="webhook">
<q-badge floating :color="socketStatus" rounded />
</q-btn>
<q-btn label="Connect" @click="statusStore.socketConnect" />
<q-btn label="Disconnect" @click="statusStore.socketDisconnect" />
<q-btn label="Emit Hello" @click="socket.emit('message', 'Hello from Vue!')" />
<q-btn icom="webhook"><q-badge floating :color="sockStatColor" rounded></q-badge></q-btn>
<q-btn label="Connect" @click="socketioStore.connect()" />
<q-btn label="Disconnect" @click="socketioStore.disconnect()" />
<q-input v-model="msgInput" filled label="Message" />
<div>
<q-btn label="Send" @click="sendMessage" />
</div>
<q-list bordered seperator v-if="socketioStore.messageList.length > 0">
<q-item v-for="(msg, index) in socketioStore.messageList" :key="index">
<q-item-section>{{ msg }}</q-item-section>
</q-item>
</q-list>
</div>
<div class="flex flex-center col q-ma-lg q-gutter-md">
<div class="text-h3">SocketIO Functions</div>
<q-input v-model="sockEvent" filled label="Event" />
<q-input v-model="eventArgs" filled label="Args" />
<div>
<q-btn label="Emit" @click="handleEmit" />
</div>
</div>
</template>

View File

@@ -1,38 +1,56 @@
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { useSocketioStore } from 'stores/socketio';
import { storeToRefs } from 'pinia';
import { ref, onMounted } from 'vue';
import { api } from 'boot/axios';
const $q = useQuasar();
const statusDev = ref({ color: 'red', deviceName: 'No connected Device' });
async function getStatus() {
try {
const { data } = await api({
method: 'get',
url: '/status',
});
console.log('API Call: usbmux/status returned:', data);
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error setting location:', error.message);
$q.notify({ type: 'negative', message: error.message });
const socketioStore = useSocketioStore();
const {
sockConnected,
deviceConnected,
tunnelConnected,
simulationRunning,
currentLocation,
nextLocation,
} = storeToRefs(socketioStore);
function statusDevColor(state: string | boolean): string {
if (typeof state === 'boolean') {
return state ? 'green' : 'red';
} else {
console.error('Error setting location:', error);
switch (state) {
case 'paused':
return 'yellow';
case 'running':
return 'green';
case 'stopped':
return 'red';
default:
return 'grey';
}
}
}
onMounted(async () => {
await getStatus();
});
</script>
<template>
<q-toolbar class="bg-primary text-white q-my-md shadow-2">
<q-badge :color="statusDev.color" rounded class="q-mr-sm" />{{ statusDev.deviceName }}
<q-toolbar class="bg-primary text-white">
<div class="flex col q-gutter-md align-center justify-start content-center">
<q-space />
<span>Status:</span>
<span>
<q-badge :color="statusDevColor(sockConnected)" rounded class="q-mr-sm" />
<q-btn label="WebSocket" @click="socketioStore.toggleSock()" />
</span>
<span>
<q-badge :color="statusDevColor(deviceConnected)" rounded class="q-mr-sm" />
Device Connection
</span>
<span>
<q-badge :color="statusDevColor(tunnelConnected)" rounded class="q-mr-sm" />
tunneld
</span>
<span>
<q-badge :color="statusDevColor(simulationRunning)" rounded class="q-mr-sm" />
Location Simulation
</span>
</div>
</q-toolbar>
</template>

View File

@@ -1,8 +1,201 @@
export interface Todo {
id: number;
content: string;
export type SimulationCommands = "start" | "pause" | "resume" | "clear" | "end" | "add";
export type DeviceCommands= "start_tunnel" | "stop_tunnel" | "shutdown";
export interface CtrlAttrs {
[key: string]: CtrlAttr;
}
export interface CtrlAttr {
name: string;
cmd: string;
cmdClass: string;
icon: string;
cnfrm: boolean;
delay: number;
}
export interface LocationQueue {
[key: string]: LocationMark
}
interface LocationMark {
loc_id: string;
latitude: number | undefined | null;
longitude: number | undefined | null;
delay?: number | undefined | null;
start_time: string | undefined | null;
end_time?: string | undefined | null ;
}
export interface SimulationControlResponse {
status: string;
command: SimulationCommands;
loc_id: string;
message?: string | undefined;
latitude?: number | undefined | null;
longitude?: number | undefined | null;
delay?: number | undefined | null;
start_time?: string | undefined | null;
end_time?: string | undefined | null;
}
interface DeviceControlResponse {
status: string;
command: DeviceCommands;
delay?: number;
}
interface StatusUpdate {
simulation_active: boolean;
set_location_enabled: boolean;
queue: number,
latitude: number | undefined | null;
longitude: number | undefined | null;
next_move?: number | undefined | null;
queue_list: LocationQueue[]
queue_state: string | undefined | null;
queue_status: boolean;
simulation_task: string | undefined | null;
test_mode: boolean
tunnel: string | undefined | null;
device_count?: number | undefined | null;
udid?: string | null;
device_name?: string | null | undefined;
product_version?: string | null | undefined;
phone_number?: string | null | undefined;
developer_mode_enabled?: boolean | undefined | null;
ddi_mounted?: boolean ;
rsd_address?: string | null | undefined;
rsd_port?: number | undefined | null;
lockdown_trusted_port?: number | undefined | null;
lockdown_untrusted_port?: number | undefined | null;
lockdown_trusted_reachable?: boolean | undefined | null;
lockdown_untrusted_reachable?: boolean | undefined | null;
dtservicehub_reachable?: boolean | undefined | null
}
export interface ServerToClientEvents {
noArg: () => void;
withAck: (a: string, callback: (b: number) => void) => void;
simulationStatus: (c: SimulationStatus) => void;
status: (d: StatusUpdate) => void;
device_status: (d: DeviceStatus) => void;
error: (data: ErrorFull) => void;
message: (e: string) => void;
}
export interface ClientToServerEvents {
message: (e: string, callback: (b: boolean, r: string) => void) => void;
simulation_control: (
args: {
command: SimulationCommands,
latitude?: number | null | undefined,
longitude?: number | null | undefined,
delay?: number | undefined
},
callback: (response: SimulationControlResponse) => void
) => void;
device_control: (
args: {
command: DeviceCommands,
delay?: number
},
callback?: (response: DeviceControlResponse) => void
) => void;
}
interface SimulationStatus {
status: boolean;
data: {
latitude: number;
longitude: number;
start: string;
end?: string;
next_move?: number;
};
}
export interface Meta {
totalCount: number;
}
interface DeviceStatus {
device_connected: boolean;
device_count: number;
udid?: string | null;
device_name?: string | null;
product_version?: string | null;
phone_number?: string | null;
developer_mode_enabled?: boolean;
ddi_mounted?: boolean;
rsd_address?: string | null;
rsd_port?: number;
lockdown_trusted_port?: number;
lockdown_untrusted_port?: number;
lockdown_trusted_reachable?: boolean;
lockdown_untrusted_reachable?: boolean;
dtservicehub_reachable?: boolean;
}
export type Control = DeviceControl | SimulationControl;
export interface DeviceControl {
id: number;
name: string;
cmd: DeviceCommands
cmdClass: "device_control"
icon: string;
confirm: boolean;
}
export interface SimulationControl {
id: number;
name: string;
cmd: SimulationCommands;
cmdClass: 'simulation_control';
icon: string;
confirm: boolean;
}
export interface coords {
lat: number;
lng: number;
}
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
export interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
}
export interface CurrentLocation {
loc_id: string;
latitude: number;
longitude: number;
next_move?: number | null
}
export interface NextLocation {
loc_id: string;
latitude: number;
longitude: number;
time_at_location?: number | null;
}
export interface ErrorFull {
type: string;
error: string;
}

62
src/constants/controls.ts Normal file
View File

@@ -0,0 +1,62 @@
import type { CtrlAttrs } from 'components/models';
export const controls: CtrlAttrs = {
sim_start: {
name: 'Start Location Sim',
cmd: 'start',
cmdClass: 'simulation_control',
icon: 'play_arrow',
cnfrm: false,
delay: 0,
},
sim_pause: {
name: 'Pause Location Sim',
cmd: 'pause',
cmdClass: 'simulation_control',
icon: 'pause',
cnfrm: false,
delay: 0,
},
sim_resume: {
name: 'Resume Location Simulation',
cmd: 'resume',
cmdClass: 'simulation_control',
icon: 'play_arrow',
cnfrm: false,
delay: 0,
},
sim_clear: {
name: 'Clear Location Queue',
cmd: 'clear',
cmdClass: 'simulation_control',
icon: 'directions_off',
cnfrm: false,
delay: 0,
},
sim_end: {
name: 'End Location Sim',
cmd: 'end',
cmdClass: 'simulation_control',
icon: 'stop',
cnfrm: true,
delay: 0,
},
dev_shutdown: {
name: 'Shutdown',
cmd: 'shutdown',
cmdClass: 'device_control',
icon: 'power_settings_new',
cnfrm: true,
delay: 5,
},
dev_reboot: {
name: 'Reboot',
cmd: 'reboot',
cmdClass: 'device_control',
icon: 'restart_alt',
cnfrm: true,
delay: 5,
},
};

View File

@@ -0,0 +1,79 @@
export const favorites = [
{
name: 'Home',
icon: 'home',
coords: {
lat: 40.910773020811,
lng: -73.891069806448,
},
},
{
name: "Work Places",
icon: "work",
subitems: [
{
name: 'Jeong',
icon: 'language_korean_latin',
coords: {
lat: 40.76624975651346,
lng: -73.81444335286128,
},
address: '35-02 150th Pl, Flushing, NY 11354',
},
{
name: 'Santos',
icon: 'rice_bowl',
coords: {
lat: 40.74504671877868,
lng: -73.8880099638491,
},
address: '77-08 Broadway, Elmhurst, NY 11373'
},
{
name: 'Natalyaa (Qns)',
icon: 'currency_ruble',
coords: {
lat: 40.69644966409178,
lng: -73.837453217826,
},
address: '110-14 Jamaica Ave, Richmond Hill, NY 11418',
},
{
name: 'Natalyaa (Bronx)',
icon: 'currency_ruble',
coords: {
lat: 40.85384419116598,
lng: -73.86314767911834,
},
address: '2109 Matthews Ave, Bronx, NY 10462',
},
{
name: 'Linwood Plaza',
icon: 'dermatology',
coords: {
lat: 40.86141832913106,
lng: -73.96997583196286,
},
address: '158 Linwood Plaza, Fort Lee, NJ 07024',
},
],
},
{
name: 'Man Mini Storage',
icon: 'box',
coords: {
lat: 40.75158955085288,
lng: -73.9328988710467,
},
address: '31-08 Northern Blvd, Long Island City, NY 11101',
},
{
name: 'Acmd',
icon: 'grocery',
coords: {
lat: 40.90930366920829,
lng: -73.87658695470259,
},
address: '31-08 Northern Blvd, Long Island City, NY 11101',
},
];

View File

@@ -12,12 +12,12 @@
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #1976d2;
$secondary: #26a69a;
$primary: #02006c;
$secondary: #010057;
$accent: #9c27b0;
$dark: #1d1d1d;
$dark-page: #121212;
$dark-page: #03002e;
$positive: #21ba45;
$negative: #c10015;

View File

@@ -0,0 +1,89 @@
import { Utilities } from "@vue-leaflet/vue-leaflet";
import type * as L from "leaflet";
import type { IRouter, IGeocoder, LineOptions } from "leaflet-routing-machine";
// Props typing
export interface RoutingControlProps {
waypoints: L.LatLng[];
router?: IRouter;
plan?: any; // L.Routing.Plan (can refine if you typed it)
fitSelectedRoutes?: string | boolean;
lineOptions?: LineOptions;
routeLine?: (...args: any[]) => any;
autoRoute?: boolean;
routeWhileDragging?: boolean;
routeDragInterval?: number;
waypointMode?: string;
useZoomParameter?: boolean;
showAlternatives?: boolean;
altLineOptions?: LineOptions;
}
// Vue-style prop definitions (still needed for runtime)
export const routingControlProps = {
waypoints: {
type: Array as () => L.LatLng[],
default: () => [],
},
router: {
type: Object as () => IRouter,
default: undefined,
},
plan: {
type: Object as () => any,
default: undefined,
},
fitSelectedRoutes: {
type: [String, Boolean] as () => string | boolean,
default: "smart",
},
lineOptions: {
type: Object as () => LineOptions,
default: undefined,
},
routeLine: {
type: Function as () => (...args: any[]) => any,
default: undefined,
},
autoRoute: {
type: Boolean,
default: true,
},
routeWhileDragging: {
type: Boolean,
default: false,
},
routeDragInterval: {
type: Number,
default: 500,
},
waypointMode: {
type: String,
default: "connect",
},
useZoomParameter: {
type: Boolean,
default: false,
},
showAlternatives: {
type: Boolean,
default: false,
},
altLineOptions: {
type: Object as () => LineOptions,
default: undefined,
},
};
// Setup function
export const setupRoutingControl = (props: RoutingControlProps) => {
const options = Utilities.propsToLeafletOptions(
props,
routingControlProps
);
return {
options,
methods: {} as Record<string, never>,
};
};

View File

@@ -1,15 +1,39 @@
<template>
<q-layout view="lHh lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn flat dense round icon="menu" aria-label="Menu"/>
<q-toolbar-title> Quasar App </q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
<q-layout view="hhh lpr fff">
<q-header>
<MenuBar @drawer="drawer = !drawer" />
</q-header>
<q-footer>
<StatusBar />
</q-footer>
<q-drawer
v-model="drawer"
:width="200"
:breakpoint="500"
overlay
:class="$q.dark.isActive ? 'bg-grey-8' : 'bg-grey-3'"
>
<q-scroll-area class="fit">
<q-list>
<template v-for="(menuItem, index) in menuList" :key="index">
<q-item
clickable
:active="menuItem.route === route.name"
v-ripple
@click="$router.push({ name: menuItem.route })"
>
<q-item-section avatar>
<q-icon :name="menuItem.icon" />
</q-item-section>
<q-item-section>
{{ menuItem.label }}
</q-item-section>
</q-item>
<q-separator :key="'sep' + index" v-if="menuItem.separator" />
</template>
</q-list>
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
@@ -17,4 +41,77 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useSocketioStore } from 'stores/socketio';
import MenuBar from 'components/MenuBar.vue';
import StatusBar from 'components/StatusBar.vue';
const socketioStore = useSocketioStore();
const drawer = ref(false);
const route = useRoute();
const menuList = [
{
icon: 'map',
label: 'Map',
separator: false,
route: 'Leaflet',
},
{
icon: 'info',
label: 'Device Info',
separator: false,
route: 'DeviceInfo',
},
{
icon: 'inbox',
label: 'Inbox',
separator: true,
route: 'ErrorNotFound',
},
{
icon: 'send',
label: 'Outbox',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'delete',
label: 'Trash',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'error',
label: 'Spam',
separator: true,
route: 'ErrorNotFound',
},
{
icon: 'settings',
label: 'Settings',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'feedback',
label: 'Send Feedback',
separator: false,
route: 'ErrorNotFound',
},
{
icon: 'help',
iconColor: 'primary',
label: 'Help',
separator: false,
route: 'ErrorNotFound',
},
];
onMounted(() => {
socketioStore.bindEvents();
socketioStore.connect();
});
</script>

59
src/pages/DeviceInfo.vue Normal file
View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { api, axios } from 'boot/axios';
import { ref } from 'vue';
const deviceInfo = ref();
async function fetchDeviceInfo(): Promise<void> {
try {
const res = await api.get('/device_info');
console.log('DeviceInfo fetched successfully:', res.data);
deviceInfo.value = res.data;
} catch (error) {
console.error('Error fetching device info:', error);
if (axios.isCancel(error)) {
console.log('Request canceled', error.message);
}
}
}
await fetchDeviceInfo();
</script>
<template>
<q-page class="row items-center justify-evenly">
<div style="width: 70vw; white-space: normal">
<h3>Device Info:</h3>
<ul>
<li v-for="(value, key) in deviceInfo" :key="key">
<strong>{{ key }}:</strong>
<span v-if="typeof value === 'object' && value !== null">
<ul>
<li v-for="(subValue, subKey) in value" :key="subKey">
<strong>{{ subKey }}:</strong>
<span v-if="typeof subValue === 'object' && subValue !== null">
<ul>
<li v-for="(subSubValue, subSubKey) in subValue" :key="subSubKey">
<strong>{{ subSubKey }}:</strong>
<span v-if="typeof subSubValue === 'object' && subSubValue !== null">
<ul>
<li v-for="(subSubSubValue, subSubSubKey) in subSubValue" :key="subSubSubKey">
<strong>{{ subSubSubKey }}:</strong> {{ subSubSubValue }}
</li>
</ul>
</span>
<span v-else>{{ subSubValue }}</span>
</li>
</ul>
</span>
<span v-else>{{ subValue }}</span>
</li>
</ul>
</span>
<span v-else>{{ value }}</span>
</li>
</ul>
</div>
</q-page>
</template>
<style scoped></style>

View File

@@ -5,8 +5,28 @@ const routes: RouteRecordRaw[] = [
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/IndexPage.vue') },
{ path: 'test', component: () => import('pages/TestPage.vue') },
{
path: '',
name: 'home',
redirect: {
name: 'Leaflet',
},
},
{
path: 'leaflet',
name: 'Leaflet',
component: () => import('pages/IndexPage.vue'),
},
{
path: 'test',
name: 'Test',
component: () => import('pages/TestPage.vue')
},
{
path: 'device-info',
name: 'DeviceInfo',
component: () => import('pages/DeviceInfo.vue'),
},
],
},
@@ -14,6 +34,7 @@ const routes: RouteRecordRaw[] = [
// but you can also remove it
{
path: '/:catchAll(.*)*',
name: 'ErrorNotFound',
component: () => import('pages/ErrorNotFound.vue'),
},
];

21
src/stores/leaflet.ts Normal file
View File

@@ -0,0 +1,21 @@
import { defineStore, acceptHMRUpdate } from 'pinia';
interface State {
zoom: number
center: [number, number]
markerLatLng: [number, number]
}
export const useLeafletStore = defineStore('leaflet', {
state: (): State => {
return {
zoom: 10,
center: [40.71278, -74.00594],
markerLatLng: [40.71278, -74.00594],
}
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useLeafletStore, import.meta.hot));
}

233
src/stores/socketio.ts Normal file
View File

@@ -0,0 +1,233 @@
import { defineStore, acceptHMRUpdate } from 'pinia';
import { socket } from 'boot/socketio';
import type {
CurrentLocation,
NextLocation,
ErrorFull,
SimulationControl,
SimulationCommands,
LocationQueue,
SimulationControlResponse,
StatusUpdate
} from 'components/models';
export const useSocketioStore = defineStore('socketio', {
state: () => {
return {
sockConnected: false as boolean,
socketID: null as string | null | undefined,
deviceConnected: false as boolean,
tunnelConnected: false as boolean,
simulationRunning: false as boolean | string,
simulationState: null as string | null | undefined,
simulationQueneLength: 0 as number,
currentLocation: null as CurrentLocation | null,
nextLocation: null as NextLocation | null,
messageList: [''] as string[],
errorList: [] as ErrorFull[],
locationQueue: [] as LocationQueue[],
leafLetZoom: 10 as number,
};
},
getters: {
sockState: (state) => state.sockConnected,
deviceState: (state) => state.deviceConnected,
markerLatLng: (state) => {
return [state.currentLocation.latitude, state.currentLocation.longitude]
},
center(): [number, number] {
return this.leafletCurrentMarker
},
zoom: (state) => state.leafletZoom,
},
actions: {
setSockStatus() {
this.sockConnected = socket.connected;
this.socketID = socket.id;
},
bindEvents() {
this.setSockStatus();
socket.on('connect', () => {
this.setSockStatus();
socket.emit('message', 'Hello from client', (e: boolean) => {
console.log('Message delivered: ' + e);
});
console.log('Connected to server');
});
socket.on('disconnect', () => {
this.setSockStatus();
console.log('Disconnected from server');
});
socket.on('message', (e: string) => {
this.setSockStatus();
this.messageList.push(e);
console.log('Websock message received!');
});
socket.on('error', (data: ErrorFull) => {
this.setSockStatus();
const errorFull = { type: data.type, error: data.error };
this.errorList.push(errorFull);
});
socket.on('status', (data: StatusUpdate): void => {
console.log("StatusUpdate received: ", data);
this.simulationRunning = data.simulation_active;
this.simulationState = data.queue_state;
this.simulationQueneLength = data.quene_length;
this.tunnelConnected = !!data.tunnel;
this.currentLocation = { loc_id: data.loc_id, latitude: data.latitude, longitude: data.longitude, next_move: data.next_mode };
});
socket.onAny((eventName, ...args) => {
this.setSockStatus();
console.log(`Received event: ${eventName}`, args);
});
},
connect() {
console.log('Connecting to server...');
socket.connect();
this.setSockStatus();
},
disconnect() {
socket.disconnect();
this.setSockStatus();
},
toggleSock() {
this.setSockStatus();
if (this.sockConnected) {
socket.disconnect();
} else {
socket.connect();
}
this.setSockStatus();
},
setSimulationRunning(isRunning: boolean, states: string ): void {
this.setSockStatus();
this.simulationState = states;
this.simulationRunning = isRunning;
},
simulationControl(command: SimulationCommands, delay?: number, latitude?: number | null, longitude?: number | null): string | never {
this.setSockStatus();
switch (command) {
case 'start':
console.log("socketStore: got command: start");
if (this.simulationRunning || this.simulationState == "RUNNING" || this.simulationState == "PAUSED") {
throw new Error('Simulation is already running' + this.simulationState);
}
console.log("Emmitting simulation_control: start")
socket.emit('simulation_control', { command: 'start', delay: 0, latitude: null, longitude: null}, (response: SimulationControlResponse) => {
if (response.status == "error") {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
return response.message;
}
});
break;
case 'pause':
if (this.simulationState !== "RUNNING" ) {
throw new Error('Simulation is not running');
}
socket.emit('simulation_control', { command: 'pause' }, (response: SimulationControlResponse) => {
if (response.status === "error") {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
return response.message;
}
});
break;
case 'resume':
if (this.simulationState !== "PAUSED") {
throw new Error('Simulation is not paused');
}
socket.emit('simulation_control', { command: 'resume' }, (response) => {
if (response.status == "error") {
throw new Error(response.message)
} else {
this.simulationState = response.status;
console.log(response.message)
return response.message
}
});
break;
case 'clear':
if (this.simulationQueueLength== 0) {
throw new Error('Simulation queue is empty');
}
if (this.simulationState == "STOPPED " || !this.simulationRunning) {
throw new Error('Simulation is not running');
}
socket.emit('simulation_control', { command: 'clear' }, (response) => {
if (response.status == 'error') {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
return response.message;
}
});
break;
case 'end':
if (this.simulationState == "ENDED" || !this.simulationRunning) {
throw new Error('Simulation is already ended');
}
socket.emit('simulation_control', { command: 'end' }, (response) => {
if (response.status == 'error') {
throw new Error(response.message);
} else {
this.simulationState = response.status;
console.log(response.message);
return response.message;
}
});
break;
case 'add':
if (this.simulationState == "ENDED" || !this.simulationRunning) {
throw new Error('Simulation is not running');
}
if (!latitude || !longitude) {
throw new Error ("latitude or longitude not set");
}
socket.emit('simulation_control',{ command: 'add', latitude: latitude, longitude: longitude, delay: delay }, (response) => {
if (response.status == "error") {
throw new Error(response.message)
} else {
this.simulationState = response.status;
console.log("response from simulate_control_add: ", response);
const locMrk = {
[response.loc_id]: {
loc_id: response.loc_id,
latitude: response.latitude,
longitude: response.longitude,
delay: response.delay,
start_time: response.start_time,
},
};
this.locationQueue.push(locMrk);
return response.message;
}
});
break;
default:
throw new Error('Invalid command');
}
},
setDeviceState(state: boolean) {
this.deviceConnected = state;
},
},
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useSocketioStore, import.meta.hot));
}

View File

@@ -1,34 +0,0 @@
import { acceptHMRUpdate, defineStore } from 'pinia';
import { socket } from 'boot/socket';
export const useStatusStore = defineStore('status', {
state: () => ({
device: {},
socketConnected: false,
}),
actions: {
bindEvents() {
socket.on('connect', () => {
this.socketConnected = true;
});
socket.on('disconnect', () => {
this.socketConnected = false;
});
socket.on('status_update', (data) => {
this.$patch((state) => {
Object.assign(state.device, data);
});
});
},
socketConnect() {
socket.connect();
},
socketDisconnect() {
socket.disconnect();
},
},
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useStatusStore, import.meta.hot));
}

View File

@@ -1,3 +1,6 @@
import type { OpenStreetMapProvider } from 'leaflet-geosearch';
export interface DeviceShort {
udid: string | null;
connection_type: string | null;
@@ -21,3 +24,23 @@ export interface StatusUpdate {
lockdown_untrusted_reachable?: boolean;
dtservicehub_reachable?: boolean;
}
export interface coords {
lat: number;
lng: number;
}
export interface SearchControlProps {
provider: OpenStreetMapProvider;
showMarker: boolean;
autoClose: boolean;
updateMap: boolean;
showPopup: boolean;
style: 'button' | 'bar';
acceptAutoLoad: boolean;
autoComplete: boolean;
autoCompleteDelay: number;
retainZoomLevel: boolean;
animateZoom: boolean;
keepResult: boolean;
}

15
src/types/leaflet-routing-machine.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
declare module "leaflet-routing-machine" {
import * as L from "leaflet";
export interface IRouter {}
export interface IGeocoder {}
export interface LineOptions extends L.PolylineOptions {}
export namespace Routing {
function control(options: any): any;
class Plan {}
}
const Routing: typeof Routing;
export default Routing;
}