From 3de71e2a5aad563651364033a8f32fe6c0e56df5 Mon Sep 17 00:00:00 2001 From: Yandrik Date: Sun, 21 Apr 2024 08:24:50 +0200 Subject: [PATCH] feat: basic navigation with bugs --- lib/components/feature_bottom_sheet.dart | 56 +- lib/components/map_render_level.dart | 70 ++- lib/controllers/map_controller.dart | 10 +- lib/data/geo/model.dart | 1 - lib/data/geo/parser.dart | 4 +- lib/main.dart | 4 + lib/map.dart | 10 +- lib/nav/graph.dart | 425 +++++++++++++++ lib/nav/graph.freezed.dart | 646 +++++++++++++++++++++++ lib/settings copy.dart | 21 - pubspec.lock | 54 +- pubspec.yaml | 6 +- test/graph_tests.dart | 176 ++++++ 13 files changed, 1391 insertions(+), 92 deletions(-) create mode 100644 lib/nav/graph.dart create mode 100644 lib/nav/graph.freezed.dart delete mode 100644 lib/settings copy.dart create mode 100644 test/graph_tests.dart diff --git a/lib/components/feature_bottom_sheet.dart b/lib/components/feature_bottom_sheet.dart index 7078dfe..e8a7ba5 100644 --- a/lib/components/feature_bottom_sheet.dart +++ b/lib/components/feature_bottom_sheet.dart @@ -19,8 +19,8 @@ Future showFeatureBottomSheet( data: ThemeData.light(), child: Container( constraints: const BoxConstraints( - minHeight: 300, - ), + // minHeight: 300, + ), width: Get.mediaQuery.size.width, decoration: const BoxDecoration( color: Colors.black, @@ -72,6 +72,28 @@ Future showFeatureBottomSheet( const SizedBox(height: 10), ], ..._buildFeatureContent(feature), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + child: const Row( + children: [ + Icon( + Icons.share_location, + color: Colors.black, + ), + SizedBox(width: 4), + Text( + "Start Navigation", + style: TextStyle(color: Colors.black), + ), + ], + ), + onPressed: () => {}, + ), + ], + ) ]), ), ), @@ -114,7 +136,35 @@ List _buildRoomContent(Feature feature) { /// Builds the content for the Door feature type. List _buildDoorContent(Feature feature, List connects) { - return [Text('Door: ${feature.name}\nConnects: $connects')]; + return [ + Text( + feature.name, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 10), + if (connects.isNotEmpty) ...[ + const Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(right: 4), + child: Text( + 'Connects:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: Wrap( + spacing: 8, + runSpacing: 4, + children: connects.map((place) { + return ColorfulChip(label: place.toString()); + }).toList(), + ), + ), + ], + ]; } /// Builds the content for the Toilet feature type. diff --git a/lib/components/map_render_level.dart b/lib/components/map_render_level.dart index 7ae26f9..c818467 100644 --- a/lib/components/map_render_level.dart +++ b/lib/components/map_render_level.dart @@ -10,38 +10,39 @@ import 'package:uninav/map.dart'; import 'package:uninav/util/geomath.dart'; import 'package:uninav/util/util.dart'; -List renderLevel(int level, {LayerHitNotifier? hitNotifier}) { +List renderLevel( + int level, +) { return [ LevelLayer( - filter: (feature) => - feature.level == level && feature.type is LectureHall, - polyConstructor: (feature) => feature - .getPolygon( - constructor: (pts) => Polygon( - points: pts, - color: Colors.orange.withOpacity(0.2), - borderColor: Colors.orange, - borderStrokeWidth: 2, - hitValue: feature, - ), - ) - .unwrap(), - markerConstructor: (feature) => Marker( - width: 50, - height: 20, - point: feature.getPoint().unwrap(), - child: Column( - children: [ - Icon( - Icons.class_, - color: Colors.black, - ), - Text('${feature.name}'), - ], - ), - alignment: Alignment.center, + filter: (feature) => + feature.level == level && feature.type is LectureHall, + polyConstructor: (feature) => feature + .getPolygon( + constructor: (pts) => Polygon( + points: pts, + color: Colors.orange.withOpacity(0.2), + borderColor: Colors.orange, + borderStrokeWidth: 2, ), - notifier: hitNotifier), + ) + .unwrap(), + markerConstructor: (feature) => Marker( + width: 50, + height: 20, + point: feature.getPoint().unwrap(), + child: Column( + children: [ + Icon( + Icons.class_, + color: Colors.black, + ), + Text('${feature.name}'), + ], + ), + alignment: Alignment.center, + ), + ), LevelLayer( filter: (feature) => feature.level == level && feature.type is Room, polyConstructor: (feature) => feature @@ -51,11 +52,9 @@ List renderLevel(int level, {LayerHitNotifier? hitNotifier}) { color: Colors.green.withOpacity(1.2), borderColor: Colors.green, borderStrokeWidth: 2, - hitValue: feature, ), ) .unwrap(), - notifier: hitNotifier, ), LevelLayer( filter: (feature) => feature.level == level && feature.type is Door, @@ -72,7 +71,6 @@ List renderLevel(int level, {LayerHitNotifier? hitNotifier}) { alignment: Alignment.center, ); }, - notifier: hitNotifier, ), LevelLayer( filter: (feature) => feature.level == level && feature.type is Toilet, @@ -91,7 +89,6 @@ List renderLevel(int level, {LayerHitNotifier? hitNotifier}) { alignment: Alignment.center, ); }, - notifier: hitNotifier, ), LevelLayer( filter: (feature) => @@ -110,7 +107,6 @@ List renderLevel(int level, {LayerHitNotifier? hitNotifier}) { alignment: Alignment.center, ); }, - notifier: hitNotifier, ), LevelLayer( filter: (feature) => @@ -129,7 +125,6 @@ List renderLevel(int level, {LayerHitNotifier? hitNotifier}) { alignment: Alignment.center, ); }, - notifier: hitNotifier, ), ]; } @@ -140,7 +135,6 @@ class LevelLayer extends StatelessWidget { final Marker Function(LatLng, String)? polyCenterMarkerConstructor; final Marker Function(Feature)? markerConstructor; final int? level; - final LayerHitNotifier? notifier; const LevelLayer({ this.level, @@ -148,7 +142,6 @@ class LevelLayer extends StatelessWidget { this.polyConstructor, this.polyCenterMarkerConstructor, this.markerConstructor, - this.notifier, super.key, }); @@ -173,7 +166,6 @@ class LevelLayer extends StatelessWidget { points: points, borderColor: Colors.black26, borderStrokeWidth: 2.0, - hitValue: feature, )) .unwrap()); } @@ -241,14 +233,12 @@ class LevelLayer extends StatelessWidget { widgets.add(TranslucentPointer( child: PolygonLayer( polygons: filteredPolygons, - hitNotifier: notifier, ), )); } else { widgets.add(TranslucentPointer( child: PolygonLayer( polygons: filteredPolygons, - hitNotifier: notifier, ), )); } diff --git a/lib/controllers/map_controller.dart b/lib/controllers/map_controller.dart index 6c8eef8..3d2411d 100644 --- a/lib/controllers/map_controller.dart +++ b/lib/controllers/map_controller.dart @@ -86,15 +86,15 @@ class MyMapController extends GetxController { final featuresList = []; - print('doing'); + // print('doing'); final geojson = GeoJSONFeatureCollection.fromJSON(geoJsonString); - print('done'); + // print('done'); for (final feature in geojson.features) { - print(feature); - print(feature?.properties); + // print(feature); + // print(feature?.properties); if (feature == null) continue; - print(feature.properties); + // print(feature.properties); final parsed = parseFeature( feature.properties ?? {}, feature.geometry); if (parsed case Ok(:final ok)) { diff --git a/lib/data/geo/model.dart b/lib/data/geo/model.dart index 5e4c22c..7621e70 100644 --- a/lib/data/geo/model.dart +++ b/lib/data/geo/model.dart @@ -36,7 +36,6 @@ class Feature with _$Feature { points: pts, borderColor: Colors.black26, borderStrokeWidth: 2.0, - hitValue: 'test${pts.length}', ); final polygon = geometry as GeoJSONPolygon; // print(polygon.geometry!.geoSeries[0].geoPoints); diff --git a/lib/data/geo/parser.dart b/lib/data/geo/parser.dart index a28df80..3d526bd 100644 --- a/lib/data/geo/parser.dart +++ b/lib/data/geo/parser.dart @@ -40,7 +40,7 @@ Result parseFeature( final building = yaml['building'] as String?; - print("yaml: $yaml"); + // print("yaml: $yaml"); var raw_type = yaml['type'] as String?; if (raw_type == null && layer?.toLowerCase() == 'buildings') { @@ -123,7 +123,7 @@ Result> stringifyList(List tramLines) { Result> getYamlList(YamlMap yaml, String key) { try { - print('yaml is ${yaml[key]}'); + // print('yaml is ${yaml[key]}'); final val = (yaml[key] as YamlList?); if (val == null) { return bail("Key $key is missing in yaml"); diff --git a/lib/main.dart b/lib/main.dart index 286c808..2d5af76 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:uninav/controllers/isar_controller.dart'; import 'package:uninav/controllers/map_controller.dart'; @@ -8,6 +9,9 @@ import 'package:uninav/settings.dart'; // TODO: maybe make not async? void main() async { Get.put(MyMapController()); + await Get.find() + .loadGeoJson(await rootBundle.loadString('assets/geo/uulm_beta.geojson')); + await Get.putAsync(() async { final controller = IsarController(); await controller.initializeIsar(); diff --git a/lib/map.dart b/lib/map.dart index 64b70d8..a1bf028 100644 --- a/lib/map.dart +++ b/lib/map.dart @@ -19,7 +19,6 @@ class MapPage extends StatelessWidget { @override Widget build(BuildContext context) { - final LayerHitNotifier hitNotifier = ValueNotifier(null); return Scaffold( drawer: MyDrawer(), appBar: AppBar( @@ -43,8 +42,6 @@ class MapPage extends StatelessWidget { floatingActionButton: FloatingActionButton( onPressed: () async { // Add onPressed logic here - await Get.find().loadGeoJson( - await rootBundle.loadString('assets/geo/uulm_beta.geojson')); }, child: const Icon(Icons.add), ), @@ -70,7 +67,6 @@ class MapPage extends StatelessWidget { TranslucentPointer( child: LevelLayer( filter: (feature) => feature.type is Building, - notifier: hitNotifier, ), ), @@ -97,10 +93,8 @@ class MapPage extends StatelessWidget { color: Colors.green.withOpacity(0.2), borderColor: Colors.green, borderStrokeWidth: 1, - hitValue: feature, )) .unwrap(), - notifier: hitNotifier, ), ), @@ -108,8 +102,8 @@ class MapPage extends StatelessWidget { Obx( () => Stack( children: renderLevel( - Get.find().currentLevel.value, - hitNotifier: hitNotifier)), + Get.find().currentLevel.value, + )), ), ], ), diff --git a/lib/nav/graph.dart b/lib/nav/graph.dart new file mode 100644 index 0000000..261424d --- /dev/null +++ b/lib/nav/graph.dart @@ -0,0 +1,425 @@ +import 'package:anyhow/anyhow.dart'; +import 'package:collection/collection.dart'; +import 'package:directed_graph/directed_graph.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:geojson_vi/geojson_vi.dart'; +import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:uninav/data/geo/model.dart'; +import 'package:uninav/util/geojson_util.dart'; +import 'package:uninav/util/util.dart'; +import 'dart:collection'; + +part 'graph.freezed.dart'; + +@freezed +class GraphFeature with _$GraphFeature { + const factory GraphFeature.buildingFloor(int floor, Feature building) = + BuildingFloor; + const factory GraphFeature.portal(int fromFloor, String from, int toFloor, + String to, Feature baseFeature) = Portal; + const factory GraphFeature.basicFeature( + int floor, String building, Feature feature) = BasicFeature; + + const GraphFeature._(); + + Result getCenter() { + return when( + buildingFloor: (floor, building) => building.getCenterPoint(), + portal: (fromFloor, from, toFloor, to, baseFeature) => + baseFeature.getCenterPoint(), + basicFeature: (floor, building, feature) => feature.getCenterPoint(), + ); + } + + double distanceTo(GraphFeature other, String unit) => distanceBetweenLatLng( + getCenter().unwrap(), other.getCenter().unwrap(), unit); + + double metersTo(GraphFeature other) => distanceTo(other, "meters"); + + @override + String toString() { + return when( + buildingFloor: (floor, building) => 'Floor (${building.name}:$floor)', + portal: (fromFloor, from, toFloor, to, _) => + 'Portal ($from:$fromFloor -> $to:$toFloor)', + basicFeature: (floor, building, feature) => + 'Feature (${formatFeatureTitle(feature)} ($building:$floor))', + ); + } +} + +bool eq(String? a, String? b) => a?.toLowerCase() == b?.toLowerCase(); + +IList wrap(Feature feature, int floor, String buildingFrom) { + return feature.type + .maybeWhen( + building: () => [GraphFeature.buildingFloor(floor, feature)], + stairs: (floors) => stairPortalGenerator(floors, floor, feature), + lift: (floors) => stairPortalGenerator(floors, floor, feature, 99), + door: (connections) => + doorPortalGenerator(connections, floor, buildingFrom, feature), + orElse: () => [GraphFeature.basicFeature(floor, buildingFrom, feature)], + ) + .lock; +} + +List doorPortalGenerator( + List connections, int floor, String from, Feature feature) { + final portals = []; + + for (final connection in connections.where((c) => !eq(c, from))) { + portals.add(GraphFeature.portal( + floor, from, floor, connection.toLowerCase(), feature)); + } + + return portals; +} + +List stairPortalGenerator( + List floors, int floor, Feature feature, + [int maxDist = 1]) { + final portals = []; + for (int i = 1; i <= maxDist; i++) { + if (floors.contains(floor - i)) { + portals.add(GraphFeature.portal(floor, feature.building!.toLowerCase(), + floor - i, feature.building!.toLowerCase(), feature)); + } + if (floors.contains(floor + i)) { + portals.add(GraphFeature.portal(floor, feature.building!.toLowerCase(), + floor + i, feature.building!.toLowerCase(), feature)); + } + } + return portals; +} + +Feature unwrap(GraphFeature feature) { + return feature.when( + buildingFloor: (floor, building) => building, + portal: (fromFloor, from, toFloor, to, baseFeature) => baseFeature, + basicFeature: (floor, building, f) => f, + ); +} + +double sum(double left, double right) => left + right; + +// WeightedDirectedGraph createGraph(Feature origin, List allFeatures) { +// +// } + +List findAdjacent( + GraphFeature feature, Iterable allFeatures) { + List adjacentFeatures = []; + + if (feature is BuildingFloor) { + // find all features in the building on the right floor + adjacentFeatures = allFeatures + .where((f) => eq(f.building, feature.building.name) || f.type is Door) + .where((f) => f.type.maybeWhen( + lift: (levels) => levels.contains(feature.floor), + stairs: (levels) => levels.contains(feature.floor), + door: (connections) => + f.level == feature.floor && + connections + .map((e) => e.toLowerCase()) + .contains(feature.building.name.toLowerCase()), + orElse: () => f.level == feature.floor)) + .mapMany((f) => wrap(f, feature.floor, feature.building.name)) + .toList(); + } else if (feature is Portal) { + adjacentFeatures = allFeatures + .where((f) => eq(f.name, feature.to) && f.type is Building) + .mapMany((f) => wrap(f, feature.toFloor, feature.to)) + .toList(); + } else if (feature is BasicFeature) { + adjacentFeatures = allFeatures + .where( + (f) => eq(f.name, feature.feature.building) && f.type is Building) + .mapMany((f) => wrap(f, feature.feature.level!, f.name)) + .toList(); + } + return adjacentFeatures; +} + +List createGraphList( + GraphFeature origin, List allFeatures, + [Set? visited]) { + // final usedFeatures = [origin]; + + visited ??= {origin}; + + final adjacent = findAdjacent(origin, allFeatures); + for (final feature in adjacent.asSet()..removeAll(visited)) { + visited.add(feature); + final deeper = createGraphList(feature, allFeatures, visited); + visited.addAll(deeper); + } + + return visited.toList(); +} + +Map> createGraphMap( + GraphFeature origin, List allFeatures) { + final graphList = createGraphList(origin, allFeatures); + final graphMap = >{}; + for (final node in graphList) { + final adjacents = node.when( + buildingFloor: (floor, building) { + return graphList + .where((f) => + f is Portal && + eq(f.from, building.name) && + f.fromFloor == floor || + f is BasicFeature && + eq(f.building, building.name) && + f.floor == floor) + .map((f) => f.when( + portal: (fromFloor, from, toFloor, to, baseFeature) => ( + f, + f.metersTo(node), + ), + basicFeature: (floor, building, feature) => + (f, f.metersTo(node)), + buildingFloor: (floor, building) => throw StateError( + "BUG: createGraphMap(): BuildingFloors shouldn't " + "be matched by BuildingFloors"), + )); + }, + portal: (fromFloor, from, toFloor, to, baseFeature) { + return graphList + .where((f) => + f is BuildingFloor && + eq(f.building.name, to) && + f.floor == toFloor) + .map((f) => f.when( + portal: (fromFloor, from, toFloor, to, baseFeature) => + throw StateError( + "BUG: createGraphMap(): Portals shouldn't " + "be matched by Portals"), + basicFeature: (floor, building, feature) => throw StateError( + "BUG: createGraphMap(): BasicFeatures shouldn't " + "be matched by BasicFeatures"), + buildingFloor: (floor, building) => ( + f, + f.metersTo(node) + + 5 /* 5 extra meters for all portals. TODO: smarter!*/ + ), + )); + }, + basicFeature: (floor, building, feature) { + return graphList + .where((f) => + f is BuildingFloor && + eq(f.building.name, building) && + f.floor == floor) + .map((f) => f.when( + portal: (fromFloor, from, toFloor, to, baseFeature) => + throw StateError( + "BUG: createGraphMap(): Portal shouldn't be matched " + "by BasicFeature"), + basicFeature: (floor, building, feature) => throw StateError( + "BUG: createGraphMap(): BasicFeatures shouldn't " + "be matched by BasicFeatures"), + buildingFloor: (floor, building) => (f, f.metersTo(node)), + )); + }, + ); + + graphMap[node] = + Map.fromEntries(adjacents.map((tup) => MapEntry(tup.$1, tup.$2))); + } + + return graphMap; +} + +WeightedDirectedGraph createGraph( + GraphFeature origin, List allFeatures) { + final map = createGraphMap(origin, allFeatures); + final graph = WeightedDirectedGraph( + map, + summation: sum, + zero: 0.0, + comparator: (a, b) => compareGraphFeatures(a, b), + ); + return graph; +} + +Result> findShortestPath( + GraphFeature origin, GraphFeature destination, List allFeatures, + [heuristicVariant = "zero", heuristicMultiplier = 0.2]) { + var graph = createGraphMap(origin, allFeatures); + + if (!(graph.keys.contains(origin) && + graph.values.firstWhereOrNull((vals) => vals.containsKey(destination)) != + null)) { + return bail("Origin or destination not in graph"); + } + + // euclidean distance heuristic + + double Function(GraphFeature) heuristic = + (GraphFeature node) => 0.0; // standard zero + if (heuristicVariant == "zero") { + heuristic = (GraphFeature node) => 0.0; + } else if (heuristicVariant == "euclidean") { + heuristic = + (GraphFeature node) => node.metersTo(destination) * heuristicMultiplier; + } + + //heuristic(GraphFeature node) => 0.0; + + // openlist + // format: (heuristic, g-val, parent?, node) + PriorityQueue<(double, double, GraphFeature?, GraphFeature)> openlist = + HeapPriorityQueue( + // reverse order (cmp b to a) because lower f-val (shorter distance) is better + (a, b) => (b.$1 + b.$2).compareTo((a.$1 + a.$2)), + ); + + final Map bestPathMap = { + origin: (null, 0.0) + }; + + openlist.add((heuristic(origin), 0.0, null, origin)); + + // closed list + Set closedlist = {}; + + var cost = 0.0; + + while (openlist.isNotEmpty) { + final (f, g, parent, node) = openlist.removeFirst(); + closedlist.add(node); + bestPathMap[node] = (parent, g); + if (node == destination) { + cost = g; + break; + // TODO: restore path + } + + // expand node + final adjacents = graph[node]!; + for (final entry in adjacents.entries) { + final adjNode = entry.key; + final adjCost = entry.value; + + if (closedlist.contains(adjNode)) { + continue; + } + + bool found = false; + for (final open in openlist.unorderedElements) { + if (open.$4 == adjNode) { + found = true; + if (g + adjCost < open.$2) { + openlist.remove(open); + openlist.add(( + open.$1 /* heuristic stays the same */, + g + adjCost, + adjNode, + open.$4 + )); + } + break; + } + } + + if (!found) { + openlist.add(( + f + heuristic(adjNode), + g + adjCost, + node, + adjNode, + )); + } + } + } + if (bestPathMap.isNotEmpty) { + final path = <(GraphFeature, double)>[]; + (GraphFeature?, double)? currentNode = (destination, cost); + while (currentNode?.$1 != null) { + final nextNode = bestPathMap[currentNode!.$1]; + path.insert( + 0, (currentNode!.$1!, currentNode.$2 - (nextNode?.$2 ?? 0.0))); + currentNode = nextNode; + } + return Ok(path); + } + + return bail("No path found"); +} + +/// Compares two [GraphFeature] instances and determines their relative order. +/// +/// The comparison is based on the specific subtypes and properties of the +/// [GraphFeature] instances. The comparison logic is as follows: +/// +/// 1. If both instances are [BuildingFloor], they are compared first by the +/// building name and then by the floor number. +/// 2. If one instance is a [Portal] and the other is a [BuildingFloor] or +/// [BasicFeature], the [Portal] is considered greater. +/// 3. If both instances are [Portal], they are compared first by the `from` +/// property, then by the `to` property, and finally by the `baseFeature` name. +/// 4. If one instance is a [BasicFeature] and the other is a [BuildingFloor] or +/// [Portal], the [BasicFeature] is considered greater. +/// 5. If both instances are [BasicFeature], they are compared first by the +/// building name, then by the floor number, and finally by the feature name. +/// +/// Returns a negative value if [a] is considered "less than" [b], a positive +/// value if [a] is considered "greater than" [b], and zero if they are considered +/// equal. +/// +/// This function can be used as a comparator for sorting or ordering +/// [GraphFeature] instances. +int compareGraphFeatures(GraphFeature a, GraphFeature b) { + return a.when( + buildingFloor: (floorA, buildingA) { + return b.when( + buildingFloor: (floorB, buildingB) { + final buildingComparison = buildingA.name.compareTo(buildingB.name); + if (buildingComparison != 0) { + return buildingComparison; + } + return floorA.compareTo(floorB); + }, + portal: (fromFloorB, fromB, toFloorB, toB, baseFeatureB) => -1, + basicFeature: (floorB, buildingB, featureB) => -1, + ); + }, + portal: (fromFloorA, fromA, toFloorA, toA, baseFeatureA) { + return b.when( + buildingFloor: (floorB, buildingB) => 1, + portal: (fromFloorB, fromB, toFloorB, toB, baseFeatureB) { + final fromComparison = fromA.compareTo(fromB); + if (fromComparison != 0) { + return fromComparison; + } + final toComparison = toA.compareTo(toB); + if (toComparison != 0) { + return toComparison; + } + return baseFeatureA.name.compareTo(baseFeatureB.name); + }, + basicFeature: (floorB, buildingB, featureB) => -1, + ); + }, + basicFeature: (floorA, buildingA, featureA) { + return b.when( + buildingFloor: (floorB, buildingB) => 1, + portal: (fromFloorB, fromB, toFloorB, toB, baseFeatureB) => 1, + basicFeature: (floorB, buildingB, featureB) { + final buildingComparison = buildingA.compareTo(buildingB); + if (buildingComparison != 0) { + return buildingComparison; + } + final floorComparison = floorA.compareTo(floorB); + if (floorComparison != 0) { + return floorComparison; + } + return featureA.name.compareTo(featureB.name); + }, + ); + }, + ); +} diff --git a/lib/nav/graph.freezed.dart b/lib/nav/graph.freezed.dart new file mode 100644 index 0000000..56f5d61 --- /dev/null +++ b/lib/nav/graph.freezed.dart @@ -0,0 +1,646 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'graph.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$GraphFeature { + @optionalTypeArgs + TResult when({ + required TResult Function(int floor, Feature building) buildingFloor, + required TResult Function(int fromFloor, String from, int toFloor, + String to, Feature baseFeature) + portal, + required TResult Function(int floor, String building, Feature feature) + basicFeature, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int floor, Feature building)? buildingFloor, + TResult? Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult? Function(int floor, String building, Feature feature)? + basicFeature, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int floor, Feature building)? buildingFloor, + TResult Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult Function(int floor, String building, Feature feature)? basicFeature, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(BuildingFloor value) buildingFloor, + required TResult Function(Portal value) portal, + required TResult Function(BasicFeature value) basicFeature, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(BuildingFloor value)? buildingFloor, + TResult? Function(Portal value)? portal, + TResult? Function(BasicFeature value)? basicFeature, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(BuildingFloor value)? buildingFloor, + TResult Function(Portal value)? portal, + TResult Function(BasicFeature value)? basicFeature, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GraphFeatureCopyWith<$Res> { + factory $GraphFeatureCopyWith( + GraphFeature value, $Res Function(GraphFeature) then) = + _$GraphFeatureCopyWithImpl<$Res, GraphFeature>; +} + +/// @nodoc +class _$GraphFeatureCopyWithImpl<$Res, $Val extends GraphFeature> + implements $GraphFeatureCopyWith<$Res> { + _$GraphFeatureCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$BuildingFloorImplCopyWith<$Res> { + factory _$$BuildingFloorImplCopyWith( + _$BuildingFloorImpl value, $Res Function(_$BuildingFloorImpl) then) = + __$$BuildingFloorImplCopyWithImpl<$Res>; + @useResult + $Res call({int floor, Feature building}); + + $FeatureCopyWith<$Res> get building; +} + +/// @nodoc +class __$$BuildingFloorImplCopyWithImpl<$Res> + extends _$GraphFeatureCopyWithImpl<$Res, _$BuildingFloorImpl> + implements _$$BuildingFloorImplCopyWith<$Res> { + __$$BuildingFloorImplCopyWithImpl( + _$BuildingFloorImpl _value, $Res Function(_$BuildingFloorImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? floor = null, + Object? building = null, + }) { + return _then(_$BuildingFloorImpl( + null == floor + ? _value.floor + : floor // ignore: cast_nullable_to_non_nullable + as int, + null == building + ? _value.building + : building // ignore: cast_nullable_to_non_nullable + as Feature, + )); + } + + @override + @pragma('vm:prefer-inline') + $FeatureCopyWith<$Res> get building { + return $FeatureCopyWith<$Res>(_value.building, (value) { + return _then(_value.copyWith(building: value)); + }); + } +} + +/// @nodoc + +class _$BuildingFloorImpl extends BuildingFloor { + const _$BuildingFloorImpl(this.floor, this.building) : super._(); + + @override + final int floor; + @override + final Feature building; + + @override + String toString() { + return 'GraphFeature.buildingFloor(floor: $floor, building: $building)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BuildingFloorImpl && + (identical(other.floor, floor) || other.floor == floor) && + (identical(other.building, building) || + other.building == building)); + } + + @override + int get hashCode => Object.hash(runtimeType, floor, building); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$BuildingFloorImplCopyWith<_$BuildingFloorImpl> get copyWith => + __$$BuildingFloorImplCopyWithImpl<_$BuildingFloorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int floor, Feature building) buildingFloor, + required TResult Function(int fromFloor, String from, int toFloor, + String to, Feature baseFeature) + portal, + required TResult Function(int floor, String building, Feature feature) + basicFeature, + }) { + return buildingFloor(floor, building); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int floor, Feature building)? buildingFloor, + TResult? Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult? Function(int floor, String building, Feature feature)? + basicFeature, + }) { + return buildingFloor?.call(floor, building); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int floor, Feature building)? buildingFloor, + TResult Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult Function(int floor, String building, Feature feature)? basicFeature, + required TResult orElse(), + }) { + if (buildingFloor != null) { + return buildingFloor(floor, building); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(BuildingFloor value) buildingFloor, + required TResult Function(Portal value) portal, + required TResult Function(BasicFeature value) basicFeature, + }) { + return buildingFloor(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(BuildingFloor value)? buildingFloor, + TResult? Function(Portal value)? portal, + TResult? Function(BasicFeature value)? basicFeature, + }) { + return buildingFloor?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(BuildingFloor value)? buildingFloor, + TResult Function(Portal value)? portal, + TResult Function(BasicFeature value)? basicFeature, + required TResult orElse(), + }) { + if (buildingFloor != null) { + return buildingFloor(this); + } + return orElse(); + } +} + +abstract class BuildingFloor extends GraphFeature { + const factory BuildingFloor(final int floor, final Feature building) = + _$BuildingFloorImpl; + const BuildingFloor._() : super._(); + + int get floor; + Feature get building; + @JsonKey(ignore: true) + _$$BuildingFloorImplCopyWith<_$BuildingFloorImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PortalImplCopyWith<$Res> { + factory _$$PortalImplCopyWith( + _$PortalImpl value, $Res Function(_$PortalImpl) then) = + __$$PortalImplCopyWithImpl<$Res>; + @useResult + $Res call( + {int fromFloor, + String from, + int toFloor, + String to, + Feature baseFeature}); + + $FeatureCopyWith<$Res> get baseFeature; +} + +/// @nodoc +class __$$PortalImplCopyWithImpl<$Res> + extends _$GraphFeatureCopyWithImpl<$Res, _$PortalImpl> + implements _$$PortalImplCopyWith<$Res> { + __$$PortalImplCopyWithImpl( + _$PortalImpl _value, $Res Function(_$PortalImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fromFloor = null, + Object? from = null, + Object? toFloor = null, + Object? to = null, + Object? baseFeature = null, + }) { + return _then(_$PortalImpl( + null == fromFloor + ? _value.fromFloor + : fromFloor // ignore: cast_nullable_to_non_nullable + as int, + null == from + ? _value.from + : from // ignore: cast_nullable_to_non_nullable + as String, + null == toFloor + ? _value.toFloor + : toFloor // ignore: cast_nullable_to_non_nullable + as int, + null == to + ? _value.to + : to // ignore: cast_nullable_to_non_nullable + as String, + null == baseFeature + ? _value.baseFeature + : baseFeature // ignore: cast_nullable_to_non_nullable + as Feature, + )); + } + + @override + @pragma('vm:prefer-inline') + $FeatureCopyWith<$Res> get baseFeature { + return $FeatureCopyWith<$Res>(_value.baseFeature, (value) { + return _then(_value.copyWith(baseFeature: value)); + }); + } +} + +/// @nodoc + +class _$PortalImpl extends Portal { + const _$PortalImpl( + this.fromFloor, this.from, this.toFloor, this.to, this.baseFeature) + : super._(); + + @override + final int fromFloor; + @override + final String from; + @override + final int toFloor; + @override + final String to; + @override + final Feature baseFeature; + + @override + String toString() { + return 'GraphFeature.portal(fromFloor: $fromFloor, from: $from, toFloor: $toFloor, to: $to, baseFeature: $baseFeature)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PortalImpl && + (identical(other.fromFloor, fromFloor) || + other.fromFloor == fromFloor) && + (identical(other.from, from) || other.from == from) && + (identical(other.toFloor, toFloor) || other.toFloor == toFloor) && + (identical(other.to, to) || other.to == to) && + (identical(other.baseFeature, baseFeature) || + other.baseFeature == baseFeature)); + } + + @override + int get hashCode => + Object.hash(runtimeType, fromFloor, from, toFloor, to, baseFeature); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PortalImplCopyWith<_$PortalImpl> get copyWith => + __$$PortalImplCopyWithImpl<_$PortalImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int floor, Feature building) buildingFloor, + required TResult Function(int fromFloor, String from, int toFloor, + String to, Feature baseFeature) + portal, + required TResult Function(int floor, String building, Feature feature) + basicFeature, + }) { + return portal(fromFloor, from, toFloor, to, baseFeature); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int floor, Feature building)? buildingFloor, + TResult? Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult? Function(int floor, String building, Feature feature)? + basicFeature, + }) { + return portal?.call(fromFloor, from, toFloor, to, baseFeature); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int floor, Feature building)? buildingFloor, + TResult Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult Function(int floor, String building, Feature feature)? basicFeature, + required TResult orElse(), + }) { + if (portal != null) { + return portal(fromFloor, from, toFloor, to, baseFeature); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(BuildingFloor value) buildingFloor, + required TResult Function(Portal value) portal, + required TResult Function(BasicFeature value) basicFeature, + }) { + return portal(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(BuildingFloor value)? buildingFloor, + TResult? Function(Portal value)? portal, + TResult? Function(BasicFeature value)? basicFeature, + }) { + return portal?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(BuildingFloor value)? buildingFloor, + TResult Function(Portal value)? portal, + TResult Function(BasicFeature value)? basicFeature, + required TResult orElse(), + }) { + if (portal != null) { + return portal(this); + } + return orElse(); + } +} + +abstract class Portal extends GraphFeature { + const factory Portal( + final int fromFloor, + final String from, + final int toFloor, + final String to, + final Feature baseFeature) = _$PortalImpl; + const Portal._() : super._(); + + int get fromFloor; + String get from; + int get toFloor; + String get to; + Feature get baseFeature; + @JsonKey(ignore: true) + _$$PortalImplCopyWith<_$PortalImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$BasicFeatureImplCopyWith<$Res> { + factory _$$BasicFeatureImplCopyWith( + _$BasicFeatureImpl value, $Res Function(_$BasicFeatureImpl) then) = + __$$BasicFeatureImplCopyWithImpl<$Res>; + @useResult + $Res call({int floor, String building, Feature feature}); + + $FeatureCopyWith<$Res> get feature; +} + +/// @nodoc +class __$$BasicFeatureImplCopyWithImpl<$Res> + extends _$GraphFeatureCopyWithImpl<$Res, _$BasicFeatureImpl> + implements _$$BasicFeatureImplCopyWith<$Res> { + __$$BasicFeatureImplCopyWithImpl( + _$BasicFeatureImpl _value, $Res Function(_$BasicFeatureImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? floor = null, + Object? building = null, + Object? feature = null, + }) { + return _then(_$BasicFeatureImpl( + null == floor + ? _value.floor + : floor // ignore: cast_nullable_to_non_nullable + as int, + null == building + ? _value.building + : building // ignore: cast_nullable_to_non_nullable + as String, + null == feature + ? _value.feature + : feature // ignore: cast_nullable_to_non_nullable + as Feature, + )); + } + + @override + @pragma('vm:prefer-inline') + $FeatureCopyWith<$Res> get feature { + return $FeatureCopyWith<$Res>(_value.feature, (value) { + return _then(_value.copyWith(feature: value)); + }); + } +} + +/// @nodoc + +class _$BasicFeatureImpl extends BasicFeature { + const _$BasicFeatureImpl(this.floor, this.building, this.feature) : super._(); + + @override + final int floor; + @override + final String building; + @override + final Feature feature; + + @override + String toString() { + return 'GraphFeature.basicFeature(floor: $floor, building: $building, feature: $feature)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BasicFeatureImpl && + (identical(other.floor, floor) || other.floor == floor) && + (identical(other.building, building) || + other.building == building) && + (identical(other.feature, feature) || other.feature == feature)); + } + + @override + int get hashCode => Object.hash(runtimeType, floor, building, feature); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$BasicFeatureImplCopyWith<_$BasicFeatureImpl> get copyWith => + __$$BasicFeatureImplCopyWithImpl<_$BasicFeatureImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int floor, Feature building) buildingFloor, + required TResult Function(int fromFloor, String from, int toFloor, + String to, Feature baseFeature) + portal, + required TResult Function(int floor, String building, Feature feature) + basicFeature, + }) { + return basicFeature(floor, building, feature); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int floor, Feature building)? buildingFloor, + TResult? Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult? Function(int floor, String building, Feature feature)? + basicFeature, + }) { + return basicFeature?.call(floor, building, feature); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int floor, Feature building)? buildingFloor, + TResult Function(int fromFloor, String from, int toFloor, String to, + Feature baseFeature)? + portal, + TResult Function(int floor, String building, Feature feature)? basicFeature, + required TResult orElse(), + }) { + if (basicFeature != null) { + return basicFeature(floor, building, feature); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(BuildingFloor value) buildingFloor, + required TResult Function(Portal value) portal, + required TResult Function(BasicFeature value) basicFeature, + }) { + return basicFeature(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(BuildingFloor value)? buildingFloor, + TResult? Function(Portal value)? portal, + TResult? Function(BasicFeature value)? basicFeature, + }) { + return basicFeature?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(BuildingFloor value)? buildingFloor, + TResult Function(Portal value)? portal, + TResult Function(BasicFeature value)? basicFeature, + required TResult orElse(), + }) { + if (basicFeature != null) { + return basicFeature(this); + } + return orElse(); + } +} + +abstract class BasicFeature extends GraphFeature { + const factory BasicFeature( + final int floor, final String building, final Feature feature) = + _$BasicFeatureImpl; + const BasicFeature._() : super._(); + + int get floor; + String get building; + Feature get feature; + @JsonKey(ignore: true) + _$$BasicFeatureImplCopyWith<_$BasicFeatureImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/settings copy.dart b/lib/settings copy.dart deleted file mode 100644 index 1ebd1ea..0000000 --- a/lib/settings copy.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:uninav/components/drawer.dart'; -import 'package:uninav/components/hamburger_menu.dart'; - -class SettingsPage extends StatelessWidget { - const SettingsPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - leading: HamburgerMenu(), - ), - drawer: MyDrawer(), - body: const Center( - child: Text('TODO'), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 49176c6..6acf2e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -154,7 +154,7 @@ packages: source: hosted version: "4.10.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a @@ -185,14 +185,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dart_earcut: - dependency: transitive - description: - name: dart_earcut - sha256: "41b493147e30a051efb2da1e3acb7f38fe0db60afba24ac1ea5684cee272721e" - url: "https://pub.dev" - source: hosted - version: "1.1.0" dart_style: dependency: transitive description: @@ -209,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + directed_graph: + dependency: "direct main" + description: + name: directed_graph + sha256: fcb45029b4a5089d383b79056b6716d6bf5af79b8e7dbac4b61d26af46410548 + url: "https://pub.dev" + source: hosted + version: "0.4.3" + exception_templates: + dependency: transitive + description: + name: exception_templates + sha256: "517f7c770da690073663f867ee2057ae2f4ffb28edae9da9faa624aa29ac76eb" + url: "https://pub.dev" + source: hosted + version: "0.3.1" fake_async: dependency: transitive description: @@ -217,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fast_immutable_collections: + dependency: "direct main" + description: + name: fast_immutable_collections + sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037" + url: "https://pub.dev" + source: hosted + version: "10.2.2" ffi: dependency: transitive description: @@ -258,10 +274,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: bee8c5bacb49a68aabcf6009c050a8b3b07ac75403f29f741d8c00d4a725e086 + sha256: cda8d72135b697f519287258b5294a57ce2f2a5ebf234f0e406aad4dc14c9399 url: "https://pub.dev" source: hosted - version: "7.0.0-dev.1" + version: "6.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -424,6 +440,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.1" + lazy_memo: + dependency: transitive + description: + name: lazy_memo + sha256: dcb30b4184a6d767e1d779d74ce784d752d38313b8fb4bad6b659ae7af4bb34d + url: "https://pub.dev" + source: hosted + version: "0.2.3" leak_tracker: dependency: transitive description: @@ -640,6 +664,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + quote_buffer: + dependency: transitive + description: + name: quote_buffer + sha256: c4cd07e55ed1b1645a1cc74278a03b2a642c9f6ea3c0528d51827fdd320acf87 + url: "https://pub.dev" + source: hosted + version: "0.2.6" rust_core: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 47b4e9f..2d90027 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,8 @@ dependencies: yaml: ^3.1.2 surrealdb: ^0.8.0 # geojson: ^1.0.0 - flutter_map: 7.0.0-dev.1 + # flutter_map: 7.0.0-dev.1 + flutter_map: ^6.0.0 # flutter_map: ^4.0.0 latlong2: ^0.9.0 # latlong2: ^0.8.0 @@ -53,6 +54,9 @@ dependencies: isar: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1 path_provider: ^2.1.3 + directed_graph: ^0.4.3 + fast_immutable_collections: ^10.2.2 + collection: ^1.18.0 dev_dependencies: flutter_test: diff --git a/test/graph_tests.dart b/test/graph_tests.dart new file mode 100644 index 0000000..a7ea265 --- /dev/null +++ b/test/graph_tests.dart @@ -0,0 +1,176 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:uninav/controllers/map_controller.dart'; +import 'package:uninav/data/geo/model.dart'; +import 'package:uninav/nav/graph.dart'; +import 'package:uninav/util/util.dart'; + +String formatGraphFeature(GraphFeature feature) { + return feature.when( + buildingFloor: (floor, building) => "(bfl ${building.name}:$floor)", + portal: (fromFloor, from, toFloor, to, baseFeat) { + return "(p ${formatFeatureTitle(baseFeat)} $from:$fromFloor -> $to:$toFloor)"; + }, + basicFeature: (lv, building, bf) => + "(bf ${formatFeatureTitle(bf)} $building:$lv)", + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('findAdjacent', () { + late MyMapController mapController; + late List allFeatures; + + setUp(() async { + // Initialize the MyMapController and load the GeoJSON data + mapController = MyMapController(); + await mapController.loadGeoJson( + await rootBundle.loadString('assets/geo/uulm_beta.geojson')); + allFeatures = mapController.features; + }); + + test('generates a graph', () { + // Find a building feature + // final buildingFeature = allFeatures.firstWhere((f) => f.type is Building); + final buildingFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o28')); + + final graph = createGraph( + wrap(buildingFeature, 2, buildingFeature.name).first, allFeatures); + // print(graph); + }); + + test('generates a graph map', () { + // Find a building feature + // final buildingFeature = allFeatures.firstWhere((f) => f.type is Building); + final buildingFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o28')); + + final graph = createGraphMap( + wrap(buildingFeature, 2, buildingFeature.name).first, allFeatures); + + print(graph.entries + .map((entry) => "${formatGraphFeature(entry.key)}: " + "{${entry.value.entries.map((e) => "${formatGraphFeature(entry.key)}: ${entry.value.map((key, value) => MapEntry(formatGraphFeature(key), value))}").join(', ')}") + .join('\n')); + }); + + test('generates a graph list', () { + // Find a building feature + // final buildingFeature = allFeatures.firstWhere((f) => f.type is Building); + final buildingFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o28')); + + final graph = createGraphList( + wrap(buildingFeature, 2, buildingFeature.name).first, allFeatures); + var text = graph.map(formatGraphFeature).join(' '); + print(text); + }); + + test('finds adjacent features for a building', () { + // Find a building feature + // final buildingFeature = allFeatures.firstWhere((f) => f.type is Building); + final buildingFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o28')); + + // Find adjacent features for the building + final adjacentFeatures = findAdjacent( + wrap(buildingFeature, 2, buildingFeature.name).first, allFeatures); + + final doorPortal = adjacentFeatures + .firstWhere((f) => f is Portal && f.baseFeature.type is Door); + print( + "adjacent for building ${buildingFeature.name}: \n${adjacentFeatures.map((e) => "${e.toString()}\n")}"); + print(adjacentFeatures.map(unwrap).map(formatFeatureTitle)); + + final doorAdjacentFeatures = findAdjacent(doorPortal, allFeatures); + + print("\n\ndoor $doorPortal : "); + print(doorAdjacentFeatures.map((e) => "${e.toString()}\n")); + print(doorAdjacentFeatures.map(unwrap).map(formatFeatureTitle)); + + final stairsPortal = adjacentFeatures + .firstWhere((f) => f is Portal && f.baseFeature.type is Stairs); + + final stairsAdjacentFeatures = findAdjacent(stairsPortal, allFeatures); + + print("\n\nstairs $stairsPortal : "); + print(stairsAdjacentFeatures.map((e) => "${e.toString()}\n")); + print(stairsAdjacentFeatures.map(unwrap).map(formatFeatureTitle)); + + final baseFeature = adjacentFeatures.firstWhere((f) => f is BasicFeature); + + final baseAdjacentFeatures = findAdjacent(baseFeature, allFeatures); + + print("\n\nbase $baseFeature : "); + print(baseAdjacentFeatures.map((e) => "${e.toString()}\n")); + print(baseAdjacentFeatures.map(unwrap).map(formatFeatureTitle)); + + // Check if all adjacent features are in the same building + expect( + true, // TODO + true); + }); + + test('tries to find a path through the graph using own method', () async { + // Find a building feature + // final buildingFeature = allFeatures.firstWhere((f) => f.type is Building); + final startFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o28')); + + // final endFeature = allFeatures + // .firstWhere((f) => f.type is Building && eq(f.name, 'o25')); + + final endFeature = allFeatures + .firstWhere((f) => f.type is LectureHall && eq(f.name, 'H1')); + + print(endFeature); + + final path = findShortestPath( + wrap(startFeature, 2, startFeature.name).first, + // wrap(endFeature, 2, endFeature.name).first, + wrap(endFeature, endFeature.level!, endFeature.building!).first, + allFeatures, + ); + print(path + .unwrap() + .map((e) => "${formatGraphFeature(e.$1)} (${e.$2}m)") + .join(' -> ')); + }); + + /* + test('tries to find a path through the graph', () async { + // Find a building feature + // final buildingFeature = allFeatures.firstWhere((f) => f.type is Building); + final startFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o28')); + + final endFeature = allFeatures + .firstWhere((f) => f.type is Building && eq(f.name, 'o25')); + + // final endFeature = allFeatures + // .firstWhere((f) => f.type is LectureHall && eq(f.name, 'H1')); + + final graph = createGraph( + wrap(startFeature, 2, startFeature.name).first, allFeatures); + + final path = await Future.any([ + Future.delayed(const Duration(seconds: 5), + () => throw TimeoutException('Test timed out after 5 seconds')), + Future(() => graph.shortestPath( + wrap(startFeature, 1, startFeature.name).first, + wrap(endFeature, 2, endFeature.name).first, + //wrap(endFeature, endFeature.level!, endFeature.building!).first, + )), + ]); + + print(path.map(formatGraphFeature).join('\n')); + }); + */ + }); +}