diff --git a/endpoint.go b/endpoint.go
index 6520ffb3..ef32c882 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -233,9 +233,20 @@ func getSignin(w http.ResponseWriter, r *http.Request) {
errorCode := r.URL.Query().Get("error")
htmlSignin(w, errorCode)
}
+
func getSignup(w http.ResponseWriter, r *http.Request) {
htmlSignup(w, r.URL.Path)
}
+
+func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
+ globalid := chi.URLParam(r, "globalid")
+ if globalid == "" {
+ respondError(w, "No globalid provided", nil, http.StatusBadRequest)
+ return
+ }
+ htmlSource(w, r, u, globalid)
+}
+
func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) {
org_id := chi.URLParam(r, "org_id")
tileset_id := chi.URLParam(r, "tileset_id")
diff --git a/html.go b/html.go
index 6667f2c0..f3036821 100644
--- a/html.go
+++ b/html.go
@@ -32,6 +32,7 @@ var (
dashboard = newBuiltTemplate("dashboard", "authenticated")
oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated")
settings = newBuiltTemplate("settings", "authenticated")
+ source = newBuiltTemplate("source", "authenticated")
)
// Unauthenticated pages
@@ -57,7 +58,7 @@ var (
)
var components = [...]string{"header", "map"}
-type BreedingSource struct {
+type BreedingSourceSummary struct {
ID string
Type string
LastInspected *time.Time
@@ -70,17 +71,21 @@ type BuiltTemplate struct {
template *template.Template
}
+type MapMarker struct {
+ LatLng LatLng
+}
type ComponentMap struct {
Center LatLng
GeoJSON interface{}
MapboxToken string
+ Markers []MapMarker
Zoom int
}
type ContentAuthenticatedPlaceholder struct {
User User
}
type ContentCell struct {
- BreedingSources []BreedingSource
+ BreedingSources []BreedingSourceSummary
CellBoundary h3.CellBoundary
Inspections []Inspection
MapData ComponentMap
@@ -114,6 +119,11 @@ type ContentSignin struct {
InvalidCredentials bool
}
type ContentSignup struct{}
+type ContentSource struct {
+ MapData ComponentMap
+ Source *BreedingSourceDetail
+ User User
+}
type LatLng struct {
Lat float64
Lng float64
@@ -467,6 +477,47 @@ func htmlSignup(w http.ResponseWriter, path string) {
renderOrError(w, signup, data)
}
+func htmlSource(w http.ResponseWriter, r *http.Request, user *models.User, id string) {
+ org, err := user.Organization().One(r.Context(), PGInstance.BobDB)
+ if err != nil {
+ respondError(w, "Failed to get org", err, http.StatusInternalServerError)
+ return
+ }
+ userContent, err := contentForUser(r.Context(), user)
+ if err != nil {
+ respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
+ return
+ }
+ s, err := sourceByGlobalId(r.Context(), org, id)
+ if err != nil {
+ respondError(w, "Failed to get source", err, http.StatusInternalServerError)
+ return
+ }
+ data := ContentSource{
+ MapData: ComponentMap{
+ Center: LatLng{
+ Lat: s.GeometryY,
+ Lng: s.GeometryX,
+ },
+ //GeoJSON:
+ MapboxToken: MapboxToken,
+ Markers: []MapMarker{
+ MapMarker{
+ LatLng: LatLng{
+ Lat: s.GeometryY,
+ Lng: s.GeometryX,
+ },
+ },
+ },
+ Zoom: 13,
+ },
+ Source: s,
+ User: userContent,
+ }
+
+ renderOrError(w, source, data)
+}
+
func gisStatement(cb h3.CellBoundary) string {
var content strings.Builder
for i, p := range cb {
@@ -671,8 +722,8 @@ func inspectionsByCell(ctx context.Context, org *models.Organization, c h3.Cell)
}
return results, nil
}
-func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSource, error) {
- var results []BreedingSource
+func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.Cell) ([]BreedingSourceSummary, error) {
+ var results []BreedingSourceSummary
boundary, err := c.Boundary()
if err != nil {
@@ -689,7 +740,7 @@ func breedingSourcesByCell(ctx context.Context, org *models.Organization, c h3.C
return results, fmt.Errorf("Failed to query rows: %w", err)
}
for _, r := range rows {
- results = append(results, BreedingSource{
+ results = append(results, BreedingSourceSummary{
ID: r.Globalid,
LastInspected: fsTimestampToTime(r.Lastinspectdate),
LastTreated: fsTimestampToTime(r.Lasttreatdate),
@@ -706,3 +757,13 @@ func uuidShort(uuid string) string {
return uuid[:3] + "..." + uuid[len(uuid)-4:]
}
+
+func sourceByGlobalId(ctx context.Context, org *models.Organization, id string) (*BreedingSourceDetail, error) {
+ row, err := org.FSPointlocations(
+ sm.Where(models.FSPointlocations.Columns.Globalid.EQ(psql.Arg(id))),
+ ).One(ctx, PGInstance.BobDB)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get point location: %w", err)
+ }
+ return ConvertToDisplayModel(row), nil
+}
diff --git a/main.go b/main.go
index f4de23c3..adf3970b 100644
--- a/main.go
+++ b/main.go
@@ -113,6 +113,7 @@ func main() {
// Authenticated endpoints
r.Method("GET", "/cell/{cell}", NewEnsureAuth(getCellDetails))
r.Method("GET", "/settings", NewEnsureAuth(getSettings))
+ r.Method("GET", "/source/{globalid}", NewEnsureAuth(getSource))
r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles))
localFS := http.Dir("./static")
diff --git a/migrations/00015_fs_geometry.sql b/migrations/00015_fs_geometry.sql
new file mode 100644
index 00000000..383fb959
--- /dev/null
+++ b/migrations/00015_fs_geometry.sql
@@ -0,0 +1,14 @@
+-- +goose Up
+ALTER TABLE fs_pointlocation ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API
+UPDATE fs_pointlocation SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857);
+
+ALTER TABLE fs_treatment ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API
+UPDATE fs_treatment SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857);
+
+ALTER TABLE fs_mosquitoinspection ADD COLUMN geom geometry(Point, 3857); -- as specified by the ArcGIS API
+UPDATE fs_mosquitoinspection SET geom = ST_SetSRID(ST_MakePoint(geometry_x, geometry_y), 3857);
+
+-- +goose Down
+ALTER TABLE fs_pointlocation DROP COLUMN geom;
+ALTER TABLE fs_treatment DROP COLUMN geom;
+ALTER TABLE fs_mosquitoinspection DROP COLUMN geom;
diff --git a/model_conversion.go b/model_conversion.go
new file mode 100644
index 00000000..62611d01
--- /dev/null
+++ b/model_conversion.go
@@ -0,0 +1,161 @@
+package main
+
+import (
+ "github.com/Gleipnir-Technology/nidus-sync/models"
+ "github.com/aarondl/opt/null"
+ "time"
+)
+
+type BreedingSourceDetail struct {
+ // Basic Information
+ OrganizationID int32 `json:"organizationId"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ LocationNumber int64 `json:"locationNumber"`
+ ObjectID int32 `json:"objectId"`
+ GlobalID string `json:"globalId"`
+ ExternalID string `json:"externalId"`
+
+ // Status Information
+ Active bool `json:"active"`
+ DeactivateReason string `json:"deactivateReason"`
+ SourceStatus string `json:"sourceStatus"`
+ Priority string `json:"priority"`
+ ScalarPriority int64 `json:"scalarPriority"`
+
+ // Classification
+ SourceType string `json:"sourceType"`
+ Habitat string `json:"habitat"`
+ UseType string `json:"useType"`
+ WaterOrigin string `json:"waterOrigin"`
+ Symbology string `json:"symbology"`
+
+ // Geographical Data
+ X float64 `json:"x"`
+ Y float64 `json:"y"`
+ GeometryX float64 `json:"geometryX"`
+ GeometryY float64 `json:"geometryY"`
+ Zone string `json:"zone"`
+ Zone2 string `json:"zone2"`
+ Jurisdiction string `json:"jurisdiction"`
+ AccessDescription string `json:"accessDescription"`
+
+ // Inspection Data
+ LarvaeInspectInterval int16 `json:"larvaeInspectInterval"`
+ LastInspectionDate time.Time `json:"lastInspectionDate"`
+ LastInspectionActivity string `json:"lastInspectionActivity"`
+ LastInspectionActionTaken string `json:"lastInspectionActionTaken"`
+ LastInspectionAverageLarvae float64 `json:"lastInspectionAverageLarvae"`
+ LastInspectionAveragePupae float64 `json:"lastInspectionAveragePupae"`
+ LastInspectionBreeding string `json:"lastInspectionBreeding"`
+ LastInspectionConditions string `json:"lastInspectionConditions"`
+ LastInspectionFieldSpecies string `json:"lastInspectionFieldSpecies"`
+ LastInspectionLifeStages string `json:"lastInspectionLifeStages"`
+
+ // Treatment Data
+ LastTreatmentDate time.Time `json:"lastTreatmentDate"`
+ LastTreatmentActivity string `json:"lastTreatmentActivity"`
+ LastTreatmentProduct string `json:"lastTreatmentProduct"`
+ LastTreatmentQuantity float64 `json:"lastTreatmentQuantity"`
+ LastTreatmentQuantityUnit string `json:"lastTreatmentQuantityUnit"`
+
+ // Assignment & Schedule
+ AssignedTechnician string `json:"assignedTechnician"`
+ NextActionScheduledDate time.Time `json:"nextActionScheduledDate"`
+
+ // Metadata
+ Created time.Time `json:"created"`
+ Creator string `json:"creator"`
+ EditedAt time.Time `json:"editedAt"`
+ Editor string `json:"editor"`
+ Updated time.Time `json:"updated"`
+ Comments string `json:"comments"`
+}
+
+// ConvertToDisplayModel transforms the DB model into the display model
+func ConvertToDisplayModel(source *models.FSPointlocation) *BreedingSourceDetail {
+ // Helper function to convert unix timestamp to time.Time
+ toTime := func(val null.Val[int64]) time.Time {
+ v, ok := val.Get()
+ if !ok {
+ return time.UnixMilli(0)
+ }
+ t := time.UnixMilli(v)
+ return t
+ }
+
+ // Helper function to convert int16 to bool
+ toBool := func(val null.Val[int16]) bool {
+ if !val.IsValue() {
+ return false
+ }
+ b := val.MustGet() != 0
+ return b
+ }
+
+ return &BreedingSourceDetail{
+ // Basic Information
+ OrganizationID: source.OrganizationID,
+ Name: source.Name.MustGet(),
+ Description: source.Description.MustGet(),
+ LocationNumber: source.Locationnumber.GetOr(0),
+ ObjectID: source.Objectid,
+ GlobalID: source.Globalid,
+ ExternalID: source.Externalid.GetOr(""),
+
+ // Status Information
+ Active: toBool(source.Active),
+ DeactivateReason: source.DeactivateReason.GetOr(""),
+ SourceStatus: source.Sourcestatus.GetOr(""),
+ Priority: source.Priority.GetOr(""),
+ ScalarPriority: source.Scalarpriority.GetOr(0),
+
+ // Classification
+ SourceType: source.Stype.GetOr(""),
+ Habitat: source.Habitat.GetOr(""),
+ UseType: source.Usetype.GetOr(""),
+ WaterOrigin: source.Waterorigin.GetOr(""),
+ Symbology: source.Symbology.GetOr(""),
+
+ // Geographical Data
+ X: source.X.GetOr(0),
+ Y: source.Y.GetOr(0),
+ GeometryX: source.GeometryX,
+ GeometryY: source.GeometryY,
+ Zone: source.Zone.GetOr(""),
+ Zone2: source.Zone2.GetOr(""),
+ Jurisdiction: source.Jurisdiction.GetOr(""),
+ AccessDescription: source.Accessdesc.GetOr(""),
+
+ // Inspection Data
+ LarvaeInspectInterval: source.Larvinspectinterval.GetOr(0),
+ LastInspectionDate: toTime(source.Lastinspectdate),
+ LastInspectionActivity: source.Lastinspectactivity.GetOr(""),
+ LastInspectionActionTaken: source.Lastinspectactiontaken.GetOr(""),
+ LastInspectionAverageLarvae: source.Lastinspectavglarvae.GetOr(0),
+ LastInspectionAveragePupae: source.Lastinspectavgpupae.GetOr(0),
+ LastInspectionBreeding: source.Lastinspectbreeding.GetOr(""),
+ LastInspectionConditions: source.Lastinspectconditions.GetOr(""),
+ LastInspectionFieldSpecies: source.Lastinspectfieldspecies.GetOr(""),
+ LastInspectionLifeStages: source.Lastinspectlstages.GetOr(""),
+
+ // Treatment Data
+ LastTreatmentDate: toTime(source.Lasttreatdate),
+ LastTreatmentActivity: source.Lasttreatactivity.GetOr(""),
+ LastTreatmentProduct: source.Lasttreatproduct.GetOr(""),
+ LastTreatmentQuantity: source.Lasttreatqty.GetOr(0),
+ LastTreatmentQuantityUnit: source.Lasttreatqtyunit.GetOr(""),
+
+ // Assignment & Schedule
+ AssignedTechnician: source.Assignedtech.GetOr(""),
+ NextActionScheduledDate: toTime(source.Nextactiondatescheduled),
+
+ // Metadata
+ Created: toTime(source.Creationdate),
+ Creator: source.Creator.GetOr(""),
+ EditedAt: toTime(source.Editdate),
+ Editor: source.Editor.GetOr(""),
+ Updated: source.Updated,
+ Comments: source.Comments.GetOr(""),
+ }
+}
diff --git a/templates/components/map.html b/templates/components/map.html
index 99fbb239..a969cf49 100644
--- a/templates/components/map.html
+++ b/templates/components/map.html
@@ -3,6 +3,21 @@