Add basic data table and map for looking at sites
This commit is contained in:
parent
26bf8ceab9
commit
82f67bdb6c
2 changed files with 209 additions and 89 deletions
173
html/static/js/table-site.js
Normal file
173
html/static/js/table-site.js
Normal 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);
|
||||||
|
|
@ -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>
|
||||||
<!--
|
<!--
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue