Add basic data table and map for looking at sites

This commit is contained in:
Eli Ribble 2026-03-12 01:16:41 +00:00
parent 26bf8ceab9
commit 82f67bdb6c
No known key found for this signature in database
2 changed files with 209 additions and 89 deletions

View file

@ -0,0 +1,173 @@
class TableSite extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this._sites = [];
}
/**
* Set the sites data and render the table
*/
set sites(value) {
this._sites = value;
this.render();
}
/**
* Get the sites data
*/
get sites() {
return this._sites;
}
connectedCallback() {
this.render();
}
/**
* Get badge color class based on report status
*/
getConditionClass(status) {
switch (status) {
case "Reported":
return "bg-warning text-dark";
case "Assigned":
return "bg-info text-dark";
case "On-Hold":
return "bg-secondary";
case "Complete":
return "bg-success";
default:
return "bg-secondary";
}
}
render() {
// Create the styles
const style = `
<style>
:host {
display: block;
}
.table {
width: 100%;
margin-bottom: 0;
border-collapse: collapse;
}
.table-light {
background-color: #f8f9fa;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.075);
}
th, td {
padding: 0.75rem;
border-bottom: 1px solid #dee2e6;
text-align: left;
}
.clickable-row {
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.clickable-row:hover {
background-color: rgba(13, 110, 253, 0.1);
}
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.bg-danger {
background-color: #dc3545;
}
.bg-primary {
background-color: #0d6efd;
}
.bg-success {
background-color: #198754;
}
.bg-warning {
background-color: #ffc107;
}
.bg-info {
background-color: #0dcaf0;
}
.bg-secondary {
background-color: #6c757d;
}
.report-type-badge {
font-size: 0.85rem;
}
.text-dark {
color: #212529 !important;
}
</style>
`;
// Create the table
let tableHTML = `
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th scope="col">Site ID</th>
<th scope="col">Condition</th>
<th scope="col">Address</th>
</tr>
</thead>
<tbody id="report-table-body">
`;
// Generate rows for each report
if (this._sites.length > 0) {
this._sites.forEach((site) => {
tableHTML += `
<tr class="clickable-row" data-site-id="${site.id}">
<td><strong>${site.id}</strong></td>
<td>${site.condition}</td>
<td>${site.address}</td>
</tr>
`;
});
} else {
tableHTML += `
<tr><td colspan="3">No sites</td></tr>
`;
}
tableHTML += `
</tbody>
</table>
`;
// Set the shadow DOM content
this.shadowRoot.innerHTML = style + tableHTML;
// Add click handlers for the rows
this.shadowRoot.querySelectorAll("tr.clickable-row").forEach((el) => {
el.addEventListener("click", (e) => {
let element = e.target;
while (element.nodeName != "TR") {
element = element.parentElement;
}
this.dispatchEvent(
new CustomEvent("row-clicked", {
bubbles: true,
composed: true, // Allows event to cross shadow DOM boundary
detail: {
reportId: element.dataset.reportId,
},
}),
);
});
});
}
}
// Register the custom element
customElements.define("table-site", TableSite);

View file

@ -7,34 +7,25 @@
src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js" src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js"
></script> ></script>
<script src="/static/js/address-suggestion.js"></script> <script src="/static/js/address-suggestion.js"></script>
<script src="/static/js/location.js"></script>
<script src="/static/js/map-multipoint.js"></script> <script src="/static/js/map-multipoint.js"></script>
<!-- ordering matters since report table depends on time-relative --> <!-- ordering matters since report table depends on time-relative -->
<script src="/static/js/time-relative.js"></script> <script src="/static/js/time-relative.js"></script>
<script src="/static/js/table-site.js"></script> <script src="/static/js/table-site.js"></script>
<script> <script>
var markers = []; function formatAddress(props) {
return `${props.address_number} ${props.address_street} ${props.address_locality}, ${props.address_region}, ${props.address_country}`;
}
// Because features come from tiled vector data, feature geometries may be split // Because features come from tiled vector data, feature geometries may be split
// or duplicated across tile boundaries. As a result, features may appear // or duplicated across tile boundaries. As a result, features may appear
// multiple times in query results. // multiple times in query results.
function getUniqueFeatures(nuisances, waters, comparatorProperty) { function getUniqueFeatures(features, comparatorProperty) {
const uniqueIds = new Set(); const uniqueIds = new Set();
const uniqueFeatures = []; const uniqueFeatures = [];
for (const feature of nuisances) { for (const feature of features) {
const id = feature.properties[comparatorProperty]; const id = feature.properties[comparatorProperty];
if (!uniqueIds.has(id)) { if (!uniqueIds.has(id)) {
uniqueIds.add(id); uniqueIds.add(id);
let f = structuredClone(feature); let f = structuredClone(feature);
f.type = "nuisance";
uniqueFeatures.push(f);
}
}
for (const feature of waters) {
const id = feature.properties[comparatorProperty];
if (!uniqueIds.has(id)) {
uniqueIds.add(id);
let f = structuredClone(feature);
f.type = "water";
uniqueFeatures.push(f); uniqueFeatures.push(f);
} }
} }
@ -42,7 +33,7 @@ function getUniqueFeatures(nuisances, waters, comparatorProperty) {
} }
function handleLookupFormSubmit(e) { function handleLookupFormSubmit(e) {
const report_id = e.target.elements["address-or-report"].value.replace(/-/g, ""); const report_id = e.target.elements["address"].value.replace(/-/g, "");
window.location = "/status/" + report_id; window.location = "/status/" + report_id;
return false; return false;
} }
@ -70,25 +61,13 @@ function onLoad() {
map.addSource('tegola', { map.addSource('tegola', {
'type': 'vector', 'type': 'vector',
'tiles': [ 'tiles': [
'{{.URL.Tegola}}maps/rmo/{z}/{x}/{y}' '{{.URL.Tegola}}maps/nidus/{z}/{x}/{y}'
] ]
}); });
map.addLayer({ map.addLayer({
'id': 'nuisance', 'id': 'feature-pool',
'source': 'tegola', 'source': 'tegola',
'source-layer': 'nuisance_location', 'source-layer': 'feature-pool',
'type': 'circle',
'paint': {
'circle-color': "#DC4535",
'circle-radius': 7,
'circle-stroke-width': 2,
'circle-stroke-color': "#9C1C28"
}
});
map.addLayer({
'id': 'rmo_water',
'source': 'tegola',
'source-layer': 'water_location',
'type': 'circle', 'type': 'circle',
'paint': { 'paint': {
'circle-color': "#0D6EfD", 'circle-color': "#0D6EfD",
@ -97,53 +76,22 @@ function onLoad() {
'circle-stroke-color': "#024AB6" 'circle-stroke-color': "#024AB6"
} }
}); });
map.on("idle", () => { function _updateSiteList() {
function _addCheckboxClick(checkbox, layer_id) { const features = map.queryRenderedFeatures({target: {layerId: 'feature-pool'}});
checkbox.onclick = function(e) { const nidus_features = features.filter((feature) => feature.source == "tegola");
if (checkbox.checked) { //const uniqueFeatures = getUniqueFeatures(nidus_features);
map.SetLayoutProperty(layer_id, "visibility", "visible");
} else {
map.SetLayoutProperty(layer_id, "visibility", "none");
}
}
}
_addCheckboxClick(checkboxNuisance, "nuisance");
_addCheckboxClick(checkboxWater, "rmo_water");
checkboxNuisance.onclick()
checkboxWater.onclick()
});
function _updateReports() {
const nuisances = map.queryRenderedFeatures({target: {layerId: 'nuisance'}});
const waters = map.queryRenderedFeatures({target: {layerId: 'water'}});
const nidus_nuisances = nuisances.filter((feature) => feature.source == "tegola");
const nidus_waters = waters.filter((feature) => feature.source == "tegola");
const uniqueFeatures = getUniqueFeatures(nidus_nuisances, nidus_waters, 'public_id');
// Populate features for the listing overlay. // Populate features for the listing overlay.
renderReports(uniqueFeatures); renderFeatures(nidus_features);
} }
map.once("idle", _updateReports); map.once("idle", _updateSiteList);
map.on('moveend', _updateReports); map.on('moveend', _updateSiteList);
getGeolocation({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}).then(position => {
map.jumpTo({
center: {
lng: position.coords.longitude,
lat: position.coords.latitude,
},
zoom: 14,
}); });
}).catch(error => { const table_site = document.querySelector('table-site');
console.log("location error", error); table_site.addEventListener("row-clicked", (e) => {
//window.location = "/status/" + e.detail.reportId;
console.log("Clicked on site", e.detail);
}) })
}); document.querySelector("address-input").addEventListener("suggestion-selected", (e) => {
const report_table = document.querySelector('report-table');
report_table.addEventListener("row-clicked", (e) => {
window.location = "/status/" + e.detail.reportId;
})
document.querySelector("address-or-report-input").addEventListener("suggestion-selected", (e) => {
maybeEnableLookupButton(e) maybeEnableLookupButton(e)
if (e.detail.type == "address") { if (e.detail.type == "address") {
map.flyTo({ map.flyTo({
@ -155,27 +103,26 @@ function onLoad() {
}); });
} }
}); });
document.querySelector("address-or-report-input").addEventListener("input", maybeEnableLookupButton); document.querySelector("address-input").addEventListener("input", maybeEnableLookupButton);
document.getElementById("lookup-form").addEventListener("submit", handleLookupFormSubmit); document.getElementById("lookup-form").addEventListener("submit", handleLookupFormSubmit);
} }
function renderReports(features) { function renderFeatures(features) {
//console.log("render reports", features); console.log("render features", features);
const report_table = document.querySelector('report-table'); const site_table = document.querySelector('table-site');
let reports = []; let sites = [];
for (const feature of features) { for (const feature of features) {
reports.push({ sites.push({
address: feature.properties.address, address: formatAddress(feature.properties),
created: feature.properties.created, condition: feature.properties.condition,
id: feature.properties.public_id, id: feature.id,
status: feature.properties.status,
type: feature.type, type: feature.type,
}); });
} }
report_table.reports = reports; site_table.sites = sites;
const report_count = document.getElementById("report-count"); const sites_count = document.getElementById("site-count");
report_count.innerHTML = reports.length + " Sites Found"; sites_count.innerHTML = sites.length + " Sites Found";
} }
document.addEventListener('DOMContentLoaded', onLoad); document.addEventListener('DOMContentLoaded', onLoad);
</script> </script>
@ -188,7 +135,7 @@ document.addEventListener('DOMContentLoaded', onLoad);
<form class="row g-3 align-items-center" action="#" id="lookup-form"> <form class="row g-3 align-items-center" action="#" id="lookup-form">
<div class="col-md-9"> <div class="col-md-9">
<address-input <address-input
name="address-or-report" name="address"
placeholder="Enter an address, neighborhood, or zip code" placeholder="Enter an address, neighborhood, or zip code"
></address-input> ></address-input>
</div> </div>
@ -222,13 +169,13 @@ document.addEventListener('DOMContentLoaded', onLoad);
class="card-header bg-primary text-white d-flex justify-content-between align-items-center" class="card-header bg-primary text-white d-flex justify-content-between align-items-center"
> >
<h5 class="mb-0"><i class="bi bi-geo-fill me-2"></i>Sites List</h5> <h5 class="mb-0"><i class="bi bi-geo-fill me-2"></i>Sites List</h5>
<span class="badge bg-light text-dark" id="report-count" <span class="badge bg-light text-dark" id="site-count"
>- Sites Found</span >- Sites Found</span
> >
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<report-table /> <table-site />
</div> </div>
</div> </div>
<!-- <!--