Build aircraft shapes
This commit is contained in:
@@ -22,47 +22,41 @@ const arc = computed(() => {
|
||||
const start = noon - 12 * 3600 * 1000;
|
||||
const end = noon + 12 * 3600 * 1000;
|
||||
|
||||
const pct = Math.max(0, Math.min(1, (now - start) / (end - start)));
|
||||
const riseT = (rise - start) / (end - start);
|
||||
const setT = (set - start) / (end - start);
|
||||
const dayPct = Math.max(0, Math.min(1, (now - rise) / dayLen));
|
||||
|
||||
const x1 = 0, x2 = W;
|
||||
const totalW = x2 - x1;
|
||||
|
||||
// Sine curve: below horizon at edges, peaks above at noon
|
||||
const getPoint = (t: number) => ({
|
||||
x: x1 + totalW * t,
|
||||
y: HORIZON - Math.sin(t * Math.PI * 2 - Math.PI / 2) * (HORIZON) + (HORIZON)
|
||||
// Simpler: just a single-hump arc above horizon + dip below
|
||||
});
|
||||
|
||||
// Generate points along full sine path
|
||||
const pts = Array.from({ length: 201 }, (_, i) => {
|
||||
const t = i / 200;
|
||||
const x = x1 + totalW * t;
|
||||
// Single smooth S: -sin gives us below->above->below
|
||||
const amplitude = (HORIZON);
|
||||
const y = HORIZON - Math.sin(t * Math.PI * 2 - Math.PI / 2) * amplitude;
|
||||
// Remap t so the arc peaks between riseT and setT only
|
||||
const arcT = (t - riseT) / (setT - riseT);
|
||||
const y = arcT >= 0 && arcT <= 1
|
||||
? HORIZON - Math.sin(arcT * Math.PI) * HORIZON
|
||||
: HORIZON + Math.abs(Math.sin(arcT * Math.PI)) * (HORIZON * 0.3); // subtle below-horizon dip
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
const splitIdx = Math.round(pct * 200);
|
||||
const { x: cx, y: cy } = <any>pts[splitIdx];
|
||||
const sunT = riseT + dayPct * (setT - riseT);
|
||||
const splitIdx = Math.round(sunT * 200);
|
||||
const { x: cx, y: cy } = pts[splitIdx];
|
||||
|
||||
// Filled traveled path: close down to horizon baseline for fill
|
||||
const traveledPts = pts.slice(0, splitIdx + 1);
|
||||
const filledPath = traveledPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ')
|
||||
+ ` L${(<any>traveledPts[traveledPts.length - 1]).x.toFixed(1)},${HORIZON}`
|
||||
+ ` L${traveledPts[traveledPts.length - 1].x.toFixed(1)},${HORIZON}`
|
||||
+ ` L${x1},${HORIZON} Z`;
|
||||
|
||||
// Remaining path stroke only
|
||||
const remainPts = pts.slice(splitIdx);
|
||||
const remainPath = remainPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
|
||||
|
||||
// Sunrise / sunset x positions
|
||||
const riseX = x1 + totalW * ((rise - start) / (end - start));
|
||||
const setX = x1 + totalW * ((set - start) / (end - start));
|
||||
const riseX = x1 + totalW * riseT;
|
||||
const setX = x1 + totalW * setT;
|
||||
const noonX = x1 + totalW * 0.5;
|
||||
|
||||
return { filledPath, remainPath, cx, cy, pct, riseX, setX, noonX };
|
||||
return { filledPath, remainPath, cx, cy, pct: dayPct, riseX, setX, noonX };
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,14 +11,6 @@ import {adjustedInterval} from '@ztimson/utils';
|
||||
|
||||
const API = BASE + '/api'
|
||||
|
||||
const COLORS: Record<string, string> = {
|
||||
'cargo': '#00ff00',
|
||||
'military': '#ff0000',
|
||||
'private': '#8450ea',
|
||||
'passenger': '#009dff',
|
||||
'unknown': '#fad106',
|
||||
}
|
||||
|
||||
const SPEED_UNITS = {
|
||||
knots: { label: 'KTS', convert: (v: number) => Math.round(v) },
|
||||
kph: { label: 'KPH', convert: (v: number) => Math.round(v * 1.852) },
|
||||
@@ -58,13 +50,6 @@ function getAltColor(alt: number): [number, number, number] {
|
||||
return [Math.round(128 * r), 0, 139]
|
||||
}
|
||||
|
||||
function buildPlaneIcon(plane: any, shapes: any): string {
|
||||
const color = COLORS[plane.type] || COLORS['unknown']
|
||||
const shape = shapes[plane.aircraft?.toLowerCase()] || shapes['unknown'];
|
||||
const paths = (Array.isArray(shape.path) ? shape.path : [shape.path]).map((p: string) => `<path d="${p}" fill="${color}" stroke="#000" stroke-width="${shape.stroke || 1}"/>`).join('')
|
||||
return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${shape.w * 2}" height="${shape.h * 2}" viewBox="${shape.viewBox}">${paths}</svg>`)}`
|
||||
}
|
||||
|
||||
function buildNavballSvg(data: any): string {
|
||||
const heading = data.heading ?? 0
|
||||
const airspeedKnots = data.speed || 0
|
||||
@@ -268,7 +253,7 @@ export class AirTrafficLayer {
|
||||
f.set('planeData', plane)
|
||||
f.setStyle(new Style({
|
||||
image: new Icon({
|
||||
src: buildPlaneIcon(plane, this.shapes),
|
||||
src: `data:image/svg+xml,${encodeURIComponent(plane.icon)}`,
|
||||
scale: 1,
|
||||
rotation: (plane.heading ?? 0) * (Math.PI / 180),
|
||||
anchor: [0.5, 0.5],
|
||||
|
||||
1299
server/src/adsb-shapes.mjs
Normal file
1299
server/src/adsb-shapes.mjs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
||||
import * as fs from 'node:fs';
|
||||
import Database from 'better-sqlite3';
|
||||
import { fromCsv } from '@ztimson/utils';
|
||||
import {getIcon} from './adsb-shapes.mjs';
|
||||
|
||||
const DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const DATA = resolve(DIR, '../data');
|
||||
@@ -182,10 +183,6 @@ export async function enrichAircraft(a) {
|
||||
return { ...a, ...row, type: classifyAircraft(row) };
|
||||
}
|
||||
|
||||
export async function getShapes() {
|
||||
return JSON.parse(await fs.promises.readFile(SHAPES_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
export async function getADSB() {
|
||||
const { ADSB_URL } = cfg();
|
||||
if (!ADSB_URL) return [];
|
||||
@@ -203,7 +200,11 @@ export async function getADSB() {
|
||||
if (trail.length > MAX_HISTORY) trail.shift();
|
||||
}
|
||||
|
||||
return Promise.all(aircraft.map(enrichAircraft));
|
||||
return Promise.all(aircraft.map(async a => {
|
||||
a = await enrichAircraft(a);
|
||||
a.icon = getIcon(a);
|
||||
return a;
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getADSBHistory(icao) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {apiReference} from '@scalar/express-api-reference';
|
||||
import {spec} from './spec.mjs';
|
||||
import {existsSync} from 'fs';
|
||||
import {getAIS} from './ais.mjs';
|
||||
import {getADSB, getADSBHistory, getADSBRange, getShapes, initAircraftDb} from './adsb.mjs';
|
||||
import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
|
||||
import {getWeatherCondition} from './openweather.mjs';
|
||||
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
|
||||
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
|
||||
@@ -159,7 +159,6 @@ app.get('/api/adsb', async (req, res) => res.json(await getADSB()));
|
||||
app.get('/api/adsb/:icao', async (req, res) => res.json(await getADSBHistory(req.params.icao)));
|
||||
app.get('/api/ais', async (req, res) => res.json(await getAIS()));
|
||||
app.get('/api/range', async (req, res) => res.json(await getADSBRange()));
|
||||
app.get('/api/vehicles', async (req, res) => res.json(await getShapes()));
|
||||
|
||||
// ── DOCS ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user