mirror of https://github.com/DJ2LS/FreeDATA.git
first version of map
parent
db9d16b241
commit
f7f3d6535d
|
@ -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",
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue