diff --git a/endpoint.go b/endpoint.go index 63246f54..6520ffb3 100644 --- a/endpoint.go +++ b/endpoint.go @@ -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") diff --git a/h3.go b/h3.go index 1162f726..ea189b8c 100644 --- a/h3.go +++ b/h3.go @@ -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) diff --git a/html.go b/html.go index a8ec9389..135a7915 100644 --- a/html.go +++ b/html.go @@ -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,12 +270,13 @@ 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, - MapboxToken: MapboxToken, - Org: org.Name.MustGet(), - RecentRequests: requests, - User: userContent, + MapData: ComponentMap{ + MapboxToken: MapboxToken, + }, + Org: org.Name.MustGet(), + RecentRequests: requests, + User: userContent, } renderOrError(w, dashboard, data) } @@ -369,11 +420,30 @@ 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, - "timeElapsed": timeElapsed, - "timeSince": timeSince, + "bigNumber": bigNumber, + "latLngDisplay": latLngDisplay, + "timeElapsed": timeElapsed, + "timeSince": timeSince, } return funcMap } diff --git a/main.go b/main.go index bead3bb1..f4de23c3 100644 --- a/main.go +++ b/main.go @@ -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)) diff --git a/strings.go b/strings.go new file mode 100644 index 00000000..077dd6b5 --- /dev/null +++ b/strings.go @@ -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 +} diff --git a/templates/cell.html b/templates/cell.html new file mode 100644 index 00000000..9e5a34f5 --- /dev/null +++ b/templates/cell.html @@ -0,0 +1,245 @@ +{{template "authenticated.html" .}} + +{{define "title"}}Dash{{end}} +{{define "extraheader"}} +{{template "map" .MapData}} + +{{end}} +{{define "content"}} +
+ +
+
+

Location Data View

+
+
+ + +
+
+
+
+
+
+
+
+
+
Approximate Address:
+

123 Main St, Anytown, ST 12345

+ +
+ +
Cell Coordinates (Hexagon):
+
+ + + {{ range $i, $cb := .CellBoundary }} + + + + + {{end}} + +
Vertex {{$i}}:{{$cb|latLngDisplay}}
+
+
+
+
+
+ + +
+ +
+ +

Mosquito Breeding Sources

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AddressSource TypeLast InspectedLast Treated
123 Main StStanding Water04/15/202304/16/2023
125 Main StCatch Basin04/15/202304/16/2023
130 Main StDrainage Ditch04/14/202304/15/2023
+
+ + +
+
+ + +

Inspections History

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Approximate AddressInspection DateTechnician Comments
123 Main St04/15/2023Found larvae in standing water near gutter downspout.
125 Main St04/15/2023Catch basin had moderate larvae activity.
130 Main St04/14/2023Drainage ditch showing signs of mosquito breeding.
+
+ + +
+
+
+ + +
+ +

Treatment History

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Approximate AddressTreatment DateInsecticide UsedTechnician Notes
123 Main St04/16/2023Bacillus thuringiensis israelensis (Bti)Applied larvicide to standing water.
125 Main St04/16/2023MethopreneTreated catch basin with long-lasting formula.
130 Main St04/15/2023Bacillus sphaericusApplied to drainage ditch, full coverage achieved.
135 Main St04/14/2023Bacillus thuringiensis israelensis (Bti)Applied to small pond area.
140 Main St04/14/2023MethopreneApplied to standing water in yard.
145 Main St04/13/2023Bacillus sphaericusTreated problem area behind property.
+
+ + +
+
+
+
+
+{{end}} diff --git a/templates/components/map.html b/templates/components/map.html new file mode 100644 index 00000000..99fbb239 --- /dev/null +++ b/templates/components/map.html @@ -0,0 +1,67 @@ +{{define "map"}} + + + + +{{end}} diff --git a/templates/dashboard.html b/templates/dashboard.html index 58778d1e..c218682f 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -5,10 +5,9 @@