diff --git a/lib/components/EventLog.dart b/lib/components/EventLog.dart new file mode 100644 index 0000000..40ce5a1 --- /dev/null +++ b/lib/components/EventLog.dart @@ -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 events; + + const EventLog({Key? key, required this.events}) : super(key: key); + + @override + _EventLogState createState() => _EventLogState(); +} + +class _EventLogState extends State { + 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), + ); diff --git a/lib/components/feature_bottom_sheet.dart b/lib/components/feature_bottom_sheet.dart index 64ef7ef..0bc6c61 100644 --- a/lib/components/feature_bottom_sheet.dart +++ b/lib/components/feature_bottom_sheet.dart @@ -3,7 +3,9 @@ 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( @@ -90,7 +92,39 @@ Future showFeatureBottomSheet( ), ], ), - onPressed: () => {}, + onPressed: () { + print("trying to start navigation..."); + final navController = Get.find(); + final mapController = Get.find(); + + // 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"); + }, ), ], ) diff --git a/lib/components/render_route.dart b/lib/components/render_route.dart new file mode 100644 index 0000000..8f501a2 --- /dev/null +++ b/lib/components/render_route.dart @@ -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().nav.iter().toList(); + + // distance-position pairs + // List polylines = []; + + List polylinePoints = + route.map((e) => e.$1.getCenter().unwrap()).toList(); + + return PolylineLayer(polylines: [ + Polyline( + points: polylinePoints, + strokeWidth: 4.0, + color: Colors.blue, + ), + ]); + }); + } +} diff --git a/lib/controllers/map_controller.dart b/lib/controllers/map_controller.dart index 9e1c26d..9e0d3af 100644 --- a/lib/controllers/map_controller.dart +++ b/lib/controllers/map_controller.dart @@ -41,6 +41,7 @@ class MyMapController extends GetxController { } newLevels.sort(); levels.value = newLevels; + update(); } Result setLevel(int level) { @@ -50,6 +51,7 @@ class MyMapController extends GetxController { } currentLevel.value = level; + update(); return const Ok(()); } @@ -111,6 +113,7 @@ class MyMapController extends GetxController { } features.value = featuresList; + update(); } catch (e) { print('Error parsing GeoJSON: $e'); } diff --git a/lib/controllers/navigation_controller.dart b/lib/controllers/navigation_controller.dart new file mode 100644 index 0000000..4aa109a --- /dev/null +++ b/lib/controllers/navigation_controller.dart @@ -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 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().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 features; + + _PathFindingData(this.start, this.endSelector, this.features); +} + +Result> _findShortestPathIsolate( + _PathFindingData data) { + return findShortestPath(data.start, data.endSelector, data.features); +} diff --git a/lib/data/geo/model.dart b/lib/data/geo/model.dart index f289550..a0e9803 100644 --- a/lib/data/geo/model.dart +++ b/lib/data/geo/model.dart @@ -92,6 +92,19 @@ class Feature with _$Feature { 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 diff --git a/lib/main.dart b/lib/main.dart index 1621bc9..a0e4c81 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ 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'; @@ -13,6 +14,8 @@ void main() { .loadString('assets/geo/uulm_beta.geojson') .then((value) => Get.find().loadGeoJson(value)); + Get.put(NavigationController()); + Get.putAsync(() async { final controller = SharedPrefsController(); await controller.initialize(); @@ -43,7 +46,7 @@ class MyApp extends StatelessWidget { ), initialRoute: '/map', getPages: [ - GetPage(name: '/map', page: () => const MapPage()), + GetPage(name: '/map', page: () => MapPage()), GetPage(name: '/settings', page: () => const SettingsPage()), ], ); diff --git a/lib/map.dart b/lib/map.dart index 7d62167..37e9f78 100644 --- a/lib/map.dart +++ b/lib/map.dart @@ -6,18 +6,45 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:get/get.dart'; import 'package: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 createState() => _MapPageState(); +} + +class _MapPageState extends State { + late final Stream _positionStream; + late final Stream _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) { @@ -67,6 +94,13 @@ class MapPage extends StatelessWidget { TileLayer( urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", maxZoom: 19, + tileBuilder: (context, tile, child) => ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.white.withOpacity(0.7), + BlendMode.srcATop, + ), + child: tile, + ), ), TranslucentPointer( child: LevelLayer( @@ -110,8 +144,26 @@ class MapPage extends StatelessWidget { )), ), CurrentLocationLayer(), + NavigationPathLayer(), ], ), + Positioned( + left: 16, + top: 16, + child: Container( + height: 450, + width: 150, + child: GetBuilder( + builder: (controller) { + if (controller.nav.isNotEmpty) { + return EventLog( + events: controller.nav.map((e) => e.$1).toList()); + } else { + return SizedBox(); + } + }, + ), + )), Positioned( left: 16, bottom: 16, @@ -231,9 +283,89 @@ void locationBottomSheet() { Expanded( flex: 2, child: Container( - height: 300, - color: Colors.transparent, - ), + height: 300, + color: Colors.transparent, + child: Column( + children: [ + const Text( + 'Level', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + GetBuilder( + 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(); + 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, diff --git a/lib/nav/graph.dart b/lib/nav/graph.dart index 9183b56..ac4f57a 100644 --- a/lib/nav/graph.dart +++ b/lib/nav/graph.dart @@ -44,6 +44,19 @@ class GraphFeature with _$GraphFeature { 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(