2024-04-19 18:07:54 +00:00
|
|
|
import 'package:anim_search_bar/anim_search_bar.dart';
|
2024-04-21 12:21:31 +00:00
|
|
|
import 'package:anyhow/anyhow.dart';
|
2024-04-19 18:07:54 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:flutter/services.dart';
|
2024-04-19 18:07:54 +00:00
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
2024-04-21 12:21:31 +00:00
|
|
|
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:get/get.dart';
|
2024-04-19 18:07:54 +00:00
|
|
|
import 'package:latlong2/latlong.dart';
|
2024-04-21 14:27:48 +00:00
|
|
|
import 'package:rust_core/iter.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:rust_core/slice.dart';
|
2024-04-21 14:27:48 +00:00
|
|
|
import 'package:uninav/components/EventLog.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:uninav/components/drawer.dart';
|
|
|
|
import 'package:uninav/components/hamburger_menu.dart';
|
2024-04-20 19:14:11 +00:00
|
|
|
import 'package:uninav/components/map_render_level.dart';
|
2024-04-21 14:27:48 +00:00
|
|
|
import 'package:uninav/components/render_route.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:uninav/controllers/map_controller.dart';
|
2024-04-21 14:27:48 +00:00
|
|
|
import 'package:uninav/controllers/navigation_controller.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:uninav/data/geo/model.dart';
|
2024-04-21 14:27:48 +00:00
|
|
|
import 'package:uninav/nav/graph.dart';
|
2024-04-20 22:35:16 +00:00
|
|
|
import 'package:uninav/util/geojson_util.dart';
|
2024-04-20 14:32:01 +00:00
|
|
|
import 'package:uninav/util/geomath.dart';
|
2024-04-21 14:27:48 +00:00
|
|
|
import 'package:uninav/util/util.dart';
|
2024-04-19 18:07:54 +00:00
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
|
2024-04-21 14:27:48 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
*/
|
2024-04-19 18:07:54 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
2024-04-20 14:32:01 +00:00
|
|
|
drawer: MyDrawer(),
|
2024-04-19 18:07:54 +00:00
|
|
|
appBar: AppBar(
|
|
|
|
title: const Text('Map'),
|
2024-04-20 14:32:01 +00:00
|
|
|
leading: HamburgerMenu(),
|
2024-04-19 18:07:54 +00:00
|
|
|
|
|
|
|
// right side expanding search bar widget
|
|
|
|
actions: [
|
|
|
|
AnimSearchBar(
|
|
|
|
width: /* media query device width */ 300,
|
|
|
|
textController: TextEditingController(),
|
|
|
|
onSuffixTap: () {
|
|
|
|
print('Search');
|
|
|
|
},
|
|
|
|
closeSearchOnSuffixTap: false,
|
|
|
|
onSubmitted: (p0) => {
|
|
|
|
print('Search: $p0'),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
]),
|
2024-04-20 14:32:01 +00:00
|
|
|
floatingActionButton: FloatingActionButton(
|
|
|
|
onPressed: () async {
|
|
|
|
// Add onPressed logic here
|
2024-04-21 12:21:31 +00:00
|
|
|
var future = Get.find<MyMapController>().getCurrentPosition();
|
|
|
|
locationBottomSheet();
|
2024-04-20 14:32:01 +00:00
|
|
|
},
|
2024-04-21 12:21:31 +00:00
|
|
|
child: const Icon(Icons.location_searching),
|
2024-04-20 14:32:01 +00:00
|
|
|
),
|
|
|
|
body: Stack(
|
2024-04-19 18:07:54 +00:00
|
|
|
children: [
|
2024-04-20 14:32:01 +00:00
|
|
|
FlutterMap(
|
2024-04-20 22:35:16 +00:00
|
|
|
mapController: Get.find<MyMapController>().mapController,
|
2024-04-20 14:32:01 +00:00
|
|
|
options: MapOptions(
|
2024-04-20 19:14:11 +00:00
|
|
|
initialCenter: const LatLng(48.422766, 9.9564),
|
|
|
|
initialZoom: 16.0,
|
2024-04-20 14:32:01 +00:00
|
|
|
// camera constraints
|
|
|
|
maxZoom: 19,
|
2024-04-20 22:35:16 +00:00
|
|
|
onTap: (tapPosition, point) {
|
|
|
|
print('Tap: $tapPosition, $point');
|
|
|
|
Get.find<MyMapController>().handleTap(tapPosition, point);
|
|
|
|
},
|
2024-04-20 14:32:01 +00:00
|
|
|
),
|
|
|
|
children: [
|
|
|
|
TileLayer(
|
|
|
|
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
|
|
maxZoom: 19,
|
2024-04-21 14:27:48 +00:00
|
|
|
tileBuilder: (context, tile, child) => ColorFiltered(
|
|
|
|
colorFilter: ColorFilter.mode(
|
|
|
|
Colors.white.withOpacity(0.7),
|
|
|
|
BlendMode.srcATop,
|
|
|
|
),
|
|
|
|
child: tile,
|
|
|
|
),
|
2024-04-20 14:32:01 +00:00
|
|
|
),
|
2024-04-20 22:35:16 +00:00
|
|
|
TranslucentPointer(
|
|
|
|
child: LevelLayer(
|
|
|
|
filter: (feature) => feature.type is Building,
|
|
|
|
),
|
|
|
|
),
|
2024-04-20 19:14:11 +00:00
|
|
|
|
2024-04-20 22:35:16 +00:00
|
|
|
// public transport
|
|
|
|
TranslucentPointer(
|
|
|
|
child: LevelLayer(
|
|
|
|
filter: (feature) =>
|
|
|
|
feature.level == null &&
|
|
|
|
feature.type is PublicTransport,
|
|
|
|
polyCenterMarkerConstructor: (center, name) => Marker(
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
point: center,
|
|
|
|
child: const Icon(
|
|
|
|
Icons.train,
|
|
|
|
color: Colors.black,
|
|
|
|
),
|
|
|
|
alignment: Alignment.center,
|
2024-04-20 14:32:01 +00:00
|
|
|
),
|
2024-04-20 22:35:16 +00:00
|
|
|
polyConstructor: (feature) => feature
|
|
|
|
.getPolygon(
|
|
|
|
constructor: (pts) => Polygon(
|
|
|
|
points: pts,
|
|
|
|
color: Colors.green.withOpacity(0.2),
|
|
|
|
borderColor: Colors.green,
|
|
|
|
borderStrokeWidth: 1,
|
|
|
|
))
|
|
|
|
.unwrap(),
|
2024-04-20 14:32:01 +00:00
|
|
|
),
|
|
|
|
),
|
2024-04-20 22:35:16 +00:00
|
|
|
|
|
|
|
// current level
|
|
|
|
Obx(
|
|
|
|
() => Stack(
|
|
|
|
children: renderLevel(
|
2024-04-21 06:24:50 +00:00
|
|
|
Get.find<MyMapController>().currentLevel.value,
|
|
|
|
)),
|
2024-04-20 22:35:16 +00:00
|
|
|
),
|
2024-04-21 12:21:31 +00:00
|
|
|
CurrentLocationLayer(),
|
2024-04-21 14:27:48 +00:00
|
|
|
NavigationPathLayer(),
|
2024-04-20 14:32:01 +00:00
|
|
|
],
|
2024-04-19 18:07:54 +00:00
|
|
|
),
|
2024-04-21 14:27:48 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)),
|
2024-04-20 14:32:01 +00:00
|
|
|
Positioned(
|
|
|
|
left: 16,
|
|
|
|
bottom: 16,
|
|
|
|
child: Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Theme.of(context).colorScheme.surface,
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
border: Border.all(
|
|
|
|
color: Colors.grey,
|
|
|
|
width: 1,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
padding:
|
|
|
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
2024-04-21 00:22:53 +00:00
|
|
|
child: Row(
|
|
|
|
children: [
|
|
|
|
Obx(
|
|
|
|
() => DropdownButton<int>(
|
|
|
|
value: Get.find<MyMapController>().currentLevel.value,
|
|
|
|
style: TextStyle(
|
|
|
|
color: Theme.of(context).colorScheme.onSurface),
|
|
|
|
dropdownColor: Theme.of(context).colorScheme.surface,
|
|
|
|
onChanged: (int? newValue) {
|
|
|
|
if (newValue != null) {
|
|
|
|
Get.find<MyMapController>().setLevel(newValue);
|
|
|
|
}
|
|
|
|
// Handle dropdown value change
|
|
|
|
},
|
|
|
|
items: Get.find<MyMapController>()
|
|
|
|
.levels
|
|
|
|
.map<DropdownMenuItem<int>>((int value) {
|
|
|
|
return DropdownMenuItem<int>(
|
|
|
|
value: value,
|
|
|
|
child: Text("Level $value"),
|
|
|
|
);
|
|
|
|
}).toList(),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.arrow_upward),
|
|
|
|
onPressed: () {
|
|
|
|
int currentLevel =
|
|
|
|
Get.find<MyMapController>().currentLevel.value;
|
|
|
|
if (currentLevel <
|
|
|
|
Get.find<MyMapController>().levels.last) {
|
|
|
|
Get.find<MyMapController>()
|
|
|
|
.setLevel(currentLevel + 1);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.arrow_downward),
|
|
|
|
onPressed: () {
|
|
|
|
int currentLevel =
|
|
|
|
Get.find<MyMapController>().currentLevel.value;
|
|
|
|
if (currentLevel >
|
|
|
|
Get.find<MyMapController>().levels.first) {
|
|
|
|
Get.find<MyMapController>()
|
|
|
|
.setLevel(currentLevel - 1);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
2024-04-20 14:32:01 +00:00
|
|
|
),
|
|
|
|
)),
|
2024-04-19 18:07:54 +00:00
|
|
|
],
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
2024-04-21 12:21:31 +00:00
|
|
|
|
|
|
|
void locationBottomSheet() {
|
|
|
|
print(Get.find<MyMapController>().position.value);
|
|
|
|
String buttonText = "Search for Location";
|
|
|
|
IconData locationIcon = Icons.location_searching;
|
|
|
|
bool spinner = false;
|
|
|
|
Get.bottomSheet(
|
|
|
|
Theme(
|
|
|
|
data: ThemeData.light(),
|
|
|
|
child: Container(
|
|
|
|
constraints: const BoxConstraints(
|
|
|
|
// minHeight: 300,
|
|
|
|
),
|
|
|
|
width: Get.mediaQuery.size.width,
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
color: Colors.black,
|
|
|
|
borderRadius: BorderRadius.only(
|
|
|
|
topLeft: Radius.circular(30),
|
|
|
|
topRight: Radius.circular(30),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
child: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
Center(
|
|
|
|
child: Container(
|
|
|
|
width: 50,
|
|
|
|
height: 5,
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Colors.white,
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
const Text(
|
|
|
|
'Select Location',
|
|
|
|
style: TextStyle(
|
|
|
|
fontSize: 24,
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
flex: 2,
|
|
|
|
child: Container(
|
2024-04-21 14:27:48 +00:00
|
|
|
height: 300,
|
|
|
|
color: Colors.transparent,
|
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
const Text(
|
|
|
|
'Level',
|
|
|
|
style: TextStyle(
|
|
|
|
fontSize: 18,
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
GetBuilder<MyMapController>(
|
|
|
|
builder: (controller) {
|
|
|
|
return Wrap(
|
|
|
|
spacing: 8,
|
|
|
|
runSpacing: 8,
|
|
|
|
children: [
|
|
|
|
for (final level in controller.levels)
|
|
|
|
GestureDetector(
|
|
|
|
onTap: () => controller.setLevel(level),
|
|
|
|
child: Chip(
|
|
|
|
label: Text("$level"),
|
|
|
|
backgroundColor:
|
|
|
|
controller.currentLevel == level
|
|
|
|
? Colors.blue
|
|
|
|
: Colors.grey[800],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Obx(() {
|
|
|
|
final navController =
|
|
|
|
Get.find<NavigationController>();
|
|
|
|
String? curBuilding;
|
|
|
|
if (navController.position.value
|
|
|
|
is BuildingFloor) {
|
|
|
|
curBuilding = (navController
|
|
|
|
.position.value as BuildingFloor)
|
|
|
|
.building
|
|
|
|
.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
final buildingList = controller.features
|
|
|
|
.where((f) => f.type is Building);
|
|
|
|
|
|
|
|
return Wrap(
|
|
|
|
spacing: 8,
|
|
|
|
runSpacing: 8,
|
|
|
|
children: [
|
|
|
|
for (final building in buildingList)
|
|
|
|
GestureDetector(
|
|
|
|
onTap: () {
|
|
|
|
// print(building.name);
|
|
|
|
// print(curBuilding);
|
|
|
|
// print(navController.position);
|
|
|
|
navController.updatePosition(wrap(
|
|
|
|
building,
|
|
|
|
controller
|
|
|
|
.currentLevel.value,
|
|
|
|
building.name)
|
|
|
|
.firstOrNull);
|
|
|
|
},
|
|
|
|
child: Chip(
|
|
|
|
label: Text(building.name),
|
|
|
|
backgroundColor:
|
|
|
|
eq(curBuilding, building.name)
|
|
|
|
? building.level ==
|
|
|
|
controller
|
|
|
|
.currentLevel
|
|
|
|
? Colors.blue
|
|
|
|
: Colors.orange
|
|
|
|
: Colors.grey[800],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
})
|
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
)),
|
2024-04-21 12:21:31 +00:00
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
flex: 1,
|
|
|
|
child: StatefulBuilder(builder: (context, setState) {
|
|
|
|
// TODO: make this persist closing the bottom sheet
|
|
|
|
return ElevatedButton(
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
fixedSize: const Size(300, 300),
|
|
|
|
backgroundColor: Colors.white,
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
borderRadius: BorderRadius.circular(30),
|
|
|
|
),
|
|
|
|
padding: const EdgeInsets.all(15),
|
|
|
|
),
|
|
|
|
onPressed: () async {
|
|
|
|
if (spinner) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setState(() {
|
|
|
|
buttonText = "Searching...";
|
|
|
|
locationIcon = Icons.location_searching;
|
|
|
|
spinner = true;
|
|
|
|
});
|
|
|
|
final pos = await Get.find<MyMapController>()
|
|
|
|
.getCurrentPosition();
|
|
|
|
if (!context.mounted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (pos case Ok(:final ok)) {
|
|
|
|
setState(() {
|
|
|
|
buttonText = "Location found!";
|
|
|
|
locationIcon = Icons.my_location;
|
|
|
|
spinner = false;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setState(() {
|
|
|
|
buttonText = "Location not found! Try again";
|
|
|
|
locationIcon = Icons.error;
|
|
|
|
spinner = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Stack(
|
|
|
|
alignment: Alignment.center,
|
|
|
|
children: [
|
|
|
|
Icon(locationIcon,
|
|
|
|
size: 80,
|
|
|
|
color: Get.theme.colorScheme.inversePrimary),
|
|
|
|
if (spinner)
|
|
|
|
CircularProgressIndicator(
|
|
|
|
color: Get.theme.colorScheme.inversePrimary,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const SizedBox(
|
|
|
|
height: 12,
|
|
|
|
),
|
|
|
|
Text(
|
|
|
|
buttonText,
|
|
|
|
softWrap: true,
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
style: const TextStyle(
|
|
|
|
color: Colors.black,
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: FontWeight.bold),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
/*
|
|
|
|
ListTile(
|
|
|
|
title: const Text(
|
|
|
|
'Current Location',
|
|
|
|
style: TextStyle(color: Colors.white),
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
Get.back();
|
|
|
|
},
|
|
|
|
),
|
|
|
|
ListTile(
|
|
|
|
title: const Text(
|
|
|
|
'Search Location',
|
|
|
|
style: TextStyle(color: Colors.white),
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
Get.back();
|
|
|
|
},
|
|
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
*/
|
|
|
|
ElevatedButton(
|
|
|
|
child: const Text(
|
|
|
|
'Cancel',
|
|
|
|
style: TextStyle(color: Colors.black),
|
|
|
|
),
|
|
|
|
onPressed: () {
|
|
|
|
Get.back();
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
isScrollControlled: true,
|
|
|
|
enterBottomSheetDuration: const Duration(milliseconds: 150),
|
|
|
|
exitBottomSheetDuration: const Duration(milliseconds: 200),
|
|
|
|
);
|
|
|
|
}
|