mirror of https://github.com/DJ2LS/FreeDATA.git
commit
c37e494324
|
@ -32,10 +32,12 @@
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"chartjs-plugin-annotation": "^3.0.1",
|
"chartjs-plugin-annotation": "^3.0.1",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"gridstack": "^10.3.0",
|
"gridstack": "^10.3.0",
|
||||||
"js-image-compressor": "^2.0.0",
|
"js-image-compressor": "^2.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"qth-locator": "^2.1.0",
|
"qth-locator": "^2.1.0",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-chartjs": "^5.3.1",
|
"vue-chartjs": "^5.3.1",
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -33,6 +33,7 @@ import grid_mycall_small from "./grid/grid_mycall small.vue";
|
||||||
import grid_scatter from "./grid/grid_scatter.vue";
|
import grid_scatter from "./grid/grid_scatter.vue";
|
||||||
import grid_stats_chart from "./grid/grid_stats_chart.vue";
|
import grid_stats_chart from "./grid/grid_stats_chart.vue";
|
||||||
import grid_swr_meter from "./grid/grid_swr_meter.vue";
|
import grid_swr_meter from "./grid/grid_swr_meter.vue";
|
||||||
|
import grid_stations_map from "./grid/grid_stations_map.vue";
|
||||||
|
|
||||||
let count = ref(0);
|
let count = ref(0);
|
||||||
let grid = null; // DO NOT use ref(null) as proxies GS will break all logic when comparing structures... see https://github.com/gridstack/gridstack.js/issues/2115
|
let grid = null; // DO NOT use ref(null) as proxies GS will break all logic when comparing structures... see https://github.com/gridstack/gridstack.js/issues/2115
|
||||||
|
@ -263,8 +264,17 @@ const gridWidgets = [
|
||||||
true,
|
true,
|
||||||
"Rig",
|
"Rig",
|
||||||
21
|
21
|
||||||
|
),
|
||||||
|
new gridWidget(
|
||||||
|
grid_stations_map,
|
||||||
|
{ x: 16, y: 0, w: 8, h: 100 },
|
||||||
|
"Station Map",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
"Other",
|
||||||
|
22
|
||||||
)
|
)
|
||||||
//Next new widget ID should be 22
|
//Next new widget ID should be 23
|
||||||
];
|
];
|
||||||
|
|
||||||
function updateFrequencyAndApply(frequency) {
|
function updateFrequencyAndApply(frequency) {
|
||||||
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button @click="zoomIn" class="btn btn-sm btn-outline-secondary"><i class="bi bi-plus-square"></i></button>
|
||||||
|
<button @click="centerMap" class="btn btn-sm btn-secondary"><i class="bi bi-house-door"></i></button>
|
||||||
|
<button @click="zoomOut" class="btn btn-sm btn-outline-secondary"><i class="bi bi-dash-square"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group input-group-sm ms-2">
|
||||||
|
<input type="text" class="form-control w-100" placeholder="Station" aria-label="Username" aria-describedby="basic-addon1" v-model="infoText" disabled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div ref="mapContainer" class="map-container"></div>
|
||||||
|
<div :style="popupStyle" class="popup">{{ infoText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { settingsStore as settings } from '../../store/settingsStore.js';
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick, watch, toRaw } from 'vue';
|
||||||
|
import * as d3 from 'd3';
|
||||||
|
import { feature } from 'topojson-client';
|
||||||
|
import { locatorToLatLng, distance } from 'qth-locator';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
import pinia from '../../store/index'; // Import your Pinia instance
|
||||||
|
import { useStateStore } from '../../store/stateStore.js';
|
||||||
|
|
||||||
|
// Activate Pinia
|
||||||
|
setActivePinia(pinia);
|
||||||
|
const state = useStateStore(pinia);
|
||||||
|
|
||||||
|
const mapContainer = ref(null);
|
||||||
|
const infoText = ref('');
|
||||||
|
const popupStyle = ref({});
|
||||||
|
let svg, path, projection, zoom;
|
||||||
|
const basePinRadius = 5; // Base radius for pins
|
||||||
|
let actualPinRadius = basePinRadius;
|
||||||
|
|
||||||
|
// Function to get distance between two grid squares
|
||||||
|
function getMaidenheadDistance(dxGrid) {
|
||||||
|
try {
|
||||||
|
return parseInt(distance(settings.remote.STATION.mygrid, dxGrid));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error calculating distance:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to draw the map
|
||||||
|
const drawMap = () => {
|
||||||
|
const containerWidth = mapContainer.value.clientWidth;
|
||||||
|
const containerHeight = mapContainer.value.clientHeight;
|
||||||
|
|
||||||
|
// Clear existing SVG if it exists
|
||||||
|
if (svg) {
|
||||||
|
svg.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Mercator projection
|
||||||
|
projection = d3.geoMercator()
|
||||||
|
.scale(containerWidth / (2 * Math.PI))
|
||||||
|
.translate([containerWidth / 2, containerHeight / 2]);
|
||||||
|
|
||||||
|
path = d3.geoPath().projection(projection);
|
||||||
|
|
||||||
|
// Create SVG element
|
||||||
|
svg = d3.select(mapContainer.value)
|
||||||
|
.append('svg')
|
||||||
|
.attr('width', '100%')
|
||||||
|
.attr('height', '100%')
|
||||||
|
.attr('viewBox', `0 0 ${containerWidth} ${containerHeight}`)
|
||||||
|
.attr('preserveAspectRatio', 'xMidYMid meet');
|
||||||
|
|
||||||
|
// Set up zoom behavior
|
||||||
|
zoom = d3.zoom()
|
||||||
|
.scaleExtent([1, 8])
|
||||||
|
.on('zoom', (event) => {
|
||||||
|
svg.selectAll('g').attr('transform', event.transform);
|
||||||
|
|
||||||
|
// Adjust pin size and line width with zoom
|
||||||
|
actualPinRadius = basePinRadius / event.transform.k
|
||||||
|
svg.selectAll('.pin').attr('r', actualPinRadius);
|
||||||
|
svg.selectAll('.connection-line').attr('stroke-width', 1 / event.transform.k);
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.call(zoom);
|
||||||
|
|
||||||
|
// Import JSON data dynamically
|
||||||
|
import('@/assets/countries-50m.json').then(worldData => {
|
||||||
|
const countriesGeoJSON = feature(worldData, worldData.objects.countries);
|
||||||
|
|
||||||
|
// Draw country paths
|
||||||
|
const g = svg.append('g');
|
||||||
|
|
||||||
|
g.selectAll('path')
|
||||||
|
.data(countriesGeoJSON.features)
|
||||||
|
.enter()
|
||||||
|
.append('path')
|
||||||
|
.attr('d', path)
|
||||||
|
.attr('fill', '#ccc')
|
||||||
|
.attr('stroke', '#333');
|
||||||
|
|
||||||
|
// Draw initial pins and lines
|
||||||
|
updatePinsAndLines(g);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to update pins and draw lines
|
||||||
|
const updatePinsAndLines = (g) => {
|
||||||
|
// Remove existing pins and lines
|
||||||
|
g.selectAll('.pin').remove();
|
||||||
|
g.selectAll('.connection-line').remove();
|
||||||
|
|
||||||
|
const heardStations = toRaw(state.heard_stations); // Ensure it's the raw data
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
// Prepare points for heard stations
|
||||||
|
heardStations.forEach(item => {
|
||||||
|
if (item.gridsquare && item.origin) { // Ensure data is valid
|
||||||
|
const [lat, lng] = locatorToLatLng(item.gridsquare); // Convert gridsquare to lat/lng
|
||||||
|
points.push({ lat, lon: lng, origin: item.origin, gridsquare: item.gridsquare }); // Add to the points array
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if 'mygrid' is defined and not empty
|
||||||
|
const mygrid = settings.remote.STATION.mygrid;
|
||||||
|
if (mygrid) {
|
||||||
|
// Your station's coordinates
|
||||||
|
const [myLat, myLng] = locatorToLatLng(mygrid);
|
||||||
|
const [myX, myY] = projection([myLng, myLat]); // Project your station's coordinates
|
||||||
|
|
||||||
|
// Draw lines from your station to each heard station
|
||||||
|
g.selectAll('.connection-line')
|
||||||
|
.data(points)
|
||||||
|
.enter()
|
||||||
|
.append('line')
|
||||||
|
.attr('class', 'connection-line')
|
||||||
|
.attr('x1', myX)
|
||||||
|
.attr('y1', myY)
|
||||||
|
.attr('x2', d => projection([d.lon, d.lat])[0])
|
||||||
|
.attr('y2', d => projection([d.lon, d.lat])[1])
|
||||||
|
.attr('stroke', 'blue')
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
.attr('stroke-opacity', 0.5);
|
||||||
|
} else {
|
||||||
|
console.error('Error: Station grid square (mygrid) is not defined. Lines will not be drawn.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pins
|
||||||
|
g.selectAll('.pin')
|
||||||
|
.data(points)
|
||||||
|
.enter()
|
||||||
|
.append('circle')
|
||||||
|
.attr('class', 'pin')
|
||||||
|
.attr('r', actualPinRadius) // Set initial radius
|
||||||
|
.attr('fill', 'red')
|
||||||
|
.attr('cx', d => projection([d.lon, d.lat])[0])
|
||||||
|
.attr('cy', d => projection([d.lon, d.lat])[1])
|
||||||
|
.on('mouseover', (event, d) => {
|
||||||
|
// Show info with station details
|
||||||
|
infoText.value = `${d.origin} - ${d.gridsquare} (${getMaidenheadDistance(d.gridsquare)} km)`;
|
||||||
|
})
|
||||||
|
.on('mouseout', () => {
|
||||||
|
infoText.value = '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
const handleResize = () => {
|
||||||
|
drawMap();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for changes in heard_stations and update pins accordingly
|
||||||
|
watch(state.heard_stations, (changedHeardStations) => {
|
||||||
|
console.log('Heard stations updated:', toRaw(changedHeardStations));
|
||||||
|
const g = svg.select('g');
|
||||||
|
updatePinsAndLines(g);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zoom in function
|
||||||
|
const zoomIn = () => {
|
||||||
|
svg.transition().call(zoom.scaleBy, 1.2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zoom out function
|
||||||
|
const zoomOut = () => {
|
||||||
|
svg.transition().call(zoom.scaleBy, 0.8);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Center the map
|
||||||
|
const centerMap = () => {
|
||||||
|
const mygrid = settings.remote.STATION.mygrid; // Get the grid square value
|
||||||
|
if (!mygrid) {
|
||||||
|
console.error('Error: Station grid square (mygrid) is not defined.');
|
||||||
|
return; // Exit if 'mygrid' is not defined
|
||||||
|
}
|
||||||
|
|
||||||
|
const [lat, lng] = locatorToLatLng(mygrid); // Convert gridsquare to lat/lng
|
||||||
|
|
||||||
|
// Project the geographic coordinates to SVG coordinates
|
||||||
|
const [x, y] = projection([lng, lat]);
|
||||||
|
|
||||||
|
// Center the map at the calculated coordinates
|
||||||
|
svg.transition().duration(750).call(
|
||||||
|
zoom.translateTo,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
drawMap();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.map-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin {
|
||||||
|
fill: red;
|
||||||
|
stroke: black;
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
fill: #ccc;
|
||||||
|
stroke: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-line {
|
||||||
|
stroke: blue;
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
z-index: 10; /* Ensure the popup is above other elements */
|
||||||
|
pointer-events: none; /* Prevent mouse events on the popup */
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -222,7 +222,6 @@ switch (data.received) {
|
||||||
stateStore.arq_is_receiving = false;
|
stateStore.arq_is_receiving = false;
|
||||||
switch (data["arq-transfer-outbound"].state) {
|
switch (data["arq-transfer-outbound"].state) {
|
||||||
case "NEW":
|
case "NEW":
|
||||||
|
|
||||||
message = `
|
message = `
|
||||||
<div>
|
<div>
|
||||||
<strong>New transmission with:</strong>
|
<strong>New transmission with:</strong>
|
||||||
|
@ -423,7 +422,7 @@ switch (data.received) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case "BURST_REPLY_SENT":
|
case "BURST_REPLY_SENT":
|
||||||
message = `
|
message = `
|
||||||
<div>
|
<div>
|
||||||
<strong>ongoing transmission with:</strong>
|
<strong>ongoing transmission with:</strong>
|
||||||
<span class="badge bg-info text-dark">${data["arq-transfer-inbound"].dxcall}</span>
|
<span class="badge bg-info text-dark">${data["arq-transfer-inbound"].dxcall}</span>
|
||||||
|
@ -433,7 +432,9 @@ message = `
|
||||||
<span class="badge bg-warning text-dark">Total Bytes: ${data["arq-transfer-outbound"].total_bytes}</span>
|
<span class="badge bg-warning text-dark">Total Bytes: ${data["arq-transfer-outbound"].total_bytes}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`; displayToast("info", "bi-info-circle", message, 5000);
|
`;
|
||||||
|
displayToast("info", "bi-info-circle", message, 5000);
|
||||||
|
|
||||||
stateStore.arq_transmission_percent = Math.round(
|
stateStore.arq_transmission_percent = Math.round(
|
||||||
(data["arq-transfer-inbound"].received_bytes /
|
(data["arq-transfer-inbound"].received_bytes /
|
||||||
data["arq-transfer-inbound"].total_bytes) *
|
data["arq-transfer-inbound"].total_bytes) *
|
||||||
|
|
|
@ -62,8 +62,6 @@ const defaultConfig = {
|
||||||
respond_to_cq: false,
|
respond_to_cq: false,
|
||||||
enable_callsign_blacklist: false,
|
enable_callsign_blacklist: false,
|
||||||
callsign_blacklist: [],
|
callsign_blacklist: [],
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
TCI: {
|
TCI: {
|
||||||
tci_ip: "127.0.0.1",
|
tci_ip: "127.0.0.1",
|
||||||
|
@ -88,15 +86,15 @@ export const settingsStore = reactive({ ...defaultConfig, local: localConfig });
|
||||||
// Function to handle remote configuration changes
|
// Function to handle remote configuration changes
|
||||||
|
|
||||||
export function onChange() {
|
export function onChange() {
|
||||||
let remote_config = settingsStore.remote
|
let remote_config = settingsStore.remote;
|
||||||
let blacklistContent = remote_config.STATION.callsign_blacklist;
|
let blacklistContent = remote_config.STATION.callsign_blacklist;
|
||||||
// Check if the content is a string
|
// Check if the content is a string
|
||||||
if (typeof blacklistContent === 'string') {
|
if (typeof blacklistContent === "string") {
|
||||||
// Split the string by newlines to create an array
|
// Split the string by newlines to create an array
|
||||||
blacklistContent = blacklistContent
|
blacklistContent = blacklistContent
|
||||||
.split('\n') // Split text by newlines
|
.split("\n") // Split text by newlines
|
||||||
.map((item) => item.trim()) // Trim whitespace from each line
|
.map((item) => item.trim()) // Trim whitespace from each line
|
||||||
.filter((item) => item !== ''); // Remove empty lines
|
.filter((item) => item !== ""); // Remove empty lines
|
||||||
|
|
||||||
// Update the settings store with the validated array
|
// Update the settings store with the validated array
|
||||||
remote_config.STATION.callsign_blacklist = blacklistContent;
|
remote_config.STATION.callsign_blacklist = blacklistContent;
|
||||||
|
@ -108,10 +106,10 @@ export function onChange() {
|
||||||
remote_config.STATION.callsign_blacklist = [];
|
remote_config.STATION.callsign_blacklist = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setConfig(remote_config).then((conf) => {
|
setConfig(remote_config).then((conf) => {
|
||||||
settingsStore.remote = conf;
|
settingsStore.remote = conf;
|
||||||
settingsStore.remote.STATION.callsign_blacklist = conf.STATION.callsign_blacklist.join('\n');
|
settingsStore.remote.STATION.callsign_blacklist =
|
||||||
|
conf.STATION.callsign_blacklist.join("\n");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,9 +118,9 @@ export function getRemote() {
|
||||||
return getConfig().then((conf) => {
|
return getConfig().then((conf) => {
|
||||||
if (conf !== undefined) {
|
if (conf !== undefined) {
|
||||||
settingsStore.remote = conf;
|
settingsStore.remote = conf;
|
||||||
settingsStore.remote.STATION.callsign_blacklist = conf.STATION.callsign_blacklist.join('\n');
|
settingsStore.remote.STATION.callsign_blacklist =
|
||||||
|
conf.STATION.callsign_blacklist.join("\n");
|
||||||
onChange();
|
onChange();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("Received undefined configuration, using default!");
|
console.warn("Received undefined configuration, using default!");
|
||||||
settingsStore.remote = defaultConfig.remote;
|
settingsStore.remote = defaultConfig.remote;
|
||||||
|
|
|
@ -119,5 +119,3 @@ https://stackoverflow.com/a/9622873
|
||||||
[data-bs-theme="dark"] {
|
[data-bs-theme="dark"] {
|
||||||
/* default dark theme mods */
|
/* default dark theme mods */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue