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:
tajniak81
2026-07-14 00:07:12 +02:00
co-authored by Claude Opus 4.8
parent 150758b0bf
commit 94f6876024
20 changed files with 637 additions and 71 deletions
+4
View File
@@ -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;
+42
View File
@@ -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();