feat: major progress

This commit is contained in:
Yandrik 2024-04-20 16:32:01 +02:00
parent ba71707514
commit be359980bb
19 changed files with 5328 additions and 50 deletions

30
.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: android
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@ -1,16 +1,8 @@
# uninav
A new Flutter project.
App to navigate in Universities. Developed at Uni Ulm, but made to be adaptable for all other universities, or complex indoor navigation environments
## Getting Started
## Building and such
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
run `flutter run` for running the app on your device.
run `dart run build_runner build` to run the code generator

View File

@ -26,3 +26,7 @@ linter:
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
errors:
invalid_annotation_target: ignore

2070
assets/geo/uulm_beta.geojson Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/get_navigation.dart';
import 'package:uninav/map.dart';
class MyDrawer extends StatelessWidget {
@override
@ -20,9 +23,11 @@ class MyDrawer extends StatelessWidget {
),
),
ListTile(
leading: Icon(Icons.home),
title: Text('Home'),
leading: Icon(Icons.map),
title: Text('Map Page'),
onTap: () {
Get.back(); // close drawer
Get.toNamed('/map');
// Handle menu item tap
},
),
@ -30,6 +35,8 @@ class MyDrawer extends StatelessWidget {
leading: Icon(Icons.settings),
title: Text('Settings'),
onTap: () {
Get.back(); // close drawer
Get.toNamed('/settings');
// Handle menu item tap
},
),

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class HamburgerMenu extends StatelessWidget {
const HamburgerMenu({
super.key,
});
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
);
}
}

View File

@ -0,0 +1,256 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
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';
List<Widget> renderLevel(int level) {
return <Widget>[
LevelLayer(
filter: (feature) =>
feature.level == level && feature.type is LectureHall,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.orange.withOpacity(0.2),
borderColor: Colors.orange,
isFilled: true,
borderStrokeWidth: 1,
),
)
.unwrap(),
markerConstructor: (feature) => Marker(
width: 150,
height: 60,
point: feature.getPoint().unwrap(),
builder: (cx) => Center(
child: Column(
children: [
Icon(
Icons.class_,
color: Colors.black,
),
Text('${feature.name}'),
],
),
),
)),
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Room,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
isFilled: true,
borderStrokeWidth: 1,
),
)
.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,
),
);
},
),
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);
});
}
}

View File

@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:anyhow/anyhow.dart';
import 'package:geojson/geojson.dart';
import 'package:get/get.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:uninav/data/geo/parser.dart';
class MyMapController extends GetxController {
// constructor that calls loadgeojson with a default geojson string
final RxList<Feature> features = <Feature>[].obs;
final currentLevel = 1.obs;
final levels = <int>[1].obs;
@override
onInit() async {
print("init");
ever(features, refreshLevels);
super.onInit();
}
void refreshLevels(List<Feature> curFeatures) {
print("refreshLevels");
final newLevels = <int>[1];
for (final feature in curFeatures) {
if (feature.level != null && !newLevels.contains(feature.level)) {
newLevels.add(feature.level!);
}
}
newLevels.sort();
levels.value = newLevels;
}
Result<void> setLevel(int level) {
// check that level is in features
if (!levels.contains(level)) {
return bail('Level $level is not in features');
}
currentLevel.value = level;
return const Ok(());
}
Future<void> loadGeoJson(String geoJsonString) async {
try {
// print(geoJsonString);
final featuresList = <Feature>[];
final geojson = GeoJson();
await geojson.parse(geoJsonString);
for (final feature in geojson.features) {
print(feature.properties);
final parsed =
parseFeature(feature.properties ?? <String, dynamic>{}, feature);
if (parsed case Ok(:final ok)) {
featuresList.add(ok);
}
}
features.value = featuresList;
} catch (e) {
print('Error parsing GeoJSON: $e');
}
}
}

70
lib/data/geo/model.dart Normal file
View File

@ -0,0 +1,70 @@
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:latlong2/latlong.dart';
part 'model.freezed.dart';
@freezed
class Feature with _$Feature {
const Feature._();
const factory Feature({
required String name,
required FeatureType type,
String? description,
required dynamic geometry,
int? level,
}) = _Feature;
bool isPolygon() {
return geometry is GeoJsonFeature<GeoJsonPolygon>;
}
bool isPoint() {
return geometry is GeoJsonFeature<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>;
// print(polygon.geometry!.geoSeries[0].geoPoints);
final points = polygon.geometry!.geoSeries[0].geoPoints
.map((e) => LatLng(e.latitude, e.longitude))
.toList();
// print(points);
return Ok(constructor(points));
} else {
return bail("Feature Geometry is not a Polygon");
}
}
Result<LatLng> getPoint() {
if (isPoint()) {
final point = geometry as GeoJsonFeature<GeoJsonPoint>;
return Ok(LatLng(point.geometry!.geoPoint.latitude,
point.geometry!.geoPoint.longitude));
} else {
return bail("Feature Geometry is not a Point");
}
}
}
@freezed
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.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.publicTransport(
List<String> bus_lines, List<String> tram_lines) = PublicTransport;
}

File diff suppressed because it is too large Load Diff

142
lib/data/geo/parser.dart Normal file
View File

@ -0,0 +1,142 @@
import 'package:anyhow/anyhow.dart';
import 'package:geojson/geojson.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:yaml/yaml.dart';
Result<Feature> parseFeature(
Map<String, dynamic> properties, dynamic geometry) {
final name = properties['name'] as String?;
final description_yaml = properties['description'] as String? ?? '';
final layer = properties['layer'] as String?;
int? level;
// check if layer key contains a digit using \d+ regex
final layerNum = RegExp(r'\d+').firstMatch(layer ?? '');
if (layerNum != null) {
level = int.parse(layerNum.group(0)!);
}
// try parse yaml
if (description_yaml == null) {
print("warn: Description key is missing for feature $name");
}
if (layer == null) {
return bail("Layer key \'layer\' is missing for feature $name");
}
dynamic yaml;
try {
yaml = loadYaml(description_yaml);
} on YamlException catch (e) {
return bail("Couldn't parse YAML in description for feature $name: $e");
}
yaml = yaml as YamlMap? ?? {};
final description = yaml['desription'] as String?;
print("yaml: $yaml");
var raw_type = yaml['type'] as String?;
if (raw_type == null && layer?.toLowerCase() == 'buildings') {
raw_type = 'building';
}
if (raw_type == null) {
return bail("Type key \'type\' is missing for feature $name");
}
FeatureType type;
try {
switch (raw_type) {
case 'building':
type = FeatureType.building();
case 'lift':
final list = getYamlList<int>(yaml, 'connects_levels')
.expect("Couldn't parse 'connects_levels' for feature $name");
type = FeatureType.lift(list);
break;
case 'stairs':
final list = getYamlList<int>(yaml, 'connects_levels')
.expect("Couldn't parse 'connects_levels' for feature $name");
type = FeatureType.stairs(list);
break;
case 'lecture_hall':
type = FeatureType.lectureHall();
break;
case 'room':
type = FeatureType.room();
break;
case 'door':
final list = getYamlList<String>(yaml, 'connects')
.expect("Couldn't parse 'connects' for feature $name");
type = FeatureType.door(list);
break;
case 'toilet':
final toiletType = getYamlKey<String>(yaml, 'toilet_type')
.expect("Couldn't parse 'toilet_type' for feature $name");
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");
type = FeatureType.publicTransport(
stringifyList(busLines)
.expect('Couldn\'t stringify \'bus_lines\' for feature $name'),
stringifyList(tramLines)
.expect('Couldn\'t stringify \'tram_lines\' for feature $name'),
);
break;
default:
return bail("Unknown feature type $raw_type for feature $name");
}
} catch (e) {
return bail("Failed to parse feature type for feature $name: $e");
}
return Ok(Feature(
name: name ?? 'Unknown',
type: type,
description: description,
geometry: geometry,
level: level,
));
}
Result<List<String>> stringifyList(List<dynamic> tramLines) {
try {
return Ok(tramLines.map((e) => e.toString()).toList());
} catch (e) {
return bail("Couldn't stringify list: $e");
}
}
Result<List<T>> getYamlList<T>(YamlMap yaml, String key) {
try {
print('yaml is ${yaml[key]}');
final val = (yaml[key] as YamlList?);
if (val == null) {
return bail("Key $key is missing in yaml");
}
return Ok(val.cast<T>());
} catch (e) {
return bail("Failed to parse yaml key $key as ${T.toString()}: $e");
}
}
Result<T> getYamlKey<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);
} catch (e) {
return bail("Failed to parse yaml key $key as ${T.toString()}: $e");
}
}

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:get/get.dart';
import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/map.dart';
import 'package:uninav/settings.dart';
void main() {
Get.put(MyMapController());
runApp(const MyApp());
}
@ -33,7 +36,11 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: MapPage(),
initialRoute: '/map',
getPages: [
GetPage(name: '/map', page: () => const MapPage()),
GetPage(name: '/settings', page: () => const SettingsPage()),
],
);
}
}

View File

@ -1,9 +1,16 @@
import 'package:anim_search_bar/anim_search_bar.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:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:uninav/components/drawer..dart';
import 'package:rust_core/slice.dart';
import 'package:uninav/components/drawer.dart';
import 'package:uninav/components/hamburger_menu.dart';
import 'package:uninav/controllers/map_controller.dart';
import 'package:uninav/data/geo/model.dart';
import 'package:uninav/util/geomath.dart';
import 'package:url_launcher/url_launcher.dart';
class MapPage extends StatelessWidget {
@ -12,18 +19,10 @@ class MapPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: MyDrawer(),
appBar: AppBar(
title: const Text('Map'),
leading: Builder(
builder: (context) {
return IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
);
}
),
leading: HamburgerMenu(),
// right side expanding search bar widget
actions: [
@ -39,29 +38,365 @@ class MapPage extends StatelessWidget {
},
),
]),
drawer: MyDrawer(),
body: FlutterMap(
mapController: MapController(),
options: MapOptions(
center: LatLng(51.5, -0.09),
zoom: 13.0,
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// Add onPressed logic here
await Get.find<MyMapController>().loadGeoJson(
await rootBundle.loadString('assets/geo/uulm_beta.geojson'));
},
child: const Icon(Icons.add),
),
body: Stack(
children: [
TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
FlutterMap(
mapController: MapController(),
options: MapOptions(
center: LatLng(48.422766, 9.9564),
zoom: 16.0,
// camera constraints
maxZoom: 19,
),
children: [
TileLayer(
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
maxZoom: 19,
),
// buildings
LevelLayer(
filter: (feature) => feature.type is Building,
),
// public transport
LevelLayer(
filter: (feature) =>
feature.level == null && feature.type is PublicTransport,
polyCenterMarkerConstructor: (center, name) => Marker(
width: 100,
height: 100,
point: center,
builder: (cx) => const Center(
child: Icon(
Icons.train,
color: Colors.black,
),
),
),
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
isFilled: true,
borderStrokeWidth: 1,
))
.unwrap(),
),
// current level
Obx(
() => Stack(
children: renderLevel(
Get.find<MyMapController>().currentLevel.value),
),
)
// RichAttributionWidget(attributions: [
// TextSourceAttribution(
// 'OpenStreetMap contributors',
// onTap: () =>
// launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
// )
// ]),
],
),
RichAttributionWidget(attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () =>
launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
)
])
// PolygonLayer(polygons: myGeoJson.polygons),
// PolylineLayer(polylines: myGeoJson.polylines),
// MarkerLayer(markers: myGeoJson.markers)
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),
child: Obx(
() => DropdownButton<int>(
value: Get.find<MyMapController>().currentLevel.value,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface),
dropdownColor: Theme.of(context).colorScheme.surface,
onChanged: (int? newValue) {
if (newValue != null) {
Get.find<MyMapController>().setLevel(newValue);
}
// Handle dropdown value change
},
items: Get.find<MyMapController>()
.levels
.map<DropdownMenuItem<int>>((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text("Level $value"),
);
}).toList(),
),
),
)),
],
));
}
}
List<Widget> renderLevel(int level) {
return <Widget>[
LevelLayer(
filter: (feature) =>
feature.level == level && feature.type is LectureHall,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.orange.withOpacity(0.2),
borderColor: Colors.orange,
isFilled: true,
borderStrokeWidth: 1,
),
)
.unwrap(),
markerConstructor: (feature) => Marker(
width: 150,
height: 60,
point: feature.getPoint().unwrap(),
builder: (cx) => Center(
child: Column(
children: [
Icon(
Icons.class_,
color: Colors.black,
),
Text('${feature.name}'),
],
),
),
)),
LevelLayer(
filter: (feature) => feature.level == level && feature.type is Room,
polyConstructor: (feature) => feature
.getPolygon(
constructor: (pts) => Polygon(
points: pts,
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green,
isFilled: true,
borderStrokeWidth: 1,
),
)
.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,
),
);
},
),
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);
});
}
}

21
lib/settings copy.dart Normal file
View File

@ -0,0 +1,21 @@
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'),
),
);
}
}

21
lib/settings.dart Normal file
View File

@ -0,0 +1,21 @@
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'),
),
);
}
}

37
lib/util/geomath.dart Normal file
View File

@ -0,0 +1,37 @@
import 'dart:math';
import 'package:latlong2/latlong.dart';
LatLng polygonCenterMinmax(List<LatLng> polygon) {
double minLat = double.infinity;
double maxLat = double.negativeInfinity;
double minLng = double.infinity;
double maxLng = double.negativeInfinity;
for (LatLng point in polygon) {
minLat = min(minLat, point.latitude);
maxLat = max(maxLat, point.latitude);
minLng = min(minLng, point.longitude);
maxLng = max(maxLng, point.longitude);
}
double centerLat = (minLat + maxLat) / 2;
double centerLng = (minLng + maxLng) / 2;
return LatLng(centerLat, centerLng);
}
LatLng polygonCenterAvg(List<LatLng> polygon) {
double sumLat = 0;
double sumLng = 0;
for (LatLng point in polygon) {
sumLat += point.latitude;
sumLng += point.longitude;
}
double centerLat = sumLat / polygon.length;
double centerLng = sumLng / polygon.length;
return LatLng(centerLat, centerLng);
}

View File

@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
version: "67.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
anim_search_bar:
dependency: "direct main"
description:
@ -9,6 +25,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
anyhow:
dependency: "direct main"
description:
name: anyhow
sha256: "8fcd8a16ab1dadcae0d77b880c4cda85367876ef9da6ca7fd68ce6ce51679bbe"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
args:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
async:
dependency: transitive
description:
@ -25,6 +57,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
build:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_config:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
url: "https://pub.dev"
source: hosted
version: "1.1.1"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
url: "https://pub.dev"
source: hosted
version: "2.4.9"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
url: "https://pub.dev"
source: hosted
version: "7.3.0"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
url: "https://pub.dev"
source: hosted
version: "8.9.2"
characters:
dependency: transitive
description:
@ -33,6 +129,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
clock:
dependency: transitive
description:
@ -41,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
url: "https://pub.dev"
source: hosted
version: "4.10.0"
collection:
dependency: transitive
description:
@ -49,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
crypto:
dependency: transitive
description:
@ -65,6 +185,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
url: "https://pub.dev"
source: hosted
version: "2.3.6"
extra_pedantic:
dependency: transitive
description:
@ -81,6 +209,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
fixnum:
dependency: transitive
description:
@ -120,6 +256,30 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1
url: "https://pub.dev"
source: hosted
version: "2.5.2"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
url: "https://pub.dev"
source: hosted
version: "2.4.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
geodesy:
dependency: transitive
description:
@ -152,6 +312,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.6.6"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
graphs:
dependency: transitive
description:
name: graphs
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
url: "https://pub.dev"
source: hosted
version: "2.3.1"
http:
dependency: transitive
description:
@ -160,6 +336,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.6"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
@ -176,6 +360,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.0"
io:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
iso:
dependency: transitive
description:
@ -184,6 +376,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev"
source: hosted
version: "4.8.1"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969
url: "https://pub.dev"
source: hosted
version: "6.7.1"
latlong2:
dependency: "direct main"
description:
@ -232,6 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
matcher:
dependency: transitive
description:
@ -264,6 +488,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mime:
dependency: transitive
description:
name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
@ -296,6 +536,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
proj4dart:
dependency: transitive
description:
@ -304,6 +552,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
url: "https://pub.dev"
source: hosted
version: "1.2.3"
rust_core:
dependency: "direct main"
description:
name: rust_core
sha256: "3b61f6e32dc8af47dc8fa27b662c5ace9db713fd4dd5e9fd77400e6577d51fb8"
url: "https://pub.dev"
source: hosted
version: "0.5.3"
shelf:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sky_engine:
dependency: transitive
description: flutter
@ -317,6 +605,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
source_span:
dependency: transitive
description:
@ -349,6 +653,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
@ -381,6 +693,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
timing:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tuple:
dependency: transitive
description:
@ -493,6 +813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:

View File

@ -39,10 +39,14 @@ dependencies:
yaml: ^3.1.2
surrealdb: ^0.8.0
geojson: ^1.0.0
flutter_map: ^4.0.0
latlong2: ^0.8.2
flutter_map: 7.0.0-dev.1
# latlong2: ^0.9.0
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
dev_dependencies:
flutter_test:
@ -54,6 +58,9 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
build_runner: ^2.4.9
freezed: ^2.5.2
json_serializable: ^6.7.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@ -67,7 +74,9 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
assets:
# - assets/geo/
- assets/geo/uulm_beta.geojson
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

1
test/geojson_tests.dart Normal file
View File

@ -0,0 +1 @@