MobileApp/lib/boxes/maps.dart
2023-09-21 00:12:01 +02:00

475 lines
14 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'dart:ffi';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:ju_rc_app/lib/boxes.dart';
import 'package:ju_rc_app/lib/sensors.dart';
import 'package:ju_rc_app/lib/serial.dart';
// c-structs
base class GnssLocData extends Struct {
@Uint8()
external int type;
@Uint8()
external int command;
@Uint8()
external int isValid;
@Double()
external double lat;
@Double()
external double lng;
}
base class SpeedLocData extends Struct {
@Uint8()
external int type;
@Uint8()
external int command;
@Uint8()
external int isValid;
@Double()
external double speed;
}
base class CourseLocData extends Struct {
@Uint8()
external int type;
@Uint8()
external int command;
@Uint8()
external int isValid;
@Double()
external double course;
}
base class AltLocData extends Struct {
@Uint8()
external int type;
@Uint8()
external int command;
@Uint8()
external int isValid;
@Double()
external double alt;
}
base class SatLocData extends Struct {
@Uint8()
external int type;
@Uint8()
external int command;
@Uint8()
external int isValid;
@Uint32()
external int n;
}
base class HdopLocData extends Struct {
@Uint8()
external int type;
@Uint8()
external int command;
@Uint8()
external int isValid;
@Double()
external double hdop;
}
// c-structs end
class Maps extends JuBox {
const Maps({super.key});
@override
State<StatefulWidget> createState() => _MapsState();
}
class _MapsState extends State<Maps> with TickerProviderStateMixin {
USerial serial = getSerial();
late Timer timer;
double _lat = 51.1667, _long = 10.4500;
int _state = 0;
bool _wasVaild = false;
final _mapController = MapController();
@override
void initState() {
super.initState();
SensorReader.listen(serialListen);
timer = Timer.periodic(const Duration(seconds: 1), (_) async {
serial.sprintln("<rfSystemSensor>2300", system: true);
});
}
@override
void dispose() {
SensorReader.removeListen(serialListen);
timer.cancel();
super.dispose();
}
void serialListen(int type, int command, Pointer<ArrayCStruct> ptr) {
if (type != 0x23 || command != 0x00) return;
var loc = ptr as Pointer<GnssLocData>;
setState(() {
_lat = loc.ref.lat;
_long = loc.ref.lng;
_state = loc.ref.isValid;
//_mapController.move(LatLng(_lat, _long), _wasVaild ? 17 : 6);
_animatedMapMove(
_mapController, this, LatLng(_lat, _long), _wasVaild ? 17 : 6);
if (_state & 1 > 0) _wasVaild = true;
});
}
@override
Widget build(BuildContext context) => FlutterMap(
options: MapOptions(
center: LatLng(_lat, _long),
zoom: _wasVaild ? 17 : 6,
maxZoom: _wasVaild ? 17 : 6,
minZoom: _wasVaild ? 17 : 6,
interactiveFlags: InteractiveFlag.none,
onTap: (TapPosition pos, LatLng ll) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SerialDetailPage()),
);
}),
mapController: _mapController,
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png',
userAgentPackageName: 'de.jusax.ju_rc_app',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(_lat, _long),
width: 100,
height: 100,
builder: (context) => Icon(
switch (_state) {
3 => Icons.location_on,
2 => Icons.location_off,
1 => Icons.location_on_outlined,
int() => Icons.location_off_outlined,
},
color: Theme.of(context).colorScheme.primary,
),
),
],
),
],
);
}
class SerialDetailPage extends StatefulWidget {
const SerialDetailPage({super.key});
@override
State<StatefulWidget> createState() => _SerialDetailPageState();
}
class _SerialDetailPageState extends State<SerialDetailPage>
with TickerProviderStateMixin {
USerial serial = getSerial();
late Timer timer;
final _mapController = MapController();
double _lat = 51.1667, _long = 10.4500;
int _state = 0;
double _mps = -1, _course = -1, _alt = -1, _hdop = -1;
bool _mpsState = false,
_courseState = false,
_altState = false,
_satNState = false,
_hdopState = false;
int _satN = -1;
bool _follow = true;
@override
void initState() {
super.initState();
timer = Timer.periodic(const Duration(seconds: 2), (_) async {
serial.sprintln("<rfSystemSensor>2303", system: true);
sleep(const Duration(milliseconds: 200));
serial.sprintln("<rfSystemSensor>2304", system: true);
sleep(const Duration(milliseconds: 200));
serial.sprintln("<rfSystemSensor>2305", system: true);
sleep(const Duration(milliseconds: 200));
serial.sprintln("<rfSystemSensor>2306", system: true);
sleep(const Duration(milliseconds: 200));
serial.sprintln("<rfSystemSensor>2307", system: true);
});
SensorReader.listen(serialListen);
}
@override
void dispose() {
SensorReader.removeListen(serialListen);
timer.cancel();
super.dispose();
}
void serialListen(int type, int command, Pointer<ArrayCStruct> ptr) {
if (type != 0x23) return;
switch (command) {
case 0:
{
var loc = ptr as Pointer<GnssLocData>;
setState(() {
_lat = loc.ref.lat;
_long = loc.ref.lng;
_state = loc.ref.isValid;
if (_follow) {
_animatedMapMove(_mapController, this, LatLng(_lat, _long),
_mapController.zoom,
currentZoom: true);
}
});
}
break;
case 3:
{
var loc = ptr as Pointer<SpeedLocData>;
setState(() {
_mps = loc.ref.speed;
_mpsState = loc.ref.isValid == 3;
});
}
break;
case 4:
{
var loc = ptr as Pointer<CourseLocData>;
setState(() {
_course = loc.ref.course;
_courseState = loc.ref.isValid == 3;
});
}
break;
case 5:
{
var loc = ptr as Pointer<AltLocData>;
setState(() {
_alt = loc.ref.alt;
_altState = loc.ref.isValid == 3;
});
}
break;
case 6:
{
var loc = ptr as Pointer<SatLocData>;
setState(() {
_satN = loc.ref.n;
_satNState = loc.ref.isValid == 3;
});
}
break;
case 7:
{
var loc = ptr as Pointer<HdopLocData>;
setState(() {
_hdop = loc.ref.hdop;
_hdopState = loc.ref.isValid == 3;
});
}
break;
case 0xff:
{}
break;
}
}
int shade() {
return MediaQuery.of(context).platformBrightness == Brightness.light
? 100
: 800;
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text(
"Maps",
style: TextStyle(
fontSize: 16,
),
),
toolbarHeight: 40,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (!_follow) _mapController.move(LatLng(_lat, _long), 17);
_follow = !_follow;
});
},
child: Icon(_follow ? Icons.near_me : Icons.near_me_outlined),
),
body: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Speed: ${_mps.toStringAsPrecision(3)}m/s",
style: TextStyle(
backgroundColor:
_mpsState ? Colors.green[shade()] : Colors.red[shade()]),
),
const VerticalDivider(width: 5),
Text("Course: ${_course.toStringAsFixed(0)}°",
style: TextStyle(
backgroundColor: _courseState
? Colors.green[shade()]
: Colors.red[shade()])),
const VerticalDivider(width: 5),
Text("Alt: ${_alt.toStringAsPrecision(4)}m",
style: TextStyle(
backgroundColor: _altState
? Colors.green[shade()]
: Colors.red[shade()])),
const VerticalDivider(width: 5),
Text("Sats: $_satN",
style: TextStyle(
backgroundColor: _satNState
? Colors.green[shade()]
: Colors.red[shade()])),
const VerticalDivider(width: 5),
Text("Hdop: ${_hdop.toStringAsPrecision(2)}",
style: TextStyle(
backgroundColor: _hdopState
? Colors.green[shade()]
: Colors.red[shade()])),
],
),
Expanded(
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
center: const LatLng(51.1667, 10.4500),
zoom: 6,
maxZoom: 18,
interactiveFlags: InteractiveFlag.drag |
InteractiveFlag.flingAnimation |
InteractiveFlag.pinchMove |
InteractiveFlag.pinchZoom |
InteractiveFlag.doubleTapZoom,
onPositionChanged: (MapPosition pos, bool user) {
if (user) {
setState(() {
_follow = false;
});
}
}),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png',
//urlTemplate: 'https://sgx.geodatenzentrum.de/wmts_basemapde/tile/1.0.0/de_basemapde_web_raster_farbe/default/GLOBAL_WEBMERCATOR/{z}/{y}/{x}.png',
userAgentPackageName: 'de.jusax.ju_rc_app',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(_lat, _long),
width: 100,
height: 100,
builder: (context) => Transform.rotate(
angle: _course *
(pi / 180), // Convert degrees to radians
child: Icon(
switch (_state) {
3 => Icons.navigation,
2 => Icons.location_off,
1 => Icons.navigation_outlined,
int() => Icons.location_off_outlined,
},
color: Theme.of(context).colorScheme.primary,
),
)),
],
),
],
))
]));
}
// animated Map from Map lib provider
void _animatedMapMove(MapController mapController, TickerProvider ticker,
LatLng destLocation, double destZoom,
{bool currentZoom = false}) {
const startedId = 'AnimatedMapController#MoveStarted';
const inProgressId = 'AnimatedMapController#MoveInProgress';
const finishedId = 'AnimatedMapController#MoveFinished';
// Create some tweens. These serve to split up the transition from one location to another.
// In our case, we want to split the transition be<tween> our current map center and the destination.
final camera = mapController;
final latTween =
Tween<double>(begin: camera.center.latitude, end: destLocation.latitude);
final lngTween = Tween<double>(
begin: camera.center.longitude, end: destLocation.longitude);
final zoomTween = Tween<double>(begin: camera.zoom, end: destZoom);
// Create a animation controller that has a duration and a TickerProvider.
final controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: ticker);
// The animation determines what path the animation will take. You can try different Curves values, although I found
// fastOutSlowIn to be my favorite.
final Animation<double> animation =
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn);
// Note this method of encoding the target destination is a workaround.
// When proper animated movement is supported (see #1263) we should be able
// to detect an appropriate animated movement event which contains the
// target zoom/center.
final startIdWithTarget =
'$startedId#${destLocation.latitude},${destLocation.longitude},$destZoom';
bool hasTriggeredMove = false;
controller.addListener(() {
final String id;
if (animation.value == 1.0) {
id = finishedId;
} else if (!hasTriggeredMove) {
id = startIdWithTarget;
} else {
id = inProgressId;
}
hasTriggeredMove |= mapController.move(
LatLng(latTween.evaluate(animation), lngTween.evaluate(animation)),
currentZoom ? mapController.zoom : zoomTween.evaluate(animation),
id: id,
);
});
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.dispose();
} else if (status == AnimationStatus.dismissed) {
controller.dispose();
}
});
controller.forward();
}