better seismic drawings, and adsb enrichment
This commit is contained in:
@@ -156,7 +156,6 @@ function onSvgClick(e: MouseEvent) {
|
||||
}
|
||||
.controls {
|
||||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
button {
|
||||
font-size: 11px; padding: 4px 12px; border-radius: 6px;
|
||||
border: 1px solid var(--border); background: none; color: var(--text-muted); cursor: pointer;
|
||||
@@ -172,9 +171,8 @@ function onSvgClick(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
.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; }
|
||||
.val-time { font-size: 11px; margin-left: 6px; opacity: 0.6; }
|
||||
}
|
||||
.chart-wrap { position: relative; }
|
||||
.loading-overlay {
|
||||
@@ -202,7 +200,7 @@ svg { width: 100%; overflow: visible; cursor: crosshair; }
|
||||
<button class="close-btn" @click="emit('close')">✕</button>
|
||||
</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 === 'daily'}" style="border-radius: 0 6px 6px 0" @click="mode = 'daily'">Daily</button>
|
||||
</div>
|
||||
@@ -211,10 +209,9 @@ svg { width: 100%; overflow: visible; cursor: crosshair; }
|
||||
<span style="font-size:11px;color:var(--text-muted)">to</span>
|
||||
<input :type="mode === 'hourly' ? 'datetime-local' : 'date'" v-model="customEnd"/>
|
||||
<button class="apply-btn" @click="fetchData">Apply</button>
|
||||
</div>
|
||||
|
||||
<div v-if="chart.dot" class="current-val">
|
||||
<span>{{ (chart.dot.v as number).toFixed(2) }}</span>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="chart-wrap">
|
||||
|
||||
@@ -1,52 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import Loading from '@/components/Loading.vue';
|
||||
import {ref, onMounted, onUnmounted} from 'vue';
|
||||
import {api} from '../services/api';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { api } from '../services/api';
|
||||
import MetricRow from './MetricRow.vue';
|
||||
|
||||
const d = ref<any>(null);
|
||||
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 rafId: number;
|
||||
let drawProgress = 0;
|
||||
let lastTimestamp = 0;
|
||||
const ANIM_DURATION = 950;
|
||||
|
||||
function toPath(pts: number[]) {
|
||||
if (!pts.length) return '';
|
||||
const mid = H / 2, amp = H / 2 - 4;
|
||||
const max = Math.max(Math.max(...pts), 1.0); // 1.0 = min scale floor
|
||||
let pointBuffer: { x: number; y: number }[] = [];
|
||||
let prevMax = 1.0;
|
||||
|
||||
// Fixed px per point — path grows left→right, never stretches
|
||||
const segW = W / (MAX_PTS - 1);
|
||||
|
||||
const subPts: { x: number; y: number }[] = [];
|
||||
function buildBuffer(pts: number[]) {
|
||||
if (pts.length < 2) return;
|
||||
const mid = H / 2, amp = H / 2 - 6;
|
||||
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) => {
|
||||
const prev = pts[i - 1] ?? v;
|
||||
const prevNorm = (prev / max) * amp;
|
||||
const norm = (v / max) * amp;
|
||||
const baseX = i * segW;
|
||||
const s = segW / 4;
|
||||
const baseX = i * SEG_W;
|
||||
const s = SEG_W / SUB;
|
||||
|
||||
subPts.push({ x: baseX, y: mid - norm * 0.5 });
|
||||
subPts.push({ x: baseX + s, y: mid - norm });
|
||||
subPts.push({ x: baseX + s * 2, y: mid + norm });
|
||||
subPts.push({ x: baseX + s * 3, y: mid });
|
||||
const a0 = prevNorm * 0.75 + norm * 0.25;
|
||||
const a1 = prevNorm * 0.25 + norm * 0.75;
|
||||
const a2 = norm;
|
||||
|
||||
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) =>
|
||||
`${i === 0 ? 'M' : 'L'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`
|
||||
).join(' ');
|
||||
pointBuffer = buf;
|
||||
}
|
||||
|
||||
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() {
|
||||
d.value = await api.current('seismic_magnitude');
|
||||
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); });
|
||||
onUnmounted(() => clearInterval(interval));
|
||||
onMounted(async () => {
|
||||
await poll();
|
||||
lastTimestamp = performance.now();
|
||||
rafId = requestAnimationFrame(draw);
|
||||
interval = setInterval(poll, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval);
|
||||
cancelAnimationFrame(rafId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
svg { width: 100%; height: 60px; overflow: visible; }
|
||||
canvas { width: 100%; height: 60px; }
|
||||
</style>
|
||||
|
||||
<template>
|
||||
@@ -59,10 +147,7 @@ svg { width: 100%; height: 60px; overflow: visible; }
|
||||
</div>
|
||||
<template v-else>
|
||||
<MetricRow label="Magnitude" :value="d.seismic_magnitude?.toFixed(1)" metric-key="seismic_magnitude" :data="d" />
|
||||
<svg :viewBox="`0 0 ${W} ${H}`" preserveAspectRatio="none">
|
||||
<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>
|
||||
<canvas ref="canvas" :width="W" :height="H" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,9 +62,13 @@ export class AirTrafficLayer {
|
||||
this._draw()
|
||||
this._attachClick()
|
||||
this.refreshTimer = adjustedInterval(async () => {
|
||||
await this._fetch()
|
||||
this._draw()
|
||||
this._refreshPopups()
|
||||
try {
|
||||
await this._fetch()
|
||||
this._draw()
|
||||
this._refreshPopups()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}, 1_000)
|
||||
}
|
||||
|
||||
|
||||
@@ -69,9 +69,13 @@ export class AISLayer {
|
||||
this._draw()
|
||||
this._attachClick()
|
||||
this.refreshTimer = adjustedInterval(async () => {
|
||||
await this._fetch()
|
||||
this._draw()
|
||||
this._refreshPopups()
|
||||
try {
|
||||
await this._fetch()
|
||||
this._draw()
|
||||
this._refreshPopups()
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 5_000)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ export class RangeLayer {
|
||||
private map: Map
|
||||
private layer!: VectorLayer<VectorSource>
|
||||
visible = false
|
||||
interval: any;
|
||||
|
||||
constructor(map: Map) { this.map = map }
|
||||
|
||||
async show() {
|
||||
if (this.visible) return
|
||||
if(this.visible) this.hide(false);
|
||||
this.visible = true
|
||||
|
||||
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] })
|
||||
this.layer = new VectorLayer({ source, zIndex: 50 })
|
||||
this.map.addLayer(this.layer)
|
||||
if(!this.interval) this.interval = setInterval(() => this.show, 60_000);
|
||||
}
|
||||
|
||||
hide() {
|
||||
hide(stop = true) {
|
||||
if (!this.visible) return
|
||||
this.visible = false
|
||||
this.map.removeLayer(this.layer)
|
||||
if(stop && this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user