Fix map color flipping
This commit is contained in:
+58
-14
@@ -6,6 +6,7 @@ const state = {
|
|||||||
markers: {},
|
markers: {},
|
||||||
trails: {}, // Track position trail polylines for each node
|
trails: {}, // Track position trail polylines for each node
|
||||||
nodeLastHeard: {}, // Track last heard times for flash detection
|
nodeLastHeard: {}, // Track last heard times for flash detection
|
||||||
|
nodeLastColor: {}, // Track last assigned color to prevent flipping
|
||||||
refreshIntervals: [],
|
refreshIntervals: [],
|
||||||
timeUpdateInterval: null,
|
timeUpdateInterval: null,
|
||||||
lastSuccessfulUpdate: Date.now(),
|
lastSuccessfulUpdate: Date.now(),
|
||||||
@@ -296,7 +297,7 @@ const ui = {
|
|||||||
return 0.2;
|
return 0.2;
|
||||||
},
|
},
|
||||||
|
|
||||||
getMarkerColor(lastHeardDate) {
|
getMarkerColor(lastHeardDate, nodeId) {
|
||||||
if (!lastHeardDate) return '#a50026'; // Darkest red for unknown
|
if (!lastHeardDate) return '#a50026'; // Darkest red for unknown
|
||||||
|
|
||||||
// Handle SQLite datetime format
|
// Handle SQLite datetime format
|
||||||
@@ -324,8 +325,32 @@ const ui = {
|
|||||||
// 8 buckets over 360 minutes = 45 minutes per bucket
|
// 8 buckets over 360 minutes = 45 minutes per bucket
|
||||||
const bucketSize = 360 / 8;
|
const bucketSize = 360 / 8;
|
||||||
const bucketIndex = Math.min(7, Math.floor(minutesSince / bucketSize));
|
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) {
|
getChannelName(channelNumber) {
|
||||||
@@ -877,14 +902,25 @@ function displayPositions(positions) {
|
|||||||
positions.forEach(pos => {
|
positions.forEach(pos => {
|
||||||
seenNodes.add(pos.node_id);
|
seenNodes.add(pos.node_id);
|
||||||
const opacity = ui.getMarkerOpacity(pos.last_heard || pos.timestamp);
|
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)
|
// Normalize timestamps for comparison
|
||||||
const currentLastHeard = pos.last_heard || pos.timestamp;
|
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 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;
|
state.nodeLastHeard[pos.node_id] = currentLastHeard;
|
||||||
|
|
||||||
// Add pulsing animation for newly heard nodes
|
// Add pulsing animation for newly heard nodes
|
||||||
@@ -897,6 +933,9 @@ function displayPositions(positions) {
|
|||||||
iconAnchor: [8, 8]
|
iconAnchor: [8, 8]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use last_heard for consistency (fallback to timestamp if not available)
|
||||||
|
const relevantTimestamp = pos.last_heard || pos.timestamp;
|
||||||
|
|
||||||
const popupContent = `
|
const popupContent = `
|
||||||
<div style="position: relative; min-width: 180px; padding-bottom: 8px;">
|
<div style="position: relative; min-width: 180px; padding-bottom: 8px;">
|
||||||
<b>${escapeHtml(pos.long_name || pos.short_name || pos.node_id)}</b><br>
|
<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>
|
Lat: ${pos.latitude.toFixed(6)}<br>
|
||||||
Lon: ${pos.longitude.toFixed(6)}<br>
|
Lon: ${pos.longitude.toFixed(6)}<br>
|
||||||
${pos.altitude ? `Alt: ${pos.altitude}m<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;">
|
<div style="margin-top: 8px; display: flex; gap: 8px; align-items: center;">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
onclick="event.preventDefault(); window.showNodeDetail('${pos.node_id}'); return false;"
|
onclick="event.preventDefault(); window.showNodeDetail('${pos.node_id}'); return false;"
|
||||||
@@ -1126,18 +1165,23 @@ function updateMapMarkerOpacity() {
|
|||||||
if (!timestampMatch) return;
|
if (!timestampMatch) return;
|
||||||
|
|
||||||
const timestamp = timestampMatch[1];
|
const timestamp = timestampMatch[1];
|
||||||
const opacity = ui.getMarkerOpacity(timestamp);
|
const newOpacity = ui.getMarkerOpacity(timestamp);
|
||||||
const color = ui.getMarkerColor(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 currentIcon = marker.getIcon();
|
||||||
const currentOpacity = currentIcon?.options?.html?.match(/opacity: ([\d.]+)/)?.[1];
|
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({
|
const icon = L.divIcon({
|
||||||
className: 'custom-marker',
|
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],
|
iconSize: [16, 16],
|
||||||
iconAnchor: [8, 8]
|
iconAnchor: [8, 8]
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user