From 1d414ff341e809463015d3193280f7a7c1c1e14b Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sat, 2 May 2026 04:21:38 -0400 Subject: [PATCH] feat(monitor/collectors/usgs): GeoJSON feed client (2.5_day, ms-since-epoch decoder) --- .../internal/collectors/usgs/client.go | 87 +++++++++++ .../internal/collectors/usgs/client_test.go | 43 ++++++ .../collectors/usgs/testdata/2_5_day.geojson | 141 ++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client_test.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/testdata/2_5_day.geojson diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client.go new file mode 100644 index 00000000..43f4e062 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client.go @@ -0,0 +1,87 @@ +// ©AngelaMos | 2026 +// client.go + +package usgs + +import ( + "context" + "fmt" + "time" + + "golang.org/x/time/rate" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/httpx" +) + +const ( + defaultUSGSBaseURL = "https://earthquake.usgs.gov" + pathQuakes2_5_day = "/earthquakes/feed/v1.0/summary/2.5_day.geojson" + defaultUSGSRate = 100 * time.Millisecond + defaultUSGSBurst = 5 + defaultUSGSBudget = 5 + defaultUSGSBreaker = 60 * time.Second +) + +type ClientConfig struct { + BaseURL string +} + +type Client struct { + hx *httpx.Client +} + +func NewClient(cfg ClientConfig) *Client { + if cfg.BaseURL == "" { + cfg.BaseURL = defaultUSGSBaseURL + } + return &Client{ + hx: httpx.New(httpx.Config{ + Name: "usgs", + BaseURL: cfg.BaseURL, + Rate: rate.Every(defaultUSGSRate), + Burst: defaultUSGSBurst, + ConsecutiveFailureBudget: defaultUSGSBudget, + BreakerTimeout: defaultUSGSBreaker, + }), + } +} + +type Feed struct { + Type string `json:"type"` + Features []Feature `json:"features"` +} + +type Feature struct { + Type string `json:"type"` + ID string `json:"id"` + Properties Properties `json:"properties"` + Geometry Geometry `json:"geometry"` +} + +type Properties struct { + Mag float64 `json:"mag"` + Place string `json:"place"` + Time int64 `json:"time"` + Updated int64 `json:"updated"` + Alert string `json:"alert"` + Tsunami int `json:"tsunami"` + URL string `json:"url"` + Type string `json:"type"` +} + +func (p Properties) OccurredAt() time.Time { + return time.UnixMilli(p.Time).UTC() +} + +type Geometry struct { + Type string `json:"type"` + Coordinates []float64 `json:"coordinates"` +} + +func (c *Client) Fetch(ctx context.Context) (Feed, error) { + var feed Feed + if err := c.hx.GetJSON(ctx, pathQuakes2_5_day, nil, &feed); err != nil { + return Feed{}, fmt.Errorf("fetch usgs feed: %w", err) + } + return feed, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client_test.go new file mode 100644 index 00000000..40d9d621 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/client_test.go @@ -0,0 +1,43 @@ +// ©AngelaMos | 2026 +// client_test.go + +package usgs_test + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/usgs" +) + +func TestClient_FetchDecodesFeatures(t *testing.T) { + body, err := os.ReadFile("testdata/2_5_day.geojson") + require.NoError(t, err) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/geo+json") + _, _ = w.Write(body) + })) + defer srv.Close() + + c := usgs.NewClient(usgs.ClientConfig{BaseURL: srv.URL}) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + feed, err := c.Fetch(ctx) + require.NoError(t, err) + require.GreaterOrEqual(t, len(feed.Features), 1) + + f := feed.Features[0] + require.NotEmpty(t, f.ID) + require.NotEmpty(t, f.Properties.Place) + require.GreaterOrEqual(t, f.Properties.Mag, 2.5) + require.Len(t, f.Geometry.Coordinates, 3) + require.False(t, f.Properties.OccurredAt().IsZero()) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/testdata/2_5_day.geojson b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/testdata/2_5_day.geojson new file mode 100644 index 00000000..60354c9e --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/usgs/testdata/2_5_day.geojson @@ -0,0 +1,141 @@ +{ + "type": "FeatureCollection", + "metadata": { + "generated": 1777710093000, + "url": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson", + "title": "USGS Magnitude 2.5+ Earthquakes, Past Day", + "status": 200, + "api": "2.4.0", + "count": 39 + }, + "features": [ + { + "type": "Feature", + "properties": { + "mag": 2.8, + "place": "18 km NNW of Jal, New Mexico", + "time": 1777707261749, + "updated": 1777707667040, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/us7000shr9", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/us7000shr9.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "reviewed", + "tsunami": 0, + "sig": 121, + "net": "us", + "code": "7000shr9", + "ids": ",us7000shr9,", + "sources": ",us,", + "types": ",origin,phase-data,", + "nst": 35, + "dmin": 0.255, + "rms": 0.56, + "gap": 57, + "magType": "mb_lg", + "type": "earthquake", + "title": "M 2.8 - 18 km NNW of Jal, New Mexico" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -103.2923, + 32.2604, + 5.109 + ] + }, + "id": "us7000shr9" + }, + { + "type": "Feature", + "properties": { + "mag": 2.97, + "place": "27 km SW of Esperanza, Puerto Rico", + "time": 1777705903970, + "updated": 1777707214900, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/pr71515393", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/pr71515393.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "reviewed", + "tsunami": 0, + "sig": 136, + "net": "pr", + "code": "71515393", + "ids": ",pr71515393,", + "sources": ",pr,", + "types": ",origin,phase-data,", + "nst": 15, + "dmin": 0.278, + "rms": 0.29, + "gap": 171, + "magType": "md", + "type": "earthquake", + "title": "M 3.0 - 27 km SW of Esperanza, Puerto Rico" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -65.6316666666667, + 17.898, + 7.84 + ] + }, + "id": "pr71515393" + }, + { + "type": "Feature", + "properties": { + "mag": 4.6, + "place": "79 km N of Calama, Chile", + "time": 1777702962852, + "updated": 1777704100040, + "tz": null, + "url": "https://earthquake.usgs.gov/earthquakes/eventpage/us7000shqp", + "detail": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/us7000shqp.geojson", + "felt": null, + "cdi": null, + "mmi": null, + "alert": null, + "status": "reviewed", + "tsunami": 0, + "sig": 326, + "net": "us", + "code": "7000shqp", + "ids": ",us7000shqp,", + "sources": ",us,", + "types": ",origin,phase-data,", + "nst": 58, + "dmin": 0.437, + "rms": 1.41, + "gap": 37, + "magType": "mb", + "type": "earthquake", + "title": "M 4.6 - 79 km N of Calama, Chile" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -68.7738, + -21.7539, + 115.882 + ] + }, + "id": "us7000shqp" + } + ], + "bbox": [ + -178.5514, + -57.7679, + 5.109, + 178.8233, + 61.621, + 532.819 + ] +}