The Paragliding App integrates with multiple weather station providers to display real-time wind data on the map. Each provider has different API capabilities, coverage areas, and data retrieval strategies.
| Provider | Coverage | API Key | bbox Support | Strategy | Update Frequency |
|---|---|---|---|---|---|
| Aviation Weather Center | Global airports | No | ✅ Yes | Direct bbox query | Varies (1-60min) |
| Bureau of Meteorology (BOM) | Australia | No | ❌ No | State-based fetch | 10 minutes |
| FFVL Beacons | France/Europe | Yes | ❌ No | Global fetch-all | 5 minutes |
| Pioupiou (OpenWindMap) | Global | No | ❌ No | Global fetch-all | 20 minutes |
| NWS Observations | US only | No | ❌ No | Grid-based lookup | 10 minutes |
Implementation: aviation_weather_center_provider.dart:159
final url = Uri.parse(
'https://aviationweather.gov/api/data/metar?bbox=$bbox&format=json',
);
bbox Format: minLat,minLon,maxLat,maxLon
Benefits:
Example Request:
https://aviationweather.gov/api/data/metar?bbox=45.50,-73.60,45.60,-73.50&format=json
These providers use alternative strategies with client-side filtering.
Implementation: bom_weather_provider.dart:66
Strategy:
Code Reference:
// Determine overlapping states
final overlappingStates = _determineOverlappingStates(bounds);
// Fetch each state in parallel
final futures = overlappingStates.map((state) => _fetchStateStations(state, bounds));
final results = await Future.wait(futures);
// In-memory filtering (line 488-504)
final filtered = stations.where((station) {
return bounds.contains(LatLng(station.latitude, station.longitude));
}).toList();
Coverage Areas:
Caching Strategy:
Performance:
Implementation: ffvl_weather_provider.dart:208
Strategy:
Code Reference:
// Fetch all beacons
final beaconListUrl = Uri.parse(
'$_baseUrl?base=balises&r=list&mode=json&key=$apiKey',
);
// Calculate bbox from stations (line 706-729)
final cachedBounds = _calculateBoundsFromStations(stations);
// Early exit optimization (line 66-76)
if (_globalCache != null && !_globalCache!.beaconListExpired) {
if (!_boundsOverlap(bounds, _globalCache!.bounds)) {
return []; // Skip this provider - no overlap
}
}
// In-memory filtering (line 688-704)
final filtered = stations.where((station) {
return bounds.contains(LatLng(station.latitude, station.longitude));
}).toList();
API Endpoints:
https://data.ffvl.fr/api/?base=balises&r=list&mode=jsonhttps://data.ffvl.fr/api/?base=balises&r=releves_meteoCaching Strategy:
Performance:
Special Features:
en_maintenance == '1')Implementation: pioupiou_weather_provider.dart:199
Strategy:
Code Reference:
// Fetch all stations
final url = Uri.parse('$_baseUrl/live-with-meta/all');
// Calculate bbox from stations (line 419-442)
final cachedBounds = _calculateBoundsFromStations(stations);
// Early exit optimization (line 66-76)
if (_globalCache != null && !_globalCache!.stationListExpired) {
if (!_boundsOverlap(bounds, _globalCache!.bounds)) {
return []; // Skip this provider - no overlap
}
}
// In-memory filtering (line 401-416)
final filtered = stations.where((station) {
return bounds.contains(LatLng(station.latitude, station.longitude));
}).toList();
API Endpoint:
http://api.pioupiou.fr/v1/live-with-meta/allCaching Strategy:
Performance:
Special Features:
status.state == 'on')Implementation: nws_weather_provider.dart:297
Strategy:
/points/{lat},{lon} → Returns grid stations URLCode Reference:
// Calculate bbox center for grid lookup (line 218-220)
final centerLat = (requestedBounds.north + requestedBounds.south) / 2;
final centerLon = (requestedBounds.east + requestedBounds.west) / 2;
// Step 1: Get grid stations URL
final gridUrl = await _getGridStationsUrl(centerLat, centerLon);
// Returns: https://api.weather.gov/gridpoints/{office}/{x},{y}/stations
// Step 2: Fetch stations for grid
final allStations = await _fetchGridStations(gridUrl);
// Calculate containing bbox (line 646-665)
final containingBbox = _calculateContainingBbox(allStations);
// In-memory filtering (line 263-266)
final filtered = allStations.where((station) {
return requestedBounds.contains(LatLng(station.latitude, station.longitude));
}).toList();
Coverage Areas:
// Continental US
'CONUS': [-125.0, 24.5, -66.9, 49.6]
// Alaska (split to handle International Date Line)
'Alaska_Main': [-180.0, 51.2, -130.0, 71.4]
'Alaska_West_Aleutians': [172.0, 51.2, 180.0, 71.4]
// Hawaii
'Hawaii': [-178.3, 18.9, -154.8, 28.4]
// Caribbean territories
'Puerto_Rico': [-67.9, 17.9, -65.2, 18.5]
'US_Virgin_Islands': [-65.1, 17.7, -64.6, 18.4]
// Pacific territories
'Guam', 'Northern_Mariana_Islands', 'American_Samoa'
Caching Strategy:
Performance:
Special Features:
Providers use separate TTLs for station lists vs. measurements:
class _CacheEntry {
final List<WeatherStation> stations;
final DateTime stationListTimestamp; // 24-hour TTL
final DateTime measurementsTimestamp; // 5-20 minute TTL
bool get stationListExpired { /* ... */ }
bool get measurementsExpired { /* ... */ }
}
Benefits:
| Provider | Station List TTL | Measurements TTL |
|---|---|---|
| Aviation Weather Center | 1 hour | 1 hour |
| BOM | 24 hours | 10 minutes |
| FFVL | 24 hours | 5 minutes |
| Pioupiou | 24 hours | 20 minutes |
| NWS | 24 hours | 10 minutes |
Global providers (FFVL, Pioupiou) calculate bbox from cached stations and exit early if view doesn’t overlap:
// Check if cached bbox overlaps with requested bounds
if (_globalCache != null && !_globalCache!.beaconListExpired) {
if (!_boundsOverlap(bounds, _globalCache!.bounds)) {
return []; // Skip this provider - no overlap with view
}
}
// Check if ANY stations exist in current view
final hasStationsInView = _globalCache!.stations.any((s) =>
bounds.contains(LatLng(s.latitude, s.longitude))
);
if (!hasStationsInView) {
return []; // Skip API call - no stations to show
}
BOM fetches multiple states in parallel when they overlap with view:
final futures = overlappingStates.map((state) =>
_fetchStateStations(state, bounds)
);
final results = await Future.wait(futures);
All providers prevent duplicate simultaneous requests:
if (_pendingRequests.containsKey(cacheKey)) {
return _pendingRequests[cacheKey]!;
}
final future = _fetchFromApi();
_pendingRequests[cacheKey] = future;
try {
return await future;
} finally {
_pendingRequests.remove(cacheKey);
}
All providers normalize wind data to a common format:
class WindData {
final double speedKmh; // Wind speed in km/h
final double directionDegrees; // 0-360, where 0 = North
final double? gustsKmh; // Peak gusts in km/h (optional)
final double? precipitationMm; // Rainfall (BOM only)
final DateTime timestamp; // Observation time (UTC)
}
| Provider | Native Unit | Conversion |
|---|---|---|
| Aviation Weather Center | Knots | × 1.852 → km/h |
| BOM | km/h | No conversion |
| FFVL | km/h | No conversion |
| Pioupiou | km/h | No conversion |
| NWS | km/h | No conversion |
| Provider | Requires API Key | Configuration |
|---|---|---|
| Aviation Weather Center | No | N/A |
| BOM | No | N/A |
| FFVL | Yes | ApiKeys.ffvlApiKey |
| Pioupiou | No | N/A |
| NWS | No | N/A |
Providers handle requests outside their coverage area gracefully:
Aviation Weather Center: Returns 204 No Content (empty list)
BOM: Returns empty list if no states overlap
FFVL: Early exit if cached bbox doesn’t overlap
Pioupiou: Early exit if cached bbox doesn’t overlap
NWS: Returns 404 from /points endpoint, cached as empty
All providers use 30-second timeouts with structured logging:
final response = await http.get(url).timeout(
const Duration(seconds: 30),
onTimeout: () {
LoggingService.structured('PROVIDER_TIMEOUT', {
'duration_ms': stopwatch.elapsedMilliseconds,
});
return http.Response('{"error": "Request timeout"}', 408);
},
);
| Provider | File Path |
|---|---|
| Aviation Weather Center | lib/services/weather_providers/aviation_weather_center_provider.dart |
| BOM | lib/services/weather_providers/bom_weather_provider.dart |
| FFVL | lib/services/weather_providers/ffvl_weather_provider.dart |
| Pioupiou | lib/services/weather_providers/pioupiou_weather_provider.dart |
| NWS | lib/services/weather_providers/nws_weather_provider.dart |
| Base Interface | lib/services/weather_providers/weather_station_provider.dart |
| Registry | lib/services/weather_providers/weather_station_provider_registry.dart |
To add a new weather station provider:
WeatherStationProvider)WeatherStationProviderRegistry)