Add location-aware automatic default bounding box
The OpenSky "Default bounding box" now follows where flying happens. A new "Automatic" picker mode (the default) resolves the live-map area from a location cascade — drone telemetry → phone GPS → browser geolocation → the user's Region country → Europe — instead of a fixed box. Manual presets and Custom coordinates still work. - Web App: new shared countries.js dataset (all countries + bbox, offline point→country); the bbox picker gains all European countries and an Automatic option (client pref prefs.autoBbox); the Region setting expands from 6 locale entries to all countries; the live map resolves the cascade each poll and sends it as ?bbox=. - API Server: the states endpoint accepts and validates a ?bbox= override (validBBox); the Web App BFF forwards the query; the hub relays new phoneLatitude/phoneLongitude telemetry to the Web App. - Fly App: reports the phone's own GPS (geolocator) alongside telemetry, used as the "your location" fallback. - API panel: the OpenSky bbox picker lists all European countries. Builds verified across web, panel, both Go modules and the Fly App APK. Region list, Automatic default and the cascade ?bbox= override verified in the browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
150758b0bf
commit
94f6876024
@@ -21,6 +21,10 @@ class FlightModel extends ChangeNotifier {
|
||||
double? altitude;
|
||||
double? latitude;
|
||||
double? longitude;
|
||||
// Phone's own GPS (independent of the drone's fix above); streamed to the
|
||||
// server as a location fallback for the Web App's automatic bounding box.
|
||||
double? phoneLatitude;
|
||||
double? phoneLongitude;
|
||||
int? batteryPercent;
|
||||
|
||||
UploadStatus upload = UploadStatus.disabled;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import 'dji_service.dart';
|
||||
import 'flight_model.dart';
|
||||
@@ -46,6 +47,7 @@ class _HomePageState extends State<HomePage> {
|
||||
final FlightModel _model = FlightModel();
|
||||
StreamSubscription<Map<String, dynamic>>? _sub;
|
||||
StreamSubscription<AuthStatus>? _authSub;
|
||||
StreamSubscription<Position>? _phoneLocSub;
|
||||
|
||||
// Streams telemetry to the API Server and receives commands back.
|
||||
late final ServerUploader _uploader;
|
||||
@@ -69,6 +71,43 @@ class _HomePageState extends State<HomePage> {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
_init();
|
||||
_startPhoneLocation();
|
||||
}
|
||||
|
||||
/// Streams the phone's own GPS (coarse, low-frequency) and reports it to the
|
||||
/// server as a telemetry field — a location fallback for the Web App's auto
|
||||
/// bounding box when the drone has no fix. Best-effort: silently gives up if
|
||||
/// location services or permission are unavailable.
|
||||
Future<void> _startPhoneLocation() async {
|
||||
try {
|
||||
if (!await Geolocator.isLocationServiceEnabled()) return;
|
||||
LocationPermission perm = await Geolocator.checkPermission();
|
||||
if (perm == LocationPermission.denied) {
|
||||
perm = await Geolocator.requestPermission();
|
||||
}
|
||||
if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) {
|
||||
return;
|
||||
}
|
||||
_phoneLocSub = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.low, // country-level is all the bbox needs
|
||||
distanceFilter: 1000, // metres — infrequent updates
|
||||
),
|
||||
).listen((Position pos) {
|
||||
_model.phoneLatitude = pos.latitude;
|
||||
_model.phoneLongitude = pos.longitude;
|
||||
_model.bump();
|
||||
// Report to the server (rides the existing telemetry channel; only the
|
||||
// phone fields are present, so it never disturbs drone telemetry).
|
||||
_uploader.onEvent(<String, dynamic>{
|
||||
'type': 'telemetry',
|
||||
'phoneLatitude': pos.latitude,
|
||||
'phoneLongitude': pos.longitude,
|
||||
});
|
||||
});
|
||||
} catch (_) {
|
||||
// Location plugin/permission unavailable — non-fatal.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
@@ -195,6 +234,8 @@ class _HomePageState extends State<HomePage> {
|
||||
if (_model.altitude != null) tel['altitude'] = _model.altitude;
|
||||
if (_model.latitude != null) tel['latitude'] = _model.latitude;
|
||||
if (_model.longitude != null) tel['longitude'] = _model.longitude;
|
||||
if (_model.phoneLatitude != null) tel['phoneLatitude'] = _model.phoneLatitude;
|
||||
if (_model.phoneLongitude != null) tel['phoneLongitude'] = _model.phoneLongitude;
|
||||
if (tel.length > 1) events.add(tel);
|
||||
return events;
|
||||
}
|
||||
@@ -235,6 +276,7 @@ class _HomePageState extends State<HomePage> {
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
_authSub?.cancel();
|
||||
_phoneLocSub?.cancel();
|
||||
_uploadSub?.cancel();
|
||||
_uploader.dispose();
|
||||
_serverHost.dispose();
|
||||
|
||||
Reference in New Issue
Block a user