Fix map color flipping
This commit is contained in:
+58
-14
@@ -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]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user