Add design doc for weather nearby API endpoint

Documents new GET /api/v1/weather/nearby endpoint for finding weather
stations within a radius of a lat/lon point. Uses PostGIS ST_DWithin
for efficient spatial queries with configurable time window and result
limits.
This commit is contained in:
Graham McIntire 2026-03-22 10:51:56 -05:00
parent f142b273d1
commit bc6fdcd9c2
No known key found for this signature in database

View file

@ -0,0 +1,220 @@
# Weather Nearby API Endpoint Design
**Date:** 2026-03-22
**Status:** Approved
## Overview
API endpoint to find weather stations within a specified radius of a geographic point, returning stations with recent weather data.
## Specification
**Endpoint:** `GET /api/v1/weather/nearby`
**Purpose:** Find weather stations within a specified radius of a geographic point.
**Query Parameters:**
- `lat` (required) - Latitude (-90 to 90)
- `lon` (required) - Longitude (-180 to 180)
- `radius` (required) - Search radius in miles (> 0, max 1000)
- `hours` (optional) - Time window for "recent" weather data in hours (default: 6, max: 168 for 7 days)
- `limit` (optional) - Maximum number of results (default: 50, max: 100)
**Example Request:**
```
GET /api/v1/weather/nearby?lat=37.7749&lon=-122.4194&radius=25&hours=6&limit=50
```
## Implementation Details
### Database Query Strategy
Use PostGIS geography type with `ST_DWithin` for efficient spatial filtering:
```sql
-- Conceptual query structure
SELECT DISTINCT ON (base_callsign) *
FROM packets
WHERE has_weather = true
AND received_at >= (NOW() - interval '6 hours')
AND ST_DWithin(
location::geography,
ST_SetSRID(ST_MakePoint(lon, lat), 4326)::geography,
radius_meters
)
ORDER BY base_callsign, received_at DESC
LIMIT 50
```
Then order the results by distance using `ST_Distance` in a subquery.
**Performance Notes:**
- Leverages existing spatial index on `location` column
- `has_weather` boolean index for fast filtering
- `DISTINCT ON` prevents duplicate SSIDs
- Subquery pattern keeps distance calculation efficient
- Miles converted to meters internally (1 mile = 1609.34 meters)
### Code Structure
1. **Controller:** `lib/aprsme_web/controllers/api/v1/weather_controller.ex`
- Parameter validation (lat/lon ranges, radius > 0, etc.)
- Call context function
- Render JSON response
2. **Context Function:** Add to `lib/aprsme/packets/prepared_queries.ex`
- `get_nearby_weather_stations(lat, lon, radius_miles, opts)`
- Uses prepared query pattern like existing `get_nearby_stations_knn`
- Converts miles to meters for PostGIS
3. **JSON View:** `lib/aprsme_web/controllers/api/v1/weather_json.ex`
- Format response structure
- Handle null weather fields
- Include metadata
4. **Router:** Add route to `/api/v1` scope in `router.ex`
## Response Format
### Success Response (200 OK)
```json
{
"data": [
{
"callsign": "N0CALL-13",
"base_callsign": "N0CALL",
"position": {
"lat": 37.7849,
"lon": -122.4094
},
"distance_miles": 0.87,
"weather": {
"temperature": 72.5,
"humidity": 65.0,
"pressure": 1013.2,
"wind_speed": 8.5,
"wind_direction": 270,
"wind_gust": 12.0,
"rain_1h": 0.0,
"rain_24h": 0.5,
"rain_since_midnight": 0.3
},
"symbol": {
"table_id": "/",
"code": "_"
},
"comment": "Davis Vantage Pro2",
"last_report": "2026-03-22T15:30:00Z"
}
],
"meta": {
"count": 12,
"params": {
"lat": 37.7749,
"lon": -122.4194,
"radius_miles": 25,
"hours": 6,
"limit": 50
}
}
}
```
**Notes:**
- Weather fields are `null` if not reported by the station
- Distance calculated as straight-line (great circle) distance
- Times in ISO 8601 UTC format
### Error Responses
- `400 Bad Request` - Invalid/missing parameters
- `422 Unprocessable Entity` - Parameters out of valid range
- `500 Internal Server Error` - Database/server error
## Error Handling & Validation
### Parameter Validation
```elixir
# Latitude: -90 to 90
validate_lat(lat) when lat >= -90 and lat <= 90
# Longitude: -180 to 180
validate_lon(lon) when lon >= -180 and lon <= 180
# Radius: > 0, reasonable max (e.g., 1000 miles)
validate_radius(radius) when radius > 0 and radius <= 1000
# Hours: 1 to 168 (7 days max)
validate_hours(hours) when hours >= 1 and hours <= 168
# Limit: 1 to 100
validate_limit(limit) when limit >= 1 and limit <= 100
```
### Error Messages
- Missing required param: `"Missing required parameter: lat"`
- Invalid format: `"Invalid latitude: must be a number between -90 and 90"`
- Out of range: `"Radius must be between 0 and 1000 miles"`
### Fallback Controller
Uses existing `AprsmeWeb.Api.V1.FallbackController` to handle:
- Validation errors → 400/422 responses
- Database errors → 500 with generic message (details logged)
- Rate limiting handled by existing `RateLimiter` plug
### Edge Cases
- No weather stations found → Return empty array with 200 OK
- Antimeridian crossing (longitude wrap) → PostGIS geography handles automatically
- Invalid callsign characters → Sanitized by database layer
- Timeout on large queries → Database query timeout (existing)
## Testing Strategy
### Unit Tests (Controller)
- Valid parameters return 200 with correct structure
- Missing required params return 400
- Out-of-range params return 422
- Parameter type coercion (string to float/int)
- Default values applied correctly
### Integration Tests (Database)
- Find stations within radius (seed test data with known positions)
- Exclude stations outside radius
- Exclude stations without recent weather data
- Exclude stations without `has_weather == true`
- Respect time window (hours parameter)
- Respect result limit
- Order by distance (closest first)
- Handle antimeridian crossing
- DISTINCT ON base_callsign (no duplicate SSIDs)
### Test Data Setup
```elixir
# Center point: 37.7749, -122.4194 (San Francisco)
# Station A: 0.5 miles away, weather 1 hour ago ✓
# Station B: 10 miles away, weather 2 hours ago ✓
# Station C: 50 miles away, weather 1 hour ago ✗ (outside radius)
# Station D: 5 miles away, weather 8 hours ago ✗ (outside time window)
# Station E: 5 miles away, no weather data ✗ (has_weather == false)
```
### Performance Tests
- Query execution time < 100ms for typical queries
- Index usage verified with EXPLAIN ANALYZE
- Large radius (1000 miles, dense area) completes reasonably
### Test File Location
- `test/aprsme_web/controllers/api/v1/weather_controller_test.exs`
## Implementation Files
1. `lib/aprsme_web/controllers/api/v1/weather_controller.ex` (new)
2. `lib/aprsme_web/controllers/api/v1/weather_json.ex` (new)
3. `lib/aprsme/packets/prepared_queries.ex` (add function)
4. `lib/aprsme_web/router.ex` (add route)
5. `test/aprsme_web/controllers/api/v1/weather_controller_test.exs` (new)