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 createState() => _MapsState(); } class _MapsState extends State 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("2300", system: true); }); } @override void dispose() { SensorReader.removeListen(serialListen); timer.cancel(); super.dispose(); } void serialListen(int type, int command, Pointer ptr) { if (type != 0x23 || command != 0x00) return; var loc = ptr as Pointer; 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 createState() => _SerialDetailPageState(); } class _SerialDetailPageState extends State 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("2303", system: true); sleep(const Duration(milliseconds: 200)); serial.sprintln("2304", system: true); sleep(const Duration(milliseconds: 200)); serial.sprintln("2305", system: true); sleep(const Duration(milliseconds: 200)); serial.sprintln("2306", system: true); sleep(const Duration(milliseconds: 200)); serial.sprintln("2307", system: true); }); SensorReader.listen(serialListen); } @override void dispose() { SensorReader.removeListen(serialListen); timer.cancel(); super.dispose(); } void serialListen(int type, int command, Pointer ptr) { if (type != 0x23) return; switch (command) { case 0: { var loc = ptr as Pointer; 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; setState(() { _mps = loc.ref.speed; _mpsState = loc.ref.isValid == 3; }); } break; case 4: { var loc = ptr as Pointer; setState(() { _course = loc.ref.course; _courseState = loc.ref.isValid == 3; }); } break; case 5: { var loc = ptr as Pointer; setState(() { _alt = loc.ref.alt; _altState = loc.ref.isValid == 3; }); } break; case 6: { var loc = ptr as Pointer; setState(() { _satN = loc.ref.n; _satNState = loc.ref.isValid == 3; }); } break; case 7: { var loc = ptr as Pointer; 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 our current map center and the destination. final camera = mapController; final latTween = Tween(begin: camera.center.latitude, end: destLocation.latitude); final lngTween = Tween( begin: camera.center.longitude, end: destLocation.longitude); final zoomTween = Tween(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 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(); }