feat: complete working navigation functions

This commit is contained in:
Yandrik 2024-04-21 10:47:29 +02:00
parent 3de71e2a5a
commit ad0c8e3124
9 changed files with 237 additions and 261 deletions

View File

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:anyhow/anyhow.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/gestures/positioned_tap_detector_2.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
@ -95,8 +94,8 @@ class MyMapController extends GetxController {
// print(feature?.properties);
if (feature == null) continue;
// print(feature.properties);
final parsed = parseFeature(
feature.properties ?? <String, dynamic>{}, feature.geometry);
final parsed = parseFeature(feature.properties ?? <String, dynamic>{},
feature.geometry, feature.id);
if (parsed case Ok(:final ok)) {
featuresList.add(ok);
}

View File

@ -20,6 +20,7 @@ class Feature with _$Feature {
required GeoJSONGeometry geometry,
int? level,
String? building,
required String id,
}) = _Feature;
bool isPolygon() {

View File

@ -22,6 +22,7 @@ mixin _$Feature {
GeoJSONGeometry get geometry => throw _privateConstructorUsedError;
int? get level => throw _privateConstructorUsedError;
String? get building => throw _privateConstructorUsedError;
String get id => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$FeatureCopyWith<Feature> get copyWith => throw _privateConstructorUsedError;
@ -38,7 +39,8 @@ abstract class $FeatureCopyWith<$Res> {
String? description,
GeoJSONGeometry geometry,
int? level,
String? building});
String? building,
String id});
$FeatureTypeCopyWith<$Res> get type;
}
@ -62,6 +64,7 @@ class _$FeatureCopyWithImpl<$Res, $Val extends Feature>
Object? geometry = null,
Object? level = freezed,
Object? building = freezed,
Object? id = null,
}) {
return _then(_value.copyWith(
name: null == name
@ -88,6 +91,10 @@ class _$FeatureCopyWithImpl<$Res, $Val extends Feature>
? _value.building
: building // ignore: cast_nullable_to_non_nullable
as String?,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
@ -113,7 +120,8 @@ abstract class _$$FeatureImplCopyWith<$Res> implements $FeatureCopyWith<$Res> {
String? description,
GeoJSONGeometry geometry,
int? level,
String? building});
String? building,
String id});
@override
$FeatureTypeCopyWith<$Res> get type;
@ -136,6 +144,7 @@ class __$$FeatureImplCopyWithImpl<$Res>
Object? geometry = null,
Object? level = freezed,
Object? building = freezed,
Object? id = null,
}) {
return _then(_$FeatureImpl(
name: null == name
@ -162,6 +171,10 @@ class __$$FeatureImplCopyWithImpl<$Res>
? _value.building
: building // ignore: cast_nullable_to_non_nullable
as String?,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
@ -175,7 +188,8 @@ class _$FeatureImpl extends _Feature {
this.description,
required this.geometry,
this.level,
this.building})
this.building,
required this.id})
: super._();
@override
@ -190,10 +204,12 @@ class _$FeatureImpl extends _Feature {
final int? level;
@override
final String? building;
@override
final String id;
@override
String toString() {
return 'Feature(name: $name, type: $type, description: $description, geometry: $geometry, level: $level, building: $building)';
return 'Feature(name: $name, type: $type, description: $description, geometry: $geometry, level: $level, building: $building, id: $id)';
}
@override
@ -209,12 +225,13 @@ class _$FeatureImpl extends _Feature {
other.geometry == geometry) &&
(identical(other.level, level) || other.level == level) &&
(identical(other.building, building) ||
other.building == building));
other.building == building) &&
(identical(other.id, id) || other.id == id));
}
@override
int get hashCode => Object.hash(
runtimeType, name, type, description, geometry, level, building);
runtimeType, name, type, description, geometry, level, building, id);
@JsonKey(ignore: true)
@override
@ -230,7 +247,8 @@ abstract class _Feature extends Feature {
final String? description,
required final GeoJSONGeometry geometry,
final int? level,
final String? building}) = _$FeatureImpl;
final String? building,
required final String id}) = _$FeatureImpl;
const _Feature._() : super._();
@override
@ -246,6 +264,8 @@ abstract class _Feature extends Feature {
@override
String? get building;
@override
String get id;
@override
@JsonKey(ignore: true)
_$$FeatureImplCopyWith<_$FeatureImpl> get copyWith =>
throw _privateConstructorUsedError;

View File

@ -4,7 +4,7 @@ import 'package:uninav/data/geo/model.dart';
import 'package:yaml/yaml.dart';
Result<Feature> parseFeature(
Map<String, dynamic> properties, GeoJSONGeometry geometry) {
Map<String, dynamic> properties, GeoJSONGeometry geometry, String id) {
final name = properties['name'] as String?;
final description_yaml = properties['description'] as String? ?? '';
final layer = properties['layer'] as String?;
@ -110,6 +110,7 @@ Result<Feature> parseFeature(
geometry: geometry,
level: level,
building: building,
id: id,
));
}

View File

@ -1,11 +1,11 @@
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';
@ -38,6 +38,12 @@ class GraphFeature with _$GraphFeature {
double metersTo(GraphFeature other) => distanceTo(other, "meters");
String get id => when(
buildingFloor: (floor, building) => building.id,
portal: (fromFloor, from, toFloor, to, baseFeature) => baseFeature.id,
basicFeature: (floor, building, feature) => feature.id,
);
@override
String toString() {
return when(
@ -48,9 +54,102 @@ class GraphFeature with _$GraphFeature {
'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,
);
}
}
bool eq(String? a, String? b) => a?.toLowerCase() == b?.toLowerCase();
class Graph {
final List<(GraphFeature, double, GraphFeature)> _edges = [];
final HashSet<GraphFeature> _nodes = HashSet();
final HashSet<(GraphFeature, GraphFeature)> _edgesSet = HashSet();
Iterable<GraphFeature> 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<GraphFeature> wrap(Feature feature, int floor, String buildingFrom) {
return feature.type
@ -60,7 +159,10 @@ IList<GraphFeature> wrap(Feature feature, int floor, String buildingFrom) {
lift: (floors) => stairPortalGenerator(floors, floor, feature, 99),
door: (connections) =>
doorPortalGenerator(connections, floor, buildingFrom, feature),
orElse: () => [GraphFeature.basicFeature(floor, buildingFrom, feature)],
orElse: () => [
GraphFeature.basicFeature(
floor, feature.building ?? buildingFrom, feature)
],
)
.lock;
}
@ -70,8 +172,7 @@ List<GraphFeature> doorPortalGenerator(
final portals = <GraphFeature>[];
for (final connection in connections.where((c) => !eq(c, from))) {
portals.add(GraphFeature.portal(
floor, from, floor, connection.toLowerCase(), feature));
portals.add(GraphFeature.portal(floor, from, floor, connection, feature));
}
return portals;
@ -83,12 +184,12 @@ List<GraphFeature> stairPortalGenerator(
final portals = <GraphFeature>[];
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));
portals.add(GraphFeature.portal(
floor, feature.building!, floor - i, feature.building!, feature));
}
if (floors.contains(floor + i)) {
portals.add(GraphFeature.portal(floor, feature.building!.toLowerCase(),
floor + i, feature.building!.toLowerCase(), feature));
portals.add(GraphFeature.portal(
floor, feature.building!, floor + i, feature.building!, feature));
}
}
return portals;
@ -142,117 +243,34 @@ List<GraphFeature> findAdjacent(
return adjacentFeatures;
}
List<GraphFeature> createGraphList(
GraphFeature origin, List<Feature> allFeatures,
[Set<GraphFeature>? visited]) {
Graph makeGraph(GraphFeature origin, List<Feature> allFeatures,
[Graph? graph]) {
// final usedFeatures = <GraphFeature>[origin];
visited ??= <GraphFeature>{origin};
graph ??= Graph();
graph.addNode(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);
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 visited.toList();
}
Map<GraphFeature, Map<GraphFeature, double>> createGraphMap(
GraphFeature origin, List<Feature> allFeatures) {
final graphList = createGraphList(origin, allFeatures);
final graphMap = <GraphFeature, Map<GraphFeature, double>>{};
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<GraphFeature, double> createGraph(
GraphFeature origin, List<Feature> allFeatures) {
final map = createGraphMap(origin, allFeatures);
final graph = WeightedDirectedGraph<GraphFeature, double>(
map,
summation: sum,
zero: 0.0,
comparator: (a, b) => compareGraphFeatures(a, b),
);
return graph;
}
Result<List<(GraphFeature, double)>> findShortestPath(
GraphFeature origin, GraphFeature destination, List<Feature> allFeatures,
[heuristicVariant = "zero", heuristicMultiplier = 0.2]) {
var graph = createGraphMap(origin, allFeatures);
Result<List<(GraphFeature, double)>> findShortestPath(GraphFeature origin,
bool Function(GraphFeature) destinationSelector, List<Feature> allFeatures,
{heuristicVariant = "zero", heuristicMultiplier = 0.2}) {
Graph graph = makeGraph(origin, allFeatures);
if (!(graph.keys.contains(origin) &&
graph.values.firstWhereOrNull((vals) => vals.containsKey(destination)) !=
null)) {
final GraphFeature? destination =
graph.nodes.firstWhereOrNull(destinationSelector);
if (!(graph.contains(origin) &&
destination != null &&
graph.contains(destination))) {
return bail("Origin or destination not in graph");
}
@ -299,10 +317,10 @@ Result<List<(GraphFeature, double)>> findShortestPath(
}
// expand node
final adjacents = graph[node]!;
for (final entry in adjacents.entries) {
final adjNode = entry.key;
final adjCost = entry.value;
final edges = graph.getEdges(node);
for (final entry in edges) {
final adjNode = entry.$3;
final adjCost = entry.$2;
if (closedlist.contains(adjNode)) {
continue;
@ -349,77 +367,3 @@ Result<List<(GraphFeature, double)>> findShortestPath(
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);
},
);
},
);
}

View File

@ -144,24 +144,6 @@ class _$BuildingFloorImpl extends BuildingFloor {
@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')
@ -341,29 +323,6 @@ class _$PortalImpl extends Portal {
@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')
@ -529,25 +488,6 @@ class _$BasicFeatureImpl extends BasicFeature {
@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')

View File

@ -1,7 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:uninav/data/geo/model.dart';
bool eq(String? a, String? b) => a?.toLowerCase() == b?.toLowerCase();
String formatDuration(Duration duration) {
final days = duration.inDays;
final hours = duration.inHours.remainder(24);

View File

@ -22,6 +22,7 @@ String formatGraphFeature(GraphFeature feature) {
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('findAdjacent', () {
late MyMapController mapController;
late List<Feature> allFeatures;
@ -34,6 +35,69 @@ void main() {
allFeatures = mapController.features;
});
test('generates a graph (new)', () {
// 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 endFeature = allFeatures
// .firstWhere((f) => f.type is Building && eq(f.name, 'o25'));
// final targetFeature = allFeatures
// .firstWhere((f) => f.type is LectureHall && eq(f.name, 'H22'));
// final wrapped =
// wrap(targetFeature, targetFeature.level!, targetFeature.building!);
// print(wrapped);
final graph = makeGraph(
wrap(buildingFeature, 2, buildingFeature.name).first, allFeatures);
final wrapped = [
graph.nodes.firstWhere((element) =>
element is BasicFeature && eq(element.feature.name, "H22"))
];
print(graph.contains(wrapped.first)); // print(graph);
print(wrapped.first.hashCode); // print(graph);
// print(wrap(targetFeature, targetFeature.level!, targetFeature.building!)
// .first
// .hashCode);
// print(wrapped.first ==
// wrap(targetFeature, targetFeature.level!, targetFeature.building!)
// .first);
// print(graph.toString());
});
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, 'o25'));
// 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, 'H22'));
print(endFeature);
final path = findShortestPath(
wrap(startFeature, 4, startFeature.name).first,
(f) => f is BasicFeature && eq(f.feature.name, 'H1'),
// wrap(endFeature, 2, "o28").first,
allFeatures,
);
print(path
.unwrap()
.map((e) => "${formatGraphFeature(e.$1)} (${e.$2}m)")
.join(' -> '));
});
/*
test('generates a graph', () {
// Find a building feature
// final buildingFeature = allFeatures.firstWhere((f) => f.type is Building);
@ -172,5 +236,6 @@ void main() {
print(path.map(formatGraphFeature).join('\n'));
});
*/
*/
});
}

5
test/scratch_1 Normal file

File diff suppressed because one or more lines are too long