Compare commits

...

5 Commits

Author SHA1 Message Date
1211c9b16e feat: proper routing 2024-04-21 16:27:48 +02:00
a6f9cfdaf4 feat: GPS and web compatible 2024-04-21 14:21:31 +02:00
ad0c8e3124 feat: complete working navigation functions 2024-04-21 10:47:29 +02:00
3de71e2a5a feat: basic navigation with bugs 2024-04-21 08:24:50 +02:00
e653010458 feat: major progress 2024-04-21 02:22:53 +02:00
36 changed files with 4596 additions and 219 deletions

View File

@ -25,6 +25,7 @@ if (flutterVersionName == null) {
android { android {
namespace "com.example.uninav" namespace "com.example.uninav"
compileSdk flutter.compileSdkVersion compileSdk flutter.compileSdkVersion
compileSdkVersion 34
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
@ -45,7 +46,8 @@ android {
applicationId "com.example.uninav" applicationId "com.example.uninav"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion // minSdkVersion flutter.minSdkVersion
minSdkVersion 20
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

View File

@ -41,4 +41,6 @@
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
</queries> </queries>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -384,7 +384,7 @@
"properties": { "properties": {
"name": "O25", "name": "O25",
"layer": "buildings", "layer": "buildings",
"description": "connections: [o26, mensa]" "description": "connections: [o26, mensa, outside]"
}, },
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
@ -2432,6 +2432,38 @@
] ]
}, },
"id": "gzMDI" "id": "gzMDI"
},
{
"type": "Feature",
"properties": {
"name": "door",
"description": "type: door\nlevel: 2\nconnects: [o25, outside]",
"layer": "layer_2"
},
"geometry": {
"type": "Point",
"coordinates": [
9.955606,
48.422081
]
},
"id": "I5NTI"
},
{
"type": "Feature",
"properties": {
"name": "door",
"description": "type: door\nlevel: 2\nconnects: [o25, mensa]",
"layer": "layer_2"
},
"geometry": {
"type": "Point",
"coordinates": [
9.955426,
48.422062
]
},
"id": "c1NDY"
} }
] ]
} }

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:uninav/nav/graph.dart';
import 'package:uninav/util/util.dart';
class EventLog extends StatefulWidget {
final List<GraphFeature> events;
const EventLog({Key? key, required this.events}) : super(key: key);
@override
_EventLogState createState() => _EventLogState();
}
class _EventLogState extends State<EventLog> {
int _selectedIndex = -1;
void _onEventTapped(int index) {
setState(() {
_selectedIndex = index;
});
_scrollController.animateTo(
index * _itemExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
final ScrollController _scrollController = ScrollController();
static const double _itemExtent = 60.0;
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData.light(),
child: SizedBox(
height: 200,
child: ListView.builder(
controller: _scrollController,
itemExtent: _itemExtent + 8.0,
itemCount: widget.events.length,
itemBuilder: (BuildContext context, int index) {
final event = widget.events[index];
final isActive = index >= _selectedIndex;
return GestureDetector(
onTap: () => _onEventTapped(index),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color:
isActive ? Colors.white : Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4.0,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Icon(getIconForEvent(event)),
const SizedBox(width: 8.0),
Text(
getLabelForEvent(event),
style: TextStyle(color: Colors.black),
),
],
),
),
);
},
),
),
);
}
}
IconData getIconForEvent(GraphFeature event) => event.when(
buildingFloor: (level, feature) => Icons.directions_walk,
portal: (fromFloor, from, toFloor, to, baseFeature) =>
baseFeature.type.maybeWhen(
door: (connects) => Icons.door_front_door,
stairs: (connects) => Icons.stairs,
lift: (connects_levels) => Icons.elevator,
orElse: () => Icons.question_mark,
),
basicFeature: (level, building, baseFeature) => Icons.location_on,
);
String getLabelForEvent(GraphFeature event) => event.when(
buildingFloor: (level, feature) => feature.name,
portal: (fromFloor, from, toFloor, to, baseFeature) =>
"$from:$fromFloor -> $to:$toFloor",
basicFeature: (level, building, baseFeature) =>
formatFeatureTitle(baseFeature),
);

View File

@ -14,7 +14,10 @@ class ColorfulChip extends StatelessWidget {
labelStyle: TextStyle( labelStyle: TextStyle(
color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white, color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
), ),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide.none,
),
); );
} }
} }
@ -22,11 +25,13 @@ class ColorfulChip extends StatelessWidget {
class ColorfulActionChip extends StatelessWidget { class ColorfulActionChip extends StatelessWidget {
final String label; final String label;
final VoidCallback onPressed; final VoidCallback onPressed;
final double? size;
const ColorfulActionChip({ const ColorfulActionChip({
Key? key, Key? key,
required this.label, required this.label,
required this.onPressed, required this.onPressed,
this.size,
}) : super(key: key); }) : super(key: key);
@override @override
@ -38,9 +43,15 @@ class ColorfulActionChip extends StatelessWidget {
backgroundColor: color, backgroundColor: color,
labelStyle: TextStyle( labelStyle: TextStyle(
color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white, color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
fontSize: size,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide.none,
), ),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
onPressed: onPressed, onPressed: onPressed,
padding: EdgeInsets.symmetric(
horizontal: (size ?? 16) / 2, vertical: (size ?? 8.0) / 2),
); );
} }
} }

View File

@ -3,7 +3,9 @@ import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:uninav/components/colorful_chips.dart'; import 'package:uninav/components/colorful_chips.dart';
import 'package:uninav/controllers/map_controller.dart'; import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/controllers/navigation_controller.dart';
import 'package:uninav/data/geo/model.dart'; import 'package:uninav/data/geo/model.dart';
import 'package:uninav/nav/graph.dart';
import 'package:uninav/util/util.dart'; import 'package:uninav/util/util.dart';
final _colorfulBoxDeco = BoxDecoration( final _colorfulBoxDeco = BoxDecoration(
@ -18,7 +20,9 @@ Future<void> showFeatureBottomSheet(
Theme( Theme(
data: ThemeData.light(), data: ThemeData.light(),
child: Container( child: Container(
height: 300, constraints: const BoxConstraints(
// minHeight: 300,
),
width: Get.mediaQuery.size.width, width: Get.mediaQuery.size.width,
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.black, color: Colors.black,
@ -26,7 +30,7 @@ Future<void> showFeatureBottomSheet(
topLeft: Radius.circular(30), topRight: Radius.circular(30)), topLeft: Radius.circular(30), topRight: Radius.circular(30)),
), ),
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Column(children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Center( Center(
child: Container( child: Container(
width: 50, width: 50,
@ -65,7 +69,65 @@ Future<void> showFeatureBottomSheet(
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
], ],
if (feature.description != null) ...[
Text(feature.description!),
const SizedBox(height: 10),
],
..._buildFeatureContent(feature), ..._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: () {
print("trying to start navigation...");
final navController = Get.find<NavigationController>();
final mapController = Get.find<MyMapController>();
// make feature into graphFeature
final wrapped = wrap(
feature,
mapController.currentLevel.value,
feature.buildingName ?? "")
.first;
print("1");
Get.back();
if (navController.position.value == null) {
print("2");
Get.snackbar(
"Navigation failed!",
"Please set your position first!",
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.only(
bottom: 20, left: 10, right: 10),
colorText: Colors.white,
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
);
} else {
print("3");
navController.navigate(navController.position.value!,
(feature) => feature.id == wrapped.id);
}
print("4");
},
),
],
)
]), ]),
), ),
), ),
@ -79,7 +141,9 @@ List<Widget> _buildFeatureContent(Feature feature) {
return feature.type.when( return feature.type.when(
building: () => _buildBuildingContent(feature), building: () => _buildBuildingContent(feature),
lectureHall: () => _buildLectureHallContent(feature), lectureHall: () => _buildLectureHallContent(feature),
room: () => _buildRoomContent(feature), room: (number) => _buildRoomContent(feature, number),
pcPool: (number) => _buildPcPoolContent(feature, number),
foodDrink: () => _buildFoodAndDrinkContent(feature),
door: (connects) => _buildDoorContent(feature, connects), door: (connects) => _buildDoorContent(feature, connects),
toilet: (toiletType) => _buildToiletContent(feature, toiletType), toilet: (toiletType) => _buildToiletContent(feature, toiletType),
stairs: (connectsLevels) => _buildStairsContent(feature, connectsLevels), stairs: (connectsLevels) => _buildStairsContent(feature, connectsLevels),
@ -102,28 +166,136 @@ List<Widget> _buildLectureHallContent(Feature feature) {
} }
/// Builds the content for the Room feature type. /// Builds the content for the Room feature type.
List<Widget> _buildRoomContent(Feature feature) { List<Widget> _buildRoomContent(Feature feature, String roomNumber) {
return [Text('Room: ${feature.name}')]; return [Text('Room: ${feature.name}')];
} }
List<Widget> _buildPcPoolContent(Feature feature, String roomNumber) {
return [Text('PC Pool: ${feature.name}')];
}
List<Widget> _buildFoodAndDrinkContent(Feature feature) {
return [Text('${feature.name} (Food/Drink)')];
}
/// Builds the content for the Door feature type. /// Builds the content for the Door feature type.
List<Widget> _buildDoorContent(Feature feature, List<String> connects) { List<Widget> _buildDoorContent(Feature feature, List<String> 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. /// Builds the content for the Toilet feature type.
List<Widget> _buildToiletContent(Feature feature, String toiletType) { List<Widget> _buildToiletContent(Feature feature, String toiletType) {
return [Text('Toilet: ${feature.name}\nType: $toiletType')]; return [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.wc,
size: 60,
color: Colors.white,
),
Icon(
findToiletIcon(toiletType),
size: 60,
color: Colors.white,
)
],
)
];
} }
/// Builds the content for the Stairs feature type. /// Builds the content for the Stairs feature type.
List<Widget> _buildStairsContent(Feature feature, List<int> connectsLevels) { List<Widget> _buildStairsContent(Feature feature, List<int> connectsLevels) {
return [Text('Stairs: ${feature.name}\nConnects Levels: $connectsLevels')]; return [
Text(
feature.name,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 10),
if (connectsLevels.isNotEmpty) ...[
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Text(
'Connects Levels:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: connectsLevels.map((level) {
return ColorfulChip(label: level.toString());
}).toList(),
),
),
],
];
} }
/// Builds the content for the Lift feature type. /// Builds the content for the Lift feature type.
List<Widget> _buildLiftContent(Feature feature, List<int> connectsLevels) { List<Widget> _buildLiftContent(Feature feature, List<int> connectsLevels) {
return [Text('Lift: ${feature.name}\nConnects Levels: $connectsLevels')]; return [
Text(
feature.name,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 10),
if (connectsLevels.isNotEmpty) ...[
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Text(
'Connects Levels:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: connectsLevels.map((level) {
return ColorfulChip(label: level.toString());
}).toList(),
),
),
],
];
} }
/// Builds the content for the PublicTransport feature type. /// Builds the content for the PublicTransport feature type.

View File

@ -8,9 +8,13 @@ import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/data/geo/model.dart'; import 'package:uninav/data/geo/model.dart';
import 'package:uninav/map.dart'; import 'package:uninav/map.dart';
import 'package:uninav/util/geomath.dart'; import 'package:uninav/util/geomath.dart';
import 'package:uninav/util/util.dart';
List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) { List<Widget> renderLevel(
int level,
) {
return <Widget>[ return <Widget>[
// Lecture Halls
LevelLayer( LevelLayer(
filter: (feature) => filter: (feature) =>
feature.level == level && feature.type is LectureHall, feature.level == level && feature.type is LectureHall,
@ -21,7 +25,6 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
color: Colors.orange.withOpacity(0.2), color: Colors.orange.withOpacity(0.2),
borderColor: Colors.orange, borderColor: Colors.orange,
borderStrokeWidth: 2, borderStrokeWidth: 2,
hitValue: feature,
), ),
) )
.unwrap(), .unwrap(),
@ -40,7 +43,9 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
), ),
alignment: Alignment.center, alignment: Alignment.center,
), ),
notifier: hitNotifier), ),
// Rooms (Seminar Rooms)
LevelLayer( LevelLayer(
filter: (feature) => feature.level == level && feature.type is Room, filter: (feature) => feature.level == level && feature.type is Room,
polyConstructor: (feature) => feature polyConstructor: (feature) => feature
@ -50,12 +55,28 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
color: Colors.green.withOpacity(1.2), color: Colors.green.withOpacity(1.2),
borderColor: Colors.green, borderColor: Colors.green,
borderStrokeWidth: 2, borderStrokeWidth: 2,
hitValue: feature,
), ),
) )
.unwrap(), .unwrap(),
notifier: hitNotifier, markerConstructor: (feature) => Marker(
width: 100,
height: 70,
point: feature.getPoint().unwrap(),
child: Column(
children: [
const Icon(
Icons.co_present_rounded,
color: Colors.amber,
), ),
Text(
(feature.type as Room).roomNumber,
style: const TextStyle(color: Colors.black),
),
],
),
alignment: Alignment.center,
)),
// Doors Layer
LevelLayer( LevelLayer(
filter: (feature) => feature.level == level && feature.type is Door, filter: (feature) => feature.level == level && feature.type is Door,
markerConstructor: (feature) { markerConstructor: (feature) {
@ -71,30 +92,77 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
alignment: Alignment.center, alignment: Alignment.center,
); );
}, },
notifier: hitNotifier,
), ),
// Food and Drink Layer
LevelLayer(
filter: (feature) => feature.level == level && feature.type is FoodDrink,
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 21,
height: 21,
point: point,
child: const Icon(
Icons.restaurant,
color: Colors.deepOrange,
),
alignment: Alignment.center,
);
},
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.deepOrange.withOpacity(0.2),
borderColor: Colors.deepOrange,
borderStrokeWidth: 2,
),
)
.unwrap(),
),
// PC Pools layer
LevelLayer(
filter: (feature) => feature.level == level && feature.type is PcPool,
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 100,
height: 70,
point: point,
child: Column(
children: [
const Icon(
Icons.computer,
color: Colors.cyan,
),
Text(
(feature.type as PcPool).roomNumber,
style: const TextStyle(color: Colors.black),
),
],
),
alignment: Alignment.center,
);
},
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.cyan.withOpacity(0.2),
borderColor: Colors.cyan,
borderStrokeWidth: 2,
),
)
.unwrap(),
),
// Toilets Layer
LevelLayer( LevelLayer(
filter: (feature) => feature.level == level && feature.type is Toilet, filter: (feature) => feature.level == level && feature.type is Toilet,
markerConstructor: (feature) { markerConstructor: (feature) {
final type = (feature.type as Toilet).toilet_type; final type = (feature.type as Toilet).toilet_type;
IconData icon;
switch (type.toLowerCase()) {
case 'male':
icon = Icons.male;
break;
case 'female':
icon = Icons.female;
break;
case 'handicap':
icon = Icons.wheelchair_pickup;
break;
default:
print("WARN: Toilet didn't have recognizable type! "
"(Level ${feature.level}, Name ${feature.name}, "
"Location: ${feature.getPoint().unwrap()})");
icon = Icons.wc;
break;
}
final point = feature.getPoint().unwrap(); final point = feature.getPoint().unwrap();
return Marker( return Marker(
@ -102,14 +170,15 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
height: 21, height: 21,
point: point, point: point,
child: Icon( child: Icon(
icon, findToiletIcon(type),
color: Colors.purple, color: Colors.blue.shade700,
), ),
alignment: Alignment.center, alignment: Alignment.center,
); );
}, },
notifier: hitNotifier,
), ),
// Stairs layer
LevelLayer( LevelLayer(
filter: (feature) => filter: (feature) =>
feature.type is Stairs && feature.type is Stairs &&
@ -127,8 +196,9 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
alignment: Alignment.center, alignment: Alignment.center,
); );
}, },
notifier: hitNotifier,
), ),
// Lift layer
LevelLayer( LevelLayer(
filter: (feature) => filter: (feature) =>
feature.type is Lift && feature.type is Lift &&
@ -146,7 +216,6 @@ List<Widget> renderLevel(int level, {LayerHitNotifier? hitNotifier}) {
alignment: Alignment.center, alignment: Alignment.center,
); );
}, },
notifier: hitNotifier,
), ),
]; ];
} }
@ -157,7 +226,6 @@ class LevelLayer extends StatelessWidget {
final Marker Function(LatLng, String)? polyCenterMarkerConstructor; final Marker Function(LatLng, String)? polyCenterMarkerConstructor;
final Marker Function(Feature)? markerConstructor; final Marker Function(Feature)? markerConstructor;
final int? level; final int? level;
final LayerHitNotifier? notifier;
const LevelLayer({ const LevelLayer({
this.level, this.level,
@ -165,7 +233,6 @@ class LevelLayer extends StatelessWidget {
this.polyConstructor, this.polyConstructor,
this.polyCenterMarkerConstructor, this.polyCenterMarkerConstructor,
this.markerConstructor, this.markerConstructor,
this.notifier,
super.key, super.key,
}); });
@ -190,7 +257,6 @@ class LevelLayer extends StatelessWidget {
points: points, points: points,
borderColor: Colors.black26, borderColor: Colors.black26,
borderStrokeWidth: 2.0, borderStrokeWidth: 2.0,
hitValue: feature,
)) ))
.unwrap()); .unwrap());
} }
@ -258,14 +324,12 @@ class LevelLayer extends StatelessWidget {
widgets.add(TranslucentPointer( widgets.add(TranslucentPointer(
child: PolygonLayer( child: PolygonLayer(
polygons: filteredPolygons, polygons: filteredPolygons,
hitNotifier: notifier,
), ),
)); ));
} else { } else {
widgets.add(TranslucentPointer( widgets.add(TranslucentPointer(
child: PolygonLayer( child: PolygonLayer(
polygons: filteredPolygons, polygons: filteredPolygons,
hitNotifier: notifier,
), ),
)); ));
} }

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:rust_core/iter.dart';
import 'package:uninav/controllers/navigation_controller.dart';
import 'package:uninav/nav/graph.dart';
class NavigationPathLayer extends StatelessWidget {
const NavigationPathLayer({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
// compute the polylines and markers
List<(GraphFeature, double)> route =
Get.find<NavigationController>().nav.iter().toList();
// distance-position pairs
// List<Polyline> polylines = [];
List<LatLng> polylinePoints =
route.map((e) => e.$1.getCenter().unwrap()).toList();
return PolylineLayer(polylines: [
Polyline(
points: polylinePoints,
strokeWidth: 4.0,
color: Colors.blue,
),
]);
});
}
}

View File

@ -0,0 +1,74 @@
import 'package:get/get.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
part 'isar_controller.g.dart';
/// Manages the initialization and access to the Isar database instance.
///
/// The `IsarController` class is responsible for initializing the Isar database
/// and providing access to the database instance. It ensures that the Isar
/// instance is properly initialized before it can be accessed.
///
/// The `initializeIsar()` method is used to initialize the Isar instance by
/// opening the database with the provided schema. Make sure to call this before
/// calling anything else!. The `isar` getter returns the
/// Isar instance, throwing an exception if the instance is not yet initialized.
class IsarController {
late Isar _isar;
late final Rx<Settings> settings;
IsarController();
Future<void> initializeIsar() async {
final dir = await getApplicationDocumentsDirectory();
_isar = await Isar.open(
[SettingsSchema],
directory: dir.path,
);
settings = (await _isar.settings.get(1) ?? Settings()).obs;
_isar.settings.watchObject(1).forEach((element) {
if (element != null) settings.value = element;
});
}
Future<void> persistSettings() async {
await _isar.writeTxn(() async {
await _isar.settings.put(settings.value);
});
}
Isar get isar {
if (_isar == null) {
throw Exception('Isar is not initialized');
}
return _isar;
}
}
@collection
class Settings {
Id id = 1;
bool showIcons = true;
bool showElevators = true;
bool showFoodAndDrink = true;
bool showLectureHalls = true;
bool showComputerPools = true;
bool showSeminarRooms = true;
bool showToilets = true;
bool showStairs = true;
bool showDoors = true;
bool maleToilets = false;
bool femaleToilets = false;
bool handicapToilets = false;
}
enum ToiletPreference {
male,
female,
disabled,
}

View File

@ -1,15 +1,17 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:anyhow/anyhow.dart'; import 'package:anyhow/anyhow.dart';
import 'package:flutter_map/flutter_map.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:geojson_vi/geojson_vi.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:uninav/components/feature_bottom_sheet.dart'; import 'package:uninav/components/feature_bottom_sheet.dart';
import 'package:uninav/data/geo/model.dart'; import 'package:uninav/data/geo/model.dart';
import 'package:uninav/data/geo/parser.dart'; import 'package:uninav/data/geo/parser.dart';
import 'package:uninav/util/geojson_util.dart'; import 'package:uninav/util/geojson_util.dart';
import 'package:uninav/util/geolocator.dart';
import 'package:uninav/util/geomath.dart'; import 'package:uninav/util/geomath.dart';
class MyMapController extends GetxController { class MyMapController extends GetxController {
@ -18,6 +20,10 @@ class MyMapController extends GetxController {
final RxList<Feature> features = <Feature>[].obs; final RxList<Feature> features = <Feature>[].obs;
final currentLevel = 1.obs; final currentLevel = 1.obs;
final levels = <int>[1].obs; final levels = <int>[1].obs;
final Rx<Position?> position = null.obs;
bool _locationEnsured = false;
@override @override
onInit() async { onInit() async {
print("init"); print("init");
@ -35,6 +41,7 @@ class MyMapController extends GetxController {
} }
newLevels.sort(); newLevels.sort();
levels.value = newLevels; levels.value = newLevels;
update();
} }
Result<void> setLevel(int level) { Result<void> setLevel(int level) {
@ -44,6 +51,7 @@ class MyMapController extends GetxController {
} }
currentLevel.value = level; currentLevel.value = level;
update();
return const Ok(()); return const Ok(());
} }
@ -86,23 +94,26 @@ class MyMapController extends GetxController {
final featuresList = <Feature>[]; final featuresList = <Feature>[];
print('doing'); // print('doing');
final geojson = GeoJSONFeatureCollection.fromJSON(geoJsonString); final geojson = GeoJSONFeatureCollection.fromJSON(geoJsonString);
print('done'); // print('done');
for (final feature in geojson.features) { for (final feature in geojson.features) {
print(feature); // print(feature);
print(feature?.properties); // print(feature?.properties);
if (feature == null) continue; if (feature == null) continue;
print(feature.properties); // print(feature.properties);
final parsed = parseFeature( final parsed = parseFeature(feature.properties ?? <String, dynamic>{},
feature.properties ?? <String, dynamic>{}, feature.geometry); feature.geometry, feature.id);
if (parsed case Ok(:final ok)) { if (parsed case Ok(:final ok)) {
featuresList.add(ok); featuresList.add(ok);
} else {
print('Error parsing feature: $parsed');
} }
} }
features.value = featuresList; features.value = featuresList;
update();
} catch (e) { } catch (e) {
print('Error parsing GeoJSON: $e'); print('Error parsing GeoJSON: $e');
} }
@ -138,4 +149,46 @@ class MyMapController extends GetxController {
} }
showFeatureBottomSheet(feature, closestFeatures); showFeatureBottomSheet(feature, closestFeatures);
} }
Future<Result<Position>> getCurrentPosition() async {
if (!_locationEnsured) {
final ensureRes = await ensureLocationPermission();
if (ensureRes is Err) {
// TODO: check that it works
return ensureRes as Err<Position>;
}
}
_locationEnsured = true;
try {
final pos = await Geolocator.getCurrentPosition(
// desiredAccuracy: LocationAccuracy.high,
timeLimit: Duration(minutes: 1),
);
position.value = pos;
return Ok(pos);
} on TimeoutException catch (e) {
return bail("Timeout while waiting for location lock: $e");
}
}
Future<Result<()>> subscribePosition() async {
if (!_locationEnsured) {
final ensureRes = await ensureLocationPermission();
if (ensureRes is Err) {
// TODO: check that it works
return ensureRes;
}
}
_locationEnsured = true;
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 100,
//timeLimit: Duration(minutes: 10)
),
).listen((pos) {
position.value = pos;
});
return const Ok(());
}
} }

View File

@ -0,0 +1,72 @@
import 'package:anyhow/anyhow.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rust_core/iter.dart';
import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:uninav/nav/graph.dart';
class NavigationController extends GetxController {
// Add controller logic and variables here
final RxList<(GraphFeature, double)> nav = RxList();
final Rx<GraphFeature?> position = Rx(null);
void navigate(
GraphFeature start, bool Function(GraphFeature) endSelector) async {
position.value = start;
final path = await compute(
(data) {
final start = data.$1;
final endSelector = data.$2;
final features = data.$3;
final path = findShortestPath(start, endSelector, features);
return path;
},
(
start,
endSelector,
Get.find<MyMapController>().features.iter().toList()
),
);
if (path.isErr()) {
Get.snackbar(
'Navigation Error',
'Unable to find a path to the destination\nMessage: ${path.unwrapErr().toString().split('\n')[0]}',
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.only(bottom: 20, left: 10, right: 10),
colorText: Colors.white,
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
);
return;
}
nav.value = path.unwrap();
update();
}
void updatePosition(GraphFeature? newPosition) {
position.value = newPosition;
update();
}
}
class _PathFindingData {
final GraphFeature start;
final bool Function(GraphFeature) endSelector;
final List<Feature> features;
_PathFindingData(this.start, this.endSelector, this.features);
}
Result<List<(GraphFeature, double)>> _findShortestPathIsolate(
_PathFindingData data) {
return findShortestPath(data.start, data.endSelector, data.features);
}

View File

@ -0,0 +1,72 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'shared_prefs_controller.freezed.dart';
part 'shared_prefs_controller.g.dart';
class SharedPrefsController {
late SharedPreferences _sharedPrefs;
final Rx<Settings> settings = Settings().obs;
SharedPrefsController();
Future<void> initialize() async {
_sharedPrefs = await SharedPreferences.getInstance();
try {
final settingsJson = _sharedPrefs.getString("settings");
if (settingsJson != null) {
settings.value = Settings.fromJson(jsonDecode(settingsJson));
} else {
settings.value = const Settings();
}
} catch (e) {
settings.value = const Settings();
}
}
Future<void> persistSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('settings', jsonEncode(settings.value));
}
Future<Settings> loadSettings() async {
final prefs = await SharedPreferences.getInstance();
final settingsJson = prefs.getString('settings');
return settingsJson != null
? Settings.fromJson(jsonDecode(settingsJson))
: const Settings();
}
}
@freezed
class Settings with _$Settings {
const factory Settings({
@Default(1) int id,
@Default(true) bool showIcons,
@Default(true) bool showElevators,
@Default(true) bool showFoodAndDrink,
@Default(true) bool showLectureHalls,
@Default(true) bool showComputerPools,
@Default(true) bool showSeminarRooms,
@Default(true) bool showToilets,
@Default(true) bool showStairs,
@Default(true) bool showDoors,
@Default(false) bool maleToilets,
@Default(false) bool femaleToilets,
@Default(false) bool handicapToilets,
}) = _Settings;
factory Settings.fromJson(Map<String, dynamic> json) =>
_$SettingsFromJson(json);
}
enum ToiletPreference {
male,
female,
disabled,
}

View File

@ -0,0 +1,433 @@
// 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 'shared_prefs_controller.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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');
Settings _$SettingsFromJson(Map<String, dynamic> json) {
return _Settings.fromJson(json);
}
/// @nodoc
mixin _$Settings {
int get id => throw _privateConstructorUsedError;
bool get showIcons => throw _privateConstructorUsedError;
bool get showElevators => throw _privateConstructorUsedError;
bool get showFoodAndDrink => throw _privateConstructorUsedError;
bool get showLectureHalls => throw _privateConstructorUsedError;
bool get showComputerPools => throw _privateConstructorUsedError;
bool get showSeminarRooms => throw _privateConstructorUsedError;
bool get showToilets => throw _privateConstructorUsedError;
bool get showStairs => throw _privateConstructorUsedError;
bool get showDoors => throw _privateConstructorUsedError;
bool get maleToilets => throw _privateConstructorUsedError;
bool get femaleToilets => throw _privateConstructorUsedError;
bool get handicapToilets => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SettingsCopyWith<Settings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SettingsCopyWith<$Res> {
factory $SettingsCopyWith(Settings value, $Res Function(Settings) then) =
_$SettingsCopyWithImpl<$Res, Settings>;
@useResult
$Res call(
{int id,
bool showIcons,
bool showElevators,
bool showFoodAndDrink,
bool showLectureHalls,
bool showComputerPools,
bool showSeminarRooms,
bool showToilets,
bool showStairs,
bool showDoors,
bool maleToilets,
bool femaleToilets,
bool handicapToilets});
}
/// @nodoc
class _$SettingsCopyWithImpl<$Res, $Val extends Settings>
implements $SettingsCopyWith<$Res> {
_$SettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? showIcons = null,
Object? showElevators = null,
Object? showFoodAndDrink = null,
Object? showLectureHalls = null,
Object? showComputerPools = null,
Object? showSeminarRooms = null,
Object? showToilets = null,
Object? showStairs = null,
Object? showDoors = null,
Object? maleToilets = null,
Object? femaleToilets = null,
Object? handicapToilets = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
showIcons: null == showIcons
? _value.showIcons
: showIcons // ignore: cast_nullable_to_non_nullable
as bool,
showElevators: null == showElevators
? _value.showElevators
: showElevators // ignore: cast_nullable_to_non_nullable
as bool,
showFoodAndDrink: null == showFoodAndDrink
? _value.showFoodAndDrink
: showFoodAndDrink // ignore: cast_nullable_to_non_nullable
as bool,
showLectureHalls: null == showLectureHalls
? _value.showLectureHalls
: showLectureHalls // ignore: cast_nullable_to_non_nullable
as bool,
showComputerPools: null == showComputerPools
? _value.showComputerPools
: showComputerPools // ignore: cast_nullable_to_non_nullable
as bool,
showSeminarRooms: null == showSeminarRooms
? _value.showSeminarRooms
: showSeminarRooms // ignore: cast_nullable_to_non_nullable
as bool,
showToilets: null == showToilets
? _value.showToilets
: showToilets // ignore: cast_nullable_to_non_nullable
as bool,
showStairs: null == showStairs
? _value.showStairs
: showStairs // ignore: cast_nullable_to_non_nullable
as bool,
showDoors: null == showDoors
? _value.showDoors
: showDoors // ignore: cast_nullable_to_non_nullable
as bool,
maleToilets: null == maleToilets
? _value.maleToilets
: maleToilets // ignore: cast_nullable_to_non_nullable
as bool,
femaleToilets: null == femaleToilets
? _value.femaleToilets
: femaleToilets // ignore: cast_nullable_to_non_nullable
as bool,
handicapToilets: null == handicapToilets
? _value.handicapToilets
: handicapToilets // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$SettingsImplCopyWith<$Res>
implements $SettingsCopyWith<$Res> {
factory _$$SettingsImplCopyWith(
_$SettingsImpl value, $Res Function(_$SettingsImpl) then) =
__$$SettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
bool showIcons,
bool showElevators,
bool showFoodAndDrink,
bool showLectureHalls,
bool showComputerPools,
bool showSeminarRooms,
bool showToilets,
bool showStairs,
bool showDoors,
bool maleToilets,
bool femaleToilets,
bool handicapToilets});
}
/// @nodoc
class __$$SettingsImplCopyWithImpl<$Res>
extends _$SettingsCopyWithImpl<$Res, _$SettingsImpl>
implements _$$SettingsImplCopyWith<$Res> {
__$$SettingsImplCopyWithImpl(
_$SettingsImpl _value, $Res Function(_$SettingsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? showIcons = null,
Object? showElevators = null,
Object? showFoodAndDrink = null,
Object? showLectureHalls = null,
Object? showComputerPools = null,
Object? showSeminarRooms = null,
Object? showToilets = null,
Object? showStairs = null,
Object? showDoors = null,
Object? maleToilets = null,
Object? femaleToilets = null,
Object? handicapToilets = null,
}) {
return _then(_$SettingsImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
showIcons: null == showIcons
? _value.showIcons
: showIcons // ignore: cast_nullable_to_non_nullable
as bool,
showElevators: null == showElevators
? _value.showElevators
: showElevators // ignore: cast_nullable_to_non_nullable
as bool,
showFoodAndDrink: null == showFoodAndDrink
? _value.showFoodAndDrink
: showFoodAndDrink // ignore: cast_nullable_to_non_nullable
as bool,
showLectureHalls: null == showLectureHalls
? _value.showLectureHalls
: showLectureHalls // ignore: cast_nullable_to_non_nullable
as bool,
showComputerPools: null == showComputerPools
? _value.showComputerPools
: showComputerPools // ignore: cast_nullable_to_non_nullable
as bool,
showSeminarRooms: null == showSeminarRooms
? _value.showSeminarRooms
: showSeminarRooms // ignore: cast_nullable_to_non_nullable
as bool,
showToilets: null == showToilets
? _value.showToilets
: showToilets // ignore: cast_nullable_to_non_nullable
as bool,
showStairs: null == showStairs
? _value.showStairs
: showStairs // ignore: cast_nullable_to_non_nullable
as bool,
showDoors: null == showDoors
? _value.showDoors
: showDoors // ignore: cast_nullable_to_non_nullable
as bool,
maleToilets: null == maleToilets
? _value.maleToilets
: maleToilets // ignore: cast_nullable_to_non_nullable
as bool,
femaleToilets: null == femaleToilets
? _value.femaleToilets
: femaleToilets // ignore: cast_nullable_to_non_nullable
as bool,
handicapToilets: null == handicapToilets
? _value.handicapToilets
: handicapToilets // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SettingsImpl implements _Settings {
const _$SettingsImpl(
{this.id = 1,
this.showIcons = true,
this.showElevators = true,
this.showFoodAndDrink = true,
this.showLectureHalls = true,
this.showComputerPools = true,
this.showSeminarRooms = true,
this.showToilets = true,
this.showStairs = true,
this.showDoors = true,
this.maleToilets = false,
this.femaleToilets = false,
this.handicapToilets = false});
factory _$SettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$SettingsImplFromJson(json);
@override
@JsonKey()
final int id;
@override
@JsonKey()
final bool showIcons;
@override
@JsonKey()
final bool showElevators;
@override
@JsonKey()
final bool showFoodAndDrink;
@override
@JsonKey()
final bool showLectureHalls;
@override
@JsonKey()
final bool showComputerPools;
@override
@JsonKey()
final bool showSeminarRooms;
@override
@JsonKey()
final bool showToilets;
@override
@JsonKey()
final bool showStairs;
@override
@JsonKey()
final bool showDoors;
@override
@JsonKey()
final bool maleToilets;
@override
@JsonKey()
final bool femaleToilets;
@override
@JsonKey()
final bool handicapToilets;
@override
String toString() {
return 'Settings(id: $id, showIcons: $showIcons, showElevators: $showElevators, showFoodAndDrink: $showFoodAndDrink, showLectureHalls: $showLectureHalls, showComputerPools: $showComputerPools, showSeminarRooms: $showSeminarRooms, showToilets: $showToilets, showStairs: $showStairs, showDoors: $showDoors, maleToilets: $maleToilets, femaleToilets: $femaleToilets, handicapToilets: $handicapToilets)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SettingsImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.showIcons, showIcons) ||
other.showIcons == showIcons) &&
(identical(other.showElevators, showElevators) ||
other.showElevators == showElevators) &&
(identical(other.showFoodAndDrink, showFoodAndDrink) ||
other.showFoodAndDrink == showFoodAndDrink) &&
(identical(other.showLectureHalls, showLectureHalls) ||
other.showLectureHalls == showLectureHalls) &&
(identical(other.showComputerPools, showComputerPools) ||
other.showComputerPools == showComputerPools) &&
(identical(other.showSeminarRooms, showSeminarRooms) ||
other.showSeminarRooms == showSeminarRooms) &&
(identical(other.showToilets, showToilets) ||
other.showToilets == showToilets) &&
(identical(other.showStairs, showStairs) ||
other.showStairs == showStairs) &&
(identical(other.showDoors, showDoors) ||
other.showDoors == showDoors) &&
(identical(other.maleToilets, maleToilets) ||
other.maleToilets == maleToilets) &&
(identical(other.femaleToilets, femaleToilets) ||
other.femaleToilets == femaleToilets) &&
(identical(other.handicapToilets, handicapToilets) ||
other.handicapToilets == handicapToilets));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
id,
showIcons,
showElevators,
showFoodAndDrink,
showLectureHalls,
showComputerPools,
showSeminarRooms,
showToilets,
showStairs,
showDoors,
maleToilets,
femaleToilets,
handicapToilets);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SettingsImplCopyWith<_$SettingsImpl> get copyWith =>
__$$SettingsImplCopyWithImpl<_$SettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SettingsImplToJson(
this,
);
}
}
abstract class _Settings implements Settings {
const factory _Settings(
{final int id,
final bool showIcons,
final bool showElevators,
final bool showFoodAndDrink,
final bool showLectureHalls,
final bool showComputerPools,
final bool showSeminarRooms,
final bool showToilets,
final bool showStairs,
final bool showDoors,
final bool maleToilets,
final bool femaleToilets,
final bool handicapToilets}) = _$SettingsImpl;
factory _Settings.fromJson(Map<String, dynamic> json) =
_$SettingsImpl.fromJson;
@override
int get id;
@override
bool get showIcons;
@override
bool get showElevators;
@override
bool get showFoodAndDrink;
@override
bool get showLectureHalls;
@override
bool get showComputerPools;
@override
bool get showSeminarRooms;
@override
bool get showToilets;
@override
bool get showStairs;
@override
bool get showDoors;
@override
bool get maleToilets;
@override
bool get femaleToilets;
@override
bool get handicapToilets;
@override
@JsonKey(ignore: true)
_$$SettingsImplCopyWith<_$SettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shared_prefs_controller.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SettingsImpl _$$SettingsImplFromJson(Map<String, dynamic> json) =>
_$SettingsImpl(
id: json['id'] as int? ?? 1,
showIcons: json['showIcons'] as bool? ?? true,
showElevators: json['showElevators'] as bool? ?? true,
showFoodAndDrink: json['showFoodAndDrink'] as bool? ?? true,
showLectureHalls: json['showLectureHalls'] as bool? ?? true,
showComputerPools: json['showComputerPools'] as bool? ?? true,
showSeminarRooms: json['showSeminarRooms'] as bool? ?? true,
showToilets: json['showToilets'] as bool? ?? true,
showStairs: json['showStairs'] as bool? ?? true,
showDoors: json['showDoors'] as bool? ?? true,
maleToilets: json['maleToilets'] as bool? ?? false,
femaleToilets: json['femaleToilets'] as bool? ?? false,
handicapToilets: json['handicapToilets'] as bool? ?? false,
);
Map<String, dynamic> _$$SettingsImplToJson(_$SettingsImpl instance) =>
<String, dynamic>{
'id': instance.id,
'showIcons': instance.showIcons,
'showElevators': instance.showElevators,
'showFoodAndDrink': instance.showFoodAndDrink,
'showLectureHalls': instance.showLectureHalls,
'showComputerPools': instance.showComputerPools,
'showSeminarRooms': instance.showSeminarRooms,
'showToilets': instance.showToilets,
'showStairs': instance.showStairs,
'showDoors': instance.showDoors,
'maleToilets': instance.maleToilets,
'femaleToilets': instance.femaleToilets,
'handicapToilets': instance.handicapToilets,
};

View File

@ -20,6 +20,7 @@ class Feature with _$Feature {
required GeoJSONGeometry geometry, required GeoJSONGeometry geometry,
int? level, int? level,
String? building, String? building,
required String id,
}) = _Feature; }) = _Feature;
bool isPolygon() { bool isPolygon() {
@ -36,7 +37,6 @@ class Feature with _$Feature {
points: pts, points: pts,
borderColor: Colors.black26, borderColor: Colors.black26,
borderStrokeWidth: 2.0, borderStrokeWidth: 2.0,
hitValue: 'test${pts.length}',
); );
final polygon = geometry as GeoJSONPolygon; final polygon = geometry as GeoJSONPolygon;
// print(polygon.geometry!.geoSeries[0].geoPoints); // print(polygon.geometry!.geoSeries[0].geoPoints);
@ -92,6 +92,19 @@ class Feature with _$Feature {
return bail("Feature Geometry is not a Polygon or Point"); return bail("Feature Geometry is not a Polygon or Point");
} }
} }
String? get buildingName => type.when(
building: () => name,
lectureHall: () => building,
room: (_) => building,
door: (_) => null,
toilet: (_) => building,
stairs: (_) => building,
lift: (_) => building,
foodDrink: () => building,
publicTransport: (_, __) => null,
pcPool: (_) => building,
);
} }
@freezed @freezed
@ -99,11 +112,13 @@ class FeatureType with _$FeatureType {
// multiple feature types like lecture hall, toliet, ... // multiple feature types like lecture hall, toliet, ...
const factory FeatureType.building() = Building; const factory FeatureType.building() = Building;
const factory FeatureType.lectureHall() = LectureHall; const factory FeatureType.lectureHall() = LectureHall;
const factory FeatureType.room() = Room; const factory FeatureType.room(String roomNumber) = Room;
const factory FeatureType.door(List<String> connects) = Door; const factory FeatureType.door(List<String> connects) = Door;
const factory FeatureType.toilet(String toilet_type) = Toilet; const factory FeatureType.toilet(String toilet_type) = Toilet;
const factory FeatureType.stairs(List<int> connects_levels) = Stairs; const factory FeatureType.stairs(List<int> connects_levels) = Stairs;
const factory FeatureType.lift(List<int> connects_levels) = Lift; const factory FeatureType.lift(List<int> connects_levels) = Lift;
const factory FeatureType.foodDrink() = FoodDrink;
const factory FeatureType.publicTransport( const factory FeatureType.publicTransport(
List<String> bus_lines, List<String> tram_lines) = PublicTransport; List<String> bus_lines, List<String> tram_lines) = PublicTransport;
const factory FeatureType.pcPool(String roomNumber) = PcPool;
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import 'package:uninav/data/geo/model.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
Result<Feature> parseFeature( Result<Feature> parseFeature(
Map<String, dynamic> properties, GeoJSONGeometry geometry) { Map<String, dynamic> properties, GeoJSONGeometry geometry, String id) {
final name = properties['name'] as String?; final name = properties['name'] as String?;
final description_yaml = properties['description'] as String? ?? ''; final description_yaml = properties['description'] as String? ?? '';
final layer = properties['layer'] as String?; final layer = properties['layer'] as String?;
@ -20,25 +20,30 @@ Result<Feature> parseFeature(
// try parse yaml // try parse yaml
if (description_yaml == null) { if (description_yaml == null) {
print("warn: Description key is missing for feature $name"); print(
"warn: Description key is missing for feature $name\n\n$geometry\n\n$properties");
} }
if (layer == null) { if (layer == null) {
return bail("Layer key \'layer\' is missing for feature $name"); return bail(
"Layer key \'layer\' is missing for feature $name\n\n$geometry\n\n$properties");
} }
dynamic yaml; dynamic yaml;
try { try {
yaml = loadYaml(description_yaml); yaml = loadYaml(description_yaml);
} on YamlException catch (e) { } on YamlException catch (e) {
return bail("Couldn't parse YAML in description for feature $name: $e"); return bail(
"Couldn't parse YAML in description for feature $name\n\n$description_yaml\n\nError: $e");
} }
yaml = yaml as YamlMap? ?? {}; yaml = yaml as YamlMap? ?? {};
final description = yaml['desription'] as String?; final description = yaml['description'] as String?;
// if (description != null) print("================ $description");
final building = yaml['building'] as String?; final building = yaml['building'] as String?;
print("yaml: $yaml"); // print("yaml: $yaml");
var raw_type = yaml['type'] as String?; var raw_type = yaml['type'] as String?;
if (raw_type == null && layer?.toLowerCase() == 'buildings') { if (raw_type == null && layer?.toLowerCase() == 'buildings') {
@ -68,8 +73,16 @@ Result<Feature> parseFeature(
case 'lecture_hall': case 'lecture_hall':
type = FeatureType.lectureHall(); type = FeatureType.lectureHall();
break; break;
case 'room': case 'seminar_room':
type = FeatureType.room(); type = FeatureType.room(getYamlKeyStringify(yaml, 'number')
.expect("Couldn't parse 'number' for seminar_room feature $name"));
break;
case 'pc_pool':
type = FeatureType.pcPool(getYamlKeyStringify(yaml, 'number')
.expect("Couldn't parse 'number' for PcPool feature $name"));
break;
case 'food_drink':
type = FeatureType.foodDrink();
break; break;
case 'door': case 'door':
final list = getYamlList<String>(yaml, 'connects') final list = getYamlList<String>(yaml, 'connects')
@ -82,10 +95,8 @@ Result<Feature> parseFeature(
type = FeatureType.toilet(toiletType); type = FeatureType.toilet(toiletType);
break; break;
case 'public_transport': case 'public_transport':
final busLines = getYamlList<dynamic>(yaml, 'bus_lines') final busLines = getYamlList<dynamic>(yaml, 'bus_lines').unwrapOr([]);
.expect("Couldn't parse 'bus_lines' for feature $name"); final tramLines = getYamlList<dynamic>(yaml, 'tram_lines').unwrapOr([]);
final tramLines = getYamlList<dynamic>(yaml, 'tram_lines')
.expect("Couldn't parse 'tram_lines' for feature $name");
type = FeatureType.publicTransport( type = FeatureType.publicTransport(
stringifyList(busLines) stringifyList(busLines)
@ -108,6 +119,7 @@ Result<Feature> parseFeature(
geometry: geometry, geometry: geometry,
level: level, level: level,
building: building, building: building,
id: id,
)); ));
} }
@ -121,7 +133,7 @@ Result<List<String>> stringifyList(List<dynamic> tramLines) {
Result<List<T>> getYamlList<T>(YamlMap yaml, String key) { Result<List<T>> getYamlList<T>(YamlMap yaml, String key) {
try { try {
print('yaml is ${yaml[key]}'); // print('yaml is ${yaml[key]}');
final val = (yaml[key] as YamlList?); final val = (yaml[key] as YamlList?);
if (val == null) { if (val == null) {
return bail("Key $key is missing in yaml"); return bail("Key $key is missing in yaml");
@ -143,3 +155,15 @@ Result<T> getYamlKey<T>(YamlMap yaml, String key) {
return bail("Failed to parse yaml key $key as ${T.toString()}: $e"); return bail("Failed to parse yaml key $key as ${T.toString()}: $e");
} }
} }
Result<String> getYamlKeyStringify<T>(YamlMap yaml, String key) {
try {
final val = yaml[key] as T?;
if (val == null) {
return bail("Key $key is missing in yaml");
}
return Ok(val.toString());
} catch (e) {
return bail("Failed to parse yaml key $key as ${T.toString()}: $e");
}
}

View File

View File

@ -1,11 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:uninav/controllers/map_controller.dart'; import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/controllers/navigation_controller.dart';
import 'package:uninav/controllers/shared_prefs_controller.dart';
import 'package:uninav/map.dart'; import 'package:uninav/map.dart';
import 'package:uninav/settings.dart'; import 'package:uninav/settings.dart';
// TODO: maybe make not async?
void main() { void main() {
Get.put(MyMapController()); Get.put(MyMapController());
rootBundle
.loadString('assets/geo/uulm_beta.geojson')
.then((value) => Get.find<MyMapController>().loadGeoJson(value));
Get.put(NavigationController());
Get.putAsync(() async {
final controller = SharedPrefsController();
await controller.initialize();
return controller;
});
runApp(const MyApp()); runApp(const MyApp());
} }
@ -31,7 +46,7 @@ class MyApp extends StatelessWidget {
), ),
initialRoute: '/map', initialRoute: '/map',
getPages: [ getPages: [
GetPage(name: '/map', page: () => const MapPage()), GetPage(name: '/map', page: () => MapPage()),
GetPage(name: '/settings', page: () => const SettingsPage()), GetPage(name: '/settings', page: () => const SettingsPage()),
], ],
); );

View File

@ -1,25 +1,53 @@
import 'package:anim_search_bar/anim_search_bar.dart'; import 'package:anim_search_bar/anim_search_bar.dart';
import 'package:anyhow/anyhow.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:rust_core/iter.dart';
import 'package:rust_core/slice.dart'; import 'package:rust_core/slice.dart';
import 'package:uninav/components/EventLog.dart';
import 'package:uninav/components/drawer.dart'; import 'package:uninav/components/drawer.dart';
import 'package:uninav/components/hamburger_menu.dart'; import 'package:uninav/components/hamburger_menu.dart';
import 'package:uninav/components/map_render_level.dart'; import 'package:uninav/components/map_render_level.dart';
import 'package:uninav/components/render_route.dart';
import 'package:uninav/controllers/map_controller.dart'; import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/controllers/navigation_controller.dart';
import 'package:uninav/data/geo/model.dart'; import 'package:uninav/data/geo/model.dart';
import 'package:uninav/nav/graph.dart';
import 'package:uninav/util/geojson_util.dart'; import 'package:uninav/util/geojson_util.dart';
import 'package:uninav/util/geomath.dart'; import 'package:uninav/util/geomath.dart';
import 'package:uninav/util/util.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class MapPage extends StatelessWidget { class MapPage extends StatefulWidget {
const MapPage({Key? key}) : super(key: key); @override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
late final Stream<LocationMarkerPosition?> _positionStream;
late final Stream<LocationMarkerHeading?> _headingStream;
/*
@override
void initState() {
super.initState();
const factory = LocationMarkerDataStreamFactory();
_positionStream =
factory.fromGeolocatorPositionStream().asBroadcastStream();
_headingStream = factory.fromCompassHeadingStream().asBroadcastStream();
_geolocatorStream =
factory.defaultPositionStreamSource().asBroadcastStream();
_compassStream = factory.defaultHeadingStreamSource().asBroadcastStream();
}
*/
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final LayerHitNotifier hitNotifier = ValueNotifier(null);
return Scaffold( return Scaffold(
drawer: MyDrawer(), drawer: MyDrawer(),
appBar: AppBar( appBar: AppBar(
@ -43,10 +71,10 @@ class MapPage extends StatelessWidget {
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
// Add onPressed logic here // Add onPressed logic here
await Get.find<MyMapController>().loadGeoJson( var future = Get.find<MyMapController>().getCurrentPosition();
await rootBundle.loadString('assets/geo/uulm_beta.geojson')); locationBottomSheet();
}, },
child: const Icon(Icons.add), child: const Icon(Icons.location_searching),
), ),
body: Stack( body: Stack(
children: [ children: [
@ -66,11 +94,17 @@ class MapPage extends StatelessWidget {
TileLayer( TileLayer(
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
maxZoom: 19, maxZoom: 19,
tileBuilder: (context, tile, child) => ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.white.withOpacity(0.7),
BlendMode.srcATop,
),
child: tile,
),
), ),
TranslucentPointer( TranslucentPointer(
child: LevelLayer( child: LevelLayer(
filter: (feature) => feature.type is Building, filter: (feature) => feature.type is Building,
notifier: hitNotifier,
), ),
), ),
@ -97,10 +131,8 @@ class MapPage extends StatelessWidget {
color: Colors.green.withOpacity(0.2), color: Colors.green.withOpacity(0.2),
borderColor: Colors.green, borderColor: Colors.green,
borderStrokeWidth: 1, borderStrokeWidth: 1,
hitValue: feature,
)) ))
.unwrap(), .unwrap(),
notifier: hitNotifier,
), ),
), ),
@ -109,10 +141,29 @@ class MapPage extends StatelessWidget {
() => Stack( () => Stack(
children: renderLevel( children: renderLevel(
Get.find<MyMapController>().currentLevel.value, Get.find<MyMapController>().currentLevel.value,
hitNotifier: hitNotifier)), )),
), ),
CurrentLocationLayer(),
NavigationPathLayer(),
], ],
), ),
Positioned(
left: 16,
top: 16,
child: Container(
height: 450,
width: 150,
child: GetBuilder<NavigationController>(
builder: (controller) {
if (controller.nav.isNotEmpty) {
return EventLog(
events: controller.nav.map((e) => e.$1).toList());
} else {
return SizedBox();
}
},
),
)),
Positioned( Positioned(
left: 16, left: 16,
bottom: 16, bottom: 16,
@ -127,7 +178,9 @@ class MapPage extends StatelessWidget {
), ),
padding: padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4), const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Obx( child: Row(
children: [
Obx(
() => DropdownButton<int>( () => DropdownButton<int>(
value: Get.find<MyMapController>().currentLevel.value, value: Get.find<MyMapController>().currentLevel.value,
style: TextStyle( style: TextStyle(
@ -149,8 +202,282 @@ class MapPage extends StatelessWidget {
}).toList(), }).toList(),
), ),
), ),
IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: () {
int currentLevel =
Get.find<MyMapController>().currentLevel.value;
if (currentLevel <
Get.find<MyMapController>().levels.last) {
Get.find<MyMapController>()
.setLevel(currentLevel + 1);
}
},
),
IconButton(
icon: const Icon(Icons.arrow_downward),
onPressed: () {
int currentLevel =
Get.find<MyMapController>().currentLevel.value;
if (currentLevel >
Get.find<MyMapController>().levels.first) {
Get.find<MyMapController>()
.setLevel(currentLevel - 1);
}
},
),
],
),
)), )),
], ],
)); ));
} }
} }
void locationBottomSheet() {
print(Get.find<MyMapController>().position.value);
String buttonText = "Search for Location";
IconData locationIcon = Icons.location_searching;
bool spinner = false;
Get.bottomSheet(
Theme(
data: ThemeData.light(),
child: Container(
constraints: const BoxConstraints(
// minHeight: 300,
),
width: Get.mediaQuery.size.width,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 10),
const Text(
'Select Location',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
flex: 2,
child: Container(
height: 300,
color: Colors.transparent,
child: Column(
children: [
const Text(
'Level',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 10),
GetBuilder<MyMapController>(
builder: (controller) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final level in controller.levels)
GestureDetector(
onTap: () => controller.setLevel(level),
child: Chip(
label: Text("$level"),
backgroundColor:
controller.currentLevel == level
? Colors.blue
: Colors.grey[800],
),
),
Obx(() {
final navController =
Get.find<NavigationController>();
String? curBuilding;
if (navController.position.value
is BuildingFloor) {
curBuilding = (navController
.position.value as BuildingFloor)
.building
.name;
}
final buildingList = controller.features
.where((f) => f.type is Building);
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final building in buildingList)
GestureDetector(
onTap: () {
// print(building.name);
// print(curBuilding);
// print(navController.position);
navController.updatePosition(wrap(
building,
controller
.currentLevel.value,
building.name)
.firstOrNull);
},
child: Chip(
label: Text(building.name),
backgroundColor:
eq(curBuilding, building.name)
? building.level ==
controller
.currentLevel
? Colors.blue
: Colors.orange
: Colors.grey[800],
),
),
],
);
})
],
);
},
),
],
)),
),
Expanded(
flex: 1,
child: StatefulBuilder(builder: (context, setState) {
// TODO: make this persist closing the bottom sheet
return ElevatedButton(
style: ElevatedButton.styleFrom(
fixedSize: const Size(300, 300),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.all(15),
),
onPressed: () async {
if (spinner) {
return;
}
setState(() {
buttonText = "Searching...";
locationIcon = Icons.location_searching;
spinner = true;
});
final pos = await Get.find<MyMapController>()
.getCurrentPosition();
if (!context.mounted) {
return;
}
if (pos case Ok(:final ok)) {
setState(() {
buttonText = "Location found!";
locationIcon = Icons.my_location;
spinner = false;
});
} else {
setState(() {
buttonText = "Location not found! Try again";
locationIcon = Icons.error;
spinner = false;
});
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
Icon(locationIcon,
size: 80,
color: Get.theme.colorScheme.inversePrimary),
if (spinner)
CircularProgressIndicator(
color: Get.theme.colorScheme.inversePrimary,
),
],
),
const SizedBox(
height: 12,
),
Text(
buttonText,
softWrap: true,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold),
),
],
),
);
}),
),
],
),
/*
ListTile(
title: const Text(
'Current Location',
style: TextStyle(color: Colors.white),
),
onTap: () {
Get.back();
},
),
ListTile(
title: const Text(
'Search Location',
style: TextStyle(color: Colors.white),
),
onTap: () {
Get.back();
},
),
const SizedBox(height: 20),
*/
ElevatedButton(
child: const Text(
'Cancel',
style: TextStyle(color: Colors.black),
),
onPressed: () {
Get.back();
},
),
],
),
),
),
isScrollControlled: true,
enterBottomSheetDuration: const Duration(milliseconds: 150),
exitBottomSheetDuration: const Duration(milliseconds: 200),
);
}

382
lib/nav/graph.dart Normal file
View File

@ -0,0 +1,382 @@
import 'package:anyhow/anyhow.dart';
import 'package:collection/collection.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.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<LatLng> 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");
String get id => when(
buildingFloor: (floor, building) => building.id,
portal: (fromFloor, from, toFloor, to, baseFeature) => baseFeature.id,
basicFeature: (floor, building, feature) => feature.id,
);
// String? get buildingName => when(
// buildingFloor: (floor, building) => building.name,
// portal: (fromFloor, from, toFloor, to, baseFeature) =>
// baseFeature.type.maybeWhen(
// door: (_) => null,
// orElse: () => baseFeature.building,
// ),
// basicFeature: (floor, building, feature) => feature.type.maybeWhen(
// door: (_) => null,
// orElse: () => building,
// ),
// );
@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<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
.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<GraphFeature> doorPortalGenerator(
List<String> connections, int floor, String from, Feature feature) {
final portals = <GraphFeature>[];
for (final connection in connections.where((c) => !eq(c, from))) {
portals.add(GraphFeature.portal(floor, from, floor, connection, feature));
}
return portals;
}
List<GraphFeature> stairPortalGenerator(
List<int> floors, int floor, Feature feature,
[int maxDist = 1]) {
final portals = <GraphFeature>[];
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<Feature> allFeatures) {
//
// }
List<GraphFeature> findAdjacent(
GraphFeature feature, Iterable<Feature> allFeatures) {
List<GraphFeature> 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<Feature> allFeatures,
[Graph? graph]) {
// final usedFeatures = <GraphFeature>[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;
}
Result<List<(GraphFeature, double)>> findShortestPath(GraphFeature origin,
bool Function(GraphFeature) destinationSelector, List<Feature> 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<GraphFeature, (GraphFeature?, double)> bestPathMap = {
origin: (null, 0.0)
};
openlist.add((heuristic(origin), 0.0, null, origin));
// closed list
Set<GraphFeature> 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");
}

647
lib/nav/graph.dart.old Normal file
View File

@ -0,0 +1,647 @@
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<LatLng> 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<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
.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<GraphFeature> doorPortalGenerator(
List<String> connections, int floor, String from, Feature feature) {
final portals = <GraphFeature>[];
for (final connection in connections.where((c) => !eq(c, from))) {
portals.add(GraphFeature.portal(floor, from, floor, connection, feature));
}
return portals;
}
List<GraphFeature> stairPortalGenerator(
List<int> floors, int floor, Feature feature,
[int maxDist = 1]) {
final portals = <GraphFeature>[];
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<Feature> allFeatures) {
//
// }
List<GraphFeature> findAdjacent(
GraphFeature feature, Iterable<Feature> allFeatures) {
List<GraphFeature> 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<Feature> allFeatures,
[Graph? graph]) {
// final usedFeatures = <GraphFeature>[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<GraphFeature> createGraphList(
GraphFeature origin, List<Feature> allFeatures,
[Set<GraphFeature>? visited]) {
// final usedFeatures = <GraphFeature>[origin];
visited ??= <GraphFeature>{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<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)>> findShortestPathUndir(GraphFeature origin,
bool Function(GraphFeature) destinationSelector, List<Feature> 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<GraphFeature, (GraphFeature?, double)> bestPathMap = {
origin: (null, 0.0)
};
openlist.add((heuristic(origin), 0.0, null, origin));
// closed list
Set<GraphFeature> 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<List<(GraphFeature, double)>> findShortestPath(
GraphFeature origin, GraphFeature destination, List<Feature> 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<GraphFeature, (GraphFeature?, double)> bestPathMap = {
origin: (null, 0.0)
};
openlist.add((heuristic(origin), 0.0, null, origin));
// closed list
Set<GraphFeature> 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);
},
);
},
);
}

586
lib/nav/graph.freezed.dart Normal file
View File

@ -0,0 +1,586 @@
// 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>(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<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
required TResult Function(BuildingFloor value) buildingFloor,
required TResult Function(Portal value) portal,
required TResult Function(BasicFeature value) basicFeature,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(BuildingFloor value)? buildingFloor,
TResult? Function(Portal value)? portal,
TResult? Function(BasicFeature value)? basicFeature,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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;
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$BuildingFloorImplCopyWith<_$BuildingFloorImpl> get copyWith =>
__$$BuildingFloorImplCopyWithImpl<_$BuildingFloorImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(BuildingFloor value)? buildingFloor,
TResult? Function(Portal value)? portal,
TResult? Function(BasicFeature value)? basicFeature,
}) {
return buildingFloor?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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;
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$PortalImplCopyWith<_$PortalImpl> get copyWith =>
__$$PortalImplCopyWithImpl<_$PortalImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(BuildingFloor value)? buildingFloor,
TResult? Function(Portal value)? portal,
TResult? Function(BasicFeature value)? basicFeature,
}) {
return portal?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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;
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$BasicFeatureImplCopyWith<_$BasicFeatureImpl> get copyWith =>
__$$BasicFeatureImplCopyWithImpl<_$BasicFeatureImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
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 extends Object?>({
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 extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(BuildingFloor value)? buildingFloor,
TResult? Function(Portal value)? portal,
TResult? Function(BasicFeature value)? basicFeature,
}) {
return basicFeature?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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;
}

View File

@ -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'),
),
);
}
}

View File

@ -1,20 +1,171 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:uninav/components/drawer.dart'; import 'package:uninav/components/drawer.dart';
import 'package:uninav/components/hamburger_menu.dart'; import 'package:uninav/components/hamburger_menu.dart';
import 'package:uninav/controllers/shared_prefs_controller.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final persistenceController = Get.find<SharedPrefsController>();
final settings = persistenceController.settings;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Settings'), title: const Text('Settings'),
leading: HamburgerMenu(), leading: HamburgerMenu(),
), ),
drawer: MyDrawer(), drawer: MyDrawer(),
body: const Center( body: SingleChildScrollView(
child: Text('TODO'), child: Column(
children: [
Obx(
() => Column(
children: [
SwitchListTile(
title: const Text('Show Icons'),
subtitle: const Text(
'Warning: disables ALL icons',
style: TextStyle(color: Colors.red, fontSize: 12),
),
value: settings.value.showIcons,
onChanged: (value) {
settings.value = settings.value.copyWith(
showIcons: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Elevators'),
value: settings.value.showElevators,
onChanged: (value) {
settings.value = settings.value.copyWith(
showElevators: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Food and Drink'),
value: settings.value.showFoodAndDrink,
onChanged: (value) {
settings.value = settings.value.copyWith(
showFoodAndDrink: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Lecture Halls'),
value: settings.value.showLectureHalls,
onChanged: (value) {
settings.value = settings.value.copyWith(
showLectureHalls: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Computer Pools'),
value: settings.value.showComputerPools,
onChanged: (value) {
settings.value = settings.value.copyWith(
showComputerPools: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Seminar Rooms'),
value: settings.value.showSeminarRooms,
onChanged: (value) {
settings.value = settings.value.copyWith(
showSeminarRooms: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Toilets'),
value: settings.value.showToilets,
onChanged: (value) {
settings.value = settings.value.copyWith(
showToilets: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Stairs'),
value: settings.value.showStairs,
onChanged: (value) {
settings.value = settings.value.copyWith(
showStairs: value,
);
persistenceController.persistSettings();
},
),
SwitchListTile(
title: const Text('Show Doors'),
value: settings.value.showDoors,
onChanged: (value) {
settings.value = settings.value.copyWith(
showDoors: value,
);
persistenceController.persistSettings();
},
),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Text('Toilet Preference'),
),
CheckboxListTile(
title: const Text('Male Toilets'),
value: settings.value.maleToilets,
onChanged: (value) {
settings.value = settings.value.copyWith(
maleToilets: value ?? false,
);
persistenceController.persistSettings();
},
),
CheckboxListTile(
title: const Text('Female Toilets'),
value: settings.value.femaleToilets,
onChanged: (value) {
settings.value = settings.value.copyWith(
femaleToilets: value ?? false,
);
persistenceController.persistSettings();
},
),
CheckboxListTile(
title: const Text('Handicap Toilets'),
value: settings.value.handicapToilets,
onChanged: (value) {
settings.value = settings.value.copyWith(
handicapToilets: value ?? false,
);
persistenceController.persistSettings();
},
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
settings.value = const Settings();
persistenceController.persistSettings();
},
child: const Text("Reset Settings"),
),
],
),
),
],
),
), ),
); );
} }

48
lib/util/geolocator.dart Normal file
View File

@ -0,0 +1,48 @@
import 'package:anyhow/anyhow.dart';
import 'package:geolocator/geolocator.dart';
/// Determine the current position of the device.
///
/// When the location services are not enabled or permissions
/// are denied the `Future` will return an error.
Future<Result<()>> ensureLocationPermission() async {
try {
bool serviceEnabled;
LocationPermission permission;
// Test if location services are enabled.
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
// Location services are not enabled don't continue
// accessing the position and request users of the
// App to enable the location services.
return bail('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
// Permissions are denied, next time you could try
// requesting permissions again (this is also where
// Android's shouldShowRequestPermissionRationale
// returned true. According to Android guidelines
// your App should show an explanatory UI now.
return bail('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
// Permissions are denied forever, handle appropriately.
return bail(
'Location permissions are permanently denied, we cannot request permissions.');
}
// When we reach here, permissions are granted and we can
// continue accessing the position of the device.
return const Ok(());
} catch (e) {
return bail("Ensuring Location Permission failed: $e");
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:uninav/data/geo/model.dart'; import 'package:uninav/data/geo/model.dart';
bool eq(String? a, String? b) => a?.toLowerCase() == b?.toLowerCase();
String formatDuration(Duration duration) { String formatDuration(Duration duration) {
final days = duration.inDays; final days = duration.inDays;
final hours = duration.inHours.remainder(24); final hours = duration.inHours.remainder(24);
@ -23,7 +26,9 @@ String formatFeatureTitle(Feature feature) {
return feature.type.when( return feature.type.when(
building: () => '${feature.name} (Building)', building: () => '${feature.name} (Building)',
lectureHall: () => '${feature.name} (Lecture Hall)', lectureHall: () => '${feature.name} (Lecture Hall)',
room: () => 'Room ${feature.name}', room: (number) => 'Room ${feature.building ?? "??"}/$number',
pcPool: (number) => 'PC Pool ${feature.name}',
foodDrink: () => '${feature.name} (Food/Drink)',
door: (_) => 'Door', door: (_) => 'Door',
toilet: (type) => 'Toilet (${formatToiletType(feature.type as Toilet)})', toilet: (type) => 'Toilet (${formatToiletType(feature.type as Toilet)})',
stairs: (_) => 'Stairs', stairs: (_) => 'Stairs',
@ -65,3 +70,17 @@ String formatDistance(int distanceInMeters) {
} }
} }
} }
IconData findToiletIcon(String type) {
switch (type.toLowerCase()) {
case 'male':
return Icons.male;
case 'female':
return Icons.female;
case 'handicap':
return Icons.accessible;
default:
print("WARN: Toilet didn't have recognizable type! Type was '$type'");
return Icons.wc;
}
}

View File

@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
isar_flutter_libs
url_launcher_linux url_launcher_linux
) )

View File

@ -5,8 +5,16 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import geolocator_apple
import isar_flutter_libs
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "67.0.0" version: "61.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.1" version: "5.13.0"
anim_search_bar: anim_search_bar:
dependency: "direct main" dependency: "direct main"
description: description:
@ -154,7 +154,7 @@ packages:
source: hosted source: hosted
version: "4.10.0" version: "4.10.0"
collection: collection:
dependency: transitive dependency: "direct main"
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
@ -185,22 +185,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.6" version: "2.3.2"
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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -209,6 +217,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" 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:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -230,6 +254,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_compass:
dependency: transitive
description:
name: flutter_compass
sha256: be642484f9f6975c1c6edff568281b001f2f1e604de27ecea18d97eebbdef22f
url: "https://pub.dev"
source: hosted
version: "0.8.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -242,10 +274,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_map name: flutter_map
sha256: bee8c5bacb49a68aabcf6009c050a8b3b07ac75403f29f741d8c00d4a725e086 sha256: cda8d72135b697f519287258b5294a57ce2f2a5ebf234f0e406aad4dc14c9399
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0-dev.1" version: "6.1.0"
flutter_map_location_marker:
dependency: "direct main"
description:
name: flutter_map_location_marker
sha256: "5873a47606b092bf181b6d17dd42a124e9a8d5d9caad58b5f98fc182e799994f"
url: "https://pub.dev"
source: hosted
version: "8.0.8"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -288,6 +328,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.3" version: "2.2.3"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd"
url: "https://pub.dev"
source: hosted
version: "11.0.0"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: f15d1536cd01b1399578f1da1eb5d566e7a718db6a3648f2c24d2e2f859f0692
url: "https://pub.dev"
source: hosted
version: "4.5.4"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd
url: "https://pub.dev"
source: hosted
version: "2.3.7"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "009a21c4bc2761e58dccf07c24f219adaebe0ff707abdfd40b0a763d4003fab9"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:
@ -352,14 +440,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
isar:
dependency: "direct main"
description:
name: isar
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
isar_flutter_libs:
dependency: "direct main"
description:
name: isar_flutter_libs
sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
js: js:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.1" version: "0.6.7"
json_annotation: json_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -384,6 +488,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.1" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -496,6 +608,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
url: "https://pub.dev"
source: hosted
version: "2.2.4"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -544,6 +712,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" 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: rust_core:
dependency: "direct main" dependency: "direct main"
description: description:
@ -552,6 +728,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.3" version: "0.5.3"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@ -797,6 +1029,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.5" version: "2.4.5"
win32:
dependency: transitive
description:
name: win32
sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:
@ -805,6 +1045,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
yaml: yaml:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -39,7 +39,8 @@ dependencies:
yaml: ^3.1.2 yaml: ^3.1.2
surrealdb: ^0.8.0 surrealdb: ^0.8.0
# geojson: ^1.0.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 # flutter_map: ^4.0.0
latlong2: ^0.9.0 latlong2: ^0.9.0
# latlong2: ^0.8.0 # latlong2: ^0.8.0
@ -50,6 +51,15 @@ dependencies:
json_annotation: ^4.8.1 json_annotation: ^4.8.1
rust_core: ^0.5.3 rust_core: ^0.5.3
anyhow: ^1.3.0 anyhow: ^1.3.0
# 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
flutter_map_location_marker: ^8.0.8
geolocator: ^11.0.0
shared_preferences: ^2.2.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -64,6 +74,7 @@ dev_dependencies:
build_runner: ^2.4.9 build_runner: ^2.4.9
freezed: ^2.5.2 freezed: ^2.5.2
json_serializable: ^6.7.1 json_serializable: ^6.7.1
# isar_generator: ^3.1.0+1
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

241
test/graph_tests.dart Normal file
View File

@ -0,0 +1,241 @@
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<Feature> 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 (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);
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'));
});
*/
*/
});
}

5
test/scratch_1 Normal file

File diff suppressed because one or more lines are too long

View File

@ -6,9 +6,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <geolocator_windows/geolocator_windows.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -3,6 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
isar_flutter_libs
url_launcher_windows url_launcher_windows
) )