A Flutter app that runs next to DJI Fly without the DJI SDK. It receives the aircraft's Remote ID (Bluetooth, BT5, Wi-Fi NaN and Beacon) in a foreground service, streams the paired aircraft to the hub over /ws/device, and turns take-off to landing into a logbook entry filed through /api/flights, queued when offline. Covers phases A0-A3 of the Fly Agent plan; needs no server change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.9 KiB
Dart
82 lines
2.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
import '../session/flight_session.dart';
|
|
import '../theme.dart';
|
|
|
|
/// OpenStreetMap with the session track, the aircraft and the pilot.
|
|
class TrackMap extends StatelessWidget {
|
|
const TrackMap({super.key, required this.track, this.aircraft, this.pilot, this.height = 220});
|
|
|
|
final List<TrackPoint> track;
|
|
final LatLng? aircraft;
|
|
final LatLng? pilot;
|
|
final double height;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final List<LatLng> line = track.map((TrackPoint p) => LatLng(p.lat, p.lon)).toList();
|
|
final List<LatLng> all = <LatLng>[...line, if (aircraft != null) aircraft!, if (pilot != null) pilot!];
|
|
if (all.isEmpty) {
|
|
return Container(
|
|
height: height,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(color: PV.surface2, borderRadius: BorderRadius.circular(PV.radius)),
|
|
child: const Text('No position yet', style: PV.caption),
|
|
);
|
|
}
|
|
final MapOptions options = all.length == 1
|
|
? MapOptions(initialCenter: all.first, initialZoom: 16)
|
|
: MapOptions(
|
|
initialCameraFit: CameraFit.coordinates(
|
|
coordinates: all,
|
|
padding: const EdgeInsets.all(32),
|
|
maxZoom: 18,
|
|
),
|
|
);
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(PV.radius),
|
|
child: SizedBox(
|
|
height: height,
|
|
child: FlutterMap(
|
|
// The camera only fits on creation; a new key refits as the track grows.
|
|
key: ValueKey<int>(all.length ~/ 10),
|
|
options: options,
|
|
children: <Widget>[
|
|
TileLayer(
|
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
userAgentPackageName: 'com.pilotvault.flyagent',
|
|
),
|
|
if (line.length >= 2)
|
|
PolylineLayer(polylines: <Polyline>[Polyline(points: line, strokeWidth: 3, color: PV.accent)]),
|
|
MarkerLayer(markers: <Marker>[
|
|
if (pilot != null)
|
|
Marker(
|
|
point: pilot!,
|
|
width: 22,
|
|
height: 22,
|
|
child: const Icon(Icons.person_pin_circle, color: PV.navy, size: 22),
|
|
),
|
|
if (aircraft != null)
|
|
Marker(
|
|
point: aircraft!,
|
|
width: 26,
|
|
height: 26,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: PV.accent,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.white, width: 2),
|
|
),
|
|
child: const Icon(Icons.flight, size: 14, color: Colors.white),
|
|
),
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|