feat: Add UTM coordinates #38

ref:
This commit is contained in:
Janez T
2026-04-26 08:28:41 +02:00
parent 132a576785
commit c847bd6465
4 changed files with 219 additions and 22 deletions

View File

@@ -1,3 +1,169 @@
import 'dart:math' as math;
enum CoordinateDisplayFormat { decimal, dms, utm }
String formatCoordinates(
double latitude,
double longitude,
CoordinateDisplayFormat format,
) {
switch (format) {
case CoordinateDisplayFormat.decimal:
return '${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}';
case CoordinateDisplayFormat.dms:
return '${formatDmsCoordinate(latitude, true)} ${formatDmsCoordinate(longitude, false)}';
case CoordinateDisplayFormat.utm:
return formatUtm(latitude, longitude);
}
}
String formatDmsCoordinate(double degrees, bool isLatitude) {
final direction = isLatitude
? (degrees >= 0 ? 'N' : 'S')
: (degrees >= 0 ? 'E' : 'W');
final absolute = degrees.abs();
final deg = absolute.floor();
final minDecimal = (absolute - deg) * 60;
final min = minDecimal.floor();
final sec = (minDecimal - min) * 60;
return '$deg°${min.toString().padLeft(2, '0')}\'${sec.toStringAsFixed(2).padLeft(5, '0')}"$direction';
}
String formatUtm(double latitude, double longitude) {
if (latitude < -80 || latitude > 84) {
return formatCoordinates(
latitude,
longitude,
CoordinateDisplayFormat.decimal,
);
}
final zone = _utmZone(latitude, longitude);
final band = _utmLatitudeBand(latitude);
final latRad = _degreesToRadians(latitude);
final lonRad = _degreesToRadians(longitude);
final centralMeridianRad = _degreesToRadians((zone - 1) * 6 - 180 + 3);
const semiMajorAxis = 6378137.0;
const flattening = 1 / 298.257223563;
const scaleFactor = 0.9996;
final eccentricitySquared = flattening * (2 - flattening);
final secondEccentricitySquared =
eccentricitySquared / (1 - eccentricitySquared);
final sinLat = math.sin(latRad);
final cosLat = math.cos(latRad);
final tanLat = math.tan(latRad);
final n = semiMajorAxis /
math.sqrt(1 - eccentricitySquared * sinLat * sinLat);
final t = tanLat * tanLat;
final c = secondEccentricitySquared * cosLat * cosLat;
final a = cosLat * (lonRad - centralMeridianRad);
final meridianArc = semiMajorAxis *
((1 -
eccentricitySquared / 4 -
3 * eccentricitySquared * eccentricitySquared / 64 -
5 *
eccentricitySquared *
eccentricitySquared *
eccentricitySquared /
256) *
latRad -
(3 * eccentricitySquared / 8 +
3 * eccentricitySquared * eccentricitySquared / 32 +
45 *
eccentricitySquared *
eccentricitySquared *
eccentricitySquared /
1024) *
math.sin(2 * latRad) +
(15 * eccentricitySquared * eccentricitySquared / 256 +
45 *
eccentricitySquared *
eccentricitySquared *
eccentricitySquared /
1024) *
math.sin(4 * latRad) -
(35 *
eccentricitySquared *
eccentricitySquared *
eccentricitySquared /
3072) *
math.sin(6 * latRad));
final easting = scaleFactor *
n *
(a +
(1 - t + c) * a * a * a / 6 +
(5 - 18 * t + t * t + 72 * c - 58 * secondEccentricitySquared) *
a *
a *
a *
a *
a /
120) +
500000;
var northing = scaleFactor *
(meridianArc +
n *
tanLat *
(a * a / 2 +
(5 - t + 9 * c + 4 * c * c) * a * a * a * a / 24 +
(61 -
58 * t +
t * t +
600 * c -
330 * secondEccentricitySquared) *
a *
a *
a *
a *
a *
a /
720));
if (latitude < 0) {
northing += 10000000;
}
return '$zone$band ${easting.round()}E ${northing.round()}N';
}
int _utmZone(double latitude, double longitude) {
var zone = ((longitude + 180) / 6).floor() + 1;
if (longitude == 180) {
zone = 60;
}
if (latitude >= 56 &&
latitude < 64 &&
longitude >= 3 &&
longitude < 12) {
zone = 32;
}
if (latitude >= 72 && latitude < 84) {
if (longitude >= 0 && longitude < 9) {
zone = 31;
} else if (longitude >= 9 && longitude < 21) {
zone = 33;
} else if (longitude >= 21 && longitude < 33) {
zone = 35;
} else if (longitude >= 33 && longitude < 42) {
zone = 37;
}
}
return zone.clamp(1, 60);
}
String _utmLatitudeBand(double latitude) {
const bands = 'CDEFGHJKLMNPQRSTUVWX';
final index = ((latitude + 80) / 8).floor().clamp(0, bands.length - 1);
return bands[index];
}
double _degreesToRadians(double degrees) => degrees * math.pi / 180;
String formatPlusCode(double lat, double lon) {
const base = '23456789CFGHJMPQRVWX';

View File

@@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/contact.dart';
import '../../../models/sar_marker.dart';
import '../../../utils/location_formats.dart';
import 'compass_math.dart';
/// Header component for the compass dialog showing compass rose,
@@ -563,21 +564,7 @@ class _LocationFormatToggle extends StatefulWidget {
}
class _LocationFormatToggleState extends State<_LocationFormatToggle> {
bool _showDMS = false;
String _formatDMS(double degrees, bool isLatitude) {
final direction = isLatitude
? (degrees >= 0 ? 'N' : 'S')
: (degrees >= 0 ? 'E' : 'W');
final absolute = degrees.abs();
final deg = absolute.floor();
final minDecimal = (absolute - deg) * 60;
final min = minDecimal.floor();
final sec = (minDecimal - min) * 60;
return '$deg°${min.toString().padLeft(2, '0')}\'${sec.toStringAsFixed(2).padLeft(5, '0')}"$direction';
}
CoordinateDisplayFormat _format = CoordinateDisplayFormat.decimal;
@override
Widget build(BuildContext context) {
@@ -589,20 +576,24 @@ class _LocationFormatToggleState extends State<_LocationFormatToggle> {
final l10n = AppLocalizations.of(context)!;
final String displayText;
if (_showDMS) {
displayText =
'${_formatDMS(position.latitude, true)} ${_formatDMS(position.longitude, false)}';
} else {
if (_format == CoordinateDisplayFormat.decimal) {
displayText = l10n.latLonFormat(
position.latitude.toStringAsFixed(5),
position.longitude.toStringAsFixed(5),
);
} else {
displayText = formatCoordinates(
position.latitude,
position.longitude,
_format,
);
}
return GestureDetector(
onTap: () {
setState(() {
_showDMS = !_showDMS;
_format = CoordinateDisplayFormat.values[
(_format.index + 1) % CoordinateDisplayFormat.values.length];
});
},
behavior: HitTestBehavior.opaque,

View File

@@ -4,6 +4,7 @@ import '../models/contact.dart';
import '../models/sar_marker.dart';
import '../models/sar_template.dart';
import '../l10n/app_localizations.dart';
import '../utils/location_formats.dart';
class MapMarkers {
static List<Marker> createTeamMemberMarkers(
@@ -253,7 +254,11 @@ class MapMarkers {
if (contact.displayLocation != null) ...[
_InfoRow(
'Location',
'${contact.displayLocation!.latitude.toStringAsFixed(6)}, ${contact.displayLocation!.longitude.toStringAsFixed(6)}',
formatCoordinates(
contact.displayLocation!.latitude,
contact.displayLocation!.longitude,
CoordinateDisplayFormat.utm,
),
),
],
if (contact.telemetry?.batteryMilliVolts != null)
@@ -313,7 +318,11 @@ class MapMarkers {
children: [
_InfoRow(
'Location',
'${marker.location.latitude.toStringAsFixed(6)}, ${marker.location.longitude.toStringAsFixed(6)}',
formatCoordinates(
marker.location.latitude,
marker.location.longitude,
CoordinateDisplayFormat.utm,
),
),
_InfoRow('Reported', marker.timeAgo),
if (marker.senderName != null)