Compare commits

...

7 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
13327da5de feat: major progress 2024-04-21 00:35:16 +02:00
b7487fc25e feat: partial broken guesture detection 2024-04-20 21:14:11 +02:00
39 changed files with 8855 additions and 1812 deletions

View File

@ -25,6 +25,7 @@ if (flutterVersionName == null) {
android {
namespace "com.example.uninav"
compileSdk flutter.compileSdkVersion
compileSdkVersion 34
ndkVersion flutter.ndkVersion
compileOptions {
@ -45,7 +46,8 @@ android {
applicationId "com.example.uninav"
// 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.
minSdkVersion flutter.minSdkVersion
// minSdkVersion flutter.minSdkVersion
minSdkVersion 20
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
class ColorfulChip extends StatelessWidget {
final String label;
const ColorfulChip({Key? key, required this.label}) : super(key: key);
@override
Widget build(BuildContext context) {
final color = Color(((label.hashCode) & 0xFFFFFF) | 0xFF000000);
return Chip(
label: Text(label),
backgroundColor: color,
labelStyle: TextStyle(
color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide.none,
),
);
}
}
class ColorfulActionChip extends StatelessWidget {
final String label;
final VoidCallback onPressed;
final double? size;
const ColorfulActionChip({
Key? key,
required this.label,
required this.onPressed,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final color = Color(
((label.hashCode - 5 /* randomness - */) & 0xFFFFFF) | 0xFF000000);
return ActionChip(
label: Text(label),
backgroundColor: color,
labelStyle: TextStyle(
color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white,
fontSize: size,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide.none,
),
onPressed: onPressed,
padding: EdgeInsets.symmetric(
horizontal: (size ?? 16) / 2, vertical: (size ?? 8.0) / 2),
);
}
}

View File

@ -0,0 +1,356 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:uninav/components/colorful_chips.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/nav/graph.dart';
import 'package:uninav/util/util.dart';
final _colorfulBoxDeco = BoxDecoration(
color: Colors.black,
border: Border.all(color: Colors.orange, width: 2),
borderRadius: BorderRadius.circular(10),
);
Future<void> showFeatureBottomSheet(
Feature feature, List<Feature>? closestFeatures) {
return 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),
Text(formatFeatureTitle(feature),
style:
const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 14),
if (closestFeatures != null) ...[
const Text('Did you mean:'),
Wrap(
spacing: 8,
children: closestFeatures
.map((nearFeature) => ColorfulActionChip(
label: formatFeatureTitle(nearFeature),
onPressed: () {
Get.back();
final newClosestFeatures = closestFeatures
.where((element) => element != nearFeature)
.toList();
newClosestFeatures.add(feature);
Get.find<MyMapController>().focusOnFeature(
nearFeature,
closestFeatures: newClosestFeatures);
},
))
.toList(),
),
const SizedBox(height: 14),
],
if (feature.description != null) ...[
Text(feature.description!),
const SizedBox(height: 10),
],
..._buildFeatureContent(feature),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
child: const Row(
children: [
Icon(
Icons.share_location,
color: Colors.black,
),
SizedBox(width: 4),
Text(
"Start Navigation",
style: TextStyle(color: Colors.black),
),
],
),
onPressed: () {
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");
},
),
],
)
]),
),
),
isScrollControlled: true,
enterBottomSheetDuration: const Duration(milliseconds: 150),
exitBottomSheetDuration: const Duration(milliseconds: 200),
);
}
List<Widget> _buildFeatureContent(Feature feature) {
return feature.type.when(
building: () => _buildBuildingContent(feature),
lectureHall: () => _buildLectureHallContent(feature),
room: (number) => _buildRoomContent(feature, number),
pcPool: (number) => _buildPcPoolContent(feature, number),
foodDrink: () => _buildFoodAndDrinkContent(feature),
door: (connects) => _buildDoorContent(feature, connects),
toilet: (toiletType) => _buildToiletContent(feature, toiletType),
stairs: (connectsLevels) => _buildStairsContent(feature, connectsLevels),
lift: (connectsLevels) => _buildLiftContent(feature, connectsLevels),
publicTransport: (busLines, tramLines) =>
_buildPublicTransportContent(feature, busLines, tramLines),
);
}
/// Builds the content for the Building feature type.
List<Widget> _buildBuildingContent(Feature feature) {
return [
Text(feature.name),
];
}
/// Builds the content for the LectureHall feature type.
List<Widget> _buildLectureHallContent(Feature feature) {
return [Text('Lecture Hall: ${feature.name}')];
}
/// Builds the content for the Room feature type.
List<Widget> _buildRoomContent(Feature feature, String roomNumber) {
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.
List<Widget> _buildDoorContent(Feature feature, List<String> 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.
List<Widget> _buildToiletContent(Feature feature, String 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.
List<Widget> _buildStairsContent(Feature feature, List<int> 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.
List<Widget> _buildLiftContent(Feature feature, List<int> 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.
List<Widget> _buildPublicTransportContent(
Feature feature, List<String> busLines, List<String> tramLines) {
return [
Text(
feature.name,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 10),
if (busLines.isNotEmpty) ...[
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Text(
'Bus Lines:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: busLines.map((line) {
return ColorfulChip(label: line);
}).toList(),
),
),
const SizedBox(height: 10),
],
if (tramLines.isNotEmpty) ...[
const Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Text(
'Tram Lines:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Wrap(
spacing: 8,
runSpacing: 4,
children: tramLines.map((line) {
return ColorfulChip(label: line);
}).toList(),
),
),
],
];
}

View File

@ -1,4 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
@ -6,104 +8,177 @@ import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:uninav/map.dart';
import 'package:uninav/util/geomath.dart';
import 'package:uninav/util/util.dart';
List<Widget> renderLevel(int level) {
List<Widget> renderLevel(
int level,
) {
return <Widget>[
// Lecture Halls
LevelLayer(
filter: (feature) =>
feature.level == level && feature.type is LectureHall,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.orange.withOpacity(0.2),
borderColor: Colors.orange,
isFilled: true,
borderStrokeWidth: 1,
),
)
.unwrap(),
markerConstructor: (feature) => Marker(
width: 150,
height: 60,
point: feature.getPoint().unwrap(),
builder: (cx) => Center(
child: Column(
children: [
Icon(
Icons.class_,
color: Colors.black,
),
Text('${feature.name}'),
],
),
),
)),
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Room,
filter: (feature) =>
feature.level == level && feature.type is LectureHall,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
isFilled: true,
borderStrokeWidth: 1,
color: Colors.orange.withOpacity(0.2),
borderColor: Colors.orange,
borderStrokeWidth: 2,
),
)
.unwrap(),
markerConstructor: (feature) => Marker(
width: 50,
height: 20,
point: feature.getPoint().unwrap(),
child: Column(
children: [
Icon(
Icons.class_,
color: Colors.black,
),
Text('${feature.name}'),
],
),
alignment: Alignment.center,
),
),
// Rooms (Seminar Rooms)
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Room,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(1.2),
borderColor: Colors.green,
borderStrokeWidth: 2,
),
)
.unwrap(),
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(
filter: (feature) => feature.level == level && feature.type is Door,
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 20,
height: 20,
width: 21,
height: 21,
point: point,
builder: (ctx) => const Icon(
child: const Icon(
Icons.door_front_door,
color: Colors.brown,
),
alignment: Alignment.center,
);
},
),
// 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(
filter: (feature) => feature.level == level && feature.type is Toilet,
markerConstructor: (feature) {
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();
return Marker(
width: 20,
height: 20,
width: 21,
height: 21,
point: point,
builder: (ctx) => Icon(
icon,
color: Colors.purple,
child: Icon(
findToiletIcon(type),
color: Colors.blue.shade700,
),
rotateAlignment: Alignment.center,
alignment: Alignment.center,
);
},
),
// Stairs layer
LevelLayer(
filter: (feature) =>
feature.type is Stairs &&
@ -111,16 +186,19 @@ List<Widget> renderLevel(int level) {
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 20,
height: 20,
width: 21,
height: 21,
point: point,
builder: (ctx) => Icon(
child: Icon(
Icons.stairs_outlined,
color: Colors.deepPurple.shade300,
),
alignment: Alignment.center,
);
},
),
// Lift layer
LevelLayer(
filter: (feature) =>
feature.type is Lift &&
@ -128,13 +206,14 @@ List<Widget> renderLevel(int level) {
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 20,
height: 20,
width: 21,
height: 21,
point: point,
builder: (ctx) => const Icon(
child: const Icon(
Icons.elevator_outlined,
color: Colors.deepPurple,
),
alignment: Alignment.center,
);
},
),
@ -172,7 +251,14 @@ class LevelLayer extends StatelessWidget {
if (polyConstructor != null) {
filteredPolygons.add(polyConstructor!(feature));
} else {
filteredPolygons.add(feature.getPolygon().unwrap());
filteredPolygons.add(feature
.getPolygon(
constructor: (points) => Polygon(
points: points,
borderColor: Colors.black26,
borderStrokeWidth: 2.0,
))
.unwrap());
}
// calculate polygon center
@ -186,7 +272,7 @@ class LevelLayer extends StatelessWidget {
width: 100,
height: 100,
point: center,
builder: (cx) => Center(
child: Center(
child: Text(
feature.name,
style: const TextStyle(
@ -195,6 +281,7 @@ class LevelLayer extends StatelessWidget {
),
),
),
alignment: Alignment.center,
));
}
} else if (feature.isPoint()) {
@ -206,7 +293,7 @@ class LevelLayer extends StatelessWidget {
width: 100,
height: 100,
point: point,
builder: (cx) => Center(
child: Center(
child: Text(
feature.name,
style: const TextStyle(
@ -215,6 +302,7 @@ class LevelLayer extends StatelessWidget {
),
),
),
alignment: Alignment.center,
));
}
}
@ -226,28 +314,37 @@ class LevelLayer extends StatelessWidget {
// print(filteredPolygons[0].points[0]);
// print(myMapController.features.length);
// filteredPolygons.forEach((element) {
// print(element.hitValue);
// });
final List<Widget> widgets = [];
if (filteredPolygons.isNotEmpty) {
if (polyConstructor != null) {
widgets.add(PolygonLayer(polygons: filteredPolygons));
widgets.add(TranslucentPointer(
child: PolygonLayer(
polygons: filteredPolygons,
),
));
} else {
widgets.add(PolygonLayer(
polygons: filteredPolygons
.map((poly) => Polygon(
points: poly.points,
borderColor: Colors.black26,
borderStrokeWidth: 2.0,
))
.toList()));
widgets.add(TranslucentPointer(
child: PolygonLayer(
polygons: filteredPolygons,
),
));
}
widgets.add(MarkerLayer(
markers: polygonCenterMarkers,
rotate: true,
widgets.add(TranslucentPointer(
child: MarkerLayer(
markers: polygonCenterMarkers,
rotate: true,
),
));
}
if (filteredMarkers.isNotEmpty) {
widgets.add(MarkerLayer(markers: filteredMarkers, rotate: true));
widgets.add(TranslucentPointer(
child: MarkerLayer(markers: filteredMarkers, rotate: true),
));
}
return Stack(children: widgets);

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,16 +1,29 @@
import 'dart:async';
import 'dart:convert';
import 'package:anyhow/anyhow.dart';
import 'package:geojson/geojson.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:uninav/components/feature_bottom_sheet.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:uninav/data/geo/parser.dart';
import 'package:uninav/util/geojson_util.dart';
import 'package:uninav/util/geolocator.dart';
import 'package:uninav/util/geomath.dart';
class MyMapController extends GetxController {
// constructor that calls loadgeojson with a default geojson string
final MapController mapController = MapController();
final RxList<Feature> features = <Feature>[].obs;
final currentLevel = 1.obs;
final levels = <int>[1].obs;
final Rx<Position?> position = null.obs;
bool _locationEnsured = false;
@override
onInit() async {
print("init");
@ -28,6 +41,7 @@ class MyMapController extends GetxController {
}
newLevels.sort();
levels.value = newLevels;
update();
}
Result<void> setLevel(int level) {
@ -37,31 +51,144 @@ class MyMapController extends GetxController {
}
currentLevel.value = level;
update();
return const Ok(());
}
List<Feature> computeHits(LatLng position, {bool Function(Feature)? filter}) {
final hits = <(Feature, double)>[];
for (final feature in features) {
if (filter != null && !filter(feature)) {
continue;
}
if (feature.isPolygon()) {
if ((feature.geometry as GeoJSONPolygon)
.isPointInside(latLonToGeoJSON(position))) {
// compute distance to center of polygon
final distance = distanceBetweenLatLng(
polygonCenterMinmax((feature.geometry as GeoJSONPolygon)
.coordinates[0]
.map(geoJSONToLatLon)
.toList()),
position,
'meters');
hits.add((feature, distance));
}
} else if (feature.isPoint()) {
final distance = distanceBetweenLatLng(
geoJSONToLatLon((feature.geometry as GeoJSONPoint).coordinates),
position,
'meters');
if (distance <= 5) {
hits.add((feature, distance));
}
}
}
hits.sort((a, b) => a.$2.compareTo(b.$2));
return hits.map((e) => e.$1).toList();
}
Future<void> loadGeoJson(String geoJsonString) async {
try {
// print(geoJsonString);
final featuresList = <Feature>[];
final geojson = GeoJson();
await geojson.parse(geoJsonString);
// print('doing');
final geojson = GeoJSONFeatureCollection.fromJSON(geoJsonString);
// print('done');
for (final feature in geojson.features) {
print(feature.properties);
final parsed =
parseFeature(feature.properties ?? <String, dynamic>{}, feature);
// print(feature);
// print(feature?.properties);
if (feature == null) continue;
// print(feature.properties);
final parsed = parseFeature(feature.properties ?? <String, dynamic>{},
feature.geometry, feature.id);
if (parsed case Ok(:final ok)) {
featuresList.add(ok);
} else {
print('Error parsing feature: $parsed');
}
}
features.value = featuresList;
update();
} catch (e) {
print('Error parsing GeoJSON: $e');
}
}
void handleTap(TapPosition tapPosition, LatLng point) {
final hits = Get.find<MyMapController>().computeHits(point,
filter: (feature) =>
feature.isOnLevel(null) /* is not on a level */ ||
feature.isOnLevel(Get.find<MyMapController>().currentLevel.value));
print('Hits: ${hits.map((e) => e.name)}');
if (hits.isNotEmpty) {
focusOnFeature(hits[0],
move: false,
closestFeatures:
hits.length > 1 ? hits.skip(1).toList() : null); // closest match
}
}
void focusOnFeature(Feature feature,
{bool move = true, List<Feature>? closestFeatures}) {
try {
if (move) {
mapController.move(
feature
.getCenterPoint()
.expect("Couldn't find Center Point of target geometry"),
mapController.camera.zoom,
);
}
} catch (e) {
print("Error moving map controller: $e");
}
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

@ -2,8 +2,10 @@ import 'package:anyhow/anyhow.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:geojson/geojson.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:latlong2/latlong.dart';
import 'package:uninav/util/geojson_util.dart';
import 'package:uninav/util/geomath.dart';
part 'model.freezed.dart';
@ -15,27 +17,32 @@ class Feature with _$Feature {
required String name,
required FeatureType type,
String? description,
required dynamic geometry,
required GeoJSONGeometry geometry,
int? level,
String? building,
required String id,
}) = _Feature;
bool isPolygon() {
return geometry is GeoJsonFeature<GeoJsonPolygon>;
return geometry is GeoJSONPolygon;
}
bool isPoint() {
return geometry is GeoJsonFeature<GeoJsonPoint>;
return geometry is GeoJSONPoint;
}
Result<Polygon> getPolygon({Polygon Function(List<LatLng>)? constructor}) {
if (isPolygon()) {
constructor ??= (pts) => Polygon(
points: pts, borderColor: Colors.black26, borderStrokeWidth: 2.0);
final polygon = geometry as GeoJsonFeature<GeoJsonPolygon>;
points: pts,
borderColor: Colors.black26,
borderStrokeWidth: 2.0,
);
final polygon = geometry as GeoJSONPolygon;
// print(polygon.geometry!.geoSeries[0].geoPoints);
final points = polygon.geometry!.geoSeries[0].geoPoints
.map((e) => LatLng(e.latitude, e.longitude))
.toList();
final points =
// polygon.coordinates[0].map((e) => LatLng(e[0], e[1])).toList();
polygon.coordinates[0].map(geoJSONToLatLon).toList();
// print(points);
return Ok(constructor(points));
@ -46,13 +53,58 @@ class Feature with _$Feature {
Result<LatLng> getPoint() {
if (isPoint()) {
final point = geometry as GeoJsonFeature<GeoJsonPoint>;
return Ok(LatLng(point.geometry!.geoPoint.latitude,
point.geometry!.geoPoint.longitude));
final point = geometry as GeoJSONPoint;
return Ok(geoJSONToLatLon(point.coordinates));
} else {
return bail("Feature Geometry is not a Point");
}
}
/// Checks if the current feature is on the specified layer.
///
/// For features that represent lifts or stairs, this method checks if the
/// feature's `connects_levels` list contains the specified layer.
///
/// For other feature types, this method simply checks if the feature's `level`
/// property matches the specified layer.
///
/// @param layer The layer to check for. **Layer can be `null`!**
/// `null` matches things such as Buildings without a layer.
/// @return `true` if the feature is on the specified layer, `false` otherwise.
bool isOnLevel(int? layer) {
if (type is Lift) {
return (type as Lift).connects_levels.contains(layer);
} else if (type is Stairs) {
return (type as Stairs).connects_levels.contains(layer);
}
return level == layer;
}
Result<LatLng> getCenterPoint() {
if (isPolygon()) {
final polygon = geometry as GeoJSONPolygon;
final points = polygon.coordinates[0].map(geoJSONToLatLon).toList();
return Ok(polygonCenterMinmax(points));
} else if (isPoint()) {
final point = geometry as GeoJSONPoint;
return Ok(geoJSONToLatLon(point.coordinates));
} else {
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
@ -60,11 +112,13 @@ class FeatureType with _$FeatureType {
// multiple feature types like lecture hall, toliet, ...
const factory FeatureType.building() = Building;
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.toilet(String toilet_type) = Toilet;
const factory FeatureType.stairs(List<int> connects_levels) = Stairs;
const factory FeatureType.lift(List<int> connects_levels) = Lift;
const factory FeatureType.foodDrink() = FoodDrink;
const factory FeatureType.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

@ -1,10 +1,10 @@
import 'package:anyhow/anyhow.dart';
import 'package:geojson/geojson.dart';
import 'package:geojson_vi/geojson_vi.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:yaml/yaml.dart';
Result<Feature> parseFeature(
Map<String, dynamic> properties, dynamic geometry) {
Map<String, dynamic> properties, GeoJSONGeometry geometry, String id) {
final name = properties['name'] as String?;
final description_yaml = properties['description'] as String? ?? '';
final layer = properties['layer'] as String?;
@ -20,23 +20,30 @@ Result<Feature> parseFeature(
// try parse yaml
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) {
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;
try {
yaml = loadYaml(description_yaml);
} 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? ?? {};
final description = yaml['desription'] as String?;
final description = yaml['description'] as String?;
print("yaml: $yaml");
// if (description != null) print("================ $description");
final building = yaml['building'] as String?;
// print("yaml: $yaml");
var raw_type = yaml['type'] as String?;
if (raw_type == null && layer?.toLowerCase() == 'buildings') {
@ -66,8 +73,16 @@ Result<Feature> parseFeature(
case 'lecture_hall':
type = FeatureType.lectureHall();
break;
case 'room':
type = FeatureType.room();
case 'seminar_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;
case 'door':
final list = getYamlList<String>(yaml, 'connects')
@ -80,10 +95,8 @@ Result<Feature> parseFeature(
type = FeatureType.toilet(toiletType);
break;
case 'public_transport':
final busLines = getYamlList<dynamic>(yaml, 'bus_lines')
.expect("Couldn't parse 'bus_lines' for feature $name");
final tramLines = getYamlList<dynamic>(yaml, 'tram_lines')
.expect("Couldn't parse 'tram_lines' for feature $name");
final busLines = getYamlList<dynamic>(yaml, 'bus_lines').unwrapOr([]);
final tramLines = getYamlList<dynamic>(yaml, 'tram_lines').unwrapOr([]);
type = FeatureType.publicTransport(
stringifyList(busLines)
@ -105,6 +118,8 @@ Result<Feature> parseFeature(
description: description,
geometry: geometry,
level: level,
building: building,
id: id,
));
}
@ -118,7 +133,7 @@ Result<List<String>> stringifyList(List<dynamic> tramLines) {
Result<List<T>> getYamlList<T>(YamlMap yaml, String key) {
try {
print('yaml is ${yaml[key]}');
// print('yaml is ${yaml[key]}');
final val = (yaml[key] as YamlList?);
if (val == null) {
return bail("Key $key is missing in yaml");
@ -140,3 +155,15 @@ Result<T> getYamlKey<T>(YamlMap yaml, String key) {
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/services.dart';
import 'package:get/get.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/settings.dart';
// TODO: maybe make not async?
void main() {
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());
}
@ -18,117 +33,22 @@ class MyApp extends StatelessWidget {
return GetMaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
darkTheme: ThemeData.dark(
useMaterial3: true,
).copyWith(
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: Colors.deepPurple,
),
),
initialRoute: '/map',
getPages: [
GetPage(name: '/map', page: () => const MapPage()),
GetPage(name: '/map', page: () => MapPage()),
GetPage(name: '/settings', page: () => const SettingsPage()),
],
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

View File

@ -1,20 +1,50 @@
import 'package:anim_search_bar/anim_search_bar.dart';
import 'package:anyhow/anyhow.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:rust_core/iter.dart';
import 'package:rust_core/slice.dart';
import 'package:uninav/components/EventLog.dart';
import 'package:uninav/components/drawer.dart';
import 'package:uninav/components/hamburger_menu.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/navigation_controller.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/geomath.dart';
import 'package:uninav/util/util.dart';
import 'package:url_launcher/url_launcher.dart';
class MapPage extends StatelessWidget {
const MapPage({Key? key}) : super(key: key);
class MapPage extends StatefulWidget {
@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
Widget build(BuildContext context) {
@ -41,76 +71,99 @@ class MapPage extends StatelessWidget {
floatingActionButton: FloatingActionButton(
onPressed: () async {
// Add onPressed logic here
await Get.find<MyMapController>().loadGeoJson(
await rootBundle.loadString('assets/geo/uulm_beta.geojson'));
var future = Get.find<MyMapController>().getCurrentPosition();
locationBottomSheet();
},
child: const Icon(Icons.add),
child: const Icon(Icons.location_searching),
),
body: Stack(
children: [
FlutterMap(
mapController: MapController(),
mapController: Get.find<MyMapController>().mapController,
options: MapOptions(
center: LatLng(48.422766, 9.9564),
zoom: 16.0,
initialCenter: const LatLng(48.422766, 9.9564),
initialZoom: 16.0,
// camera constraints
maxZoom: 19,
onTap: (tapPosition, point) {
print('Tap: $tapPosition, $point');
Get.find<MyMapController>().handleTap(tapPosition, point);
},
),
children: [
TileLayer(
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
maxZoom: 19,
tileBuilder: (context, tile, child) => ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.white.withOpacity(0.7),
BlendMode.srcATop,
),
child: tile,
),
),
// buildings
LevelLayer(
filter: (feature) => feature.type is Building,
TranslucentPointer(
child: LevelLayer(
filter: (feature) => feature.type is Building,
),
),
// public transport
LevelLayer(
filter: (feature) =>
feature.level == null && feature.type is PublicTransport,
polyCenterMarkerConstructor: (center, name) => Marker(
width: 100,
height: 100,
point: center,
builder: (cx) => const Center(
child: Icon(
TranslucentPointer(
child: LevelLayer(
filter: (feature) =>
feature.level == null &&
feature.type is PublicTransport,
polyCenterMarkerConstructor: (center, name) => Marker(
width: 100,
height: 100,
point: center,
child: const Icon(
Icons.train,
color: Colors.black,
),
alignment: Alignment.center,
),
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
borderStrokeWidth: 1,
))
.unwrap(),
),
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
isFilled: true,
borderStrokeWidth: 1,
))
.unwrap(),
),
// current level
Obx(
() => Stack(
children: renderLevel(
Get.find<MyMapController>().currentLevel.value),
),
)
// RichAttributionWidget(attributions: [
// TextSourceAttribution(
// 'OpenStreetMap contributors',
// onTap: () =>
// launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
// )
// ]),
children: renderLevel(
Get.find<MyMapController>().currentLevel.value,
)),
),
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(
left: 16,
bottom: 16,
@ -125,27 +178,55 @@ class MapPage extends StatelessWidget {
),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Obx(
() => DropdownButton<int>(
value: Get.find<MyMapController>().currentLevel.value,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface),
dropdownColor: Theme.of(context).colorScheme.surface,
onChanged: (int? newValue) {
if (newValue != null) {
Get.find<MyMapController>().setLevel(newValue);
}
// Handle dropdown value change
},
items: Get.find<MyMapController>()
.levels
.map<DropdownMenuItem<int>>((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text("Level $value"),
);
}).toList(),
),
child: Row(
children: [
Obx(
() => DropdownButton<int>(
value: Get.find<MyMapController>().currentLevel.value,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface),
dropdownColor: Theme.of(context).colorScheme.surface,
onChanged: (int? newValue) {
if (newValue != null) {
Get.find<MyMapController>().setLevel(newValue);
}
// Handle dropdown value change
},
items: Get.find<MyMapController>()
.levels
.map<DropdownMenuItem<int>>((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text("Level $value"),
);
}).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);
}
},
),
],
),
)),
],
@ -153,250 +234,250 @@ class MapPage extends StatelessWidget {
}
}
List<Widget> renderLevel(int level) {
return <Widget>[
LevelLayer(
filter: (feature) =>
feature.level == level && feature.type is LectureHall,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.orange.withOpacity(0.2),
borderColor: Colors.orange,
isFilled: true,
borderStrokeWidth: 1,
),
)
.unwrap(),
markerConstructor: (feature) => Marker(
width: 150,
height: 60,
point: feature.getPoint().unwrap(),
builder: (cx) => Center(
child: Column(
children: [
Icon(
Icons.class_,
color: Colors.black,
),
Text('${feature.name}'),
],
),
),
)),
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Room,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
isFilled: true,
borderStrokeWidth: 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,
),
)
.unwrap(),
),
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Door,
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 20,
height: 20,
point: point,
builder: (ctx) => const Icon(
Icons.door_front_door,
color: Colors.brown,
width: Get.mediaQuery.size.width,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
);
},
),
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Toilet,
markerConstructor: (feature) {
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();
return Marker(
width: 20,
height: 20,
point: point,
builder: (ctx) => Icon(
icon,
color: Colors.purple,
),
rotateAlignment: Alignment.center,
);
},
),
LevelLayer(
filter: (feature) =>
feature.type is Stairs &&
(feature.type as Stairs).connects_levels.contains(level),
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 20,
height: 20,
point: point,
builder: (ctx) => Icon(
Icons.stairs_outlined,
color: Colors.deepPurple.shade300,
),
);
},
),
LevelLayer(
filter: (feature) =>
feature.type is Lift &&
(feature.type as Lift).connects_levels.contains(level),
markerConstructor: (feature) {
final point = feature.getPoint().unwrap();
return Marker(
width: 20,
height: 20,
point: point,
builder: (ctx) => const Icon(
Icons.elevator_outlined,
color: Colors.deepPurple,
),
);
},
),
];
}
class LevelLayer extends StatelessWidget {
final bool Function(Feature)? filter;
final Polygon Function(Feature)? polyConstructor;
final Marker Function(LatLng, String)? polyCenterMarkerConstructor;
final Marker Function(Feature)? markerConstructor;
final int? level;
const LevelLayer({
this.level,
this.filter,
this.polyConstructor,
this.polyCenterMarkerConstructor,
this.markerConstructor,
super.key,
});
@override
Widget build(BuildContext context) {
final myMapController = Get.find<MyMapController>();
return Obx(() {
final List<Polygon> filteredPolygons = [];
final List<Marker> polygonCenterMarkers = [];
final List<Marker> filteredMarkers = [];
for (final feature in myMapController.features) {
if (filter == null || filter!(feature)) {
if (feature.isPolygon()) {
if (polyConstructor != null) {
filteredPolygons.add(polyConstructor!(feature));
} else {
filteredPolygons.add(feature.getPolygon().unwrap());
}
// calculate polygon center
final center =
polygonCenterMinmax(feature.getPolygon().unwrap().points);
if (polyCenterMarkerConstructor != null) {
polygonCenterMarkers
.add(polyCenterMarkerConstructor!(center, feature.name));
} else {
polygonCenterMarkers.add(Marker(
width: 100,
height: 100,
point: center,
builder: (cx) => Center(
child: Text(
feature.name,
style: const TextStyle(
color: Colors.black54,
// backgroundColor: Colors.white,
),
),
),
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),
),
));
}
} else if (feature.isPoint()) {
if (markerConstructor != null) {
filteredMarkers.add(markerConstructor!(feature));
} else {
final point = feature.getPoint().unwrap();
filteredMarkers.add(Marker(
width: 100,
height: 100,
point: point,
builder: (cx) => Center(
child: Text(
feature.name,
style: const TextStyle(
color: Colors.black54,
// backgroundColor: Colors.white,
),
),
),
),
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],
),
),
],
);
})
],
);
},
),
],
)),
),
));
}
}
}
}
// print(filteredPolygons.length);
// print(filteredPolygons);
// print(filteredPolygons[0].points[0]);
// print(myMapController.features.length);
final List<Widget> widgets = [];
if (filteredPolygons.isNotEmpty) {
if (polyConstructor != null) {
widgets.add(PolygonLayer(polygons: filteredPolygons));
} else {
widgets.add(PolygonLayer(
polygons: filteredPolygons
.map((poly) => Polygon(
points: poly.points,
borderColor: Colors.black26,
borderStrokeWidth: 2.0,
))
.toList()));
}
widgets.add(MarkerLayer(
markers: polygonCenterMarkers,
rotate: true,
));
}
if (filteredMarkers.isNotEmpty) {
widgets.add(MarkerLayer(markers: filteredMarkers, rotate: true));
}
return Stack(children: widgets);
});
}
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:get/get.dart';
import 'package:uninav/components/drawer.dart';
import 'package:uninav/components/hamburger_menu.dart';
import 'package:uninav/controllers/shared_prefs_controller.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
final persistenceController = Get.find<SharedPrefsController>();
final settings = persistenceController.settings;
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
leading: HamburgerMenu(),
),
drawer: MyDrawer(),
body: const Center(
child: Text('TODO'),
body: SingleChildScrollView(
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"),
),
],
),
),
],
),
),
);
}

View File

@ -0,0 +1,96 @@
import 'package:geojson_vi/geojson_vi.dart';
import 'package:latlong2/latlong.dart';
import 'dart:math';
void swapLatLonGeojsonGeometry(GeoJSONGeometry geometry) {
if (geometry is GeoJSONPolygon) {
geometry.coordinates[0] =
geometry.coordinates[0].map((e) => [e[1], e[0]]).toList();
} else if (geometry is GeoJSONPoint) {
geometry.coordinates = [geometry.coordinates[1], geometry.coordinates[0]];
} else {
throw UnimplementedError(
"Geometry of type ${geometry.runtimeType} point swapping is unimplemented");
}
}
List<double> latLonToGeoJSON(LatLng point) {
return [point.longitude, point.latitude];
}
LatLng geoJSONToLatLon(List<double> point) {
return LatLng(point[1], point[0]);
}
double degreesToRadians(double degrees) {
return degrees * (pi / 180);
}
double radiansToDegrees(double radians) {
return radians * (180 / pi);
}
double bearingBetweenLatLng(LatLng point1, LatLng point2) {
return bearingBetween(
point1.latitude,
point1.longitude,
point2.latitude,
point2.longitude,
);
}
double bearingBetween(double lat1, double lon1, double lat2, double lon2) {
var dLon = degreesToRadians(lon2 - lon1);
var y = sin(dLon) * cos(degreesToRadians(lat2));
var x = cos(degreesToRadians(lat1)) * sin(degreesToRadians(lat2)) -
sin(degreesToRadians(lat1)) * cos(degreesToRadians(lat2)) * cos(dLon);
var angle = atan2(y, x);
return (radiansToDegrees(angle) + 360) % 360;
}
double distanceBetweenLatLng(LatLng point1, LatLng point2, String unit) {
return distanceBetween(point1.latitude, point1.longitude, point2.latitude,
point2.longitude, unit);
}
double distanceBetween(
double lat1, double lon1, double lat2, double lon2, String unit) {
const earthRadius = 6371; // in km
// assuming earth is a perfect sphere(it's not)
// Convert degrees to radians
final lat1Rad = degreesToRadians(lat1);
final lon1Rad = degreesToRadians(lon1);
final lat2Rad = degreesToRadians(lat2);
final lon2Rad = degreesToRadians(lon2);
final dLat = lat2Rad - lat1Rad;
final dLon = lon2Rad - lon1Rad;
// Haversine formula
final a = pow(sin(dLat / 2), 2) +
cos(lat1Rad) * cos(lat2Rad) * pow(sin(dLon / 2), 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
final distance = earthRadius * c;
return toRequestedUnit(unit, distance);
// return distance; // in km
}
double toRequestedUnit(String unit, double distanceInKm) {
switch (unit) {
case 'kilometers':
return distanceInKm;
case 'meters':
return distanceInKm * 1000;
case 'miles':
return (distanceInKm * 1000) / 1609.344;
case 'yards':
return distanceInKm * 1093.61;
case '':
return distanceInKm;
}
return distanceInKm;
}

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

@ -35,3 +35,24 @@ LatLng polygonCenterAvg(List<LatLng> polygon) {
return LatLng(centerLat, centerLng);
}
bool isPointInPolygonRaycast(LatLng point, List<LatLng> polygon) {
bool inside = false;
int len = polygon.length;
for (int i = 0, j = len - 1; i < len; j = i++) {
LatLng pi = polygon[i];
LatLng pj = polygon[j];
if (((pi.latitude > point.latitude) != (pj.latitude > point.latitude)) &&
(point.longitude <
(pj.longitude - pi.longitude) *
(point.latitude - pi.latitude) /
(pj.latitude - pi.latitude) +
pi.longitude)) {
inside = !inside;
}
}
return inside;
}

86
lib/util/util.dart Normal file
View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:uninav/data/geo/model.dart';
bool eq(String? a, String? b) => a?.toLowerCase() == b?.toLowerCase();
String formatDuration(Duration duration) {
final days = duration.inDays;
final hours = duration.inHours.remainder(24);
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
String plural(int thing) => thing == 1 ? '' : 's';
if (days > 0) {
return '$days day${plural(days)}, $hours hour${plural(hours)}';
} else if (hours > 0) {
return '$hours hour${plural(hours)}, $minutes minute${plural(minutes)}';
} else if (minutes > 0) {
return '$minutes minute${plural(minutes)}, $seconds second${plural(seconds)}';
} else {
return '$seconds second${plural(seconds)}';
}
}
String formatFeatureTitle(Feature feature) {
return feature.type.when(
building: () => '${feature.name} (Building)',
lectureHall: () => '${feature.name} (Lecture Hall)',
room: (number) => 'Room ${feature.building ?? "??"}/$number',
pcPool: (number) => 'PC Pool ${feature.name}',
foodDrink: () => '${feature.name} (Food/Drink)',
door: (_) => 'Door',
toilet: (type) => 'Toilet (${formatToiletType(feature.type as Toilet)})',
stairs: (_) => 'Stairs',
lift: (_) => 'Lift',
publicTransport: (_, __) => 'Public Transport',
);
}
String formatToiletType(Toilet toilet) {
final type = toilet.toilet_type.toLowerCase();
switch (type) {
case 'male':
return 'Male';
case 'female':
return 'Female';
case 'handicap':
return 'Handicap';
default:
return 'Unknown';
}
}
String formatDistance(int distanceInMeters) {
if (distanceInMeters < 1000) {
// If the distance is less than 1 kilometer, display it in meters.
return '${distanceInMeters}m';
} else {
// If the distance is 1 kilometer or more, display it in kilometers and meters.
final kilometers =
distanceInMeters ~/ 1000; // Integer division to get whole kilometers.
final meters =
distanceInMeters % 1000; // Remainder to get the remaining meters.
if (meters == 0) {
// If there are no remaining meters, display only kilometers.
return '${kilometers}km';
} else {
// If there are remaining meters, display both kilometers and meters.
return '${kilometers}km ${meters}m';
}
}
}
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 <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

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

View File

@ -5,8 +5,16 @@
import FlutterMacOS
import Foundation
import geolocator_apple
import isar_flutter_libs
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
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"))
}

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
url: "https://pub.dev"
source: hosted
version: "67.0.0"
version: "61.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
url: "https://pub.dev"
source: hosted
version: "6.4.1"
version: "5.13.0"
anim_search_bar:
dependency: "direct main"
description:
@ -154,7 +154,7 @@ packages:
source: hosted
version: "4.10.0"
collection:
dependency: transitive
dependency: "direct main"
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
@ -189,18 +189,26 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
url: "https://pub.dev"
source: hosted
version: "2.3.6"
extra_pedantic:
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: extra_pedantic
sha256: eb9cc0842dc1c980f00fd226364456d2169d54f7118b8ae16443188063edce0b
name: exception_templates
sha256: "517f7c770da690073663f867ee2057ae2f4ffb28edae9da9faa624aa29ac76eb"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "0.3.1"
fake_async:
dependency: transitive
description:
@ -209,6 +217,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
fast_immutable_collections:
dependency: "direct main"
description:
name: fast_immutable_collections
sha256: "38fbc50df5b219dcfb83ebbc3275ec09872530ca1153858fc56fceadb310d037"
url: "https://pub.dev"
source: hosted
version: "10.2.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
@ -230,6 +254,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct dev"
description:
@ -242,10 +274,18 @@ packages:
dependency: "direct main"
description:
name: flutter_map
sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3"
sha256: cda8d72135b697f519287258b5294a57ce2f2a5ebf234f0e406aad4dc14c9399
url: "https://pub.dev"
source: hosted
version: "4.0.0"
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:
dependency: "direct dev"
description: flutter
@ -280,30 +320,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
geodesy:
dependency: transitive
description:
name: geodesy
sha256: d9959000de938adf760f946546ccbf9ebdff8f4f6d0b5c54e8b8b1ed350b1028
url: "https://pub.dev"
source: hosted
version: "0.4.0-nullsafety.0"
geojson:
geojson_vi:
dependency: "direct main"
description:
name: geojson
sha256: "8aab8116d074e92ef2d1ade25ec5ae90ea8bf024a920ab46703c433ffe08878f"
name: geojson_vi
sha256: ffba1991df4d3f98cfd7fee02bcde00b76a39d4daa838ba8a0ba8b83cbff0705
url: "https://pub.dev"
source: hosted
version: "1.0.0"
geopoint:
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: geopoint
sha256: "594afb50a689e6584b80b7de8332c83a78e50725dc4324b2c014d19c56de5e3f"
name: geolocator_android
sha256: f15d1536cd01b1399578f1da1eb5d566e7a718db6a3648f2c24d2e2f859f0692
url: "https://pub.dev"
source: hosted
version: "1.0.0"
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:
dependency: "direct main"
description:
@ -332,10 +404,10 @@ packages:
dependency: transitive
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
version: "1.2.1"
http_multi_server:
dependency: transitive
description:
@ -368,22 +440,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
iso:
dependency: transitive
isar:
dependency: "direct main"
description:
name: iso
sha256: "7030a1a096f7924deb6cccde6c7d80473dddd54eeedf20402e3d6e51b1672b27"
name: isar
sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
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:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.7.1"
version: "0.6.7"
json_annotation:
dependency: "direct main"
description:
@ -404,10 +484,18 @@ packages:
dependency: "direct main"
description:
name: latlong2
sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0"
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
url: "https://pub.dev"
source: hosted
version: "0.8.2"
version: "0.9.1"
lazy_memo:
dependency: transitive
description:
name: lazy_memo
sha256: dcb30b4184a6d767e1d779d74ce784d752d38313b8fb4bad6b659ae7af4bb34d
url: "https://pub.dev"
source: hosted
version: "0.2.3"
leak_tracker:
dependency: transitive
description:
@ -448,6 +536,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
logger:
dependency: transitive
description:
name: logger
sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
logging:
dependency: transitive
description:
@ -512,14 +608,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
pedantic:
dependency: transitive
path_provider:
dependency: "direct main"
description:
name: pedantic
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "1.11.1"
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:
dependency: transitive
description:
@ -568,6 +712,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.3"
quote_buffer:
dependency: transitive
description:
name: quote_buffer
sha256: c4cd07e55ed1b1645a1cc74278a03b2a642c9f6ea3c0528d51827fdd320acf87
url: "https://pub.dev"
source: hosted
version: "0.2.6"
rust_core:
dependency: "direct main"
description:
@ -576,6 +728,62 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -597,14 +805,6 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
slugify:
dependency: transitive
description:
name: slugify
sha256: b272501565cb28050cac2d96b7bf28a2d24c8dae359280361d124f3093d337c3
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_gen:
dependency: transitive
description:
@ -701,14 +901,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:
@ -837,6 +1029,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.5"
win32:
dependency: transitive
description:
name: win32
sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
wkt_parser:
dependency: transitive
description:
@ -845,6 +1045,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:

View File

@ -38,15 +38,28 @@ dependencies:
get: ^4.6.6
yaml: ^3.1.2
surrealdb: ^0.8.0
geojson: ^1.0.0
flutter_map: 7.0.0-dev.1
# latlong2: ^0.9.0
# geojson: ^1.0.0
# flutter_map: 7.0.0-dev.1
flutter_map: ^6.0.0
# flutter_map: ^4.0.0
latlong2: ^0.9.0
# latlong2: ^0.8.0
geojson_vi: ^2.2.3
url_launcher: ^6.2.6
anim_search_bar: ^2.0.3
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
rust_core: ^0.5.3
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:
flutter_test:
@ -61,6 +74,7 @@ dev_dependencies:
build_runner: ^2.4.9
freezed: ^2.5.2
json_serializable: ^6.7.1
# isar_generator: ^3.1.0+1
# For information on the generic Dart part of this file, see the
# 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 <geolocator_windows/geolocator_windows.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

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