Updated adsb + ais popups to include images
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const BASE_SPEED_UNITS = {
|
const BASE_SPEED_UNITS = {
|
||||||
knots: { label: 'KTS', convert: (v: number) => Math.round(v) },
|
knots: { label: 'KTS', convert: (v: number) => Math.round(v) },
|
||||||
@@ -48,6 +48,23 @@ const climbVal = computed(() => { const v = vertical.value.convert(props.plan
|
|||||||
const description = computed(() => props.plane.desc || [props.plane.manufacturer, props.plane.model].filter(Boolean).join(' ') || '')
|
const description = computed(() => props.plane.desc || [props.plane.manufacturer, props.plane.model].filter(Boolean).join(' ') || '')
|
||||||
const operator = computed(() => props.plane.operator || props.plane.owner || '')
|
const operator = computed(() => props.plane.operator || props.plane.owner || '')
|
||||||
|
|
||||||
|
// ── Aircraft photo ────────────────────────────────────────────────────────────
|
||||||
|
const photoUrl = ref<string | null>(null)
|
||||||
|
const photoLoading = ref(false)
|
||||||
|
|
||||||
|
async function fetchPhoto(icao: string) {
|
||||||
|
photoUrl.value = null
|
||||||
|
if (!icao) return
|
||||||
|
photoLoading.value = true
|
||||||
|
const res = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao.toLowerCase()}`)
|
||||||
|
const data = await res.json()
|
||||||
|
const photo = data?.photos?.[0]
|
||||||
|
if (photo) photoUrl.value = photo.thumbnail_large?.src ?? photo.thumbnail?.src ?? null
|
||||||
|
photoLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.plane.icao || props.plane.hex, (icao) => fetchPhoto(icao), { immediate: true })
|
||||||
|
|
||||||
// ── Drag ──────────────────────────────────────────────────────────────────────
|
// ── Drag ──────────────────────────────────────────────────────────────────────
|
||||||
const pos = ref({ ...props.position })
|
const pos = ref({ ...props.position })
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
@@ -203,7 +220,7 @@ const navball = computed(() => {
|
|||||||
<button class="ap-close" @click.stop="emit('close')">×</button>
|
<button class="ap-close" @click.stop="emit('close')">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Meta -->
|
||||||
<div class="ap-meta flex-r justify-between">
|
<div class="ap-meta flex-r justify-between">
|
||||||
<div class="flex-c">
|
<div class="flex-c">
|
||||||
<span>{{ operator || 'Unknown Owner' }}</span>
|
<span>{{ operator || 'Unknown Owner' }}</span>
|
||||||
@@ -214,6 +231,13 @@ const navball = computed(() => {
|
|||||||
<span style="text-transform: capitalize">{{ plane.class || 'Unknown' }} • {{ plane.type || 'Unknown' }}</span>
|
<span style="text-transform: capitalize">{{ plane.class || 'Unknown' }} • {{ plane.type || 'Unknown' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Photo -->
|
||||||
|
<div v-if="photoUrl" class="ap-photo-wrap">
|
||||||
|
<img class="ap-photo" :src="photoUrl" :alt="callsign" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
<div class="ap-body">
|
<div class="ap-body">
|
||||||
<div class="ap-gauges">
|
<div class="ap-gauges">
|
||||||
<div class="ap-gauge-wrap">
|
<div class="ap-gauge-wrap">
|
||||||
@@ -309,6 +333,24 @@ const navball = computed(() => {
|
|||||||
}
|
}
|
||||||
.ap-close:hover { background: rgba(255,255,255,0.35); }
|
.ap-close:hover { background: rgba(255,255,255,0.35); }
|
||||||
|
|
||||||
|
/* ── Photo ── */
|
||||||
|
.ap-photo-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
background: #111;
|
||||||
|
border-bottom: 1px solid rgba(200,200,220,0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.ap-photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.ap-body {
|
.ap-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{ boat: any; position: { x: number; y: number } }>()
|
const props = defineProps<{ boat: any; position: { x: number; y: number } }>()
|
||||||
const emit = defineEmits<{ (e: 'close'): void; (e: 'bringToFront'): void }>()
|
const emit = defineEmits<{ (e: 'close'): void; (e: 'bringToFront'): void }>()
|
||||||
@@ -28,6 +28,16 @@ const name = computed(() => props.boat.name?.trim() || props.boat.callsign?.
|
|||||||
const heading = computed(() => props.boat.heading ?? props.boat.course ?? 0)
|
const heading = computed(() => props.boat.heading ?? props.boat.course ?? 0)
|
||||||
const speedVal = computed(() => speed.value.convert(props.boat.speed ?? 0))
|
const speedVal = computed(() => speed.value.convert(props.boat.speed ?? 0))
|
||||||
|
|
||||||
|
// ── Aircraft photo ────────────────────────────────────────────────────────────
|
||||||
|
const photoUrl = ref<string | null>(null)
|
||||||
|
const photoError = ref(false)
|
||||||
|
|
||||||
|
watch(() => props.boat.mmsi, (mmsi) => {
|
||||||
|
photoError.value = false
|
||||||
|
photoUrl.value = mmsi ? `https://www.marinetraffic.com/getAssetDefaultPhoto/?photo_size=800&asset_id=${mmsi}&asset_type_id=0` : null
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// ── Drag ──────────────────────────────────────────────────────────────────────
|
||||||
const pos = ref({ ...props.position })
|
const pos = ref({ ...props.position })
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragStart = ref({ mx: 0, my: 0, ex: 0, ey: 0 })
|
const dragStart = ref({ mx: 0, my: 0, ex: 0, ey: 0 })
|
||||||
@@ -73,7 +83,6 @@ function onTouchEnd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log(props.boat);
|
|
||||||
document.addEventListener('mousemove', onMouseMove)
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
document.addEventListener('mouseup', onMouseUp)
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||||
@@ -92,7 +101,6 @@ const compass = computed(() => {
|
|||||||
const hdg = heading.value
|
const hdg = heading.value
|
||||||
const course = props.boat.cog ?? hdg
|
const course = props.boat.cog ?? hdg
|
||||||
|
|
||||||
// ── Compass tape ──────────────────────────────────────────────────────────
|
|
||||||
const tapeTicks: string[] = []
|
const tapeTicks: string[] = []
|
||||||
for (let offset = -60; offset <= 60; offset += 5) {
|
for (let offset = -60; offset <= 60; offset += 5) {
|
||||||
const deg = ((hdg + offset) % 360 + 360) % 360
|
const deg = ((hdg + offset) % 360 + 360) % 360
|
||||||
@@ -113,9 +121,8 @@ const compass = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rose centre & radii ───────────────────────────────────────────────────
|
const CX = 100, CY = 140
|
||||||
const CX = 100, CY = 140 // moved down from 145
|
const R = 68
|
||||||
const R = 68 // grown from 55
|
|
||||||
|
|
||||||
const rudder = Math.max(-30, Math.min(30, ((course - hdg + 540) % 360) - 180))
|
const rudder = Math.max(-30, Math.min(30, ((course - hdg + 540) % 360) - 180))
|
||||||
const rudX = CX + (rudder / 30) * 40
|
const rudX = CX + (rudder / 30) * 40
|
||||||
@@ -130,7 +137,6 @@ const compass = computed(() => {
|
|||||||
const bNeedleX = CX + 52 * Math.sin(bearRad)
|
const bNeedleX = CX + 52 * Math.sin(bearRad)
|
||||||
const bNeedleY = CY - 52 * Math.cos(bearRad)
|
const bNeedleY = CY - 52 * Math.cos(bearRad)
|
||||||
|
|
||||||
// Ring tick marks scaled to new R
|
|
||||||
const ringTicks = Array.from({ length: 36 }, (_, i) => {
|
const ringTicks = Array.from({ length: 36 }, (_, i) => {
|
||||||
const a = i * 10
|
const a = i * 10
|
||||||
const rad = (a - 90) * Math.PI / 180
|
const rad = (a - 90) * Math.PI / 180
|
||||||
@@ -211,6 +217,11 @@ const compass = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Photo -->
|
||||||
|
<div v-if="photoUrl && !photoError" class="sp-photo-wrap">
|
||||||
|
<img class="sp-photo" :src="photoUrl" :alt="name" @error="photoError = true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sp-body">
|
<div class="sp-body">
|
||||||
<div class="sp-gauges">
|
<div class="sp-gauges">
|
||||||
<div class="sp-gauge-wrap">
|
<div class="sp-gauge-wrap">
|
||||||
@@ -301,6 +312,21 @@ const compass = computed(() => {
|
|||||||
}
|
}
|
||||||
.sp-close:hover { background: rgba(255,255,255,0.35); }
|
.sp-close:hover { background: rgba(255,255,255,0.35); }
|
||||||
|
|
||||||
|
/* ── Photo ── */
|
||||||
|
.sp-photo-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
background: #111;
|
||||||
|
border-bottom: 1px solid rgba(200,200,220,0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sp-photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.sp-body {
|
.sp-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -332,6 +358,5 @@ const compass = computed(() => {
|
|||||||
.sp-gauge:hover { border-color: #0f0; }
|
.sp-gauge:hover { border-color: #0f0; }
|
||||||
.sp-gauge-val { font-size: 20px; font-weight: bold; color: #0ff; }
|
.sp-gauge-val { font-size: 20px; font-weight: bold; color: #0ff; }
|
||||||
.sp-gauge-unit { font-size: 11px; color: #0ff; margin-bottom: 5px; }
|
.sp-gauge-unit { font-size: 11px; color: #0ff; margin-bottom: 5px; }
|
||||||
|
|
||||||
.sp-compass { flex: 1; }
|
.sp-compass { flex: 1; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import { bringToFront } from './zindex'
|
|||||||
const API = BASE + '/api'
|
const API = BASE + '/api'
|
||||||
|
|
||||||
const SHIP_COLORS: Record<string, string> = {
|
const SHIP_COLORS: Record<string, string> = {
|
||||||
'Base Station': '#00aaff',
|
'Base Station': '#ffffff',
|
||||||
'Class A': '#00ff00',
|
'Class A': '#00ff00',
|
||||||
'Class B': '#8450ea',
|
'Class B': '#8450ea',
|
||||||
'SAR Aircraft': '#ff0000',
|
'SAR Aircraft': '#ff0000',
|
||||||
'AtoN': '#00aaff',
|
'AtoN': '#ffffff',
|
||||||
'Class B/CS': '#8450ea',
|
'Class B/CS': '#8450ea',
|
||||||
'Sart/Epirb/MOB': '#00aaff',
|
'Sart/Epirb/MOB': '#00aaff',
|
||||||
'Unknown': '#fad106',
|
'Unknown': '#fad106',
|
||||||
|
|||||||
Reference in New Issue
Block a user