skyview/assets/static/js/modules/map-manager.js

372 lines
13 KiB
JavaScript
Raw Normal View History

// Map and visualization management module
export class MapManager {
constructor() {
this.map = null;
this.coverageMap = null;
this.mapOrigin = null;
// Source markers and overlays
this.sourceMarkers = new Map();
this.rangeCircles = new Map();
this.showSources = true;
this.showRange = false;
this.selectedSource = null;
this.heatmapLayer = null;
// Data references
this.sourcesData = new Map();
// Map theme
this.isDarkMode = false;
this.currentTileLayer = null;
this.coverageTileLayer = null;
}
async initializeMap() {
// Get origin from server
let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback
try {
const response = await fetch('/api/origin');
if (response.ok) {
origin = await response.json();
}
} catch (error) {
console.warn('Could not fetch origin, using default:', error);
}
// Store origin for reset functionality
this.mapOrigin = origin;
this.map = L.map('map').setView([origin.latitude, origin.longitude], 10);
// Light tile layer by default
this.currentTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
maxZoom: 19
}).addTo(this.map);
// Add scale control for distance estimation
L.control.scale({
metric: true,
imperial: true,
position: 'bottomright'
}).addTo(this.map);
return this.map;
}
async initializeCoverageMap() {
if (!this.coverageMap) {
// Get origin from server
let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback
try {
const response = await fetch('/api/origin');
if (response.ok) {
origin = await response.json();
}
} catch (error) {
console.warn('Could not fetch origin for coverage map, using default:', error);
}
this.coverageMap = L.map('coverage-map').setView([origin.latitude, origin.longitude], 10);
this.coverageTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(this.coverageMap);
// Add scale control for distance estimation
L.control.scale({
metric: true,
imperial: true,
position: 'bottomright'
}).addTo(this.coverageMap);
}
return this.coverageMap;
}
updateSourcesData(data) {
if (data.sources) {
this.sourcesData.clear();
data.sources.forEach(source => {
this.sourcesData.set(source.id, source);
});
}
}
updateSourceMarkers() {
if (!this.map || !this.showSources) return;
// Remove markers for sources that no longer exist
const currentSourceIds = new Set(this.sourcesData.keys());
for (const [id, marker] of this.sourceMarkers) {
if (!currentSourceIds.has(id)) {
this.map.removeLayer(marker);
this.sourceMarkers.delete(id);
}
}
// Update or create markers for current sources
for (const [id, source] of this.sourcesData) {
if (source.latitude && source.longitude) {
if (this.sourceMarkers.has(id)) {
// Update existing marker
const marker = this.sourceMarkers.get(id);
// Update marker style if status changed
marker.setStyle({
radius: source.active ? 10 : 6,
fillColor: source.active ? '#00d4ff' : '#666666',
fillOpacity: 0.8
});
// Update popup content if it's open
if (marker.isPopupOpen()) {
marker.setPopupContent(this.createSourcePopupContent(source));
}
} else {
// Create new marker
const marker = L.circleMarker([source.latitude, source.longitude], {
radius: source.active ? 10 : 6,
fillColor: source.active ? '#00d4ff' : '#666666',
color: '#ffffff',
weight: 2,
fillOpacity: 0.8,
className: 'source-marker'
}).addTo(this.map);
marker.bindPopup(this.createSourcePopupContent(source), {
maxWidth: 300
});
this.sourceMarkers.set(id, marker);
}
}
}
this.updateSourcesLegend();
}
updateRangeCircles() {
if (!this.map || !this.showRange) return;
// Clear existing circles
this.rangeCircles.forEach(circle => this.map.removeLayer(circle));
this.rangeCircles.clear();
// Add range circles for active sources
for (const [id, source] of this.sourcesData) {
if (source.active && source.latitude && source.longitude) {
// Add multiple range circles (50km, 100km, 200km)
const ranges = [50000, 100000, 200000];
ranges.forEach((range, index) => {
const circle = L.circle([source.latitude, source.longitude], {
radius: range,
fillColor: 'transparent',
color: '#00d4ff',
weight: 2,
opacity: 0.7 - (index * 0.15),
dashArray: '8,4'
}).addTo(this.map);
this.rangeCircles.set(`${id}_${range}`, circle);
});
}
}
}
createSourcePopupContent(source, aircraftData) {
const aircraftCount = aircraftData ? Array.from(aircraftData.values())
.filter(aircraft => aircraft.sources && aircraft.sources[source.id]).length : 0;
return `
<div class="source-popup">
<h3>${source.name}</h3>
<p><strong>ID:</strong> ${source.id}</p>
<p><strong>Location:</strong> ${source.latitude.toFixed(4)}°, ${source.longitude.toFixed(4)}°</p>
<p><strong>Status:</strong> ${source.active ? 'Active' : 'Inactive'}</p>
<p><strong>Aircraft:</strong> ${aircraftCount}</p>
<p><strong>Messages:</strong> ${source.messages || 0}</p>
<p><strong>Last Seen:</strong> ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}</p>
</div>
`;
}
updateSourcesLegend() {
const legend = document.getElementById('sources-legend');
if (!legend) return;
legend.innerHTML = '';
for (const [id, source] of this.sourcesData) {
const item = document.createElement('div');
item.className = 'legend-item';
item.innerHTML = `
<span class="legend-icon" style="background: ${source.active ? '#00d4ff' : '#666666'}"></span>
<span title="${source.host}:${source.port}">${source.name}</span>
`;
legend.appendChild(item);
}
}
resetMap() {
if (this.mapOrigin && this.map) {
this.map.setView([this.mapOrigin.latitude, this.mapOrigin.longitude], 10);
}
}
toggleRangeCircles() {
this.showRange = !this.showRange;
if (this.showRange) {
this.updateRangeCircles();
} else {
this.rangeCircles.forEach(circle => this.map.removeLayer(circle));
this.rangeCircles.clear();
}
return this.showRange;
}
toggleSources() {
this.showSources = !this.showSources;
if (this.showSources) {
this.updateSourceMarkers();
} else {
this.sourceMarkers.forEach(marker => this.map.removeLayer(marker));
this.sourceMarkers.clear();
}
return this.showSources;
}
toggleDarkMode() {
this.isDarkMode = !this.isDarkMode;
const lightUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
const darkUrl = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
const tileUrl = this.isDarkMode ? darkUrl : lightUrl;
const tileOptions = {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
maxZoom: 19
};
// Update main map
if (this.map && this.currentTileLayer) {
this.map.removeLayer(this.currentTileLayer);
this.currentTileLayer = L.tileLayer(tileUrl, tileOptions).addTo(this.map);
}
// Update coverage map
if (this.coverageMap && this.coverageTileLayer) {
this.coverageMap.removeLayer(this.coverageTileLayer);
this.coverageTileLayer = L.tileLayer(tileUrl, {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(this.coverageMap);
}
return this.isDarkMode;
}
// Coverage map methods
updateCoverageControls() {
const select = document.getElementById('coverage-source');
if (!select) return;
select.innerHTML = '<option value="">Select Source</option>';
for (const [id, source] of this.sourcesData) {
const option = document.createElement('option');
option.value = id;
option.textContent = source.name;
select.appendChild(option);
}
}
async updateCoverageDisplay() {
if (!this.selectedSource || !this.coverageMap) return;
try {
const response = await fetch(`/api/coverage/${this.selectedSource}`);
const data = await response.json();
// Clear existing coverage markers
this.coverageMap.eachLayer(layer => {
if (layer instanceof L.CircleMarker) {
this.coverageMap.removeLayer(layer);
}
});
// Add coverage points
data.points.forEach(point => {
const intensity = Math.max(0, (point.signal + 50) / 50); // Normalize signal strength
L.circleMarker([point.lat, point.lon], {
radius: 3,
fillColor: this.getSignalColor(point.signal),
color: 'white',
weight: 1,
fillOpacity: intensity
}).addTo(this.coverageMap);
});
} catch (error) {
console.error('Failed to load coverage data:', error);
}
}
async toggleHeatmap() {
if (!this.selectedSource) {
alert('Please select a source first');
return false;
}
if (this.heatmapLayer) {
this.coverageMap.removeLayer(this.heatmapLayer);
this.heatmapLayer = null;
return false;
} else {
try {
const response = await fetch(`/api/heatmap/${this.selectedSource}`);
const data = await response.json();
// Create heatmap layer (simplified)
this.createHeatmapOverlay(data);
return true;
} catch (error) {
console.error('Failed to load heatmap data:', error);
return false;
}
}
}
getSignalColor(signal) {
if (signal > -10) return '#00ff88';
if (signal > -20) return '#ffff00';
if (signal > -30) return '#ff8c00';
return '#ff4444';
}
createHeatmapOverlay(data) {
// Simplified heatmap implementation
// In production, would use proper heatmap library like Leaflet.heat
}
setSelectedSource(sourceId) {
this.selectedSource = sourceId;
}
getSourcePositions() {
const positions = [];
for (const [id, source] of this.sourcesData) {
if (source.latitude && source.longitude) {
positions.push([source.latitude, source.longitude]);
}
}
return positions;
}
}