diff --git a/lib/area.dart b/lib/area.dart index 5a890df..2722b9d 100644 --- a/lib/area.dart +++ b/lib/area.dart @@ -38,7 +38,7 @@ class Area { // get surface wind from nearest airport String wind = WindsCache.getWind0kFromMetar(Gps.toLatLng(position)); // get aloft wind from nearest station - String? station = WindsCache.locateNearestStation(Gps.toLatLng(position)); + var (station, dist, bearing) = WindsCache.locateNearestStation(Gps.toLatLng(position)); WindsAloft? wa = Storage().winds.get("${station}06H") as WindsAloft?; if(null != wa) { // combine surface and aloft wind @@ -55,4 +55,4 @@ class Area { } return (direction, speed); } -} \ No newline at end of file +} diff --git a/lib/geo_calculations.dart b/lib/geo_calculations.dart index 3588025..78eba5a 100644 --- a/lib/geo_calculations.dart +++ b/lib/geo_calculations.dart @@ -72,27 +72,27 @@ class GeoCalculations { String dir; double bearing = getMagneticHeading(bearingIn, declination); if ((bearing > dirDegrees) && (bearing <= (90 - dirDegrees))) { - dir = "SW of"; + dir = "SW"; } else { if ((bearing > (90 - dirDegrees)) && (bearing <= (90 + dirDegrees))) { - dir = "W of"; + dir = "W"; } else { if ((bearing > (90 + dirDegrees)) && (bearing <= (180 - dirDegrees))) { - dir = "NW of"; + dir = "NW"; } else { if ((bearing > (180 - dirDegrees)) && (bearing <= (180 + dirDegrees))) { - dir = "N of"; + dir = "N"; } else { if ((bearing > (180 + dirDegrees)) && (bearing <= (270 - dirDegrees))) { - dir = "NE of"; + dir = "NE"; } else { if ((bearing > (270 - dirDegrees)) && (bearing <= (270 + dirDegrees))) { - dir = "E of"; + dir = "E"; } else { if ((bearing > (270 + dirDegrees)) && (bearing <= (360 - dirDegrees))) { - dir = "SE of"; + dir = "SE"; } else { - dir = "S of"; + dir = "S"; } } } diff --git a/lib/longpress_screen.dart b/lib/longpress_screen.dart index 3c828a6..3234746 100644 --- a/lib/longpress_screen.dart +++ b/lib/longpress_screen.dart @@ -96,7 +96,7 @@ class LongPressScreenState extends State { LatLng ll = LatLng(Storage().position.latitude, Storage().position.longitude); double distance = geo.calculateDistance(ll, widget.destinations[0].coordinate); double bearing = geo.calculateBearing(ll, widget.destinations[0].coordinate); - String direction = ("${distance.round()} ${GeoCalculations.getGeneralDirectionFrom(bearing, Storage().area.variation)}"); + String direction = ("${distance.round()} ${Storage().units.distanceName} ${GeoCalculations.getGeneralDirectionFrom(bearing, Storage().area.variation)}"); String facility = showDestination.facilityName.length > 16 ? showDestination.facilityName.substring(0, 16) : showDestination.facilityName; List pages = List.generate(labels.length, (index) => null); String label = "$facility (${showDestination.locationID}) $direction${showDestination.elevation != null ? "; EL ${showDestination.elevation!.round()}" : ""}"; @@ -123,7 +123,7 @@ class LongPressScreenState extends State { ]); } pages[labels.indexOf("NOTAM")] = FutureBuilder(future: Storage().notam.getSync(showDestination.locationID), - builder: (context, snapshot) { // notmas are downloaded when not in cache and can be slow to download so do async + builder: (context, snapshot) { // notams are downloaded when not in cache and can be slow to download so do async if (snapshot.hasData) { return snapshot.data != null ? SingleChildScrollView(child: Padding(padding: const EdgeInsets.all(10), child:Text((snapshot.data as Notam).text))) @@ -159,13 +159,13 @@ class LongPressScreenState extends State { } Weather? winds; - String? station = WindsCache.locateNearestStation(showDestination.coordinate); + var (station, dist, stationBearing) = WindsCache.locateNearestStation(showDestination.coordinate); if(station != null) { winds = Storage().winds.get("${station}06H"); // 6HR wind if(winds != null) { WindsAloft wa = winds as WindsAloft; pages[labels.indexOf("Wind")] = ListView(children: [ - ListTile(title: Text(winds.toString())), + ListTile(title: Text('${dist.round()} ${Storage().units.distanceName} ${GeoCalculations.getGeneralDirectionFrom(stationBearing, 0)} @ ${winds.toString()}')), for((String, String) wl in wa.toList()) ListTile(leading: Text(wl.$1), title: Text(wl.$2)), ]); diff --git a/lib/time_zone.dart b/lib/time_zone.dart new file mode 100644 index 0000000..4433f3d --- /dev/null +++ b/lib/time_zone.dart @@ -0,0 +1,25 @@ + +class TimeZone { + //calculates the timestamp from the passed Zulu time in METAR format (e.g. DDHHMMZ) + //any trailing values after the Z are disregarded + //if there isn't 7 characters to create a Zulu time, or letters in first six locations, returns null + static DateTime? parseZuluTime(String s) + { + if(s.length < 7 || int.tryParse(s.substring(0,5)) == null) + { + return null; + } + DateTime now = DateTime.now().toUtc(); + DateTime expires = DateTime.utc( + now.year, + now.month, + now.day, //day + 0, + 0); + int from = int.parse(s[2]!); + int to = int.parse(s[3]!); + // if from > to then its next day + expires = expires.add(Duration(days: to < from ? 1 : 0, hours: int.parse(s[3]!.substring(0, 2)), minutes: int.parse(s[5]!.substring(0, 2)))); + return expires; + } +} diff --git a/lib/unit_conversion.dart b/lib/unit_conversion.dart index 679f25a..0e9bc68 100644 --- a/lib/unit_conversion.dart +++ b/lib/unit_conversion.dart @@ -8,6 +8,7 @@ class UnitConversion { mpsTo = 2.23694; toMps = 0.44704; knotsTo = 1.15078; + distanceName = 'mi'; } } @@ -28,4 +29,7 @@ class UnitConversion { // knots for wind. double knotsTo = 1; -} \ No newline at end of file + //name of distance unit + String distanceName = 'nm'; + +} diff --git a/lib/weather/sounding.dart b/lib/weather/sounding.dart index 25411fe..6ab70e7 100644 --- a/lib/weather/sounding.dart +++ b/lib/weather/sounding.dart @@ -7,20 +7,27 @@ import '../geo_calculations.dart'; class Sounding { - static String? _locateNearestStation(LatLng location) { + static (String?, double, double) _locateNearestStation(LatLng location) { // find distance GeoCalculations geo = GeoCalculations(); double distanceMin = double.maxFinite; String? station; + LatLng? stationLocation; for(MapEntry map in _stationMap.entries) { double distance = geo.calculateDistance(map.value, location); if(distance < distanceMin) { distanceMin = distance; station = map.key; + stationLocation = map.value; } } - return station; + double? bearing; + if(stationLocation != null) + { + bearing = geo.calculateBearing(stationLocation, location); + } + return (station, distanceMin, bearing ?? 0); } static Widget? getSoundingImage(LatLng coordinate, BuildContext context) { @@ -29,20 +36,35 @@ class Sounding { return const Center(child: Text('Error downloading the Sounding Analysis for this area.')); } - String? station = _locateNearestStation(coordinate); + var (station, dist, bearing) = _locateNearestStation(coordinate); if(null == station) { return null; } - DateTime now = DateTime.now().toUtc(); + DateTime now = DateTime.timestamp(); now = now.subtract(const Duration(hours: 1)); // 1 hour delayed on website - String hour = ((now.hour / 12).floor() * 12).toString().padLeft(2, '0'); - String year = now.year.toString().substring(2); - String day = now.day.toString().padLeft(2, '0'); - String month = now.month.toString().padLeft(2, '0'); + DateTime obsTime = DateTime.utc(now.year, now.month, now.day, (now.hour/ 12).floor() * 12); + String hour = obsTime.hour.toString().padLeft(2, '0'); + String year = obsTime.year.toString().substring(2); + String day = obsTime.day.toString().padLeft(2, '0'); + String month = obsTime.month.toString().padLeft(2, '0'); String url = "https://www.spc.noaa.gov/exper/soundings/$year$month$day${hour}_OBS/$station.gif"; + Duration timeSinceObs = DateTime.timestamp().difference(obsTime); CachedNetworkImage image = CachedNetworkImage(imageUrl: url, cacheManager: FileCacheManager().networkCacheManager, errorWidget: errorImage,); - return Container(padding: const EdgeInsets.all(10), child: - InteractiveViewer(child: Container(color: Colors.white , alignment: Alignment.center, child: image))); + return ListView( + children: [ + ListTile(title: Text("${dist.round()} ${Storage().units.distanceName} ${GeoCalculations.getGeneralDirectionFrom(bearing, 0)} @ ${station} (${timeSinceObs.inHours}:${(timeSinceObs.inMinutes % 60).toString().padLeft(2, '0')} ago)")), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: InteractiveViewer( + child: Container( + color: Colors.white, + alignment: Alignment.center, + child: image + ) + ) + ) + ] + ); } // List of station codes from the HTML area elements diff --git a/lib/weather/winds_aloft.dart b/lib/weather/winds_aloft.dart index 950d1d2..85493f9 100644 --- a/lib/weather/winds_aloft.dart +++ b/lib/weather/winds_aloft.dart @@ -47,6 +47,10 @@ class WindsAloft extends Weather { return(dir, (speed.toDouble() * Storage().units.knotsTo).round()); } + static String invertTemperature(String wind) { + return wind.replaceRange(4,4,'-'); + } + (double?, double?) getWindAtAltitude(double altitude) { // dir, speed String wHigher; String wLower; @@ -176,13 +180,13 @@ class WindsAloft extends Weather { return w24k; } else if (altitude == 30000) { - return w30k; + return invertTemperature(w30k); } else if (altitude == 34000) { - return w34k; + return invertTemperature(w34k); } else if (altitude == 39000) { - return w39k; + return invertTemperature(w39k); } return "N/A"; } @@ -242,7 +246,7 @@ class WindsAloft extends Weather { toString() { DateTime zulu = expires.toUtc(); // winds in Zulu time // boilerplate - String wind = "$station (Temps negative above 24000)\nValid till ${zulu.day .toString().padLeft(2, "0")}${zulu.hour.toString().padLeft(2, "0")}00Z"; + String wind = "$station\nValid till ${expires.hour.toString().padLeft(2, '0')}:${expires.minute.toString().padLeft(2, '0')} (${zulu.day.toString().padLeft(2, '0')}${zulu.hour.toString().padLeft(2, '0')}00Z)"; return wind; } } diff --git a/lib/weather/winds_cache.dart b/lib/weather/winds_cache.dart index e9cc2a4..1060c82 100644 --- a/lib/weather/winds_cache.dart +++ b/lib/weather/winds_cache.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:avaremp/data/weather_database_helper.dart'; import 'package:avaremp/geo_calculations.dart'; +import 'package:avaremp/time_zone.dart'; import 'package:avaremp/storage.dart'; import 'package:avaremp/weather/weather_cache.dart'; import 'package:avaremp/weather/winds_aloft.dart'; @@ -34,7 +35,7 @@ class WindsCache extends WeatherCache { double? ws; double? wd; String foreString = fore.toString().padLeft(2, "0"); // stations has fore in the name - String? station = WindsCache.locateNearestStation(coordinate); + var (station, dist, bearing) = WindsCache.locateNearestStation(coordinate); var ww = Storage().winds.get("$station${foreString}H"); (wd, ws) = WindsCache.getWindAtAltitude(altitude, ww == null ? null : ww as WindsAloft); return (wd, ws); @@ -50,12 +51,22 @@ class WindsCache extends WeatherCache { for(Uint8List datum in data) { + RegExp exp1 = RegExp("VALID\\s*([0-9]*)Z\\s*FOR USE\\s*([0-9]*)-([0-9]*)Z"); String dataString = utf8.decode(datum); // parse winds, set expire time 6 hrs in future - DateTime expires = DateTime.now().add(const Duration(hours: 6)); + DateTime? expires; List lines = dataString.split('\n'); + for (String line in lines) { + line = line.trim(); + RegExpMatch? match = exp1.firstMatch(line); + if (match != null) { + expires = TimeZone.parseZuluTime(match.toString()); + break; + } + } + bool start = false; // parse winds, first check if a new download is needed _WindsAloftProduct? p = _getWindsAloftProductType(lines); @@ -70,6 +81,9 @@ class WindsCache extends WeatherCache { if(!start) { continue; } + if(expires == null) { + continue; + } // find fore in winds. Stations are suffixes with 06H 12H or 24H for 6 12 and 24 hour forecasts String fore = p.toString().substring(p.toString().length - 3); @@ -184,19 +198,27 @@ class WindsCache extends WeatherCache { return null; } - static String? locateNearestStation(LatLng location) { + //returns station name, distance, bearing + static (String?, double, double) locateNearestStation(LatLng location) { // find distance GeoCalculations geo = GeoCalculations(); double distanceMin = double.maxFinite; String? station; + LatLng? stationLocation; for(MapEntry map in _stationMap.entries) { double distance = geo.calculateDistance(map.value, location); if(distance < distanceMin) { distanceMin = distance; station = map.key; + stationLocation = map.value; } } - return station; + double? bearing; + if(stationLocation != null) + { + bearing = geo.calculateBearing(stationLocation, location); + } + return (station, distanceMin, bearing ?? 0); } // dir, speed