Add cell debug page.

This makes it much easier to troubleshoot information related to a cell
by showing detailed data about a single cell. At this point much is a
placeholder, but we at least get the cell boundary coordinates and a
map.

This also starts to make some code common around doing things like
mapping.
This commit is contained in:
Eli Ribble 2025-11-19 15:21:06 +00:00
parent 7c2d7eef25
commit c0b527c9a3
No known key found for this signature in database
9 changed files with 465 additions and 48 deletions

View file

@ -36,6 +36,21 @@ func getArcgisOauthCallback(w http.ResponseWriter, r *http.Request) {
}
http.Redirect(w, r, BaseURL+"/", http.StatusFound)
}
func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) {
cell_str := chi.URLParam(r, "cell")
if cell_str == "" {
respondError(w, "There should always be a cell", nil, http.StatusBadRequest)
return
}
cell, err := HexToInt64(cell_str)
if err != nil {
respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest)
return
}
htmlCell(r.Context(), w, user, cell)
}
func getFavicon(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "image/x-icon")

39
h3.go
View file

@ -9,27 +9,20 @@ import (
"github.com/uber/h3-go/v4"
)
func h3Indexes() []h3.Cell {
//[] uint64{0x852a134ffffffff})
/*result := make([]h3.H3Index, 0)
for _, v := range values {
result = append(result, v)
}
return result*/
return []h3.Cell{
0x8629aab2fffffff,
0x8629a8627ffffff,
0x8629a8607ffffff,
0x8629a8717ffffff,
0x8629a8617ffffff,
0x8629a8407ffffff,
0x8629a871fffffff,
0x8629a845fffffff,
0x8629aab27ffffff,
0x8629a84e7ffffff,
/*
func h3ToBoundsGeoJSON(c h3.Cell) (string, error) {
b, err := h3.CellToBoundary(c)
if err != nil {
respondError(w, "Failed to get cell boundary", err, http.StatusInternalServerError)
return
}
features, err := geojson2h3.ToFeatureCollection(b)
if err != nil {
return "", fmt.Errorf("Failed to convert boundary to
}
func h3ToGeoJSON(indexes []h3.Cell) (string, error) {
*/
func h3ToGeoJSON(indexes []h3.Cell) (interface{}, error) {
featureCollection, err := geojson2h3.ToFeatureCollection(indexes)
if err != nil {
return "", fmt.Errorf("Failed to get feature collection: %w", err)
@ -37,14 +30,6 @@ func h3ToGeoJSON(indexes []h3.Cell) (string, error) {
return featureCollection.JSON(), nil
}
func sampleGeoJSON() (string, error) {
indexes := h3Indexes()
featureCollection, err := geojson2h3.ToFeatureCollection(indexes)
if err != nil {
return "", fmt.Errorf("Failed to get feature collection: %w", err)
}
return featureCollection.JSON(), nil
}
func main2() {
resolution := 9
object, err := geojson.Parse(`{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"shape":"Polygon","name":"Unnamed Layer","category":"default"},"geometry":{"type":"Polygon","coordinates":[[[-73.901303,40.756892],[-73.893924,40.743755],[-73.871476,40.756278],[-73.863378,40.764175],[-73.871444,40.768467],[-73.879852,40.760014],[-73.885515,40.764045],[-73.891522,40.761054],[-73.901303,40.756892]]]},"id":"a6ca1b7e-9ddf-4425-ad07-8a895f7d6ccf"}]}`, nil)

88
html.go
View file

@ -19,6 +19,7 @@ import (
"github.com/aarondl/opt/null"
//"github.com/riverqueue/river/rivershared/util/slogutil"
"github.com/stephenafamo/bob/dialect/psql/sm"
"github.com/uber/h3-go/v4"
)
//go:embed templates/*
@ -26,6 +27,7 @@ var embeddedFiles embed.FS
// Authenticated pages
var (
cell = newBuiltTemplate("cell", "authenticated")
dashboard = newBuiltTemplate("dashboard", "authenticated")
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
settings = newBuiltTemplate("settings", "authenticated")
@ -52,7 +54,7 @@ var (
signin = newBuiltTemplate("signin", "base")
signup = newBuiltTemplate("signup", "base")
)
var components = [...]string{"header"}
var components = [...]string{"header", "map"}
type BuiltTemplate struct {
files []string
@ -60,13 +62,20 @@ type BuiltTemplate struct {
template *template.Template
}
type Link struct {
Href string
Title string
type ComponentMap struct {
Center LatLng
GeoJSON interface{}
MapboxToken string
Zoom int
}
type ContentAuthenticatedPlaceholder struct {
User User
}
type ContentCell struct {
CellBoundary h3.CellBoundary
MapData ComponentMap
User User
}
type ContentPhoneCall struct {
DistrictName string
}
@ -82,8 +91,8 @@ type ContentDashboard struct {
CountMosquitoSources int
CountServiceRequests int
Geo template.JS
MapboxToken string
LastSync *time.Time
MapData ComponentMap
Org string
RecentRequests []ServiceRequestSummary
User User
@ -94,6 +103,14 @@ type ContentSignin struct {
InvalidCredentials bool
}
type ContentSignup struct{}
type LatLng struct {
Lat float64
Lng float64
}
type Link struct {
Href string
Title string
}
type ServiceRequestSummary struct {
Date time.Time
Location string
@ -165,12 +182,45 @@ func extractInitials(name string) string {
return initials.String()
}
func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
geo, err := sampleGeoJSON()
func htmlCell(ctx context.Context, w http.ResponseWriter, user *models.User, c int64) {
userContent, err := contentForUser(ctx, user)
if err != nil {
respondError(w, "Failed to get geo", err, http.StatusInternalServerError)
respondError(w, "Failed to get user", err, http.StatusInternalServerError)
return
}
center, err := h3.Cell(c).LatLng()
if err != nil {
respondError(w, "Failed to get center", err, http.StatusInternalServerError)
return
}
boundary, err := h3.Cell(c).Boundary()
if err != nil {
respondError(w, "Failed to get boundary", err, http.StatusInternalServerError)
return
}
geojson, err := h3ToGeoJSON([]h3.Cell{h3.Cell(c)})
if err != nil {
respondError(w, "Failed to get boundaries", err, http.StatusInternalServerError)
return
}
resolution := h3.Cell(c).Resolution()
data := ContentCell{
CellBoundary: boundary,
MapData: ComponentMap{
Center: LatLng{
Lat: center.Lat,
Lng: center.Lng,
},
GeoJSON: geojson,
MapboxToken: MapboxToken,
Zoom: resolution + 5,
},
User: userContent,
}
renderOrError(w, cell, &data)
}
func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
org, err := user.Organization().One(ctx, PGInstance.BobDB)
if err != nil {
respondError(w, "Failed to get org", err, http.StatusInternalServerError)
@ -220,9 +270,10 @@ func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User
CountInspections: int(inspectionCount),
CountMosquitoSources: int(sourceCount),
CountServiceRequests: int(serviceCount),
Geo: template.JS(geo),
LastSync: lastSync,
MapData: ComponentMap{
MapboxToken: MapboxToken,
},
Org: org.Name.MustGet(),
RecentRequests: requests,
User: userContent,
@ -369,9 +420,28 @@ func htmlSignup(w http.ResponseWriter, path string) {
renderOrError(w, signup, data)
}
func latLngDisplay(ll h3.LatLng) string {
latDir := "N"
latVal := ll.Lat
if ll.Lat < 0 {
latDir = "S"
latVal = -ll.Lat
}
lngDir := "E"
lngVal := ll.Lng
if ll.Lng < 0 {
lngDir = "W"
lngVal = -ll.Lng
}
return fmt.Sprintf("%.4f° %s, %.4f° %s", latVal, latDir, lngVal, lngDir)
}
func makeFuncMap() template.FuncMap {
funcMap := template.FuncMap{
"bigNumber": bigNumber,
"latLngDisplay": latLngDisplay,
"timeElapsed": timeElapsed,
"timeSince": timeSince,
}

View file

@ -111,6 +111,7 @@ func main() {
r.Post("/signup", postSignup)
// Authenticated endpoints
r.Method("GET", "/cell/{cell}", NewEnsureAuth(getCellDetails))
r.Method("GET", "/settings", NewEnsureAuth(getSettings))
r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles))

34
strings.go Normal file
View file

@ -0,0 +1,34 @@
package main
import (
"strconv"
"strings"
)
func HexToInt64(hexStr string) (int64, error) {
// Remove "0x" prefix if present
hexStr = strings.TrimPrefix(hexStr, "0x")
hexStr = strings.TrimPrefix(hexStr, "0X")
// Parse hex string to uint64
value, err := strconv.ParseInt(hexStr, 16, 64)
if err != nil {
return 0, err
}
return value, nil
}
func HexToUint64(hexStr string) (uint64, error) {
// Remove "0x" prefix if present
hexStr = strings.TrimPrefix(hexStr, "0x")
hexStr = strings.TrimPrefix(hexStr, "0X")
// Parse hex string to uint64
value, err := strconv.ParseUint(hexStr, 16, 64)
if err != nil {
return 0, err
}
return value, nil
}

245
templates/cell.html Normal file
View file

@ -0,0 +1,245 @@
{{template "authenticated.html" .}}
{{define "title"}}Dash{{end}}
{{define "extraheader"}}
{{template "map" .MapData}}
<style>
.address-container {
display: flex;
flex-direction: column;
justify-content: center;
}
.section-header {
margin-top: 30px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
</style>
{{end}}
{{define "content"}}
<div class="container mt-4 mb-5">
<!-- Location Header Section -->
<div class="row mb-4">
<div class="col-12">
<h1>Location Data View</h1>
</div>
</div>
<!-- Map and Address Section - Side by Side -->
<div class="row mb-4">
<div class="col-md-8">
<div class="map-container">
<div id="map"></div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body address-container">
<h5>Approximate Address:</h5>
<p class="lead" id="location-address">123 Main St, Anytown, ST 12345</p>
<hr class="location-divider">
<h5>Cell Coordinates (Hexagon):</h5>
<div class="table-responsive">
<table class="coordinates-table">
<tbody>
{{ range $i, $cb := .CellBoundary }}
<tr>
<td><strong>Vertex {{$i}}:</strong></td>
<td>{{$cb|latLngDisplay}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Two-Column Layout for Tables -->
<div class="row">
<!-- Left Column -->
<div class="col-md-6">
<!-- Breeding Sources Section -->
<h2 class="section-header">Mosquito Breeding Sources</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Address</th>
<th>Source Type</th>
<th>Last Inspected</th>
<th>Last Treated</th>
</tr>
</thead>
<tbody>
<tr>
<td>123 Main St</td>
<td>Standing Water</td>
<td>04/15/2023</td>
<td>04/16/2023</td>
</tr>
<tr>
<td>125 Main St</td>
<td>Catch Basin</td>
<td>04/15/2023</td>
<td>04/16/2023</td>
</tr>
<tr>
<td>130 Main St</td>
<td>Drainage Ditch</td>
<td>04/14/2023</td>
<td>04/15/2023</td>
</tr>
</tbody>
</table>
</div>
<nav aria-label="Breeding sources pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- Inspections Section -->
<h2 class="section-header">Inspections History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Approximate Address</th>
<th>Inspection Date</th>
<th>Technician Comments</th>
</tr>
</thead>
<tbody>
<tr>
<td>123 Main St</td>
<td>04/15/2023</td>
<td>Found larvae in standing water near gutter downspout.</td>
</tr>
<tr>
<td>125 Main St</td>
<td>04/15/2023</td>
<td>Catch basin had moderate larvae activity.</td>
</tr>
<tr>
<td>130 Main St</td>
<td>04/14/2023</td>
<td>Drainage ditch showing signs of mosquito breeding.</td>
</tr>
</tbody>
</table>
</div>
<nav aria-label="Inspections pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- Right Column -->
<div class="col-md-6">
<!-- Treatments Section -->
<h2 class="section-header">Treatment History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Approximate Address</th>
<th>Treatment Date</th>
<th>Insecticide Used</th>
<th>Technician Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>123 Main St</td>
<td>04/16/2023</td>
<td>Bacillus thuringiensis israelensis (Bti)</td>
<td>Applied larvicide to standing water.</td>
</tr>
<tr>
<td>125 Main St</td>
<td>04/16/2023</td>
<td>Methoprene</td>
<td>Treated catch basin with long-lasting formula.</td>
</tr>
<tr>
<td>130 Main St</td>
<td>04/15/2023</td>
<td>Bacillus sphaericus</td>
<td>Applied to drainage ditch, full coverage achieved.</td>
</tr>
<tr>
<td>135 Main St</td>
<td>04/14/2023</td>
<td>Bacillus thuringiensis israelensis (Bti)</td>
<td>Applied to small pond area.</td>
</tr>
<tr>
<td>140 Main St</td>
<td>04/14/2023</td>
<td>Methoprene</td>
<td>Applied to standing water in yard.</td>
</tr>
<tr>
<td>145 Main St</td>
<td>04/13/2023</td>
<td>Bacillus sphaericus</td>
<td>Treated problem area behind property.</td>
</tr>
</tbody>
</table>
</div>
<nav aria-label="Treatments pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -0,0 +1,67 @@
{{define "map"}}
<script src='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' />
<script>
const geojson = JSON.parse({{.GeoJSON}})
function onLoad() {
console.log("Setting up the map...", geojson);
mapboxgl.accessToken = {{ .MapboxToken }};
const map = new mapboxgl.Map({
container: "map",
center: [{{.Center.Lng}}, {{.Center.Lat}}],
style: 'mapbox://styles/mapbox/streets-v12', // style URL
zoom: {{.Zoom}},
});
map.on("load", function() {
console.log("Map post-load...");
const sourceId = 'h3-hexes';
const layerId = 'h3-hexes-layer';
let source = map.getSource(sourceId);
if (!source) {
map.addSource(sourceId, {
type: 'geojson',
data: geojson
});
map.addLayer({
id: layerId,
source: sourceId,
type: 'fill',
interactive: false,
paint: {
'fill-color': '#F00000',
'fill-opacity': 0.3
}
});
source = map.getSource(sourceId);
}
source.setData(geojson);
console.log("Map post-load done.");
});
console.log("Map init done.");
}
window.addEventListener("load", onLoad);
</script>
<style>
.map-container {
background-color: #e9ecef;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
height: 500px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
}
#map {
height: 500px;
width:100%;
margin-bottom: 10px;
}
#map img {
max-width: none;
min-width: 0px;
height: auto;
}
</style>
{{end}}

View file

@ -5,10 +5,9 @@
<script src='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' />
<script>
const geojson = {{.Geo}};
function onLoad() {
console.log("Setting up the map...", geojson);
mapboxgl.accessToken = {{ .MapboxToken }};
mapboxgl.accessToken = {{ .MapData.MapboxToken }};
const map = new mapboxgl.Map({
container: 'map', // container ID
style: 'mapbox://styles/mapbox/standard', // style URL
@ -67,10 +66,11 @@ function onLoad() {
const properties = e.feature.properties;
//console.log("Coordinates", coordinates[0]);
//console.log("Properties", properties.cell, properties.count_);
new mapboxgl.Popup()
/*new mapboxgl.Popup()
.setLngLat(coordinates[0][0])
.setHTML("Cell: " + properties.cell)
.addTo(map);
.addTo(map);*/
window.location.href = '/cell/' + properties.cell;
}
});
map.addInteraction('nidus-mouseenter-interaction', {

View file

@ -1,7 +1,7 @@
{{template "authenticated.html" .}}
{{define "title"}}Dash{{end}}
{{define "style"}}
{{define "extraheader"}}
{{end}}
{{define "content"}}
{{end}}