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();
+80
View File
@@ -73,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@@ -105,6 +113,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@@ -144,6 +160,54 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
geolocator:
dependency: "direct main"
description:
name: geolocator
sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2
url: "https://pub.dev"
source: hosted
version: "13.0.4"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
url: "https://pub.dev"
source: hosted
version: "4.6.2"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
url: "https://pub.dev"
source: hosted
version: "2.3.14"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
url: "https://pub.dev"
source: hosted
version: "4.2.8"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
url: "https://pub.dev"
source: hosted
version: "4.1.4"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
image:
dependency: transitive
description:
@@ -437,6 +501,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
+4
View File
@@ -43,6 +43,10 @@ dependencies:
# the platform BiometricPrompt). Requires FlutterFragmentActivity on Android.
local_auth: ^2.3.0
# Phone GPS — reported alongside telemetry as a location fallback for the Web
# App's automatic bounding box when the drone has no fix.
geolocator: ^13.0.1
dev_dependencies:
flutter_test:
sdk: flutter