better seismic drawings, and adsb enrichment
This commit is contained in:
@@ -156,7 +156,6 @@ function onSvgClick(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||||
margin-bottom: 16px;
|
|
||||||
button {
|
button {
|
||||||
font-size: 11px; padding: 4px 12px; border-radius: 6px;
|
font-size: 11px; padding: 4px 12px; border-radius: 6px;
|
||||||
border: 1px solid var(--border); background: none; color: var(--text-muted); cursor: pointer;
|
border: 1px solid var(--border); background: none; color: var(--text-muted); cursor: pointer;
|
||||||
@@ -172,9 +171,8 @@ function onSvgClick(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.current-val {
|
.current-val {
|
||||||
font-size: 13px; color: var(--text-muted); margin-bottom: 12px;
|
font-size: 13px; color: var(--text-muted);
|
||||||
span { color: var(--accent); font-weight: 700; }
|
span { color: var(--accent); font-weight: 700; }
|
||||||
.val-time { font-size: 11px; margin-left: 6px; opacity: 0.6; }
|
|
||||||
}
|
}
|
||||||
.chart-wrap { position: relative; }
|
.chart-wrap { position: relative; }
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
@@ -202,7 +200,7 @@ svg { width: 100%; overflow: visible; cursor: crosshair; }
|
|||||||
<button class="close-btn" @click="emit('close')">✕</button>
|
<button class="close-btn" @click="emit('close')">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls gap-0">
|
<div class="controls gap-0 mb-2">
|
||||||
<button :class="{active: mode === 'hourly'}" style="border-radius: 6px 0 0 6px" @click="mode = 'hourly'">Hourly</button>
|
<button :class="{active: mode === 'hourly'}" style="border-radius: 6px 0 0 6px" @click="mode = 'hourly'">Hourly</button>
|
||||||
<button :class="{active: mode === 'daily'}" style="border-radius: 0 6px 6px 0" @click="mode = 'daily'">Daily</button>
|
<button :class="{active: mode === 'daily'}" style="border-radius: 0 6px 6px 0" @click="mode = 'daily'">Daily</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,10 +209,9 @@ svg { width: 100%; overflow: visible; cursor: crosshair; }
|
|||||||
<span style="font-size:11px;color:var(--text-muted)">to</span>
|
<span style="font-size:11px;color:var(--text-muted)">to</span>
|
||||||
<input :type="mode === 'hourly' ? 'datetime-local' : 'date'" v-model="customEnd"/>
|
<input :type="mode === 'hourly' ? 'datetime-local' : 'date'" v-model="customEnd"/>
|
||||||
<button class="apply-btn" @click="fetchData">Apply</button>
|
<button class="apply-btn" @click="fetchData">Apply</button>
|
||||||
</div>
|
<div v-if="chart.dot" class="flex-fill flex-r justify-end align-items-center current-val">
|
||||||
|
<span>{{ (chart.dot.v as number).toFixed(2) }}</span>
|
||||||
<div v-if="chart.dot" class="current-val">
|
</div>
|
||||||
<span>{{ (chart.dot.v as number).toFixed(2) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-wrap">
|
<div class="chart-wrap">
|
||||||
|
|||||||
@@ -1,52 +1,140 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Loading from '@/components/Loading.vue';
|
import Loading from '@/components/Loading.vue';
|
||||||
import {ref, onMounted, onUnmounted} from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import {api} from '../services/api';
|
import { api } from '../services/api';
|
||||||
import MetricRow from './MetricRow.vue';
|
import MetricRow from './MetricRow.vue';
|
||||||
|
|
||||||
const d = ref<any>(null);
|
const d = ref<any>(null);
|
||||||
const history = ref<number[]>([]);
|
const history = ref<number[]>([]);
|
||||||
const W = 320, H = 60, MAX_PTS = 120;
|
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const W = 640, H = 120, MAX_PTS = 120;
|
||||||
|
const SEG_W = W / (MAX_PTS - 1);
|
||||||
|
const SUB = 4;
|
||||||
|
const PAD_SEGS = 3; // segments of breathing room at the right edge
|
||||||
|
|
||||||
let interval: ReturnType<typeof setInterval>;
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
let rafId: number;
|
||||||
|
let drawProgress = 0;
|
||||||
|
let lastTimestamp = 0;
|
||||||
|
const ANIM_DURATION = 950;
|
||||||
|
|
||||||
function toPath(pts: number[]) {
|
let pointBuffer: { x: number; y: number }[] = [];
|
||||||
if (!pts.length) return '';
|
let prevMax = 1.0;
|
||||||
const mid = H / 2, amp = H / 2 - 4;
|
|
||||||
const max = Math.max(Math.max(...pts), 1.0); // 1.0 = min scale floor
|
|
||||||
|
|
||||||
// Fixed px per point — path grows left→right, never stretches
|
function buildBuffer(pts: number[]) {
|
||||||
const segW = W / (MAX_PTS - 1);
|
if (pts.length < 2) return;
|
||||||
|
const mid = H / 2, amp = H / 2 - 6;
|
||||||
const subPts: { x: number; y: number }[] = [];
|
const newMax = Math.max(...pts, 1.0);
|
||||||
|
prevMax = prevMax + (newMax - prevMax) * 0.15;
|
||||||
|
const max = prevMax;
|
||||||
|
const buf: { x: number; y: number }[] = [];
|
||||||
|
|
||||||
pts.forEach((v, i) => {
|
pts.forEach((v, i) => {
|
||||||
|
const prev = pts[i - 1] ?? v;
|
||||||
|
const prevNorm = (prev / max) * amp;
|
||||||
const norm = (v / max) * amp;
|
const norm = (v / max) * amp;
|
||||||
const baseX = i * segW;
|
const baseX = i * SEG_W;
|
||||||
const s = segW / 4;
|
const s = SEG_W / SUB;
|
||||||
|
|
||||||
subPts.push({ x: baseX, y: mid - norm * 0.5 });
|
const a0 = prevNorm * 0.75 + norm * 0.25;
|
||||||
subPts.push({ x: baseX + s, y: mid - norm });
|
const a1 = prevNorm * 0.25 + norm * 0.75;
|
||||||
subPts.push({ x: baseX + s * 2, y: mid + norm });
|
const a2 = norm;
|
||||||
subPts.push({ x: baseX + s * 3, y: mid });
|
|
||||||
|
buf.push({ x: baseX, y: mid - a0 }); // up
|
||||||
|
buf.push({ x: baseX + s, y: mid - a1 }); // up peak
|
||||||
|
buf.push({ x: baseX + s * 2, y: mid + a2 }); // down
|
||||||
|
buf.push({ x: baseX + s * 3, y: mid }); // back to exact center ✓
|
||||||
});
|
});
|
||||||
|
|
||||||
return subPts.map((p, i) =>
|
pointBuffer = buf;
|
||||||
`${i === 0 ? 'M' : 'L'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`
|
}
|
||||||
).join(' ');
|
|
||||||
|
function draw(timestamp: number) {
|
||||||
|
const ctx = canvas.value?.getContext('2d');
|
||||||
|
if (!ctx) { rafId = requestAnimationFrame(draw); return; }
|
||||||
|
|
||||||
|
const dt = Math.min(timestamp - lastTimestamp, 100);
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
drawProgress = Math.min(1, drawProgress + dt / ANIM_DURATION);
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
|
||||||
|
// Center line — full width
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.moveTo(0, H / 2);
|
||||||
|
ctx.lineTo(W, H / 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
if (pointBuffer.length < 2) { rafId = requestAnimationFrame(draw); return; }
|
||||||
|
|
||||||
|
// Shift the whole path left by PAD_SEGS segments so tip never hits the edge
|
||||||
|
const xOffset = PAD_SEGS * SEG_W;
|
||||||
|
|
||||||
|
const settledCount = pointBuffer.length - SUB;
|
||||||
|
const anchor = pointBuffer[settledCount - 1];
|
||||||
|
const totalSteps = SUB + 1;
|
||||||
|
const animFrac = drawProgress * totalSteps;
|
||||||
|
const animFloor = Math.floor(animFrac);
|
||||||
|
const animRemainder = animFrac - animFloor;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = '#4d9fff';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
|
for (let i = 0; i < settledCount; i++) {
|
||||||
|
const p = pointBuffer[i];
|
||||||
|
const x = p.x - xOffset;
|
||||||
|
i === 0 ? ctx.moveTo(x, p.y) : ctx.lineTo(x, p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
const animPoints = [
|
||||||
|
anchor,
|
||||||
|
...Array.from({ length: SUB }, (_, i) => pointBuffer[settledCount + i]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 1; i <= animFloor && i < animPoints.length; i++) {
|
||||||
|
ctx.lineTo(animPoints[i].x - xOffset, animPoints[i].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animFloor < animPoints.length - 1 && animRemainder > 0) {
|
||||||
|
const curr = animPoints[animFloor];
|
||||||
|
const next = animPoints[animFloor + 1];
|
||||||
|
ctx.lineTo(
|
||||||
|
curr.x + (next.x - curr.x) * animRemainder - xOffset,
|
||||||
|
curr.y + (next.y - curr.y) * animRemainder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
rafId = requestAnimationFrame(draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function poll() {
|
async function poll() {
|
||||||
d.value = await api.current('seismic_magnitude');
|
d.value = await api.current('seismic_magnitude');
|
||||||
history.value.push(d.value.seismic_magnitude ?? 0);
|
history.value.push(d.value.seismic_magnitude ?? 0);
|
||||||
if(history.value.length > MAX_PTS) history.value.shift();
|
if (history.value.length > MAX_PTS) history.value.shift();
|
||||||
|
buildBuffer(history.value);
|
||||||
|
drawProgress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => { await poll(); interval = setInterval(poll, 1000); });
|
onMounted(async () => {
|
||||||
onUnmounted(() => clearInterval(interval));
|
await poll();
|
||||||
|
lastTimestamp = performance.now();
|
||||||
|
rafId = requestAnimationFrame(draw);
|
||||||
|
interval = setInterval(poll, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
svg { width: 100%; height: 60px; overflow: visible; }
|
canvas { width: 100%; height: 60px; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -59,10 +147,7 @@ svg { width: 100%; height: 60px; overflow: visible; }
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<MetricRow label="Magnitude" :value="d.seismic_magnitude?.toFixed(1)" metric-key="seismic_magnitude" :data="d" />
|
<MetricRow label="Magnitude" :value="d.seismic_magnitude?.toFixed(1)" metric-key="seismic_magnitude" :data="d" />
|
||||||
<svg :viewBox="`0 0 ${W} ${H}`" preserveAspectRatio="none">
|
<canvas ref="canvas" :width="W" :height="H" />
|
||||||
<line :x1="0" :y1="H/2" :x2="W" :y2="H/2" stroke="var(--border)" stroke-width="1" />
|
|
||||||
<path :d="toPath(history)" fill="none" stroke="var(--accent)" stroke-width="1.5" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -62,9 +62,13 @@ export class AirTrafficLayer {
|
|||||||
this._draw()
|
this._draw()
|
||||||
this._attachClick()
|
this._attachClick()
|
||||||
this.refreshTimer = adjustedInterval(async () => {
|
this.refreshTimer = adjustedInterval(async () => {
|
||||||
await this._fetch()
|
try {
|
||||||
this._draw()
|
await this._fetch()
|
||||||
this._refreshPopups()
|
this._draw()
|
||||||
|
this._refreshPopups()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}, 1_000)
|
}, 1_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,13 @@ export class AISLayer {
|
|||||||
this._draw()
|
this._draw()
|
||||||
this._attachClick()
|
this._attachClick()
|
||||||
this.refreshTimer = adjustedInterval(async () => {
|
this.refreshTimer = adjustedInterval(async () => {
|
||||||
await this._fetch()
|
try {
|
||||||
this._draw()
|
await this._fetch()
|
||||||
this._refreshPopups()
|
this._draw()
|
||||||
|
this._refreshPopups()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}, 5_000)
|
}, 5_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ export class RangeLayer {
|
|||||||
private map: Map
|
private map: Map
|
||||||
private layer!: VectorLayer<VectorSource>
|
private layer!: VectorLayer<VectorSource>
|
||||||
visible = false
|
visible = false
|
||||||
|
interval: any;
|
||||||
|
|
||||||
constructor(map: Map) { this.map = map }
|
constructor(map: Map) { this.map = map }
|
||||||
|
|
||||||
async show() {
|
async show() {
|
||||||
if (this.visible) return
|
if(this.visible) this.hide(false);
|
||||||
this.visible = true
|
this.visible = true
|
||||||
|
|
||||||
const points: [number, number][] = await fetch(`${BASE}/api/range`).then(r => r.json())
|
const points: [number, number][] = await fetch(`${BASE}/api/range`).then(r => r.json())
|
||||||
@@ -30,11 +31,16 @@ export class RangeLayer {
|
|||||||
const source = new VectorSource({ features: [feature] })
|
const source = new VectorSource({ features: [feature] })
|
||||||
this.layer = new VectorLayer({ source, zIndex: 50 })
|
this.layer = new VectorLayer({ source, zIndex: 50 })
|
||||||
this.map.addLayer(this.layer)
|
this.map.addLayer(this.layer)
|
||||||
|
if(!this.interval) this.interval = setInterval(() => this.show, 60_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide(stop = true) {
|
||||||
if (!this.visible) return
|
if (!this.visible) return
|
||||||
this.visible = false
|
this.visible = false
|
||||||
this.map.removeLayer(this.layer)
|
this.map.removeLayer(this.layer)
|
||||||
|
if(stop && this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.interval = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,31 +170,145 @@ export function classifyAircraft(row) {
|
|||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function scrapeHexDatabase(icao) {
|
||||||
|
const url = `https://hexdatabase.com/h/${icao}`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const html = await resp.text();
|
||||||
|
const rowMatch = html.match(new RegExp(`<tr[^>]*>.*?<span[^>]*>.*?${icao}.*?<\/span>.*?<\/tr>`, 's'));
|
||||||
|
if (!rowMatch) return null;
|
||||||
|
// 0: Hex (ICAO), 1: Co (Country), 2: Reg (Registration), 3: Atype (Aircraft Type), 4: Operator, 5: Source, 6: MSN (Serial number), 7: Remarks (Description)
|
||||||
|
const cells = rowMatch.match(/<td[^>]*>.*?<\/td>/g) || [] || null;
|
||||||
|
const country = cells[1]?.replace(/<[^>]*>/g, '').trim() || null;
|
||||||
|
const registration = cells[2]?.replace(/<[^>]*>/g, '').trim() || null;
|
||||||
|
const aircraft = cells[3]?.replace(/<[^>]*>/g, '').trim() || null;
|
||||||
|
const operator = cells[4]?.replace(/<[^>]*>/g, '').trim() || null;
|
||||||
|
const serialNumber = cells[6]?.replace(/<[^>]*>/g, '').trim() || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
country,
|
||||||
|
registration,
|
||||||
|
aircraft,
|
||||||
|
operator,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeHexDatabase(icao) {
|
||||||
|
const url = `https://hexdatabase.com/h/${icao}`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const html = await resp.text();
|
||||||
|
const rowMatch = html.match(new RegExp(`<tr[^>]*>.*?<span[^>]*>.*?${icao}.*?<\/span>.*?<\/tr>`, 's'));
|
||||||
|
if (!rowMatch) return null;
|
||||||
|
const cells = rowMatch.match(/<td[^>]*>.*?<\/td>/g) || [];
|
||||||
|
return {
|
||||||
|
country: cells[1]?.replace(/<[^>]*>/g, '').trim() || null,
|
||||||
|
registration: cells[2]?.replace(/<[^>]*>/g, '').trim() || null,
|
||||||
|
aircraft: cells[3]?.replace(/<[^>]*>/g, '').trim() || null,
|
||||||
|
operator: cells[4]?.replace(/<[^>]*>/g, '').trim() || null,
|
||||||
|
serialNumber: cells[6]?.replace(/<[^>]*>/g, '').trim() || null,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchHexDb(icao) {
|
||||||
|
const resp = await fetchWithTimeout(`https://hexdb.io/api/v1/aircraft/${icao}`);
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const found = await resp.json();
|
||||||
|
return {
|
||||||
|
registration: found.Registration || null,
|
||||||
|
manufacturer: found.Manufacturer || null,
|
||||||
|
aircraft: found.ICAOTypeCode || null,
|
||||||
|
model: found.Type || null,
|
||||||
|
operator: found.RegisteredOwners || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function backfillFromSimilar(aircraft) {
|
||||||
|
if (!aircraft) return {};
|
||||||
|
const similar = db.prepare(`
|
||||||
|
SELECT manufacturer, model, engines, categoryDescription, class
|
||||||
|
FROM aircraft
|
||||||
|
WHERE aircraft = ?
|
||||||
|
AND (manufacturer IS NOT NULL OR model IS NOT NULL OR engines IS NOT NULL)
|
||||||
|
LIMIT 1
|
||||||
|
`).get(aircraft);
|
||||||
|
if (!similar) return {};
|
||||||
|
return {
|
||||||
|
manufacturer: similar.manufacturer || null,
|
||||||
|
model: similar.model || null,
|
||||||
|
engines: similar.engines || null,
|
||||||
|
categoryDescription: similar.categoryDescription || null,
|
||||||
|
class: similar.class || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function enrichAircraft(a) {
|
export async function enrichAircraft(a) {
|
||||||
if (!a.hex) return a;
|
if (!a.hex) return a;
|
||||||
const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(a.hex.toUpperCase());
|
const icao = a.hex.toUpperCase();
|
||||||
if (!row) {
|
|
||||||
const resp = await fetch(`https://hexdb.io/api/v1/aircraft/${a.hex.toUpperCase()}`);
|
// 1. Check own DB first
|
||||||
if (resp.ok) {
|
const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(icao);
|
||||||
const found = await resp.json();
|
if (row?.manufacturer && row?.model) {
|
||||||
const insert = db.prepare(`
|
return { ...a, ...row, type: classifyAircraft(row) };
|
||||||
INSERT OR IGNORE INTO aircraft (icao24, registration, manufacturer, aircraft, model, operator)
|
|
||||||
VALUES (@icao24, @registration, @manufacturer, @aircraft, @model, @operator)
|
|
||||||
`);
|
|
||||||
const mapped = {
|
|
||||||
icao24: a.hex.toUpperCase(),
|
|
||||||
registration: found.Registration || null,
|
|
||||||
manufacturer: found.Manufacturer || null,
|
|
||||||
aircraft: found.ICAOTypeCode || null,
|
|
||||||
model: found.Type || null,
|
|
||||||
operator: found.RegisteredOwners || null,
|
|
||||||
};
|
|
||||||
insert.run(mapped);
|
|
||||||
return { ...a, ...mapped, type: classifyAircraft(mapped) };
|
|
||||||
}
|
|
||||||
return { ...a, type: isIcaoInMilitaryRange(a.hex) ? 'military' : 'unknown' };
|
|
||||||
}
|
}
|
||||||
return { ...a, ...row, type: classifyAircraft(row) };
|
|
||||||
|
// 2. Race the two external sources
|
||||||
|
let found = await Promise.any([
|
||||||
|
fetchHexDb(icao),
|
||||||
|
scrapeHexDatabase(icao),
|
||||||
|
]).catch(() => null);
|
||||||
|
|
||||||
|
// Merge with any partial DB row we already have
|
||||||
|
const merged = { ...row, ...found };
|
||||||
|
|
||||||
|
// 3. Backfill manufacturer/model/engines from similar aircraft type in DB
|
||||||
|
if (merged.aircraft && (!merged.manufacturer || !merged.model)) {
|
||||||
|
const similar = backfillFromSimilar(merged.aircraft);
|
||||||
|
Object.assign(merged, { ...similar, ...merged }); // don't overwrite fetched data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save back to DB
|
||||||
|
if (found) {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO aircraft (icao24, registration, manufacturer, aircraft, model, operator, country, serialNumber, engines, categoryDescription, class)
|
||||||
|
VALUES (@icao24, @registration, @manufacturer, @aircraft, @model, @operator, @country, @serialNumber, @engines, @categoryDescription, @class)
|
||||||
|
ON CONFLICT(icao24) DO UPDATE SET
|
||||||
|
registration = COALESCE(excluded.registration, registration),
|
||||||
|
manufacturer = COALESCE(excluded.manufacturer, manufacturer),
|
||||||
|
aircraft = COALESCE(excluded.aircraft, aircraft),
|
||||||
|
model = COALESCE(excluded.model, model),
|
||||||
|
operator = COALESCE(excluded.operator, operator),
|
||||||
|
country = COALESCE(excluded.country, country),
|
||||||
|
serialNumber = COALESCE(excluded.serialNumber, serialNumber),
|
||||||
|
engines = COALESCE(excluded.engines, engines),
|
||||||
|
categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription),
|
||||||
|
class = COALESCE(excluded.class, class)
|
||||||
|
`).run({
|
||||||
|
icao24: icao,
|
||||||
|
registration: merged.registration || null,
|
||||||
|
manufacturer: merged.manufacturer || null,
|
||||||
|
aircraft: merged.aircraft || null,
|
||||||
|
model: merged.model || null,
|
||||||
|
operator: merged.operator || null,
|
||||||
|
country: merged.country || null,
|
||||||
|
serialNumber: merged.serialNumber || null,
|
||||||
|
engines: merged.engines || null,
|
||||||
|
categoryDescription: merged.categoryDescription || null,
|
||||||
|
class: merged.class || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { icao24: icao, ...merged };
|
||||||
|
return { ...a, ...result, type: classifyAircraft(result) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getADSB() {
|
export async function getADSB() {
|
||||||
|
|||||||
Reference in New Issue
Block a user