Compare commits
7 Commits
be359980bb
...
main
Author | SHA1 | Date | |
---|---|---|---|
1211c9b16e | |||
a6f9cfdaf4 | |||
ad0c8e3124 | |||
3de71e2a5a | |||
e653010458 | |||
13327da5de | |||
b7487fc25e |
@ -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
|
||||
|
@ -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>
|
||||
|
2293
assets/geo/uulm_beta copy.geojson
Normal file
2293
assets/geo/uulm_beta copy.geojson
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
98
lib/components/EventLog.dart
Normal file
98
lib/components/EventLog.dart
Normal 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),
|
||||
);
|
57
lib/components/colorful_chips.dart
Normal file
57
lib/components/colorful_chips.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
356
lib/components/feature_bottom_sheet.dart
Normal file
356
lib/components/feature_bottom_sheet.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
@ -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,9 +8,13 @@ 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,
|
||||
@ -18,16 +24,14 @@ List<Widget> renderLevel(int level) {
|
||||
points: pts,
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
borderColor: Colors.orange,
|
||||
isFilled: true,
|
||||
borderStrokeWidth: 1,
|
||||
borderStrokeWidth: 2,
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
markerConstructor: (feature) => Marker(
|
||||
width: 150,
|
||||
height: 60,
|
||||
width: 50,
|
||||
height: 20,
|
||||
point: feature.getPoint().unwrap(),
|
||||
builder: (cx) => Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
@ -37,73 +41,144 @@ List<Widget> renderLevel(int level) {
|
||||
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(0.2),
|
||||
color: Colors.green.withOpacity(1.2),
|
||||
borderColor: Colors.green,
|
||||
isFilled: true,
|
||||
borderStrokeWidth: 1,
|
||||
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(
|
||||
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);
|
||||
|
34
lib/components/render_route.dart
Normal file
34
lib/components/render_route.dart
Normal 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,
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
74
lib/controllers/isar_controller.dart.old
Normal file
74
lib/controllers/isar_controller.dart.old
Normal 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,
|
||||
}
|
@ -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(());
|
||||
}
|
||||
}
|
||||
|
72
lib/controllers/navigation_controller.dart
Normal file
72
lib/controllers/navigation_controller.dart
Normal 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);
|
||||
}
|
72
lib/controllers/shared_prefs_controller.dart
Normal file
72
lib/controllers/shared_prefs_controller.dart
Normal 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,
|
||||
}
|
433
lib/controllers/shared_prefs_controller.freezed.dart
Normal file
433
lib/controllers/shared_prefs_controller.freezed.dart
Normal 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;
|
||||
}
|
41
lib/controllers/shared_prefs_controller.g.dart
Normal file
41
lib/controllers/shared_prefs_controller.g.dart
Normal 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,
|
||||
};
|
@ -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
@ -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");
|
||||
}
|
||||
}
|
||||
|
0
lib/data/settings/settings_model.dart
Normal file
0
lib/data/settings/settings_model.dart
Normal file
128
lib/main.dart
128
lib/main.dart
@ -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.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
613
lib/map.dart
613
lib/map.dart
@ -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,46 +71,58 @@ 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,
|
||||
),
|
||||
|
||||
// buildings
|
||||
LevelLayer(
|
||||
child: tile,
|
||||
),
|
||||
),
|
||||
TranslucentPointer(
|
||||
child: LevelLayer(
|
||||
filter: (feature) => feature.type is Building,
|
||||
),
|
||||
),
|
||||
|
||||
// public transport
|
||||
LevelLayer(
|
||||
TranslucentPointer(
|
||||
child: LevelLayer(
|
||||
filter: (feature) =>
|
||||
feature.level == null && feature.type is PublicTransport,
|
||||
feature.level == null &&
|
||||
feature.type is PublicTransport,
|
||||
polyCenterMarkerConstructor: (center, name) => Marker(
|
||||
width: 100,
|
||||
height: 100,
|
||||
point: center,
|
||||
builder: (cx) => const Center(
|
||||
child: Icon(
|
||||
child: const Icon(
|
||||
Icons.train,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
polyConstructor: (feature) => feature
|
||||
.getPolygon(
|
||||
@ -88,29 +130,40 @@ class MapPage extends StatelessWidget {
|
||||
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),
|
||||
Get.find<MyMapController>().currentLevel.value,
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
// RichAttributionWidget(attributions: [
|
||||
// TextSourceAttribution(
|
||||
// 'OpenStreetMap contributors',
|
||||
// onTap: () =>
|
||||
// launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
|
||||
// )
|
||||
// ]),
|
||||
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,7 +178,9 @@ class MapPage extends StatelessWidget {
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Obx(
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => DropdownButton<int>(
|
||||
value: Get.find<MyMapController>().currentLevel.value,
|
||||
style: TextStyle(
|
||||
@ -147,256 +202,282 @@ class MapPage extends StatelessWidget {
|
||||
}).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);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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(),
|
||||
markerConstructor: (feature) => Marker(
|
||||
width: 150,
|
||||
height: 60,
|
||||
point: feature.getPoint().unwrap(),
|
||||
builder: (cx) => Center(
|
||||
width: Get.mediaQuery.size.width,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Select Location',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
height: 300,
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.class_,
|
||||
color: Colors.black,
|
||||
const Text(
|
||||
'Level',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
GetBuilder<MyMapController>(
|
||||
builder: (controller) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final level in controller.levels)
|
||||
GestureDetector(
|
||||
onTap: () => controller.setLevel(level),
|
||||
child: Chip(
|
||||
label: Text("$level"),
|
||||
backgroundColor:
|
||||
controller.currentLevel == level
|
||||
? Colors.blue
|
||||
: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final navController =
|
||||
Get.find<NavigationController>();
|
||||
String? curBuilding;
|
||||
if (navController.position.value
|
||||
is BuildingFloor) {
|
||||
curBuilding = (navController
|
||||
.position.value as BuildingFloor)
|
||||
.building
|
||||
.name;
|
||||
}
|
||||
|
||||
final buildingList = controller.features
|
||||
.where((f) => f.type is Building);
|
||||
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final building in buildingList)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// print(building.name);
|
||||
// print(curBuilding);
|
||||
// print(navController.position);
|
||||
navController.updatePosition(wrap(
|
||||
building,
|
||||
controller
|
||||
.currentLevel.value,
|
||||
building.name)
|
||||
.firstOrNull);
|
||||
},
|
||||
child: Chip(
|
||||
label: Text(building.name),
|
||||
backgroundColor:
|
||||
eq(curBuilding, building.name)
|
||||
? building.level ==
|
||||
controller
|
||||
.currentLevel
|
||||
? Colors.blue
|
||||
: Colors.orange
|
||||
: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
// TODO: make this persist closing the bottom sheet
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
fixedSize: const Size(300, 300),
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
padding: const EdgeInsets.all(15),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (spinner) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
buttonText = "Searching...";
|
||||
locationIcon = Icons.location_searching;
|
||||
spinner = true;
|
||||
});
|
||||
final pos = await Get.find<MyMapController>()
|
||||
.getCurrentPosition();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
if (pos case Ok(:final ok)) {
|
||||
setState(() {
|
||||
buttonText = "Location found!";
|
||||
locationIcon = Icons.my_location;
|
||||
spinner = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
buttonText = "Location not found! Try again";
|
||||
locationIcon = Icons.error;
|
||||
spinner = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Icon(locationIcon,
|
||||
size: 80,
|
||||
color: Get.theme.colorScheme.inversePrimary),
|
||||
if (spinner)
|
||||
CircularProgressIndicator(
|
||||
color: Get.theme.colorScheme.inversePrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Text(
|
||||
buttonText,
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
/*
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Current Location',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
'Search Location',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
*/
|
||||
ElevatedButton(
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
)
|
||||
.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,
|
||||
),
|
||||
isScrollControlled: true,
|
||||
enterBottomSheetDuration: const Duration(milliseconds: 150),
|
||||
exitBottomSheetDuration: const Duration(milliseconds: 200),
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
} 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
382
lib/nav/graph.dart
Normal file
382
lib/nav/graph.dart
Normal 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
647
lib/nav/graph.dart.old
Normal 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
586
lib/nav/graph.freezed.dart
Normal 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;
|
||||
}
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
96
lib/util/geojson_util.dart
Normal file
96
lib/util/geojson_util.dart
Normal 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
48
lib/util/geolocator.dart
Normal 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");
|
||||
}
|
||||
}
|
@ -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
86
lib/util/util.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
isar_flutter_libs
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
@ -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"))
|
||||
}
|
||||
|
330
pubspec.lock
330
pubspec.lock
@ -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:
|
||||
|
20
pubspec.yaml
20
pubspec.yaml
@ -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
241
test/graph_tests.dart
Normal 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
5
test/scratch_1
Normal file
File diff suppressed because one or more lines are too long
@ -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"));
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
geolocator_windows
|
||||
isar_flutter_libs
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user