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:flutter/foundation.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:rust_core/iter.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))', ); } @override int get hashCode { return when( buildingFloor: (floor, building) => Object.hash(floor, building), portal: (fromFloor, from, toFloor, to, baseFeature) => Object.hash(fromFloor, from, toFloor, to, baseFeature), basicFeature: (floor, building, feature) => Object.hash(floor, building, feature), ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is GraphFeature && other.when( buildingFloor: (floor, building) => this is BuildingFloor && (this as BuildingFloor).floor == floor && (this as BuildingFloor).building == building, portal: (fromFloor, from, toFloor, to, baseFeature) => this is Portal && (this as Portal).fromFloor == fromFloor && (this as Portal).from == from && (this as Portal).toFloor == toFloor && (this as Portal).to == to && (this as Portal).baseFeature == baseFeature, basicFeature: (floor, building, feature) => this is BasicFeature && (this as BasicFeature).floor == floor && (this as BasicFeature).building == building && (this as BasicFeature).feature == feature, ); } } class Graph { final List<(GraphFeature, double, GraphFeature)> _edges = []; final HashSet _nodes = HashSet(); final HashSet<(GraphFeature, GraphFeature)> _edgesSet = HashSet(); Iterable get nodes => _nodes.iter(); void addNode(GraphFeature node) { _nodes.add(node); if (node is BasicFeature && node.feature.name == 'H22') { print(node); print(node.hashCode); } } void addEdge(GraphFeature from, GraphFeature to, double weight) { addNode(from); addNode(to); if (!_edgesSet.contains((from, to))) { _edgesSet.add((from, to)); _edges.add((from, weight, to)); } if (!_edgesSet.contains((to, from))) { _edgesSet.add((to, from)); _edges.add((to, weight, from)); } } List<(GraphFeature, double, GraphFeature)> getEdges(GraphFeature node) { return _edges.where((edge) => edge.$1 == node).toList(); } bool contains(GraphFeature node) { return _nodes.contains(node); } bool containsEdge(GraphFeature from, GraphFeature to) { return _edgesSet.contains((from, to)); } @override String toString() { return 'Graph(_edges: $_edges, _nodes: $_nodes, _edgesSet: $_edgesSet)'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is Graph && listEquals(other._edges, _edges) && setEquals(other._nodes, _nodes) && setEquals(other._edgesSet, _edgesSet); } @override int get hashCode => _edges.hashCode ^ _nodes.hashCode ^ _edgesSet.hashCode; } 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, feature.building ?? 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, 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!, floor - i, feature.building!, feature)); } if (floors.contains(floor + i)) { portals.add(GraphFeature.portal( floor, feature.building!, floor + i, feature.building!, 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; } Graph makeGraph(GraphFeature origin, List allFeatures, [Graph? graph]) { // final usedFeatures = [origin]; graph ??= Graph(); graph.addNode(origin); final adjacent = findAdjacent(origin, allFeatures); for (final feature in adjacent.asSet()..removeAll(graph.nodes)) { graph.addEdge(origin, feature, origin.metersTo(feature)); final _ = makeGraph(feature, allFeatures, graph); // graph.addAll(deeper); } return graph; } 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> findShortestPathUndir(GraphFeature origin, bool Function(GraphFeature) destinationSelector, List allFeatures, {heuristicVariant = "zero", heuristicMultiplier = 0.2}) { Graph graph = makeGraph(origin, allFeatures); final GraphFeature? destination = graph.nodes.firstWhereOrNull(destinationSelector); if (!(graph.contains(origin) && destination != null && graph.contains(destination))) { 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 edges = graph.getEdges(node); for (final entry in edges) { final adjNode = entry.$3; final adjCost = entry.$2; 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"); } 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); }, ); }, ); }