historical packets
This commit is contained in:
parent
d6ac5cccfc
commit
8fdafcf01e
5 changed files with 444 additions and 98 deletions
|
|
@ -181,8 +181,22 @@ body.home-page main > div {
|
||||||
|
|
||||||
/* Trail polyline hover effect */
|
/* Trail polyline hover effect */
|
||||||
.leaflet-interactive:hover {
|
.leaflet-interactive:hover {
|
||||||
stroke-width: 4;
|
stroke-width: 5;
|
||||||
|
stroke-opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Historical trail lines */
|
||||||
|
.leaflet-interactive[stroke="#1E90FF"] {
|
||||||
|
stroke-width: 3;
|
||||||
stroke-opacity: 0.8;
|
stroke-opacity: 0.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-interactive[stroke="#1E90FF"]:hover {
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 4px #1e90ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Position dot hover effect */
|
/* Position dot hover effect */
|
||||||
|
|
@ -210,3 +224,52 @@ body.home-page main > div {
|
||||||
background-color: #1e90ff;
|
background-color: #1e90ff;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Historical dot marker styles */
|
||||||
|
.historical-dot-marker {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-dot-marker:hover {
|
||||||
|
transform: scale(1.5);
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-dot-marker div {
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-dot-marker:hover div {
|
||||||
|
box-shadow: 0 0 8px rgba(255, 107, 107, 0.8);
|
||||||
|
border-color: #ff6b6b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Historical trail line styling */
|
||||||
|
.historical-trail-line {
|
||||||
|
stroke: #1e90ff;
|
||||||
|
stroke-width: 3;
|
||||||
|
stroke-opacity: 0.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: none;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-trail-line:hover {
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 4px #1e90ff);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure markers are above trail lines */
|
||||||
|
.leaflet-marker-icon {
|
||||||
|
z-index: 10 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-dot-marker {
|
||||||
|
z-index: 15 !important;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,29 +32,62 @@ export class TrailManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addPosition(markerId: string, lat: number, lng: number, timestamp: number) {
|
addPosition(
|
||||||
|
markerId: string,
|
||||||
|
lat: number,
|
||||||
|
lng: number,
|
||||||
|
timestamp: number,
|
||||||
|
isHistoricalDot: boolean = false,
|
||||||
|
) {
|
||||||
if (!this.showTrails) return;
|
if (!this.showTrails) return;
|
||||||
|
|
||||||
let trailState = this.trails.get(markerId);
|
// Extract base callsign from markerId to group positions by callsign
|
||||||
|
const baseCallsign = this.extractBaseCallsign(markerId);
|
||||||
|
|
||||||
|
let trailState = this.trails.get(baseCallsign);
|
||||||
if (!trailState) {
|
if (!trailState) {
|
||||||
trailState = { positions: [] };
|
trailState = { positions: [] };
|
||||||
this.trails.set(markerId, trailState);
|
this.trails.set(baseCallsign, trailState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add if position is different from the last one
|
// Only add if position is different from the last one (more than ~1 meter)
|
||||||
const lastPos = trailState.positions[trailState.positions.length - 1];
|
const lastPos = trailState.positions[trailState.positions.length - 1];
|
||||||
if (!lastPos || Math.abs(lastPos.lat - lat) > 0.0001 || Math.abs(lastPos.lng - lng) > 0.0001) {
|
if (
|
||||||
|
!lastPos ||
|
||||||
|
Math.abs(lastPos.lat - lat) > 0.00001 ||
|
||||||
|
Math.abs(lastPos.lng - lng) > 0.00001
|
||||||
|
) {
|
||||||
trailState.positions.push({ lat, lng, timestamp });
|
trailState.positions.push({ lat, lng, timestamp });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter positions to keep only recent ones
|
// For historical dots, keep all positions. For live positions, filter by time.
|
||||||
const cutoffTime = Date.now() - this.trailDuration;
|
if (!isHistoricalDot) {
|
||||||
trailState.positions = trailState.positions.filter((pos) => pos.timestamp >= cutoffTime);
|
const cutoffTime = Date.now() - this.trailDuration;
|
||||||
|
trailState.positions = trailState.positions.filter((pos) => {
|
||||||
|
// Ensure timestamp is a number for comparison
|
||||||
|
const posTimestamp =
|
||||||
|
typeof pos.timestamp === "string" ? new Date(pos.timestamp).getTime() : pos.timestamp;
|
||||||
|
return posTimestamp >= cutoffTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.updateTrailVisualization(markerId, trailState);
|
// Always update trail visualization
|
||||||
|
this.updateTrailVisualization(baseCallsign, trailState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTrailVisualization(markerId: string, trailState: TrailState) {
|
private extractBaseCallsign(markerId: string | any): string {
|
||||||
|
// Ensure markerId is a string
|
||||||
|
const id = typeof markerId === "string" ? markerId : String(markerId);
|
||||||
|
|
||||||
|
// Extract base callsign from various ID formats
|
||||||
|
if (id.startsWith("hist_")) {
|
||||||
|
// Remove hist_ prefix and any trailing index
|
||||||
|
return id.replace(/^hist_/, "").replace(/_\d+$/, "");
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTrailVisualization(baseCallsign: string, trailState: TrailState) {
|
||||||
// Remove old trail and dots
|
// Remove old trail and dots
|
||||||
if (trailState.trail) {
|
if (trailState.trail) {
|
||||||
this.trailLayer.removeLayer(trailState.trail);
|
this.trailLayer.removeLayer(trailState.trail);
|
||||||
|
|
@ -69,45 +102,25 @@ export class TrailManager {
|
||||||
const L = (window as any).L;
|
const L = (window as any).L;
|
||||||
const latLngs = trailState.positions.map((pos) => [pos.lat, pos.lng]);
|
const latLngs = trailState.positions.map((pos) => [pos.lat, pos.lng]);
|
||||||
|
|
||||||
// Create polyline for the trail
|
// Create blue polyline connecting the historical position dots
|
||||||
trailState.trail = L.polyline(latLngs, {
|
trailState.trail = L.polyline(latLngs, {
|
||||||
color: "#1E90FF",
|
color: "#1E90FF",
|
||||||
weight: 3,
|
weight: 3,
|
||||||
opacity: 0.5,
|
opacity: 0.8,
|
||||||
smoothFactor: 1,
|
smoothFactor: 1,
|
||||||
|
lineCap: "round",
|
||||||
|
lineJoin: "round",
|
||||||
|
className: "historical-trail-line",
|
||||||
}).addTo(this.trailLayer);
|
}).addTo(this.trailLayer);
|
||||||
|
|
||||||
// Create dots for each position
|
// Don't create additional dots here since historical positions are now shown as markers
|
||||||
trailState.dots = trailState.positions.map((pos, index) => {
|
trailState.dots = [];
|
||||||
const age = (Date.now() - pos.timestamp) / this.trailDuration;
|
|
||||||
const opacity = Math.max(0.4, 1 - age * 0.6);
|
|
||||||
const isCurrentPosition = index === trailState.positions.length - 1;
|
|
||||||
const radius = isCurrentPosition ? 5 : 3;
|
|
||||||
|
|
||||||
const dot = L.circleMarker([pos.lat, pos.lng], {
|
|
||||||
radius: radius,
|
|
||||||
fillColor: "#FF4500",
|
|
||||||
color: "#8B0000",
|
|
||||||
weight: 1,
|
|
||||||
opacity: opacity,
|
|
||||||
fillOpacity: opacity * 0.8,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add tooltip with timestamp
|
|
||||||
const time = new Date(pos.timestamp);
|
|
||||||
dot.bindTooltip(time.toLocaleTimeString(), {
|
|
||||||
permanent: false,
|
|
||||||
direction: "top",
|
|
||||||
className: "trail-tooltip",
|
|
||||||
});
|
|
||||||
|
|
||||||
return dot.addTo(this.trailLayer);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTrail(markerId: string) {
|
removeTrail(markerId: string) {
|
||||||
const trailState = this.trails.get(markerId);
|
const baseCallsign = this.extractBaseCallsign(markerId);
|
||||||
|
const trailState = this.trails.get(baseCallsign);
|
||||||
if (trailState) {
|
if (trailState) {
|
||||||
if (trailState.trail) {
|
if (trailState.trail) {
|
||||||
this.trailLayer.removeLayer(trailState.trail);
|
this.trailLayer.removeLayer(trailState.trail);
|
||||||
|
|
@ -115,7 +128,7 @@ export class TrailManager {
|
||||||
if (trailState.dots) {
|
if (trailState.dots) {
|
||||||
trailState.dots.forEach((dot) => this.trailLayer.removeLayer(dot));
|
trailState.dots.forEach((dot) => this.trailLayer.removeLayer(dot));
|
||||||
}
|
}
|
||||||
this.trails.delete(markerId);
|
this.trails.delete(baseCallsign);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,12 +139,21 @@ export class TrailManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupOldPositions() {
|
cleanupOldPositions() {
|
||||||
const cutoffTime = Date.now() - this.trailDuration;
|
// Don't clean up historical positions - only clean up very old live positions
|
||||||
|
const veryOldCutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 hours
|
||||||
this.trails.forEach((trailState, markerId) => {
|
this.trails.forEach((trailState, markerId) => {
|
||||||
trailState.positions = trailState.positions.filter((pos) => pos.timestamp >= cutoffTime);
|
const originalLength = trailState.positions.length;
|
||||||
|
trailState.positions = trailState.positions.filter((pos) => {
|
||||||
|
// Ensure timestamp is a number for comparison
|
||||||
|
const posTimestamp =
|
||||||
|
typeof pos.timestamp === "string" ? new Date(pos.timestamp).getTime() : pos.timestamp;
|
||||||
|
// Keep all positions newer than 24 hours (includes all historical data we care about)
|
||||||
|
return posTimestamp >= veryOldCutoff;
|
||||||
|
});
|
||||||
|
|
||||||
if (trailState.positions.length === 0) {
|
if (trailState.positions.length === 0) {
|
||||||
this.removeTrail(markerId);
|
this.removeTrail(markerId);
|
||||||
} else {
|
} else if (trailState.positions.length !== originalLength) {
|
||||||
this.updateTrailVisualization(markerId, trailState);
|
this.updateTrailVisualization(markerId, trailState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
113
assets/js/map.ts
113
assets/js/map.ts
|
|
@ -39,6 +39,8 @@ interface MarkerData {
|
||||||
historical?: boolean;
|
historical?: boolean;
|
||||||
color?: string;
|
color?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
|
is_most_recent_for_callsign?: boolean;
|
||||||
|
callsign_group?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BoundsData {
|
interface BoundsData {
|
||||||
|
|
@ -60,6 +62,8 @@ interface MarkerState {
|
||||||
symbol_code: string;
|
symbol_code: string;
|
||||||
popup?: string;
|
popup?: string;
|
||||||
historical?: boolean;
|
historical?: boolean;
|
||||||
|
is_most_recent_for_callsign?: boolean;
|
||||||
|
callsign_group?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MapEventData {
|
interface MapEventData {
|
||||||
|
|
@ -536,11 +540,41 @@ let MapAPRSMap = {
|
||||||
// Handle bulk loading of historical packets
|
// Handle bulk loading of historical packets
|
||||||
self.handleEvent("add_historical_packets", (data: { packets: MarkerData[] }) => {
|
self.handleEvent("add_historical_packets", (data: { packets: MarkerData[] }) => {
|
||||||
if (data.packets && Array.isArray(data.packets)) {
|
if (data.packets && Array.isArray(data.packets)) {
|
||||||
|
// Group packets by callsign to process them in chronological order for proper trail drawing
|
||||||
|
const packetsByCallsign = new Map<string, MarkerData[]>();
|
||||||
|
|
||||||
data.packets.forEach((packet) => {
|
data.packets.forEach((packet) => {
|
||||||
self.addMarker({
|
const callsign = packet.callsign_group || packet.callsign || packet.id;
|
||||||
...packet,
|
if (!packetsByCallsign.has(callsign)) {
|
||||||
historical: true,
|
packetsByCallsign.set(callsign, []);
|
||||||
popup: self.buildPopupContent(packet),
|
}
|
||||||
|
packetsByCallsign.get(callsign)!.push(packet);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each callsign group in chronological order (oldest first)
|
||||||
|
packetsByCallsign.forEach((packets, callsign) => {
|
||||||
|
// Sort by timestamp (oldest first) to ensure proper trail line drawing
|
||||||
|
const sortedPackets = packets.sort((a, b) => {
|
||||||
|
const timeA = a.timestamp
|
||||||
|
? typeof a.timestamp === "number"
|
||||||
|
? a.timestamp
|
||||||
|
: new Date(a.timestamp).getTime()
|
||||||
|
: 0;
|
||||||
|
const timeB = b.timestamp
|
||||||
|
? typeof b.timestamp === "number"
|
||||||
|
? b.timestamp
|
||||||
|
: new Date(b.timestamp).getTime()
|
||||||
|
: 0;
|
||||||
|
return timeA - timeB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add markers in chronological order
|
||||||
|
sortedPackets.forEach((packet) => {
|
||||||
|
self.addMarker({
|
||||||
|
...packet,
|
||||||
|
historical: true,
|
||||||
|
popup: self.buildPopupContent(packet),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -561,7 +595,7 @@ let MapAPRSMap = {
|
||||||
const markersToRemove: string[] = [];
|
const markersToRemove: string[] = [];
|
||||||
self.markers!.forEach((marker: any, id: any) => {
|
self.markers!.forEach((marker: any, id: any) => {
|
||||||
if ((marker as any)._isHistorical) {
|
if ((marker as any)._isHistorical) {
|
||||||
markersToRemove.push(id);
|
markersToRemove.push(String(id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
markersToRemove.forEach((id) => self.removeMarker(id));
|
markersToRemove.forEach((id) => self.removeMarker(id));
|
||||||
|
|
@ -643,7 +677,13 @@ let MapAPRSMap = {
|
||||||
|
|
||||||
if (positionChanged && self.trailManager) {
|
if (positionChanged && self.trailManager) {
|
||||||
// Position changed, update trail
|
// Position changed, update trail
|
||||||
self.trailManager.addPosition(data.id, lat, lng, data.timestamp || Date.now());
|
const isHistoricalDot = data.historical && !data.is_most_recent_for_callsign;
|
||||||
|
const timestamp = data.timestamp
|
||||||
|
? typeof data.timestamp === "string"
|
||||||
|
? new Date(data.timestamp).getTime()
|
||||||
|
: data.timestamp
|
||||||
|
: Date.now();
|
||||||
|
self.trailManager.addPosition(data.id, lat, lng, timestamp, isHistoricalDot);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!positionChanged && !dataChanged) {
|
if (!positionChanged && !dataChanged) {
|
||||||
|
|
@ -659,7 +699,7 @@ let MapAPRSMap = {
|
||||||
// Remove existing marker if it exists
|
// Remove existing marker if it exists
|
||||||
self.removeMarker(data.id);
|
self.removeMarker(data.id);
|
||||||
|
|
||||||
// Create marker using APRS symbol icon
|
// Create marker - use simple dot for older historical positions, APRS icon for most recent
|
||||||
const marker = L.marker([lat, lng], {
|
const marker = L.marker([lat, lng], {
|
||||||
icon: self.createMarkerIcon(data),
|
icon: self.createMarkerIcon(data),
|
||||||
});
|
});
|
||||||
|
|
@ -697,11 +737,19 @@ let MapAPRSMap = {
|
||||||
symbol_code: data.symbol_code || ">",
|
symbol_code: data.symbol_code || ">",
|
||||||
popup: data.popup,
|
popup: data.popup,
|
||||||
historical: data.historical,
|
historical: data.historical,
|
||||||
|
is_most_recent_for_callsign: data.is_most_recent_for_callsign,
|
||||||
|
callsign_group: data.callsign_group,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize trail for new marker
|
// Initialize trail for new marker - always add to trail for line drawing
|
||||||
if (self.trailManager) {
|
if (self.trailManager) {
|
||||||
self.trailManager.addPosition(data.id, lat, lng, data.timestamp || Date.now());
|
const isHistoricalDot = data.historical && !data.is_most_recent_for_callsign;
|
||||||
|
const timestamp = data.timestamp
|
||||||
|
? typeof data.timestamp === "string"
|
||||||
|
? new Date(data.timestamp).getTime()
|
||||||
|
: data.timestamp
|
||||||
|
: Date.now();
|
||||||
|
self.trailManager.addPosition(data.id, lat, lng, timestamp, isHistoricalDot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open popup if requested
|
// Open popup if requested
|
||||||
|
|
@ -710,18 +758,21 @@ let MapAPRSMap = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeMarker(id: string) {
|
removeMarker(id: string | any) {
|
||||||
const self = this as unknown as LiveViewHookContext;
|
const self = this as unknown as LiveViewHookContext;
|
||||||
const marker = self.markers!.get(id);
|
// Ensure id is a string
|
||||||
|
const markerId = typeof id === "string" ? id : String(id);
|
||||||
|
|
||||||
|
const marker = self.markers!.get(markerId);
|
||||||
if (marker) {
|
if (marker) {
|
||||||
self.markerLayer!.removeLayer(marker);
|
self.markerLayer!.removeLayer(marker);
|
||||||
self.markers!.delete(id);
|
self.markers!.delete(markerId);
|
||||||
self.markerStates!.delete(id);
|
self.markerStates!.delete(markerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove trail
|
// Remove trail
|
||||||
if (self.trailManager) {
|
if (self.trailManager) {
|
||||||
self.trailManager.removeTrail(id);
|
self.trailManager.removeTrail(markerId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -743,7 +794,13 @@ let MapAPRSMap = {
|
||||||
if (positionChanged) {
|
if (positionChanged) {
|
||||||
existingMarker.setLatLng([lat, lng]);
|
existingMarker.setLatLng([lat, lng]);
|
||||||
if (self.trailManager) {
|
if (self.trailManager) {
|
||||||
self.trailManager.addPosition(data.id, lat, lng, data.timestamp || Date.now());
|
const isHistoricalDot = data.historical && !data.is_most_recent_for_callsign;
|
||||||
|
const timestamp = data.timestamp
|
||||||
|
? typeof data.timestamp === "string"
|
||||||
|
? new Date(data.timestamp).getTime()
|
||||||
|
: data.timestamp
|
||||||
|
: Date.now();
|
||||||
|
self.trailManager.addPosition(data.id, lat, lng, timestamp, isHistoricalDot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -802,10 +859,32 @@ let MapAPRSMap = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove out-of-bounds markers
|
// Remove out-of-bounds markers
|
||||||
markersToRemove.forEach((id) => self.removeMarker(id));
|
markersToRemove.forEach((id) => self.removeMarker(String(id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
createMarkerIcon(data: MarkerData): L.DivIcon {
|
createMarkerIcon(data: MarkerData): L.DivIcon {
|
||||||
|
// For historical packets that are not the most recent for their callsign,
|
||||||
|
// show a simple red dot (only positions where lat/lon actually changed)
|
||||||
|
if (data.historical && !data.is_most_recent_for_callsign) {
|
||||||
|
const iconHtml = `<div style="
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #FF6B6B;
|
||||||
|
border: 2px solid #FFFFFF;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
box-shadow: 0 0 2px rgba(0,0,0,0.3);
|
||||||
|
" title="Historical position for ${data.callsign} (position changed)"></div>`;
|
||||||
|
|
||||||
|
return L.divIcon({
|
||||||
|
html: iconHtml,
|
||||||
|
className: "historical-dot-marker",
|
||||||
|
iconSize: [12, 12],
|
||||||
|
iconAnchor: [6, 6],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For current packets or most recent historical packets, show the full APRS symbol icon
|
||||||
const symbolTableId = data.symbol_table_id || "/";
|
const symbolTableId = data.symbol_table_id || "/";
|
||||||
const symbolCode = getValidSymbolCode(data.symbol_code, symbolTableId);
|
const symbolCode = getValidSymbolCode(data.symbol_code, symbolTableId);
|
||||||
|
|
||||||
|
|
@ -841,7 +920,7 @@ let MapAPRSMap = {
|
||||||
background-size: 512px 192px;
|
background-size: 512px 192px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
opacity: ${data.historical ? "0.7" : "1.0"};
|
opacity: ${data.historical && data.is_most_recent_for_callsign ? "0.9" : "1.0"};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
" data-symbol-table="${symbolTableId}" data-symbol-code="${symbolCode}" data-sprite-position="${x},${y}" data-expected-index="${index}" title="Symbol: ${symbolCode} (${charCode}) at row ${row}, col ${column}"></div>`;
|
" data-symbol-table="${symbolTableId}" data-symbol-code="${symbolCode}" data-sprite-position="${x},${y}" data-expected-index="${index}" title="Symbol: ${symbolCode} (${charCode}) at row ${row}, col ${column}"></div>`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -720,21 +720,94 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
now
|
now
|
||||||
)
|
)
|
||||||
|
|
||||||
socket =
|
if Enum.empty?(packets) do
|
||||||
|
# No historical packets found
|
||||||
|
socket
|
||||||
|
else
|
||||||
|
# Clear any previous historical packets from the map
|
||||||
|
socket = push_event(socket, "clear_historical_packets", %{})
|
||||||
|
|
||||||
|
# Sort packets by inserted_at to identify the most recent
|
||||||
|
# Convert NaiveDateTime to DateTime if needed for proper comparison
|
||||||
|
sorted_packets =
|
||||||
|
Enum.sort_by(
|
||||||
|
packets,
|
||||||
|
fn packet ->
|
||||||
|
case packet.inserted_at do
|
||||||
|
%NaiveDateTime{} = naive_dt ->
|
||||||
|
DateTime.from_naive!(naive_dt, "Etc/UTC")
|
||||||
|
|
||||||
|
%DateTime{} = dt ->
|
||||||
|
dt
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
DateTime.utc_now()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{:desc, DateTime}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter out packets with unchanged positions (only keep if lat/lon changed)
|
||||||
|
unique_position_packets = filter_unique_positions(sorted_packets)
|
||||||
|
|
||||||
|
# Build packet data for all positions, marking which is the most recent
|
||||||
|
packet_data_list =
|
||||||
|
unique_position_packets
|
||||||
|
|> Enum.with_index()
|
||||||
|
|> Enum.map(fn {packet, index} ->
|
||||||
|
case build_packet_data(packet) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
packet_data ->
|
||||||
|
# Generate a unique ID for this historical packet
|
||||||
|
packet_id =
|
||||||
|
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}_#{index}"
|
||||||
|
|
||||||
|
is_most_recent = index == 0
|
||||||
|
callsign = socket.assigns.callsign
|
||||||
|
|
||||||
|
packet_data
|
||||||
|
|> Map.put("id", packet_id)
|
||||||
|
|> Map.put("is_historical", true)
|
||||||
|
|> Map.put("is_most_recent_for_callsign", is_most_recent)
|
||||||
|
|> Map.put("callsign_group", callsign)
|
||||||
|
|> Map.put(
|
||||||
|
"timestamp",
|
||||||
|
case packet.inserted_at do
|
||||||
|
%NaiveDateTime{} = naive_dt ->
|
||||||
|
DateTime.to_unix(DateTime.from_naive!(naive_dt, "Etc/UTC"), :millisecond)
|
||||||
|
|
||||||
|
%DateTime{} = dt ->
|
||||||
|
DateTime.to_unix(dt, :millisecond)
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
DateTime.to_unix(DateTime.utc_now(), :millisecond)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
|
# Send all historical packets at once
|
||||||
|
socket = push_event(socket, "add_historical_packets", %{packets: packet_data_list})
|
||||||
|
|
||||||
|
# Store historical packets in assigns for reference
|
||||||
|
historical_packets_map =
|
||||||
|
packet_data_list
|
||||||
|
|> Enum.zip(unique_position_packets)
|
||||||
|
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
|
||||||
|
Map.put(acc, packet_data["id"], packet)
|
||||||
|
end)
|
||||||
|
|
||||||
assign(socket,
|
assign(socket,
|
||||||
replay_packets: packets,
|
historical_packets: historical_packets_map,
|
||||||
|
replay_packets: [],
|
||||||
replay_index: 0,
|
replay_index: 0,
|
||||||
replay_start_time: one_hour_ago,
|
replay_start_time: one_hour_ago,
|
||||||
replay_end_time: now,
|
replay_end_time: now
|
||||||
historical_packets: %{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start replay immediately if we have packets
|
|
||||||
if length(packets) > 0 do
|
|
||||||
Process.send_after(self(), :replay_next_packet, 100)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
socket
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_historical_packets_for_callsign(callsign, start_time, end_time) do
|
defp fetch_historical_packets_for_callsign(callsign, start_time, end_time) do
|
||||||
|
|
@ -746,6 +819,43 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Filter packets to only include those with unique positions (lat/lon changed)
|
||||||
|
@spec filter_unique_positions([struct()]) :: [struct()]
|
||||||
|
defp filter_unique_positions(packets) do
|
||||||
|
packets
|
||||||
|
|> Enum.reduce([], fn packet, acc ->
|
||||||
|
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||||
|
|
||||||
|
if lat && lng do
|
||||||
|
# Check if this position is different from the last position
|
||||||
|
case acc do
|
||||||
|
[] ->
|
||||||
|
# First packet, always include
|
||||||
|
[packet | acc]
|
||||||
|
|
||||||
|
[last_packet | _] ->
|
||||||
|
if position_changed?(packet, last_packet) do
|
||||||
|
[packet | acc]
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.reverse()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if position changed significantly between two packets (more than ~1 meter)
|
||||||
|
@spec position_changed?(struct(), struct()) :: boolean()
|
||||||
|
defp position_changed?(packet1, packet2) do
|
||||||
|
{lat1, lng1, _} = MapHelpers.get_coordinates(packet1)
|
||||||
|
{lat2, lng2, _} = MapHelpers.get_coordinates(packet2)
|
||||||
|
|
||||||
|
abs(lat1 - lat2) > 0.00001 || abs(lng1 - lng2) > 0.00001
|
||||||
|
end
|
||||||
|
|
||||||
defp build_packet_data(packet) do
|
defp build_packet_data(packet) do
|
||||||
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||||
callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
||||||
|
|
|
||||||
|
|
@ -594,43 +594,79 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
# Clear any previous historical packets from the map
|
# Clear any previous historical packets from the map
|
||||||
socket = push_event(socket, "clear_historical_packets", %{})
|
socket = push_event(socket, "clear_historical_packets", %{})
|
||||||
|
|
||||||
# Group packets by callsign and get only the most recent for each
|
# Group packets by callsign-ssid and process all positions
|
||||||
latest_packets_by_callsign =
|
packet_data_list =
|
||||||
historical_packets
|
historical_packets
|
||||||
|> Enum.group_by(&generate_callsign/1)
|
|> Enum.group_by(&generate_callsign/1)
|
||||||
|> Enum.map(fn {_callsign, packets} ->
|
|> Enum.flat_map(fn {callsign, packets} ->
|
||||||
# Sort by received_at descending and take the most recent
|
# Sort packets by inserted_at to identify the most recent
|
||||||
Enum.max_by(packets, & &1.received_at, DateTime)
|
# Convert NaiveDateTime to DateTime if needed for proper comparison
|
||||||
|
sorted_packets =
|
||||||
|
Enum.sort_by(
|
||||||
|
packets,
|
||||||
|
fn packet ->
|
||||||
|
case packet.inserted_at do
|
||||||
|
%NaiveDateTime{} = naive_dt ->
|
||||||
|
DateTime.from_naive!(naive_dt, "Etc/UTC")
|
||||||
|
|
||||||
|
%DateTime{} = dt ->
|
||||||
|
dt
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
DateTime.utc_now()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{:desc, DateTime}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter out packets with unchanged positions (only keep if lat/lon changed)
|
||||||
|
unique_position_packets = filter_unique_positions(sorted_packets)
|
||||||
|
|
||||||
|
# Process all packets with unique positions, marking which is the most recent
|
||||||
|
unique_position_packets
|
||||||
|
|> Enum.with_index()
|
||||||
|
|> Enum.map(fn {packet, index} ->
|
||||||
|
case build_packet_data(packet) do
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
|
||||||
|
packet_data ->
|
||||||
|
# Generate a unique ID for this historical packet
|
||||||
|
packet_id =
|
||||||
|
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}_#{index}"
|
||||||
|
|
||||||
|
is_most_recent = index == 0
|
||||||
|
|
||||||
|
packet_data
|
||||||
|
|> Map.put("id", packet_id)
|
||||||
|
|> Map.put("is_historical", true)
|
||||||
|
|> Map.put("is_most_recent_for_callsign", is_most_recent)
|
||||||
|
|> Map.put("callsign_group", callsign)
|
||||||
|
|> Map.put(
|
||||||
|
"timestamp",
|
||||||
|
case packet.inserted_at do
|
||||||
|
%NaiveDateTime{} = naive_dt ->
|
||||||
|
DateTime.to_unix(DateTime.from_naive!(naive_dt, "Etc/UTC"), :millisecond)
|
||||||
|
|
||||||
|
%DateTime{} = dt ->
|
||||||
|
DateTime.to_unix(dt, :millisecond)
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
DateTime.to_unix(DateTime.utc_now(), :millisecond)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.filter(& &1)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Build packet data for all latest packets
|
|
||||||
packet_data_list =
|
|
||||||
latest_packets_by_callsign
|
|
||||||
|> Enum.map(fn packet ->
|
|
||||||
case build_packet_data(packet) do
|
|
||||||
nil ->
|
|
||||||
nil
|
|
||||||
|
|
||||||
packet_data ->
|
|
||||||
# Generate a unique ID for this historical packet
|
|
||||||
packet_id =
|
|
||||||
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
|
|
||||||
|
|
||||||
packet_data
|
|
||||||
|> Map.put("id", packet_id)
|
|
||||||
|> Map.put("is_historical", true)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
# Remove nil values
|
|
||||||
|> Enum.filter(& &1)
|
|
||||||
|
|
||||||
# Send all historical packets at once
|
# Send all historical packets at once
|
||||||
socket = push_event(socket, "add_historical_packets", %{packets: packet_data_list})
|
socket = push_event(socket, "add_historical_packets", %{packets: packet_data_list})
|
||||||
|
|
||||||
# Store historical packets in assigns for reference
|
# Store historical packets in assigns for reference
|
||||||
historical_packets_map =
|
historical_packets_map =
|
||||||
packet_data_list
|
packet_data_list
|
||||||
|> Enum.zip(latest_packets_by_callsign)
|
|> Enum.zip(historical_packets)
|
||||||
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
|
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
|
||||||
Map.put(acc, packet_data["id"], packet)
|
Map.put(acc, packet_data["id"], packet)
|
||||||
end)
|
end)
|
||||||
|
|
@ -645,6 +681,42 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Filter packets to only include those with unique positions (lat/lon changed)
|
||||||
|
@spec filter_unique_positions([struct()]) :: [struct()]
|
||||||
|
defp filter_unique_positions(packets) do
|
||||||
|
packets
|
||||||
|
|> Enum.reduce([], fn packet, acc ->
|
||||||
|
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||||
|
|
||||||
|
if lat && lng do
|
||||||
|
case acc do
|
||||||
|
[] ->
|
||||||
|
# First packet, always include
|
||||||
|
[packet | acc]
|
||||||
|
|
||||||
|
[last_packet | _] ->
|
||||||
|
if position_changed?(packet, last_packet) do
|
||||||
|
[packet | acc]
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.reverse()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if position changed significantly between two packets (more than ~1 meter)
|
||||||
|
@spec position_changed?(struct(), struct()) :: boolean()
|
||||||
|
defp position_changed?(packet1, packet2) do
|
||||||
|
{lat1, lng1, _} = MapHelpers.get_coordinates(packet1)
|
||||||
|
{lat2, lng2, _} = MapHelpers.get_coordinates(packet2)
|
||||||
|
|
||||||
|
abs(lat1 - lat2) > 0.00001 || abs(lng1 - lng2) > 0.00001
|
||||||
|
end
|
||||||
|
|
||||||
# Fetch historical packets from the database
|
# Fetch historical packets from the database
|
||||||
@spec fetch_historical_packets(list(), DateTime.t(), DateTime.t()) :: [struct()]
|
@spec fetch_historical_packets(list(), DateTime.t(), DateTime.t()) :: [struct()]
|
||||||
defp fetch_historical_packets(bounds, start_time, end_time) do
|
defp fetch_historical_packets(bounds, start_time, end_time) do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue