Better AutoMode

This commit is contained in:
2026-09-15 11:47:29 -04:00
parent 28478251cd
commit 4c7a7fd1f5
8 changed files with 530 additions and 933 deletions
+103 -149
View File
@@ -1,4 +1,5 @@
import {BASE} from '@/services/api.ts';
import {greatCircleDist} from '@/services/units.ts';
import {h, render as vueRender} from 'vue';
import Map from 'ol/Map';
import {Feature} from 'ol';
@@ -8,21 +9,13 @@ import {fromLonLat} from 'ol/proj';
import {Style, Icon, Stroke} from 'ol/style';
import {Vector as VectorLayer} from 'ol/layer';
import {Vector as VectorSource} from 'ol/source';
import {adjustedInterval} from '@ztimson/utils';
import {adjustedInterval, sortByProp} from '@ztimson/utils';
import AircraftPopup from '@/components/Aircraft.vue';
import {bringToFront} from './zindex';
const API = BASE + '/api';
const EARTH_R = 6371;
const AUTO_RANGE = 92.6;
function greatCircleDist(lat1: number, lon1: number, lat2: number, lon2: number): number {
const toRad = (d: number) => d * Math.PI / 180;
const dLat = toRad(lat2 - lat1), dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return EARTH_R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function isPriorityAircraft(plane: any): boolean {
if (plane.military === true || plane.is_military === true || plane.sar === true || plane.is_sar === true) return true;
@@ -75,112 +68,22 @@ export class AirTrafficLayer {
this.map = map;
}
async show() {
if (this.visible) return;
private _attachClick() {
this.clickHandler = evt => {
const f = this.map.forEachFeatureAtPixel(evt.pixel, f => f.get('planeData') ? f : null);
if (!f) return;
this.visible = true;
this.layer = new VectorLayer({source: new VectorSource(), zIndex: 100});
this.map.addLayer(this.layer);
const plane = f.get('planeData');
await this._fetch();
this._draw();
this._attachClick();
this.refreshTimer = adjustedInterval(async () => {
try {
await this._fetch();
this._draw();
this._refreshPopups();
} catch (err) {
console.error(err);
if (this.popups[plane.icao]) {
this.closePopup(plane.icao);
return;
}
}, 1_000);
}
hide() {
if (!this.visible) return;
this.openPopup(plane);
};
this.visible = false;
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
if (this.clickHandler) {
this.map.un('singleclick', this.clickHandler);
this.clickHandler = null;
}
for (const icao of Object.keys(this.popups)) this._closePopup(icao);
if (this.layer) this.map.removeLayer(this.layer);
Object.values(this.traceLayers).forEach(l => this.map.removeLayer(l));
this.traceLayers = {};
this.traceSegs = {};
}
getClosest(latitude: number, longitude: number): any | null {
let closest: any = null;
let closestDistance = Infinity;
for (const plane of this.data) {
if (plane.latitude == null || plane.longitude == null) continue;
const distance = greatCircleDist(latitude, longitude, plane.latitude, plane.longitude);
if (distance < closestDistance) {
closestDistance = distance;
closest = plane;
}
}
return closest;
}
getAutoCandidates(latitude: number, longitude: number, limit = Infinity): any[] {
return this.data.filter(plane => {
if (plane.latitude == null || plane.longitude == null) return false;
return greatCircleDist(latitude, longitude, plane.latitude, plane.longitude) <= AUTO_RANGE;
}).sort((a, b) => {
const priority = Number(isPriorityAircraft(b)) - Number(isPriorityAircraft(a));
if (priority) return priority;
return greatCircleDist(latitude, longitude, a.latitude, a.longitude) - greatCircleDist(latitude, longitude, b.latitude, b.longitude);
}).slice(0, limit);
}
isPriority(plane: any): boolean {
return isPriorityAircraft(plane);
}
getAutoDistance(latitude: number, longitude: number, plane: any): number {
return greatCircleDist(latitude, longitude, plane.latitude, plane.longitude);
}
getById(icao: string): any | null {
return this.data.find(p => String(p.icao) === String(icao)) ?? null;
}
openAutoPopup(plane: any) {
this._openPopup(plane);
}
closeAutoPopup(icao: string) {
this._closePopup(String(icao));
}
private async _fetch() {
const j = await fetch(`${API}/adsb`).then(r => r.ok ? r.json() : []);
this.data = (j || []).map((a: any) => ({
...a,
icao: a.hex,
latitude: a.lat,
longitude: a.lon,
heading: a.track,
speed: a.gs,
climb: a.baro_rate ?? a.geom_rate ?? 0,
name: a.flight?.trim(),
}));
this.map.on('singleclick', this.clickHandler);
}
private _draw() {
@@ -208,6 +111,21 @@ export class AirTrafficLayer {
}
}
private async _fetch() {
const j = await fetch(`${API}/adsb`).then(r => r.ok ? r.json() : []);
this.data = (j || []).map((a: any) => ({
...a,
icao: a.hex,
latitude: a.lat,
longitude: a.lon,
heading: a.track,
speed: a.gs,
climb: a.baro_rate ?? a.geom_rate ?? 0,
name: a.flight?.trim(),
}));
}
private async _fetchTrace(plane: any) {
const icao = plane.icao;
@@ -229,6 +147,7 @@ export class AirTrafficLayer {
if (!last || last.latitude !== cur.latitude || last.longitude !== cur.longitude) history.push(cur);
const traceHistory = history;
if (traceHistory.length < 2) {
const popup = this.popups[icao];
if (popup) popup.update(plane, history);
@@ -275,9 +194,76 @@ export class AirTrafficLayer {
if (popup) popup.update(plane, history);
}
private _openPopup(plane: any) {
private _refreshPopups() {
for (const icao of Object.keys(this.popups)) {
const plane = this.data.find(p => p.icao === icao);
if (!plane) {
this.closePopup(icao);
continue;
}
this._fetchTrace(plane);
}
}
hide() {
if(!this.visible) return;
this.visible = false;
if(this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
if(this.clickHandler) {
this.map.un('singleclick', this.clickHandler);
this.clickHandler = null;
}
for(const icao of Object.keys(this.popups)) this.closePopup(icao);
if(this.layer) this.map.removeLayer(this.layer);
Object.values(this.traceLayers).forEach(l => this.map.removeLayer(l));
this.traceLayers = {};
this.traceSegs = {};
}
async show() {
if(this.visible) return;
this.visible = true;
this.layer = new VectorLayer({source: new VectorSource(), zIndex: 100});
this.map.addLayer(this.layer);
await this._fetch();
this._draw();
this._attachClick();
this.refreshTimer = adjustedInterval(async () => {
try {
await this._fetch();
this._draw();
this._refreshPopups();
} catch (err) {
console.error(err);
}
}, 1_000);
}
getById(icao: string): any | null {
return this.data.find(p => String(p.icao) === String(icao)) ?? null;
}
getCandidates(latitude: number, longitude: number, limit = Infinity): any[] {
function score(plane: any): number {
const ratio = Math.min(greatCircleDist(latitude, longitude, plane.latitude, plane.longitude) / AUTO_RANGE, 1);
return isPriorityAircraft(plane) ? 2 - ratio : 1 - ratio;
}
return this.data.map(plane => ({...plane, score: score(plane)})).toSorted(sortByProp('score', true));
}
openPopup(plane: any) {
const icao = String(plane.icao);
if (this.popups[icao]) return;
if(this.popups[icao]) return;
const container = document.createElement('div');
document.body.appendChild(container);
@@ -289,7 +275,7 @@ export class AirTrafficLayer {
position: mobile
? {x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16}
: {x: 16, y: 16},
onClose: () => this._closePopup(icao),
onClose: () => this.closePopup(icao),
onBringToFront: () => bringToFront(container),
});
@@ -306,52 +292,20 @@ export class AirTrafficLayer {
this._fetchTrace(plane);
}
private _closePopup(icao: string) {
closePopup(icao: string) {
const popup = this.popups[icao];
if (popup) {
if(popup) {
popup.unmount();
delete this.popups[icao];
}
const segs = this.traceSegs[icao], layer = this.traceLayers[icao];
if (segs && layer) segs.forEach(f => layer.getSource()!.removeFeature(f));
if (layer) {
if(segs && layer) segs.forEach(f => layer.getSource()!.removeFeature(f));
if(layer) {
this.map.removeLayer(layer);
delete this.traceLayers[icao];
}
delete this.traceSegs[icao];
}
private _refreshPopups() {
for (const icao of Object.keys(this.popups)) {
const plane = this.data.find(p => p.icao === icao);
if (!plane) {
this._closePopup(icao);
continue;
}
this._fetchTrace(plane);
}
}
private _attachClick() {
this.clickHandler = evt => {
const f = this.map.forEachFeatureAtPixel(evt.pixel, f => f.get('planeData') ? f : null);
if (!f) return;
const plane = f.get('planeData');
if (this.popups[plane.icao]) {
this._closePopup(plane.icao);
return;
}
this._openPopup(plane);
};
this.map.on('singleclick', this.clickHandler);
}
}