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"
></script>
<script src="/static/js/address-suggestion.js"></script>
<script src="/static/js/location.js"></script>
<script src="/static/js/map-multipoint.js"></script>
<!-- ordering matters since report table depends on time-relative -->
<script src="/static/js/time-relative.js"></script>
<script src="/static/js/table-site.js"></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
// or duplicated across tile boundaries. As a result, features may appear
// multiple times in query results.
function getUniqueFeatures(nuisances, waters, comparatorProperty) {
function getUniqueFeatures(features, comparatorProperty) {
const uniqueIds = new Set();
const uniqueFeatures = [];
for (const feature of nuisances) {
for (const feature of features) {
const id = feature.properties[comparatorProperty];
if (!uniqueIds.has(id)) {
uniqueIds.add(id);
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);
}
}
@ -42,7 +33,7 @@ function getUniqueFeatures(nuisances, waters, comparatorProperty) {
}
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;
return false;
}
@ -70,25 +61,13 @@ function onLoad() {
map.addSource('tegola', {
'type': 'vector',
'tiles': [
'{{.URL.Tegola}}maps/rmo/{z}/{x}/{y}'
'{{.URL.Tegola}}maps/nidus/{z}/{x}/{y}'
]
});
map.addLayer({
'id': 'nuisance',
'id': 'feature-pool',
'source': 'tegola',
'source-layer': 'nuisance_location',
'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',
'source-layer': 'feature-pool',
'type': 'circle',
'paint': {
'circle-color': "#0D6EfD",
@ -97,53 +76,22 @@ function onLoad() {
'circle-stroke-color': "#024AB6"
}
});
map.on("idle", () => {
function _addCheckboxClick(checkbox, layer_id) {
checkbox.onclick = function(e) {
if (checkbox.checked) {
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');
function _updateSiteList() {
const features = map.queryRenderedFeatures({target: {layerId: 'feature-pool'}});
const nidus_features = features.filter((feature) => feature.source == "tegola");
//const uniqueFeatures = getUniqueFeatures(nidus_features);
// Populate features for the listing overlay.
renderReports(uniqueFeatures);
renderFeatures(nidus_features);
}
map.once("idle", _updateReports);
map.on('moveend', _updateReports);
getGeolocation({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}).then(position => {
map.jumpTo({
center: {
lng: position.coords.longitude,
lat: position.coords.latitude,
},
zoom: 14,
map.once("idle", _updateSiteList);
map.on('moveend', _updateSiteList);
});
}).catch(error => {
console.log("location error", error);
const table_site = document.querySelector('table-site');
table_site.addEventListener("row-clicked", (e) => {
//window.location = "/status/" + e.detail.reportId;
console.log("Clicked on site", e.detail);
})
});
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) => {
document.querySelector("address-input").addEventListener("suggestion-selected", (e) => {
maybeEnableLookupButton(e)
if (e.detail.type == "address") {
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);
}
function renderReports(features) {
//console.log("render reports", features);
function renderFeatures(features) {
console.log("render features", features);
const report_table = document.querySelector('report-table');
let reports = [];
const site_table = document.querySelector('table-site');
let sites = [];
for (const feature of features) {
reports.push({
address: feature.properties.address,
created: feature.properties.created,
id: feature.properties.public_id,
status: feature.properties.status,
sites.push({
address: formatAddress(feature.properties),
condition: feature.properties.condition,
id: feature.id,
type: feature.type,
});
}
report_table.reports = reports;
site_table.sites = sites;
const report_count = document.getElementById("report-count");
report_count.innerHTML = reports.length + " Sites Found";
const sites_count = document.getElementById("site-count");
sites_count.innerHTML = sites.length + " Sites Found";
}
document.addEventListener('DOMContentLoaded', onLoad);
</script>
@ -188,7 +135,7 @@ document.addEventListener('DOMContentLoaded', onLoad);
<form class="row g-3 align-items-center" action="#" id="lookup-form">
<div class="col-md-9">
<address-input
name="address-or-report"
name="address"
placeholder="Enter an address, neighborhood, or zip code"
></address-input>
</div>
@ -222,13 +169,13 @@ document.addEventListener('DOMContentLoaded', onLoad);
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>
<span class="badge bg-light text-dark" id="report-count"
<span class="badge bg-light text-dark" id="site-count"
>- Sites Found</span
>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<report-table />
<table-site />
</div>
</div>
<!--