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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0F1E3D" />
<title>PilotVault · API Server</title>
<script type="module" crossorigin src="/assets/index--4fMszzi.js"></script>
<script type="module" crossorigin src="/assets/index-CwiTp2Sb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CVh8EDzq.css">
</head>
<body>
+34 -1
View File
@@ -7,9 +7,34 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// validBBox reports whether s is a well-formed "lamin,lomin,lamax,lomax" bounding
// box: four numbers, latitudes in [-90,90], longitudes in [-180,180], and min < max
// on each axis. Used to vet the Live map's client-supplied ?bbox= override before
// it reaches OpenSky.
func validBBox(s string) bool {
parts := strings.Split(s, ",")
if len(parts) != 4 {
return false
}
n := make([]float64, 4)
for i, p := range parts {
v, err := strconv.ParseFloat(strings.TrimSpace(p), 64)
if err != nil {
return false
}
n[i] = v
}
laMin, loMin, laMax, loMax := n[0], n[1], n[2], n[3]
if laMin < -90 || laMax > 90 || loMin < -180 || loMax > 180 {
return false
}
return laMin < laMax && loMin < loMax
}
// Integrations exposes the OpenSky plugin's settings to end users under a
// three-layer cascade (superadmin/global → organization → user). Each of the
// four settings resolves independently, top wins, and a blank field falls
@@ -586,11 +611,19 @@ func (s *Server) handleOpenSkyStates(w http.ResponseWriter, r *http.Request) {
return
}
// The Live map may request a specific area (auto cascade: drone → device →
// region). Honour a well-formed ?bbox= override; otherwise use the resolved
// config bbox. Malformed input is ignored rather than erroring.
bbox := res.eff.Bbox
if q := strings.TrimSpace(r.URL.Query().Get("bbox")); q != "" && validBBox(q) {
bbox = q
}
cfg := map[string]string{
"clientId": res.eff.ClientID,
"clientSecret": res.eff.ClientSecret,
"plan": res.eff.Plan,
"bbox": res.eff.Bbox,
"bbox": bbox,
"allowAnonymous": boolStr(res.allowAnon),
}
raw, err := s.plugins.InvokeWith(r.Context(), openSkyPlugin, cfg, "states.bbox", nil)
+10
View File
@@ -11,6 +11,10 @@ type Telemetry struct {
Altitude *float64 `json:"altitude,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
// Phone's own GPS (reported by the Fly App), distinct from the drone's fix
// above — used as a location fallback for the Web App's auto bounding box.
PhoneLatitude *float64 `json:"phoneLatitude,omitempty"`
PhoneLongitude *float64 `json:"phoneLongitude,omitempty"`
VelocityX *float64 `json:"velocityX,omitempty"`
VelocityY *float64 `json:"velocityY,omitempty"`
VelocityZ *float64 `json:"velocityZ,omitempty"`
@@ -100,6 +104,12 @@ func applyTelemetry(t *Telemetry, raw map[string]any) {
if v, ok := toFloat(raw["longitude"]); ok {
t.Longitude = &v
}
if v, ok := toFloat(raw["phoneLatitude"]); ok {
t.PhoneLatitude = &v
}
if v, ok := toFloat(raw["phoneLongitude"]); ok {
t.PhoneLongitude = &v
}
if v, ok := toFloat(raw["velocityX"]); ok {
t.VelocityX = &v
}
+52 -6
View File
@@ -190,15 +190,61 @@ const BBOX_GROUPS = [
{ value: "-56,-82,13,-34", label: "South America" },
{ value: "-48,110,-10,180", label: "Oceania" },
] },
{ label: "Countries", options: [
{ value: "49,14.1,54.9,24.2", label: "Poland" },
{ value: "50.5,3.2,53.7,7.3", label: "Netherlands" },
{ value: "47.2,5.8,55.1,15.1", label: "Germany" },
// European countries (mirrors Web App/web/src/countries.js — keep in sync).
{ label: "European countries", options: [
{ value: "39.6,19.3,42.7,21.1", label: "Albania" },
{ value: "42.4,1.4,42.7,1.8", label: "Andorra" },
{ value: "46.4,9.5,49.0,17.2", label: "Austria" },
{ value: "51.2,23.2,56.2,32.8", label: "Belarus" },
{ value: "49.5,2.5,51.5,6.4", label: "Belgium" },
{ value: "42.6,15.7,45.3,19.6", label: "Bosnia and Herzegovina" },
{ value: "41.2,22.4,44.2,28.6", label: "Bulgaria" },
{ value: "42.4,13.5,46.6,19.4", label: "Croatia" },
{ value: "34.6,32.3,35.7,34.6", label: "Cyprus" },
{ value: "48.6,12.1,51.1,18.9", label: "Czechia" },
{ value: "54.6,8.1,57.8,12.7", label: "Denmark" },
{ value: "57.5,21.8,59.7,28.2", label: "Estonia" },
{ value: "59.8,20.6,70.1,31.6", label: "Finland" },
{ value: "41.3,-5.2,51.1,9.6", label: "France" },
{ value: "49.9,-8.7,59,1.8", label: "United Kingdom" },
{ value: "35.9,-9.6,43.8,3.4", label: "Spain" },
{ value: "47.2,5.8,55.1,15.1", label: "Germany" },
{ value: "34.8,19.4,41.8,28.3", label: "Greece" },
{ value: "45.7,16.1,48.6,22.9", label: "Hungary" },
{ value: "63.3,-24.6,66.6,-13.5", label: "Iceland" },
{ value: "51.4,-10.6,55.4,-6.0", label: "Ireland" },
{ value: "36.6,6.6,47.1,18.6", label: "Italy" },
{ value: "41.8,20.0,43.3,21.8", label: "Kosovo" },
{ value: "55.7,20.9,58.1,28.2", label: "Latvia" },
{ value: "47.0,9.4,47.3,9.6", label: "Liechtenstein" },
{ value: "53.9,20.9,56.5,26.9", label: "Lithuania" },
{ value: "49.4,5.7,50.2,6.5", label: "Luxembourg" },
{ value: "35.8,14.1,36.1,14.6", label: "Malta" },
{ value: "45.4,26.6,48.5,30.2", label: "Moldova" },
{ value: "43.72,7.40,43.75,7.44", label: "Monaco" },
{ value: "41.8,18.4,43.6,20.4", label: "Montenegro" },
{ value: "50.7,3.3,53.7,7.2", label: "Netherlands" },
{ value: "40.8,20.4,42.4,23.0", label: "North Macedonia" },
{ value: "57.9,4.6,71.2,31.1", label: "Norway" },
{ value: "49.0,14.1,54.9,24.2", label: "Poland" },
{ value: "36.9,-9.5,42.2,-6.2", label: "Portugal" },
{ value: "43.6,20.2,48.3,29.7", label: "Romania" },
{ value: "41.2,19.6,81.9,180", label: "Russia" },
{ value: "43.89,12.40,43.99,12.52", label: "San Marino" },
{ value: "42.2,18.8,46.2,23.0", label: "Serbia" },
{ value: "47.7,16.8,49.6,22.6", label: "Slovakia" },
{ value: "45.4,13.4,46.9,16.6", label: "Slovenia" },
{ value: "35.9,-9.4,43.8,3.4", label: "Spain" },
{ value: "55.3,11.1,69.1,24.2", label: "Sweden" },
{ value: "45.8,5.9,47.8,10.5", label: "Switzerland" },
{ value: "35.8,25.7,42.3,44.8", label: "Turkey" },
{ value: "44.4,22.1,52.4,40.2", label: "Ukraine" },
{ value: "49.9,-8.7,60.9,1.8", label: "United Kingdom" },
{ value: "41.900,12.445,41.908,12.458", label: "Vatican City" },
] },
{ label: "Other countries", options: [
{ value: "24,-125,49.5,-66.5", label: "United States" },
{ value: "41.7,-141,83.1,-52.6", label: "Canada" },
{ value: "-43.6,113.3,-10.7,153.6", label: "Australia" },
{ value: "24,122.9,45.5,145.8", label: "Japan" },
] },
];
const BBOX_FLAT = BBOX_GROUPS.flatMap((g) => g.options);