mirror of
https://github.com/zvx-echo6/central.git
synced 2026-05-22 02:24:38 +02:00
feat: full events feed UX iteration
A. Color-code polygons by adapter with legend B. Click popup on polygons with "View details" link C. Viewport-driven spatial filter - pan/zoom updates table via HTMX Map never auto-fits after initial load (user controls viewport) D. Expandable row details showing full event data payload Changes: - _events_rows.html: add data-event-id, expand button, detail row - events_list.html: eventLayerGroup pattern, buildPopup, rebindEventLayers Fit to results button, expand/collapse handlers, CSS.escape for IDs
This commit is contained in:
parent
2852f4c850
commit
d17a97dd7e
2 changed files with 227 additions and 143 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
<table class="events-table">
|
<table class="events-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width: 2rem;"></th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
<th>Adapter</th>
|
<th>Adapter</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
|
|
@ -17,18 +18,36 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
<tr data-row-idx="{{ loop.index0 }}"
|
<tr class="event-row" data-row-idx="{{ loop.index0 }}"
|
||||||
|
data-event-id="{{ event.id }}"
|
||||||
data-adapter="{{ event.adapter }}"
|
data-adapter="{{ event.adapter }}"
|
||||||
data-category="{{ event.category }}"
|
data-category="{{ event.category }}"
|
||||||
data-time="{{ event.time }}"
|
data-time="{{ event.time }}"
|
||||||
data-subject="{{ event.subject or '' }}"
|
data-subject="{{ event.subject or '' }}"
|
||||||
{% if event.geometry %}data-geometry='{{ event.geometry | tojson }}'{% endif %}>
|
{% if event.geometry %}data-geometry='{{ event.geometry | tojson }}'{% endif %}>
|
||||||
|
<td><button type="button" class="expand-row" aria-label="Expand">▸</button></td>
|
||||||
<td>{{ event.time }}</td>
|
<td>{{ event.time }}</td>
|
||||||
<td>{{ event.adapter }}</td>
|
<td>{{ event.adapter }}</td>
|
||||||
<td>{{ event.category }}</td>
|
<td>{{ event.category }}</td>
|
||||||
<td>{{ event.geometry_summary }}</td>
|
<td>{{ event.geometry_summary }}</td>
|
||||||
<td>{{ event.subject or '—' }}</td>
|
<td>{{ event.subject or '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="event-detail" hidden>
|
||||||
|
<td colspan="6">
|
||||||
|
<dl class="event-detail-list">
|
||||||
|
<dt>Event ID</dt>
|
||||||
|
<dd><code>{{ event.id }}</code></dd>
|
||||||
|
<dt>Received</dt>
|
||||||
|
<dd>{{ event.received }}</dd>
|
||||||
|
{% if event.regions %}
|
||||||
|
<dt>Regions</dt>
|
||||||
|
<dd>{{ event.regions | join(", ") }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
<dt>Data</dt>
|
||||||
|
<dd><pre class="event-data-pre">{{ event.data | tojson(indent=2) }}</pre></dd>
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,15 @@
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
border-radius: var(--pico-border-radius);
|
border-radius: var(--pico-border-radius);
|
||||||
}
|
}
|
||||||
|
.map-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
.map-legend {
|
.map-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.map-legend-item {
|
.map-legend-item {
|
||||||
|
|
@ -27,19 +32,60 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid rgba(0,0,0,0.2);
|
border: 1px solid rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
#fit-to-results {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
.events-table {
|
.events-table {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.events-table td {
|
.events-table td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.events-table tr:hover {
|
.events-table tr.event-row:hover {
|
||||||
background-color: var(--pico-primary-focus);
|
background-color: var(--pico-primary-focus);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.events-table tr.highlighted {
|
.events-table tr.event-row.highlighted {
|
||||||
background-color: var(--pico-primary-background);
|
background-color: var(--pico-primary-background);
|
||||||
}
|
}
|
||||||
|
.expand-row {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--pico-color);
|
||||||
|
}
|
||||||
|
.expand-row:hover {
|
||||||
|
color: var(--pico-primary);
|
||||||
|
}
|
||||||
|
.event-detail td {
|
||||||
|
background-color: var(--pico-card-background-color);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.event-detail-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.event-detail-list dt {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--pico-muted-color);
|
||||||
|
}
|
||||||
|
.event-detail-list dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.event-data-pre {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background: var(--pico-code-background-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--pico-border-radius);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.filter-form .grid {
|
.filter-form .grid {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
@ -115,19 +161,22 @@
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div id="events-map"></div>
|
<div id="events-map"></div>
|
||||||
<div class="map-legend">
|
<div class="map-controls">
|
||||||
<div class="map-legend-item">
|
<div class="map-legend">
|
||||||
<div class="map-legend-swatch" style="background-color: #f59e0b;"></div>
|
<div class="map-legend-item">
|
||||||
<span>NWS (Weather)</span>
|
<div class="map-legend-swatch" style="background-color: #f59e0b;"></div>
|
||||||
</div>
|
<span>NWS (Weather)</span>
|
||||||
<div class="map-legend-item">
|
</div>
|
||||||
<div class="map-legend-swatch" style="background-color: #dc2626;"></div>
|
<div class="map-legend-item">
|
||||||
<span>FIRMS (Fire)</span>
|
<div class="map-legend-swatch" style="background-color: #dc2626;"></div>
|
||||||
</div>
|
<span>FIRMS (Fire)</span>
|
||||||
<div class="map-legend-item">
|
</div>
|
||||||
<div class="map-legend-swatch" style="background-color: #7c3aed;"></div>
|
<div class="map-legend-item">
|
||||||
<span>USGS (Quake)</span>
|
<div class="map-legend-swatch" style="background-color: #7c3aed;"></div>
|
||||||
|
<span>USGS (Quake)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" id="fit-to-results" class="outline secondary">Fit map to results</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="events-rows">
|
<div id="events-rows">
|
||||||
|
|
@ -159,10 +208,11 @@
|
||||||
maxZoom: 18
|
maxZoom: 18
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Layer groups for event geometries
|
// Layer group for event geometries
|
||||||
var eventLayers = {};
|
var eventLayerGroup = L.layerGroup().addTo(map);
|
||||||
var highlightedRow = null;
|
var highlightedRow = null;
|
||||||
var highlightedLayer = null;
|
var highlightedLayer = null;
|
||||||
|
var isInitialLoad = true;
|
||||||
|
|
||||||
// Region input elements
|
// Region input elements
|
||||||
var northInput = document.getElementById("region_north");
|
var northInput = document.getElementById("region_north");
|
||||||
|
|
@ -170,21 +220,10 @@
|
||||||
var eastInput = document.getElementById("region_east");
|
var eastInput = document.getElementById("region_east");
|
||||||
var westInput = document.getElementById("region_west");
|
var westInput = document.getElementById("region_west");
|
||||||
|
|
||||||
// Flag to distinguish programmatic vs user map moves
|
// Viewport-driven filter with debounce
|
||||||
var programmaticMove = false;
|
|
||||||
var viewportDebounceTimer = null;
|
var viewportDebounceTimer = null;
|
||||||
var isInitialLoad = true;
|
|
||||||
|
|
||||||
// Viewport-driven filter
|
|
||||||
map.on("moveend", function() {
|
map.on("moveend", function() {
|
||||||
if (programmaticMove) {
|
|
||||||
programmaticMove = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isInitialLoad) {
|
|
||||||
isInitialLoad = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (viewportDebounceTimer) clearTimeout(viewportDebounceTimer);
|
if (viewportDebounceTimer) clearTimeout(viewportDebounceTimer);
|
||||||
viewportDebounceTimer = setTimeout(applyViewportFilter, 400);
|
viewportDebounceTimer = setTimeout(applyViewportFilter, 400);
|
||||||
});
|
});
|
||||||
|
|
@ -200,18 +239,29 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadEventGeometries(fitBounds) {
|
function buildPopup(row) {
|
||||||
// Clear existing event layers
|
var adapter = row.dataset.adapter || "";
|
||||||
for (var key in eventLayers) {
|
var category = row.dataset.category || "";
|
||||||
map.removeLayer(eventLayers[key].layer);
|
var time = row.dataset.time ? new Date(row.dataset.time).toLocaleString() : "";
|
||||||
delete eventLayers[key];
|
var subject = row.dataset.subject || "";
|
||||||
|
var eventId = row.dataset.eventId || "";
|
||||||
|
|
||||||
|
var html = "<strong>" + adapter + "</strong><br>" +
|
||||||
|
category + "<br>" +
|
||||||
|
"<small>" + time + "</small>";
|
||||||
|
if (subject) {
|
||||||
|
html += "<br><em>" + subject + "</em>";
|
||||||
}
|
}
|
||||||
|
html += '<br><a href="#" data-show-row="' + eventId + '">View details ▾</a>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
var rows = document.querySelectorAll("#events-rows tr[data-geometry]");
|
function rebindEventLayers() {
|
||||||
var bounds = L.latLngBounds();
|
eventLayerGroup.clearLayers();
|
||||||
var hasGeometries = false;
|
|
||||||
|
|
||||||
rows.forEach(function(row, idx) {
|
var rows = document.querySelectorAll("#events-rows tr.event-row[data-geometry]");
|
||||||
|
|
||||||
|
rows.forEach(function(row) {
|
||||||
var geomStr = row.dataset.geometry;
|
var geomStr = row.dataset.geometry;
|
||||||
if (!geomStr || geomStr === "") return;
|
if (!geomStr || geomStr === "") return;
|
||||||
|
|
||||||
|
|
@ -222,15 +272,13 @@
|
||||||
var adapter = row.dataset.adapter || "";
|
var adapter = row.dataset.adapter || "";
|
||||||
var color = getAdapterColor(adapter);
|
var color = getAdapterColor(adapter);
|
||||||
|
|
||||||
var style = {
|
|
||||||
color: color,
|
|
||||||
weight: 2,
|
|
||||||
fillColor: color,
|
|
||||||
fillOpacity: 0.25
|
|
||||||
};
|
|
||||||
|
|
||||||
var layer = L.geoJSON(geom, {
|
var layer = L.geoJSON(geom, {
|
||||||
style: style,
|
style: {
|
||||||
|
color: color,
|
||||||
|
weight: 2,
|
||||||
|
fillColor: color,
|
||||||
|
fillOpacity: 0.25
|
||||||
|
},
|
||||||
pointToLayer: function(feature, latlng) {
|
pointToLayer: function(feature, latlng) {
|
||||||
return L.circleMarker(latlng, {
|
return L.circleMarker(latlng, {
|
||||||
radius: 8,
|
radius: 8,
|
||||||
|
|
@ -242,136 +290,153 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind popup with event details
|
layer.bindPopup(buildPopup(row));
|
||||||
var time = row.dataset.time ? new Date(row.dataset.time).toLocaleString() : "";
|
layer.on("click", function() {
|
||||||
var category = row.dataset.category || "";
|
highlightRow(row, layer, color);
|
||||||
var subject = row.dataset.subject || "";
|
|
||||||
var popupContent = "<strong>" + adapter + "</strong><br>" +
|
|
||||||
category + "<br>" +
|
|
||||||
"<small>" + time + "</small>";
|
|
||||||
if (subject) {
|
|
||||||
popupContent += "<br><em>" + subject + "</em>";
|
|
||||||
}
|
|
||||||
layer.bindPopup(popupContent);
|
|
||||||
|
|
||||||
layer.addTo(map);
|
|
||||||
eventLayers[idx] = { layer: layer, color: color };
|
|
||||||
|
|
||||||
try {
|
|
||||||
bounds.extend(layer.getBounds());
|
|
||||||
hasGeometries = true;
|
|
||||||
} catch (e) {
|
|
||||||
if (geom.type === "Point" && geom.coordinates) {
|
|
||||||
bounds.extend(L.latLng(geom.coordinates[1], geom.coordinates[0]));
|
|
||||||
hasGeometries = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.on("click", function(e) {
|
|
||||||
highlightRow(row, eventLayers[idx]);
|
|
||||||
L.DomEvent.stopPropagation(e);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
layer.addTo(eventLayerGroup);
|
||||||
|
|
||||||
|
// Store reference for row highlighting
|
||||||
|
row._mapLayer = layer;
|
||||||
|
row._mapColor = color;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error parsing geometry:", e);
|
console.error("Error parsing geometry:", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasGeometries && fitBounds) {
|
|
||||||
programmaticMove = true;
|
|
||||||
map.fitBounds(bounds.pad(0.1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightRow(row, entry) {
|
function highlightRow(row, layer, originalColor) {
|
||||||
|
// Reset previous highlight
|
||||||
if (highlightedRow) {
|
if (highlightedRow) {
|
||||||
highlightedRow.classList.remove("highlighted");
|
highlightedRow.classList.remove("highlighted");
|
||||||
}
|
}
|
||||||
if (highlightedLayer) {
|
if (highlightedLayer && highlightedLayer._originalColor) {
|
||||||
for (var key in eventLayers) {
|
highlightedLayer.setStyle({
|
||||||
if (eventLayers[key].layer === highlightedLayer) {
|
color: highlightedLayer._originalColor,
|
||||||
highlightedLayer.setStyle({
|
weight: 2,
|
||||||
color: eventLayers[key].color,
|
fillColor: highlightedLayer._originalColor,
|
||||||
weight: 2,
|
fillOpacity: 0.25
|
||||||
fillColor: eventLayers[key].color,
|
});
|
||||||
fillOpacity: 0.25
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set new highlight
|
||||||
row.classList.add("highlighted");
|
row.classList.add("highlighted");
|
||||||
highlightedRow = row;
|
highlightedRow = row;
|
||||||
|
|
||||||
if (entry && entry.layer) {
|
if (layer) {
|
||||||
entry.layer.setStyle({
|
layer._originalColor = originalColor;
|
||||||
|
layer.setStyle({
|
||||||
color: "#ff3333",
|
color: "#ff3333",
|
||||||
weight: 4,
|
weight: 4,
|
||||||
fillColor: "#ff3333",
|
fillColor: "#ff3333",
|
||||||
fillOpacity: 0.4
|
fillOpacity: 0.4
|
||||||
});
|
});
|
||||||
highlightedLayer = entry.layer;
|
highlightedLayer = layer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachRowHandlers() {
|
function fitToAllLayers() {
|
||||||
var rows = document.querySelectorAll("#events-rows tr[data-row-idx]");
|
var layers = eventLayerGroup.getLayers();
|
||||||
rows.forEach(function(row) {
|
if (layers.length === 0) return;
|
||||||
var idx = parseInt(row.dataset.rowIdx);
|
var group = L.featureGroup(layers);
|
||||||
row.addEventListener("click", function() {
|
map.fitBounds(group.getBounds(), { padding: [20, 20] });
|
||||||
var entry = eventLayers[idx];
|
|
||||||
if (entry) {
|
|
||||||
highlightRow(row, entry);
|
|
||||||
try {
|
|
||||||
programmaticMove = true;
|
|
||||||
map.fitBounds(entry.layer.getBounds().pad(0.2));
|
|
||||||
} catch (e) {
|
|
||||||
var geomStr = row.dataset.geometry;
|
|
||||||
if (geomStr) {
|
|
||||||
var geom = JSON.parse(geomStr);
|
|
||||||
if (geom && geom.type === "Point" && geom.coordinates) {
|
|
||||||
programmaticMove = true;
|
|
||||||
map.setView([geom.coordinates[1], geom.coordinates[0]], 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
row.addEventListener("mouseenter", function() {
|
|
||||||
var entry = eventLayers[idx];
|
|
||||||
if (entry && entry.layer !== highlightedLayer) {
|
|
||||||
entry.layer.setStyle({
|
|
||||||
color: entry.color,
|
|
||||||
weight: 3,
|
|
||||||
fillColor: entry.color,
|
|
||||||
fillOpacity: 0.25
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
row.addEventListener("mouseleave", function() {
|
|
||||||
var entry = eventLayers[idx];
|
|
||||||
if (entry && entry.layer !== highlightedLayer) {
|
|
||||||
entry.layer.setStyle({
|
|
||||||
color: entry.color,
|
|
||||||
weight: 2,
|
|
||||||
fillColor: entry.color,
|
|
||||||
fillOpacity: 0.25
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEventGeometries(true);
|
// Row click handler (event delegation)
|
||||||
attachRowHandlers();
|
document.addEventListener("click", function(e) {
|
||||||
|
// Expand/collapse handler
|
||||||
|
if (e.target.classList.contains("expand-row")) {
|
||||||
|
var row = e.target.closest("tr");
|
||||||
|
var detail = row.nextElementSibling;
|
||||||
|
if (detail && detail.classList.contains("event-detail")) {
|
||||||
|
detail.hidden = !detail.hidden;
|
||||||
|
e.target.innerHTML = detail.hidden ? "▸" : "▾";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.body.addEventListener("htmx:afterSwap", function(evt) {
|
// Popup "View details" link handler
|
||||||
if (evt.detail.target.id === "events-rows") {
|
if (e.target.matches("[data-show-row]")) {
|
||||||
loadEventGeometries(true);
|
e.preventDefault();
|
||||||
attachRowHandlers();
|
var eventId = e.target.dataset.showRow;
|
||||||
|
var targetRow = document.querySelector(
|
||||||
|
'tr[data-event-id="' + CSS.escape(eventId) + '"]'
|
||||||
|
);
|
||||||
|
if (!targetRow) return;
|
||||||
|
targetRow.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
var detail = targetRow.nextElementSibling;
|
||||||
|
if (detail && detail.classList.contains("event-detail")) {
|
||||||
|
detail.hidden = false;
|
||||||
|
var button = targetRow.querySelector(".expand-row");
|
||||||
|
if (button) button.innerHTML = "▾";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row click to highlight and pan
|
||||||
|
var row = e.target.closest("tr.event-row");
|
||||||
|
if (row && row._mapLayer) {
|
||||||
|
highlightRow(row, row._mapLayer, row._mapColor);
|
||||||
|
try {
|
||||||
|
map.fitBounds(row._mapLayer.getBounds(), { padding: [50, 50] });
|
||||||
|
} catch (err) {
|
||||||
|
// Point geometries
|
||||||
|
var geomStr = row.dataset.geometry;
|
||||||
|
if (geomStr) {
|
||||||
|
var geom = JSON.parse(geomStr);
|
||||||
|
if (geom && geom.type === "Point" && geom.coordinates) {
|
||||||
|
map.setView([geom.coordinates[1], geom.coordinates[0]], 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Row hover handlers (event delegation)
|
||||||
|
document.addEventListener("mouseenter", function(e) {
|
||||||
|
var row = e.target.closest && e.target.closest("tr.event-row");
|
||||||
|
if (row && row._mapLayer && row._mapLayer !== highlightedLayer) {
|
||||||
|
row._mapLayer.setStyle({
|
||||||
|
color: row._mapColor,
|
||||||
|
weight: 3,
|
||||||
|
fillColor: row._mapColor,
|
||||||
|
fillOpacity: 0.25
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.addEventListener("mouseleave", function(e) {
|
||||||
|
var row = e.target.closest && e.target.closest("tr.event-row");
|
||||||
|
if (row && row._mapLayer && row._mapLayer !== highlightedLayer) {
|
||||||
|
row._mapLayer.setStyle({
|
||||||
|
color: row._mapColor,
|
||||||
|
weight: 2,
|
||||||
|
fillColor: row._mapColor,
|
||||||
|
fillOpacity: 0.25
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Fit to results button
|
||||||
|
document.getElementById("fit-to-results").addEventListener("click", fitToAllLayers);
|
||||||
|
|
||||||
|
// Initial load - bind layers and fit bounds
|
||||||
|
rebindEventLayers();
|
||||||
|
if (isInitialLoad) {
|
||||||
|
fitToAllLayers();
|
||||||
|
isInitialLoad = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-bind layers after HTMX swap (but do NOT fit bounds)
|
||||||
|
document.body.addEventListener("htmx:afterSwap", function(evt) {
|
||||||
|
if (evt.detail.target.id === "events-rows") {
|
||||||
|
rebindEventLayers();
|
||||||
|
// Do NOT call fitToAllLayers - preserve user viewport
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix map rendering after container shows
|
||||||
setTimeout(function() { map.invalidateSize(); }, 100);
|
setTimeout(function() { map.invalidateSize(); }, 100);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue