mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-03-21 18:33:51 +00:00
Sort nodes by squared distance from viewport center before applying the render limit, so visible nodes always make the cut instead of arbitrary selection causing gaps that shift as you pan. Also: inject node provider for testability, deduplicate validity filter, and reduce debug log spam to state transitions only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
165 lines
5.3 KiB
Dart
165 lines
5.3 KiB
Dart
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
import 'package:deflockapp/models/osm_node.dart';
|
|
import 'package:deflockapp/app_state.dart';
|
|
import 'package:deflockapp/widgets/map/map_data_manager.dart';
|
|
|
|
void main() {
|
|
OsmNode nodeAt(int id, double lat, double lng) {
|
|
return OsmNode(id: id, coord: LatLng(lat, lng), tags: {'surveillance': 'outdoor'});
|
|
}
|
|
|
|
group('Node render prioritization', () {
|
|
late MapDataManager dataManager;
|
|
late List<OsmNode> testNodes;
|
|
|
|
setUp(() {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
testNodes = [];
|
|
dataManager = MapDataManager(
|
|
getNodesForBounds: (_) => testNodes,
|
|
);
|
|
});
|
|
|
|
test('closest nodes to viewport center are kept', () {
|
|
final bounds = LatLngBounds(LatLng(38.0, -78.0), LatLng(39.0, -77.0));
|
|
// Center is (38.5, -77.5)
|
|
testNodes = [
|
|
nodeAt(1, 38.9, -77.9), // far from center
|
|
nodeAt(2, 38.5, -77.5), // at center
|
|
nodeAt(3, 38.1, -77.1), // far from center
|
|
nodeAt(4, 38.51, -77.49), // very close to center
|
|
nodeAt(5, 38.0, -78.0), // corner — farthest
|
|
];
|
|
|
|
final result = dataManager.getNodesForRendering(
|
|
currentZoom: 14,
|
|
mapBounds: bounds,
|
|
uploadMode: UploadMode.production,
|
|
maxNodes: 3,
|
|
);
|
|
|
|
expect(result.isLimitActive, isTrue);
|
|
expect(result.nodesToRender.length, 3);
|
|
final ids = result.nodesToRender.map((n) => n.id).toSet();
|
|
expect(ids.contains(2), isTrue, reason: 'Node at center should be kept');
|
|
expect(ids.contains(4), isTrue, reason: 'Node near center should be kept');
|
|
expect(ids.contains(5), isFalse, reason: 'Node at corner should be dropped');
|
|
});
|
|
|
|
test('returns all nodes when under the limit', () {
|
|
final bounds = LatLngBounds(LatLng(38.0, -78.0), LatLng(39.0, -77.0));
|
|
testNodes = [
|
|
nodeAt(1, 38.5, -77.5),
|
|
nodeAt(2, 38.6, -77.6),
|
|
];
|
|
|
|
final result = dataManager.getNodesForRendering(
|
|
currentZoom: 14,
|
|
mapBounds: bounds,
|
|
uploadMode: UploadMode.production,
|
|
maxNodes: 10,
|
|
);
|
|
|
|
expect(result.isLimitActive, isFalse);
|
|
expect(result.nodesToRender.length, 2);
|
|
});
|
|
|
|
test('returns empty when below minimum zoom', () {
|
|
final bounds = LatLngBounds(LatLng(38.0, -78.0), LatLng(39.0, -77.0));
|
|
testNodes = [nodeAt(1, 38.5, -77.5)];
|
|
|
|
final result = dataManager.getNodesForRendering(
|
|
currentZoom: 5,
|
|
mapBounds: bounds,
|
|
uploadMode: UploadMode.production,
|
|
maxNodes: 10,
|
|
);
|
|
|
|
expect(result.nodesToRender, isEmpty);
|
|
});
|
|
|
|
test('panning viewport changes which nodes are prioritized', () {
|
|
final nodes = [
|
|
nodeAt(1, 38.0, -78.0), // SW
|
|
nodeAt(2, 38.5, -77.5), // middle
|
|
nodeAt(3, 39.0, -77.0), // NE
|
|
];
|
|
|
|
// Viewport centered near SW
|
|
testNodes = List.from(nodes);
|
|
final swBounds = LatLngBounds(LatLng(37.5, -78.5), LatLng(38.5, -77.5));
|
|
final swResult = dataManager.getNodesForRendering(
|
|
currentZoom: 14,
|
|
mapBounds: swBounds,
|
|
uploadMode: UploadMode.production,
|
|
maxNodes: 1,
|
|
);
|
|
expect(swResult.nodesToRender.first.id, 1,
|
|
reason: 'SW node closest to SW-centered viewport');
|
|
|
|
// Viewport centered near NE
|
|
testNodes = List.from(nodes);
|
|
final neBounds = LatLngBounds(LatLng(38.5, -77.5), LatLng(39.5, -76.5));
|
|
final neResult = dataManager.getNodesForRendering(
|
|
currentZoom: 14,
|
|
mapBounds: neBounds,
|
|
uploadMode: UploadMode.production,
|
|
maxNodes: 1,
|
|
);
|
|
expect(neResult.nodesToRender.first.id, 3,
|
|
reason: 'NE node closest to NE-centered viewport');
|
|
});
|
|
|
|
test('order is stable for repeated calls with same viewport', () {
|
|
final bounds = LatLngBounds(LatLng(38.0, -78.0), LatLng(39.0, -77.0));
|
|
makeNodes() => [
|
|
nodeAt(1, 38.9, -77.9),
|
|
nodeAt(2, 38.5, -77.5),
|
|
nodeAt(3, 38.1, -77.1),
|
|
nodeAt(4, 38.51, -77.49),
|
|
nodeAt(5, 38.0, -78.0),
|
|
];
|
|
|
|
testNodes = makeNodes();
|
|
final result1 = dataManager.getNodesForRendering(
|
|
currentZoom: 14, mapBounds: bounds,
|
|
uploadMode: UploadMode.production, maxNodes: 3,
|
|
);
|
|
|
|
testNodes = makeNodes();
|
|
final result2 = dataManager.getNodesForRendering(
|
|
currentZoom: 14, mapBounds: bounds,
|
|
uploadMode: UploadMode.production, maxNodes: 3,
|
|
);
|
|
|
|
expect(
|
|
result1.nodesToRender.map((n) => n.id).toList(),
|
|
result2.nodesToRender.map((n) => n.id).toList(),
|
|
);
|
|
});
|
|
|
|
test('filters out invalid coordinates before prioritizing', () {
|
|
final bounds = LatLngBounds(LatLng(38.0, -78.0), LatLng(39.0, -77.0));
|
|
testNodes = [
|
|
nodeAt(1, 0, 0), // invalid (0,0)
|
|
nodeAt(2, 38.5, -77.5), // valid, at center
|
|
nodeAt(3, 200, -77.5), // invalid lat
|
|
];
|
|
|
|
final result = dataManager.getNodesForRendering(
|
|
currentZoom: 14,
|
|
mapBounds: bounds,
|
|
uploadMode: UploadMode.production,
|
|
maxNodes: 10,
|
|
);
|
|
|
|
expect(result.nodesToRender.length, 1);
|
|
expect(result.nodesToRender.first.id, 2);
|
|
});
|
|
});
|
|
}
|