Update to Vue3

Also switch from parcel to vite, from vuelayers to vue3-openlayers,
and update openlayers to v6
This commit is contained in:
Adam Goldsmith 2022-03-04 13:17:53 -05:00
parent a7a5daeacf
commit 55bf9a43b3
9 changed files with 1567 additions and 5681 deletions

View File

@ -4,27 +4,23 @@
"description": "", "description": "",
"private": true, "private": true,
"dependencies": { "dependencies": {
"aprs-parser": "^1.0.4", "aprs-parser": "github:ad1217/npm-aprs-parser#no-dynamic-require",
"distinct-colors": "^1.0.4", "distinct-colors": "^1.0.4",
"ol": "^5.3.3", "ol": "^6.13.0",
"vue": "^2.6.11", "vue": "^3.2.31",
"vue-hot-reload-api": "^2.3.4", "vue3-openlayers": "^0.1.63",
"vuelayers": "^0.11.23",
"ws": "^5.2.2" "ws": "^5.2.2"
}, },
"devDependencies": { "devDependencies": {
"@vue/component-compiler-utils": "^3.1.2", "@modyfi/vite-plugin-yaml": "^1.0.1",
"parcel": "^1.12.4", "@rollup/plugin-yaml": "^3.1.0",
"vue-template-compiler": "^2.6.11" "@vitejs/plugin-vue": "^2.2.4",
"vite": "^2.8.6"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"monkeyPatch": "sed -i '8s| APRSIS| //APRSIS|' node_modules/aprs-parser/lib/index.js",
"serve": "node src/server.js", "serve": "node src/server.js",
"prestart": "npm run monkeyPatch", "start": "vite",
"start": "parcel src/index.html", "build": "vite build"
"prebuild": "npm run monkeyPatch",
"build": "parcel build --public-url ./ src/index.html"
}, },
"author": "Adam Goldsmith <contact@adamgoldsmith.name>", "author": "Adam Goldsmith <contact@adamgoldsmith.name>",
"license": "ISC" "license": "ISC"

6298
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,110 +1,106 @@
<template> <template>
<vl-map data-projection="EPSG:4326"> <ol-map
<vl-view :zoom="10" :center="[-72.15, 43.9]"> :loadTilesWhileAnimating="true"
<vl-layer-tile> :loadTilesWhileInteracting="true"
<vl-source-osm> </vl-source-osm> class="map"
</vl-layer-tile> >
<vl-layer-group> <ol-view :zoom="10" :center="[-72.15, 43.9]" projection="EPSG:4326" />
<vl-layer-vector v-for="(gpxURL, name) in routes" :key="name">
<vl-source-vector :url="gpxURL" :format-factory="gpxFormatFactory"> <ol-tile-layer>
</vl-source-vector> <ol-source-osm />
<vl-style-box> </ol-tile-layer>
<vl-style-stroke color="hsl(200, 90%, 30%)" :width="5">
</vl-style-stroke> <ol-vector-layer v-for="gpxURL in routes" :key="gpxURL">
</vl-style-box> <ol-source-vector :url="gpxURL" :format="new GPX()"> </ol-source-vector>
</vl-layer-vector> <ol-style>
</vl-layer-group> <ol-style-stroke color="hsl(200, 90%, 30%)" :width="5">
</ol-style-stroke>
</ol-style>
</ol-vector-layer>
<!-- Station Paths --> <!-- Station Paths -->
<vl-layer-group> <div>
<vl-layer-group <div v-for="(packets, callsign, idx) in stationPaths" :key="callsign">
v-for="(packets, callsign, idx) in stationPaths"
:key="callsign"
>
<!--Paths --> <!--Paths -->
<vl-layer-vector render-mode="image"> <ol-vector-layer render-mode="image">
<vl-source-vector> <ol-source-vector>
<vl-feature> <ol-feature>
<vl-geom-line-string <ol-geom-line-string
:coordinates="packetsToStationPathPoints(packets)" :coordinates="packetsToStationPathPoints(packets)"
> >
</vl-geom-line-string> </ol-geom-line-string>
</vl-feature> </ol-feature>
</vl-source-vector> </ol-source-vector>
<vl-style-box> <ol-style>
<vl-style-stroke :color="stationColors[idx].hex()" :width="2"> <ol-style-stroke :color="stationColors[idx].hex()" :width="2">
</vl-style-stroke> </ol-style-stroke>
</vl-style-box> </ol-style>
</vl-layer-vector> </ol-vector-layer>
<!-- Points --> <!-- Points -->
<vl-layer-vector render-mode="image"> <ol-vector-layer render-mode="image">
<vl-source-vector> <ol-source-vector>
<vl-feature> <ol-feature>
<vl-geom-multi-point <ol-geom-multi-point
:coordinates="packetsToStationPathPoints(packets)" :coordinates="packetsToStationPathPoints(packets)"
> >
</vl-geom-multi-point> </ol-geom-multi-point>
</vl-feature> </ol-feature>
</vl-source-vector> </ol-source-vector>
<vl-style-box> <ol-style>
<vl-style-circle :radius="3"> <ol-style-circle :radius="3">
<vl-style-fill :color="stationColors[idx].hex()"> <ol-style-fill :color="stationColors[idx].hex()"> </ol-style-fill>
</vl-style-fill> </ol-style-circle>
</vl-style-circle> </ol-style>
</vl-style-box> </ol-vector-layer>
</vl-layer-vector> </div>
</vl-layer-group> </div>
</vl-layer-group>
<!-- Digipeater locations --> <!-- Digipeater locations -->
<vl-layer-vector> <ol-vector-layer>
<vl-source-vector> <ol-source-vector>
<vl-feature v-for="(position, callsign) in digiPos" :key="callsign"> <ol-feature v-for="(position, callsign) in digiPos" :key="callsign">
<vl-geom-point :coordinates="position"> </vl-geom-point> <ol-geom-point :coordinates="position"> </ol-geom-point>
<vl-style-box> <ol-style>
<vl-style-circle> <ol-style-circle>
<vl-style-fill :color="digiColors[callsign].hex()"> <ol-style-fill :color="digiColors[callsign].hex()">
</vl-style-fill </ol-style-fill>
></vl-style-circle> </ol-style-circle>
<vl-style-text :text="callsign" :offsetY="12"> </vl-style-text> <ol-style-text :text="callsign" :offsetY="12"> </ol-style-text>
</vl-style-box> </ol-style>
</vl-feature> </ol-feature>
</vl-source-vector> </ol-source-vector>
</vl-layer-vector> </ol-vector-layer>
<!-- Packet Paths --> <!-- Packet Paths -->
<vl-layer-vector render-mode="image"> <ol-vector-layer>
<vl-source-vector :features="packetPathsGeoJSON"> </vl-source-vector> <ol-source-vector :features="packetPaths"> </ol-source-vector>
<vl-style-func :factory="() => packetPathStyleFunc"> </vl-style-func> <!-- TODO: fix style -->
</vl-layer-vector> <!-- <ol-style :overrideStyleFunction="packetPathStyleFunc"> </ol-style> -->
</vl-view> </ol-vector-layer>
</vl-map> </ol-map>
</template> </template>
<script> <script setup>
import Vue from 'vue'; import { computed, ref } from 'vue';
import { APRSParser } from 'aprs-parser'; import APRSParser from 'aprs-parser/lib/APRSParser';
import distinctColors from 'distinct-colors'; import distinctColors from 'distinct-colors';
import VueLayers from 'vuelayers';
import { createStyle, createLineGeom } from 'vuelayers/lib/ol-ext';
import { Control } from 'ol/control';
import { GPX } from 'ol/format'; import { GPX } from 'ol/format';
import Feature from 'ol/Feature';
import MultiLineString from 'ol/geom/MultiLineString';
import LineString from 'ol/geom/LineString';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import 'vuelayers/lib/style.css'; import packetLog from '/../IS_packets.txt?raw';
const routes = Object.values(import.meta.globEager('./gpx/*.gpx')).map(
(gpx) => gpx.default
);
Vue.use(VueLayers); const parser = new APRSParser();
const packets = packetLog
import route_data from 'gpx/*.gpx';
import { readFileSync } from 'fs';
const packetLog = readFileSync(__dirname + '/../IS_packets.txt', 'utf-8');
function parsePackets(packetLog) {
let parser = new APRSParser();
return (
packetLog
.trim() .trim()
.split('\n') .split('\n')
// parse to Date and APRS packet // parse to Date and APRS packet
@ -112,55 +108,37 @@ function parsePackets(packetLog) {
let packet = parser.parse(line.slice(29)); let packet = parser.parse(line.slice(29));
packet.date = new Date(line.slice(0, 18)); packet.date = new Date(line.slice(0, 18));
return packet; return packet;
}) });
);
function packetsToStationPathPoints(packets) {
return packets.map((packet) => [packet.data.longitude, packet.data.latitude]);
} }
export default { function pathToString(path) {
data() {
return {
packets: parsePackets(packetLog),
routes: route_data,
};
},
methods: {
gpxFormatFactory(options) {
return new GPX(options);
},
packetsToStationPathPoints(packets) {
return packets.map((packet) => [
packet.data.longitude,
packet.data.latitude,
]);
},
pathToString(path) {
return path return path
.filter( .filter(
(station) => !station.call.match(/WIDE[12]|qA?|UV[123]|.*\*$|UNCAN/) (station) => !station.call.match(/WIDE[12]|qA?|UV[123]|.*\*$|UNCAN/)
) )
.map((station) => station.toString().trim().replace(/\*$/, '')); .map((station) => station.toString().trim().replace(/\*$/, ''));
}, }
groupByCall(acc, packet) { function groupByCall(acc, packet) {
let callsign = packet.from.toString().trim(); let callsign = packet.from.toString().trim();
if (!(callsign in acc)) acc[callsign] = []; if (!(callsign in acc)) acc[callsign] = [];
acc[callsign].push(packet); acc[callsign].push(packet);
return acc; return acc;
}, }
colorForDigi(digi) { function colorForDigi(digi) {
if (digi in this.digiColors) { if (digi in digiColors.value) {
return this.digiColors[digi].hex(); return digiColors.value[digi].hex();
} else { } else {
return '#000000'; return '#000000';
} }
}, }
packetPathStyleFunc(feature, resolution) { function packetPathStyleFunc(feature, resolution) {
let paths = feature.getProperties().paths.slice(0); let paths = feature.getProperties().properties.paths.slice(0);
let styles = []; let styles = [];
feature feature
@ -169,26 +147,25 @@ export default {
.forEach((ls) => { .forEach((ls) => {
let path = paths.shift().slice(0); let path = paths.shift().slice(0);
ls.forEachSegment((start, end) => { ls.forEachSegment((start, end) => {
let color = this.colorForDigi(path.shift()); let color = colorForDigi(path.shift());
styles.push( styles.push(
createStyle({ new Style({
geom: createLineGeom([start, end]), geometry: new LineString([start, end]),
strokeColor: color, stroke: new Stroke({ color: color, width: 2 }),
strokeWidth: 2,
}) })
); );
}); });
}); });
return styles; console.log(styles);
},
},
computed: { return styles;
positionalPackets() { }
const positionalPackets = computed(() => {
return ( return (
this.packets packets
.filter( .filter(
(packet) => (packet) =>
packet.date > new Date('2018-07-13') && packet.date > new Date('2018-07-13') &&
@ -197,87 +174,81 @@ export default {
// filter to just positional data // filter to just positional data
.filter((packet) => 'data' in packet && 'latitude' in packet.data) .filter((packet) => 'data' in packet && 'latitude' in packet.data)
); );
}, });
stationPaths() { const stationPaths = computed(() => {
// group by callsign // group by callsign
return this.positionalPackets.reduce(this.groupByCall, {}); return positionalPackets.value.reduce(groupByCall, {});
}, });
digis() { const digis = computed(() => {
let digiCalls = new Set( let digiCalls = new Set(
this.packets packets
.map((packet) => this.pathToString(packet.via)) .map((packet) => pathToString(packet.via))
.reduce((acc, stations) => acc.concat(stations)) .reduce((acc, stations) => acc.concat(stations))
); );
return ( return (
this.packets packets
// filter to digis // filter to digis
.filter((packet) => digiCalls.has(packet.from.toString().trim())) .filter((packet) => digiCalls.has(packet.from.toString().trim()))
// filter to just positional data // filter to just positional data
.filter((packet) => 'data' in packet && 'latitude' in packet.data) .filter((packet) => 'data' in packet && 'latitude' in packet.data)
// group by call // group by call
.reduce(this.groupByCall, {}) .reduce(groupByCall, {})
); );
}, });
digiPos() { const digiPos = computed(() => {
return Object.entries(this.digis).reduce((acc, [digi, packets]) => { return Object.entries(digis.value).reduce((acc, [digi, packets]) => {
let lastPacket = packets[packets.length - 1]; let lastPacket = packets[packets.length - 1];
acc[digi] = [lastPacket.data.longitude, lastPacket.data.latitude]; acc[digi] = [lastPacket.data.longitude, lastPacket.data.latitude];
return acc; return acc;
}, {}); }, {});
}, });
packetPathsGeoJSON() { const packetPaths = computed(() => {
let digiPos = { ...this.digiPos }; // localize for performance let digipeaterPostitions = digiPos.value;
return Object.entries(this.stationPaths).map(([station, packets]) => { return Object.entries(stationPaths.value).map(([station, packets]) => {
let lines = packets.map((packet) => { let lines = packets.map((packet) => {
let path = this.pathToString(packet.via); let path = pathToString(packet.via);
return { return {
// first point in path is originating station // first point in path is originating station
coords: [ coords: [
[packet.data.longitude, packet.data.latitude], [packet.data.longitude, packet.data.latitude],
...path.map((hop) => digiPos[hop] || [0, 0]), ...path.map((hop) => digipeaterPostitions[hop] || [0, 0]),
], ],
path: path, path: path,
}; };
}); });
return { return new Feature({
type: 'Feature',
id: station, id: station,
geometry: { geometry: new MultiLineString(lines.map((p) => p.coords)),
type: 'MultiLineString',
coordinates: lines.map((p) => p.coords),
},
properties: { paths: lines.map((p) => p.path) }, properties: { paths: lines.map((p) => p.path) },
};
}); });
}, });
});
stationColors() { const stationColors = computed(() => {
return distinctColors({ return distinctColors({
count: Object.keys(this.stationPaths).length, count: Object.keys(stationPaths.value).length,
lightMin: 20, lightMin: 20,
lightMax: 80, lightMax: 80,
}); });
}, });
digiColors() { const digiColors = computed(() => {
let colors = distinctColors({ let colors = distinctColors({
count: Object.keys(this.digis).length, count: Object.keys(digis.value).length,
lightMin: 20, lightMin: 20,
lightMax: 80, lightMax: 80,
}); });
return Object.keys(this.digis).reduce((acc, callsign, index) => { return Object.keys(digis.value).reduce((acc, callsign, index) => {
acc[callsign] = colors[index]; acc[callsign] = colors[index];
return acc; return acc;
}, {}); }, {});
}, });
},
};
</script> </script>
<style> <style>
@ -288,8 +259,8 @@ body {
} }
.map { .map {
height: 100%; width: 100vw;
width: 100%; height: 100vh;
} }
.ol-control.layer-toggles { .ol-control.layer-toggles {

View File

@ -1,14 +1,14 @@
<template> <template>
<tr :class="{ timedOut, lowVoltage, neverHeard: !status.lastHeard }"> <tr :class="{ timedOut, lowVoltage, neverHeard: !stationStatus.lastHeard }">
<td :title="callsign">{{ tacticalAndOrCall }}</td> <td :title="callsign">{{ tacticalAndOrCall }}</td>
<template v-if="status.lastHeard"> <template v-if="stationStatus.lastHeard">
<td>{{ formatTime(status.lastHeard) }}</td> <td>{{ formatTime(stationStatus.lastHeard) }}</td>
<td>{{ formatTime(now - status.lastHeard, true) }}</td> <td>{{ formatTime(now - stationStatus.lastHeard, true) }}</td>
<td>{{ formatTime(Math.round(status.avgDelta), true) }}</td> <td>{{ formatTime(Math.round(stationStatus.avgDelta), true) }}</td>
<td>{{ status.lastMicE }}</td> <td>{{ stationStatus.lastMicE }}</td>
<td>{{ status.lastVoltage || '' }}</td> <td>{{ stationStatus.lastVoltage || '' }}</td>
<td>{{ status.lastTemperature || '' }}</td> <td>{{ stationStatus.lastTemperature || '' }}</td>
<td>{{ status.lastComment }}</td> <td>{{ stationStatus.lastComment }}</td>
</template> </template>
<template v-else> <template v-else>
<td>Never Heard</td> <td>Never Heard</td>
@ -22,37 +22,36 @@
</tr> </tr>
</template> </template>
<script> <script setup>
import { ref, computed, watch } from 'vue';
import config from './status_config.yaml'; import config from './status_config.yaml';
export default { const props = defineProps({
name: 'StationRow', callsign: String,
props: { callsign: String, tactical: String, messages: Array, now: Date }, tactical: String,
messages: Array,
now: Date,
});
data() { const stationStatus = ref({
return {
status: {
lastHeard: null, lastHeard: null,
delta: null, delta: null,
lastVoltage: null, lastVoltage: null,
lastTemperature: null, lastTemperature: null,
}, });
};
},
methods: { function notify(title, body) {
notify(title, body) {
return new Notification(title, { body: body, requireInteraction: true }); return new Notification(title, { body: body, requireInteraction: true });
}, }
formatTime(time, isDuration = false) { function formatTime(time, isDuration = false) {
return new Date(time).toLocaleTimeString( return new Date(time).toLocaleTimeString(
'en-GB', 'en-GB',
isDuration ? { timeZone: 'UTC' } : {} isDuration ? { timeZone: 'UTC' } : {}
); );
}, }
prettyDuration(duration) { function prettyDuration(duration) {
let date = new Date(duration); let date = new Date(duration);
return [ return [
...Object.entries({ ...Object.entries({
@ -65,14 +64,36 @@ export default {
.filter(([suffix, num]) => num > 0) .filter(([suffix, num]) => num > 0)
.map(([suffix, num]) => num + ' ' + suffix) .map(([suffix, num]) => num + ' ' + suffix)
.join(' '); .join(' ');
}, }
},
watch: { const tacticalAndOrCall = computed(() => {
messages() { return props.tactical
? `${props.tactical} [${props.callsign}]`
: props.callsign;
});
const timedOut = computed(() => {
if (!stationStatus.value.lastHeard) {
return false;
}
let nowDelta = new Date(props.now.value - stationStatus.value.lastHeard);
return nowDelta.getTime() > config.timeoutLength;
});
const lowVoltage = computed(() => {
return (
stationStatus.value.lastVoltage &&
stationStatus.value.lastVoltage < config.lowVoltage
);
});
watch(
() => props.messages,
() => {
Object.assign( Object.assign(
this.status, stationStatus.value,
this.messages.reduce((acc, message, idx, arr) => { props.messages.reduce((acc, message, idx, arr) => {
acc.lastHeard = message.date.getTime(); acc.lastHeard = message.date.getTime();
if (idx === 0) { if (idx === 0) {
acc.avgDelta = 0; acc.avgDelta = 0;
@ -91,56 +112,35 @@ export default {
return acc; return acc;
}, {}) }, {})
); );
}, }
);
lowVoltage(newVal) { watch(
() => props.lowVoltage,
(newVal) => {
if (newVal) { if (newVal) {
this.notify( notify(
`${this.tacticalAndOrCall}'s battery has dropepd below ${config.lowVoltage}V`, `${tacticalAndOrCall}'s battery has dropepd below ${config.lowVoltage}V`,
`Voltage: ${this.status.lastVoltage}` `Voltage: ${stationStatus.value.lastVoltage}`
); );
} }
}, }
);
timedOut(newVal) { watch(timedOut, (newVal) => {
if (newVal) { if (newVal) {
this.notify( notify(
`${ `${tacticalAndOrCall.value} has not been heard for over ${prettyDuration(
this.tacticalAndOrCall
} has not been heard for over ${this.prettyDuration(
config.timeoutLength config.timeoutLength
)}!`, )}!`,
`Last Heard: ${this.formatTime( `Last Heard: ${formatTime(
this.status.lastHeard stationStatus.value.lastHeard
)} (${this.prettyDuration(this.now - this.status.lastHeard)} ago!)` )} (${prettyDuration(
props.now.value - stationStatus.value.lastHeard
)} ago!)`
); );
} }
}, });
},
computed: {
tacticalAndOrCall() {
return this.tactical
? `${this.tactical} [${this.callsign}]`
: this.callsign;
},
timedOut() {
if (!this.status.lastHeard) {
return false;
}
let nowDelta = new Date(this.now - this.status.lastHeard);
return nowDelta.getTime() > config.timeoutLength;
},
lowVoltage() {
return (
this.status.lastVoltage && this.status.lastVoltage < config.lowVoltage
);
},
},
};
</script> </script>
<style> <style>

View File

@ -25,28 +25,21 @@
</div> </div>
</template> </template>
<script> <script setup>
import aprs from 'aprs-parser'; import { ref, onMounted } from 'vue';
import APRSParser from 'aprs-parser/lib/APRSParser';
import StationRow from './StationRow.vue'; import StationRow from './StationRow.vue';
import config from './status_config.yaml'; import config from './status_config.yaml';
export default { const parser = new APRSParser();
name: 'StationStatus', let aprsStream = null;
components: { StationRow }, const messages = ref([]);
data() { const messagesFromStation = ref({});
return { const now = ref(new Date());
aprsStream: null, const trackedStations = ref(config.trackedStations);
parser: new aprs.APRSParser(),
messages: [],
messagesFromStation: {},
now: new Date(),
trackedStations: config.trackedStations,
};
},
mounted() { onMounted(() => {
// request notification permissions // request notification permissions
if (Notification.permission !== 'granted') { if (Notification.permission !== 'granted') {
Notification.requestPermission((permission) => { Notification.requestPermission((permission) => {
@ -57,37 +50,39 @@ export default {
} }
// Connect to websocket aprs stream // Connect to websocket aprs stream
this.connectToStream(); connectToStream();
// update shared current time every second // update shared current time every second
window.setInterval(() => (this.now = new Date()), 1000); window.setInterval(() => (now.value = new Date()), 1000);
}, });
methods: { function connectToStream() {
connectToStream() { aprsStream = new WebSocket('ws://localhost:4321');
this.aprsStream = new WebSocket('ws://localhost:4321'); aprsStream.onclose = () => {
this.aprsStream.onclose = () => {
// Try to reconnect every 5 seconds // Try to reconnect every 5 seconds
let interval = window.setTimeout(() => { let interval = window.setTimeout(() => {
window.clearInterval(interval); window.clearInterval(interval);
this.connectToStream(); connectToStream();
}, 5000); }, 5000);
}; };
this.aprsStream.onmessage = (event) => aprsStream.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data)); if (event.data !== '') {
}, handleMessage(JSON.parse(event.data));
}
};
}
handleMessage(packet) { function handleMessage(packet) {
let message = this.parser.parse(packet[1]); let message = parser.parse(packet[1]);
message.date = new Date(packet[0]); message.date = new Date(packet[0]);
console.log(message); console.info(message);
this.messages.push(message); messages.value.push(message);
let callsign = message.from && message.from.toString(); let callsign = message.from && message.from.toString();
if (callsign in this.messagesFromStation) { if (callsign in messagesFromStation.value) {
this.messagesFromStation[callsign].push(message); messagesFromStation.value[callsign].push(message);
} else { } else {
this.$set(this.messagesFromStation, callsign, [message]); messagesFromStation.value[callsign] = [message];
} }
// message to TACTICAL setting a tactical nickname from an // message to TACTICAL setting a tactical nickname from an
@ -101,15 +96,13 @@ export default {
message.data.text.split(';').map((tac_assoc) => { message.data.text.split(';').map((tac_assoc) => {
let [call, tac] = tac_assoc.split('=', 2); let [call, tac] = tac_assoc.split('=', 2);
if (tac) { if (tac) {
this.trackedStations[call] = tac; trackedStations.value[call] = tac;
} else { } else {
delete this.trackedStations[call]; delete trackedStations.value[call];
} }
}); });
} }
}, }
},
};
</script> </script>
<style> <style>

View File

@ -1,7 +0,0 @@
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
<script src="./index.js"></script>
</body>

View File

@ -1,7 +1,12 @@
import Vue from 'vue'; import * as Vue from 'vue';
import App from './StatusScreen.vue';
new Vue({ // import OpenLayersMap from 'vue3-openlayers';
el: '#app', // import 'vue3-openlayers/dist/vue3-openlayers.css';
render: (h) => h(App),
}); import StatusScreen from './StatusScreen.vue';
// import Map from './Map.vue';
const app = Vue.createApp(StatusScreen);
// app.use(OpenLayersMap);
app.mount('#app');

View File

@ -27,8 +27,8 @@ client.on('data', function (data) {
// strip whitespace, then handle multiple APRS packets per TCP packet // strip whitespace, then handle multiple APRS packets per TCP packet
str.split('\r\n').forEach((packet) => { str.split('\r\n').forEach((packet) => {
if (!packet.startsWith('#')) { // ignore comments and empty lines
// ignore comments if (!packet.startsWith('#') || packet === '') {
let date = new Date(); let date = new Date();
// create log dir if it doesn't exist // create log dir if it doesn't exist
if (!fs.existsSync('log')) fs.mkdirSync('log'); if (!fs.existsSync('log')) fs.mkdirSync('log');
@ -52,6 +52,7 @@ wss.on('connection', (ws) => {
fs.readFileSync(filename) fs.readFileSync(filename)
.toString() .toString()
.split('\n') .split('\n')
.filter((line) => line !== '')
.forEach((line) => ws.send(line)); .forEach((line) => ws.send(line));
} }
}); });

9
vite.config.js Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import yaml from '@rollup/plugin-yaml';
// https://vitejs.dev/config/
export default defineConfig({
assetsInclude: ['**/*.gpx'],
plugins: [vue(), yaml()],
});