Add region picker for OpenSky default bounding box

Replace the free-text bounding-box field in the Web App OpenSky
settings with a grouped preset picker (World / Continents / Countries)
plus a Custom option that keeps the manual lamin,lomin,lamax,lomax
entry. Saved values that match a preset render as the friendly region
name, including in the locked/inherited display.

Change the API Server default bounding box to Europe to match the new
"Europe" preset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-13 23:20:08 +02:00
co-authored by Claude Opus 4.8
parent 6a007fd25e
commit c5e34e32be
6 changed files with 102 additions and 28 deletions
@@ -25,8 +25,10 @@ import (
const (
apiBase = "https://opensky-network.org/api"
tokenURL = "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token"
// Small default bounding box (Netherlands) keeps the health probe cheap.
defaultBBox = "50.5,3.2,53.7,7.3" // lamin,lomin,lamax,lomax
// Default bounding box covers Europe; matches the "Europe" preset offered in
// the Web App bounding-box picker. Users can narrow it (e.g. a single country)
// to reduce per-query credit cost.
defaultBBox = "34,-25,72,45" // lamin,lomin,lamax,lomax (Europe)
)
func init() {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -35,8 +35,8 @@
})()
</script>
<title>PilotVault — Control Panel</title>
<script type="module" crossorigin src="./assets/index-BdIdzNiz.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CYeDXsw9.css">
<script type="module" crossorigin src="./assets/index-CznFJTz4.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-D0ghlckk.css">
</head>
<body>
<div id="app"></div>
+75 -3
View File
@@ -206,6 +206,68 @@ const OS_SCOPE_OPTS = [
{ value: 'org', label: 'Organization', icon: 'users' },
]
// Predefined bounding boxes (lamin,lomin,lamax,lomax). The picker offers these
// plus a "Custom…" option that reveals the free-text field for manual entry.
// Europe is the default (kept in sync with the API Server's defaultBBox).
const OS_BBOX_GROUPS = [
{ label: 'World', options: [
{ value: '-90,-180,90,180', label: 'World' },
] },
{ label: 'Continents', options: [
{ value: '34,-25,72,45', label: 'Europe' },
{ value: '-35,-18,38,52', label: 'Africa' },
{ value: '5,25,82,180', label: 'Asia' },
{ value: '7,-168,72,-52', label: 'North America' },
{ 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' },
{ 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: '36.6,6.6,47.1,18.6', label: 'Italy' },
{ value: '24,-125,49.5,-66.5', label: 'United States' },
] },
]
const OS_BBOX_FLAT = OS_BBOX_GROUPS.flatMap((g) => g.options)
// Normalise a bbox string to "n1,n2,n3,n4" so preset matching ignores spacing
// and trailing-zero differences; returns '' when it isn't four valid numbers.
function osNormBbox(s) {
const parts = String(s || '').split(',').map((x) => x.trim())
if (parts.length !== 4) return ''
const nums = parts.map(Number)
if (nums.some((n) => Number.isNaN(n))) return ''
return nums.join(',')
}
// Friendly label for a bbox value when it matches a preset (else '').
function osBboxLabel(v) {
const norm = osNormBbox(v)
const m = norm && OS_BBOX_FLAT.find((o) => osNormBbox(o.value) === norm)
return m ? m.label : ''
}
// Manual-entry toggle: sticky once the user picks "Custom…", and implied when
// the current value doesn't match any preset.
const osBboxCustom = ref(false)
const osBboxPreset = computed({
get() {
if (osBboxCustom.value) return '__custom__'
const norm = osNormBbox(osForm.bbox)
const match = norm && OS_BBOX_FLAT.find((o) => osNormBbox(o.value) === norm)
return match ? match.value : '__custom__'
},
set(v) {
if (v === '__custom__') { osBboxCustom.value = true; return }
osBboxCustom.value = false
osForm.bbox = v
},
})
const osBboxManual = computed(() => osBboxPreset.value === '__custom__')
// Superadmin edits the global layer in the API panel — read-only in the Web App.
const osReadOnly = computed(() => os.isSuperadmin)
// The scope key currently in view (superadmin always sees the single read-only view).
@@ -233,6 +295,8 @@ function fillOsForm() {
osForm.clientSecret = osField('clientSecret').own || ''
osForm.plan = osField('plan').own || ''
osForm.bbox = osField('bbox').own || ''
// Show the preset dropdown; the getter falls back to Custom for non-preset values.
osBboxCustom.value = false
}
function applyOpenSkyView(body) {
@@ -1433,14 +1497,22 @@ onBeforeUnmount(() => {
</Row>
<!-- bounding box -->
<Row title="Default bounding box" desc="lamin,lomin,lamax,lomax — used for live queries and the health probe." keywords="bounding box bbox area">
<Row title="Default bounding box" desc="Pick a region, or choose Custom to enter lamin,lomin,lamax,lomax by hand — used for live queries and the health probe." keywords="bounding box bbox area region country continent world europe custom coordinates">
<template v-if="osLocked('bbox')">
<span class="inline-flex items-center gap-2 font-mono text-sm text-ink">
{{ osField('bbox').effective || '' }}
{{ osBboxLabel(osField('bbox').effective) || osField('bbox').effective || '' }}
<span v-if="osSourceLabel('bbox')" class="inline-flex items-center gap-1 rounded-full bg-surface-2 px-2 py-0.5 text-[10px] font-semibold text-ink-secondary"><Icon name="lock" :size="10" />{{ osSourceLabel('bbox') }}</span>
</span>
</template>
<input v-else v-model="osForm.bbox" class="field w-64 font-mono" placeholder="50.5,3.2,53.7,7.3" />
<div v-else class="flex flex-col items-end gap-2">
<select v-model="osBboxPreset" class="field w-64">
<optgroup v-for="g in OS_BBOX_GROUPS" :key="g.label" :label="g.label">
<option v-for="o in g.options" :key="o.value" :value="o.value">{{ o.label }}</option>
</optgroup>
<option value="__custom__">Custom…</option>
</select>
<input v-if="osBboxManual" v-model="osForm.bbox" class="field w-64 font-mono" placeholder="50.5,3.2,53.7,7.3" />
</div>
</Row>
<!-- client id -->