Fix map color flipping

This commit is contained in:
Will Bradley
2025-10-12 05:28:20 -07:00
parent 6a2148ad14
commit 74f41c5044
+58 -14
View File
@@ -6,6 +6,7 @@ const state = {
markers: {},
trails: {}, // Track position trail polylines for each node
nodeLastHeard: {}, // Track last heard times for flash detection
nodeLastColor: {}, // Track last assigned color to prevent flipping
refreshIntervals: [],
timeUpdateInterval: null,
lastSuccessfulUpdate: Date.now(),
@@ -296,7 +297,7 @@ const ui = {
return 0.2;
},
getMarkerColor(lastHeardDate) {
getMarkerColor(lastHeardDate, nodeId) {
if (!lastHeardDate) return '#a50026'; // Darkest red for unknown
// Handle SQLite datetime format
@@ -324,8 +325,32 @@ const ui = {
// 8 buckets over 360 minutes = 45 minutes per bucket
const bucketSize = 360 / 8;
const bucketIndex = Math.min(7, Math.floor(minutesSince / bucketSize));
const newColor = colors[bucketIndex];
return colors[bucketIndex];
// Apply hysteresis: only change color if we're at least 2 minutes into the new bucket
// This prevents flipping at boundaries
if (nodeId && state.nodeLastColor[nodeId]) {
const lastColor = state.nodeLastColor[nodeId];
const lastIndex = colors.indexOf(lastColor);
// If we're in a different bucket, check if we're far enough in
if (lastIndex !== -1 && lastIndex !== bucketIndex) {
const minutesIntoBucket = minutesSince % bucketSize;
const minutesFromBoundary = Math.min(minutesIntoBucket, bucketSize - minutesIntoBucket);
// Need to be at least 2 minutes away from boundary to change color
if (minutesFromBoundary < 2) {
return lastColor; // Keep the old color
}
}
}
// Store the color for this node
if (nodeId) {
state.nodeLastColor[nodeId] = newColor;
}
return newColor;
},
getChannelName(channelNumber) {
@@ -877,14 +902,25 @@ function displayPositions(positions) {
positions.forEach(pos => {
seenNodes.add(pos.node_id);
const opacity = ui.getMarkerOpacity(pos.last_heard || pos.timestamp);
const color = ui.getMarkerColor(pos.last_heard || pos.timestamp);
const color = ui.getMarkerColor(pos.last_heard || pos.timestamp, pos.node_id);
// Check if node is newly heard (last_heard changed recently)
const currentLastHeard = pos.last_heard || pos.timestamp;
// Normalize timestamps for comparison
const normalizeTimestamp = (ts) => {
if (!ts) return null;
let dateStr = ts;
if (ts.includes(' ') && !ts.includes('T')) {
dateStr = ts.replace(' ', 'T') + 'Z';
}
return new Date(dateStr).getTime();
};
// Check if node is newly heard (last_heard changed in the last 10 seconds)
const currentLastHeard = normalizeTimestamp(pos.last_heard || pos.timestamp);
const previousLastHeard = state.nodeLastHeard[pos.node_id];
const isNewlyHeard = previousLastHeard && currentLastHeard !== previousLastHeard;
const timeSinceUpdate = currentLastHeard - (previousLastHeard || 0);
const isNewlyHeard = previousLastHeard && timeSinceUpdate > 0 && timeSinceUpdate < 10000;
// Update tracked last heard time
// Update tracked last heard time (store normalized timestamp)
state.nodeLastHeard[pos.node_id] = currentLastHeard;
// Add pulsing animation for newly heard nodes
@@ -897,6 +933,9 @@ function displayPositions(positions) {
iconAnchor: [8, 8]
});
// Use last_heard for consistency (fallback to timestamp if not available)
const relevantTimestamp = pos.last_heard || pos.timestamp;
const popupContent = `
<div style="position: relative; min-width: 180px; padding-bottom: 8px;">
<b>${escapeHtml(pos.long_name || pos.short_name || pos.node_id)}</b><br>
@@ -904,7 +943,7 @@ function displayPositions(positions) {
Lat: ${pos.latitude.toFixed(6)}<br>
Lon: ${pos.longitude.toFixed(6)}<br>
${pos.altitude ? `Alt: ${pos.altitude}m<br>` : ''}
<span class="relative-time" data-timestamp="${pos.timestamp}">${ui.formatRelativeTime(pos.timestamp)}</span>
<span class="relative-time" data-timestamp="${relevantTimestamp}">${ui.formatRelativeTime(relevantTimestamp)}</span>
<div style="margin-top: 8px; display: flex; gap: 8px; align-items: center;">
<a href="#"
onclick="event.preventDefault(); window.showNodeDetail('${pos.node_id}'); return false;"
@@ -1126,18 +1165,23 @@ function updateMapMarkerOpacity() {
if (!timestampMatch) return;
const timestamp = timestampMatch[1];
const opacity = ui.getMarkerOpacity(timestamp);
const color = ui.getMarkerColor(timestamp);
const newOpacity = ui.getMarkerOpacity(timestamp);
const newColor = ui.getMarkerColor(timestamp, nodeId);
// Update marker icon with new opacity and color (only if it changed significantly)
// Get current icon properties
const currentIcon = marker.getIcon();
const currentOpacity = currentIcon?.options?.html?.match(/opacity: ([\d.]+)/)?.[1];
const currentColor = currentIcon?.options?.html?.match(/background-color: ([^;]+);/)?.[1];
const currentColor = currentIcon?.options?.html?.match(/background-color: ([#\w]+)/)?.[1];
if (!currentOpacity || Math.abs(parseFloat(currentOpacity) - opacity) > 0.05 || currentColor !== color) {
// Only update icon if opacity changed significantly (>0.1) OR color bucket changed
const opacityChanged = !currentOpacity || Math.abs(parseFloat(currentOpacity) - newOpacity) > 0.1;
const colorChanged = currentColor !== newColor;
// Only update if there's a meaningful change (avoid flashing on minor updates)
if (opacityChanged || colorChanged) {
const icon = L.divIcon({
className: 'custom-marker',
html: `<div style="opacity: ${opacity}; background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
html: `<div style="opacity: ${newOpacity}; background-color: ${newColor}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});