Added Line Of Sight Feature for repeater placement, Added app wide Units Setting (#198)

* feat: add LOS workflow, global units, l10n cleanup, and mobile UI overflow fixes

Squashes prior PR commits into one changeset including: LOS map/service/tests, global metric/imperial unit system adoption, notification/BLE safety fixes, app-wide localization backfill/mojibake cleanup, and responsive UI title/overflow hardening.

* l10n: revert unrelated locale churn for LOS feature

* feat: keep LOS with app-wide unit settings

* fix: resolve post-merge app bar/import analyzer errors

* style: format screen files for CI
This commit is contained in:
just_stuff_tm
2026-02-21 01:08:23 -05:00
committed by GitHub
parent d2b693e5ce
commit f4b18d97a1
52 changed files with 6078 additions and 214 deletions
@@ -0,0 +1,72 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/services/line_of_sight_service.dart';
void main() {
List<LatLng> makePoints(int count) {
return List<LatLng>.generate(count, (i) => LatLng(0, i * 0.00001));
}
test('computeFromElevations reports clear LOS on flat terrain', () {
final points = makePoints(21);
final elevations = List<double>.filled(points.length, 100);
final result = LineOfSightService.computeFromElevations(
points: points,
elevations: elevations,
startAntennaHeightMeters: 2,
endAntennaHeightMeters: 2,
);
expect(result.hasData, isTrue);
expect(result.isClear, isTrue);
expect(result.maxObstructionMeters, equals(0));
expect(result.firstObstructionDistanceMeters, isNull);
});
test(
'computeFromElevations reports blocked LOS with central obstruction',
() {
final points = makePoints(21);
final elevations = List<double>.filled(points.length, 100);
elevations[10] = 300;
final result = LineOfSightService.computeFromElevations(
points: points,
elevations: elevations,
startAntennaHeightMeters: 1.5,
endAntennaHeightMeters: 1.5,
);
expect(result.hasData, isTrue);
expect(result.isClear, isFalse);
expect(result.maxObstructionMeters, greaterThan(0));
expect(result.firstObstructionDistanceMeters, isNotNull);
},
);
test('analyzePath summarizes clear and blocked segments', () async {
final service = LineOfSightService(
elevationDataSource: (points) async {
final elevations = List<double?>.filled(points.length, 100);
if (points.first.longitude > 0.00005) {
elevations[elevations.length ~/ 2] = 300;
}
return elevations;
},
);
final path = [
const LatLng(0, 0),
const LatLng(0, 0.0001),
const LatLng(0, 0.0002),
];
final result = await service.analyzePath(path);
expect(result.segments.length, 2);
expect(result.clearSegments, 1);
expect(result.blockedSegments, 1);
expect(result.unknownSegments, 0);
});
}