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.
6.3 KiB
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:
-- 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
locationcolumn has_weatherboolean index for fast filteringDISTINCT ONprevents duplicate SSIDs- Subquery pattern keeps distance calculation efficient
- Miles converted to meters internally (1 mile = 1609.34 meters)
Code Structure
-
Controller:
lib/aprsme_web/controllers/api/v1/weather_controller.ex- Parameter validation (lat/lon ranges, radius > 0, etc.)
- Call context function
- Render JSON response
-
Context Function: Add to
lib/aprsme/packets/prepared_queries.exget_nearby_weather_stations(lat, lon, radius_miles, opts)- Uses prepared query pattern like existing
get_nearby_stations_knn - Converts miles to meters for PostGIS
-
JSON View:
lib/aprsme_web/controllers/api/v1/weather_json.ex- Format response structure
- Handle null weather fields
- Include metadata
-
Router: Add route to
/api/v1scope inrouter.ex
Response Format
Success Response (200 OK)
{
"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
nullif 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 parameters422 Unprocessable Entity- Parameters out of valid range500 Internal Server Error- Database/server error
Error Handling & Validation
Parameter Validation
# 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
RateLimiterplug
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
# 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
lib/aprsme_web/controllers/api/v1/weather_controller.ex(new)lib/aprsme_web/controllers/api/v1/weather_json.ex(new)lib/aprsme/packets/prepared_queries.ex(add function)lib/aprsme_web/router.ex(add route)test/aprsme_web/controllers/api/v1/weather_controller_test.exs(new)