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",
|
||||
"chartjs-plugin-annotation": "^3.0.1",
|
||||
"core-js": "^3.8.3",
|
||||
"d3": "^7.9.0",
|
||||
"gridstack": "^10.3.0",
|
||||
"js-image-compressor": "^2.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"qth-locator": "^2.1.0",
|
||||
"topojson-client": "^3.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.2.13",
|
||||
"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_stats_chart from "./grid/grid_stats_chart.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 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,
|
||||
"Rig",
|
||||
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) {
|
||||
|
|
|
@ -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