cleanup
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { AirTrafficLayer } from '@/services/adsb.ts'
|
||||
import { AISLayer } from '@/services/ais.ts'
|
||||
import {api} from '@/services/api.ts';
|
||||
import {RangeLayer} from '@/services/range.ts';
|
||||
import VectorTileLayer from 'ol/layer/VectorTile';
|
||||
import VectorTileSource from 'ol/source/VectorTile';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { api } from '@/services/api.ts'
|
||||
import { RangeLayer } from '@/services/range.ts'
|
||||
import VectorTileLayer from 'ol/layer/VectorTile'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import Map from 'ol/Map'
|
||||
import View from 'ol/View'
|
||||
import TileLayer from 'ol/layer/Tile'
|
||||
@@ -18,27 +17,23 @@ import CircleGeom from 'ol/geom/Circle'
|
||||
import { defaults as defaultInteractions } from 'ol/interaction'
|
||||
import { fromLonLat, toLonLat } from 'ol/proj'
|
||||
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style'
|
||||
import { applyStyle } from 'ol-mapbox-style'
|
||||
import 'ol/ol.css'
|
||||
|
||||
const current = ref({latitude: 0, longitude: 0});
|
||||
const props = defineProps<{ dark: boolean }>()
|
||||
const mapEl = ref<HTMLDivElement>()
|
||||
const current = ref({ latitude: 0, longitude: 0 })
|
||||
const props = defineProps<{ dark: boolean }>()
|
||||
const mapEl = ref<HTMLDivElement>()
|
||||
const showOverlays = ref(false)
|
||||
const atActive = ref(true)
|
||||
|
||||
const NM = 1852
|
||||
|
||||
const radarFrames = ref<{ path: string; time: number }[]>([])
|
||||
const radarFrameIndex = ref(0)
|
||||
const radarPath = computed(() => radarFrames.value[radarFrameIndex.value]?.path ?? '')
|
||||
const nowcastStartIndex = ref(0)
|
||||
|
||||
const showWind = ref(false)
|
||||
const windSrc = ref('')
|
||||
|
||||
const OVERLAYS = [
|
||||
{ id: 'rain', label: 'Rain', icon: '🌧️', url: () => `https://tilecache.rainviewer.com${radarPath.value}/256/{z}/{x}/{y}/2/1_1.png` },
|
||||
{ id: 'wind', label: 'Wind', icon: '💨', url: () => '' },
|
||||
{ id: 'rain', label: 'Rain', icon: '🌧️' },
|
||||
{ id: 'wind', label: 'Wind', icon: '💨' },
|
||||
]
|
||||
|
||||
const activeOverlays = ref<Set<string>>(new Set(['rain']))
|
||||
@@ -56,43 +51,65 @@ function buildWindSrc(lat: number, lon: number, zoom: number) {
|
||||
}
|
||||
|
||||
function syncWindSrc() {
|
||||
const view = map.getView()
|
||||
const view = map.getView()
|
||||
const center: any = toLonLat(view.getCenter()!)
|
||||
windSrc.value = buildWindSrc(center[1], center[0], view.getZoom() ?? 8)
|
||||
}
|
||||
|
||||
async function fetchRadarFrames() {
|
||||
const res = await fetch('https://api.rainviewer.com/public/weather-maps.json')
|
||||
const data = await res.json()
|
||||
const past = data.radar.past ?? []
|
||||
const nowcast = data.radar.nowcast ?? []
|
||||
radarFrames.value = [...past, ...nowcast]
|
||||
nowcastStartIndex.value = past.length
|
||||
function showWindOverlay() {
|
||||
syncWindSrc()
|
||||
showWind.value = true
|
||||
map.getInteractions().forEach(i => i.setActive(false))
|
||||
}
|
||||
|
||||
function buildRainSource(url: string): XYZ {
|
||||
return new XYZ({
|
||||
url,
|
||||
attributions: '',
|
||||
maxZoom: 7,
|
||||
tileLoadFunction: (tile: any, src: string) => {
|
||||
const img = tile.getImage()
|
||||
img.onerror = () => tile.setState(3)
|
||||
img.src = src
|
||||
},
|
||||
})
|
||||
function hideWindOverlay() {
|
||||
showWind.value = false
|
||||
map.getInteractions().forEach(i => i.setActive(true))
|
||||
}
|
||||
|
||||
function seekRadar(index: number) {
|
||||
radarFrameIndex.value = index
|
||||
const layer = overlayLayers['rain']
|
||||
if (layer) {
|
||||
layer.setSource(buildRainSource(OVERLAYS.find(o => o.id === 'rain')!.url()))
|
||||
async function fetchRadarUrl(): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch('https://api.rainviewer.com/public/weather-maps.json')
|
||||
const data = await res.json()
|
||||
const past = data.radar?.past ?? []
|
||||
const latest = past[past.length - 1]
|
||||
if (!latest) return null
|
||||
return `https://tilecache.rainviewer.com${latest.path}/256/{z}/{x}/{y}/2/1_1.png`
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function buildRainLayer(url: string): TileLayer<XYZ> {
|
||||
return new TileLayer({
|
||||
source: new XYZ({
|
||||
url,
|
||||
maxZoom: 7,
|
||||
tileLoadFunction: (tile: any, src: string) => {
|
||||
const img = tile.getImage()
|
||||
img.onerror = () => tile.setState(3)
|
||||
img.src = src
|
||||
},
|
||||
}),
|
||||
opacity: 0.2,
|
||||
zIndex: 5,
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshRain() {
|
||||
const url = await fetchRadarUrl()
|
||||
if (!url) return
|
||||
|
||||
if (overlayLayers['rain']) map.removeLayer(overlayLayers['rain'])
|
||||
if (!activeOverlays.value.has('rain')) return
|
||||
|
||||
const layer = buildRainLayer(url)
|
||||
overlayLayers['rain'] = layer
|
||||
map.addLayer(layer)
|
||||
}
|
||||
|
||||
function toggleAT() {
|
||||
if(atActive.value) {
|
||||
if (atActive.value) {
|
||||
aisLayer.hide()
|
||||
airTraffic.hide()
|
||||
range.hide()
|
||||
@@ -108,58 +125,42 @@ function toggleOverlay(id: string) {
|
||||
if (id === 'wind') {
|
||||
if (activeOverlays.value.has('wind')) {
|
||||
activeOverlays.value.delete('wind')
|
||||
showWind.value = false
|
||||
hideWindOverlay()
|
||||
} else {
|
||||
activeOverlays.value.add('wind')
|
||||
syncWindSrc()
|
||||
showWind.value = true
|
||||
showWindOverlay()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (activeOverlays.value.has(id)) {
|
||||
activeOverlays.value.delete(id)
|
||||
const layer = overlayLayers[id]
|
||||
if (layer) { map.removeLayer(layer); delete overlayLayers[id] }
|
||||
} else {
|
||||
activeOverlays.value.add(id)
|
||||
const layer = buildOverlayLayer(id)
|
||||
overlayLayers[id] = layer
|
||||
map.addLayer(layer)
|
||||
if (id === 'rain') {
|
||||
if (activeOverlays.value.has('rain')) {
|
||||
activeOverlays.value.delete('rain')
|
||||
if (overlayLayers['rain']) { map.removeLayer(overlayLayers['rain']); delete overlayLayers['rain'] }
|
||||
} else {
|
||||
activeOverlays.value.add('rain')
|
||||
refreshRain()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function buildBaseLayer(dark: boolean) {
|
||||
return new VectorTileLayer({
|
||||
source: new VectorTileSource({
|
||||
url: dark
|
||||
? 'https://tiles.zakscode.com/data/osm/{z}/{x}/{y}.pbf'
|
||||
: 'https://tiles.zakscode.com/styles/light/512/{z}/{x}/{y}.png',
|
||||
// ? 'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}.png'
|
||||
// : 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}.png',
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
function buildOverlayLayer(id: string): TileLayer<XYZ> {
|
||||
const def = OVERLAYS.find(o => o.id === id)!
|
||||
return new TileLayer({
|
||||
source: buildRainSource(def.url()),
|
||||
opacity: 0.2,
|
||||
zIndex: 5,
|
||||
})
|
||||
const layer = new VectorTileLayer({ declutter: true })
|
||||
applyStyle(layer, dark ? '/dark-theme.json' : '/light-theme.json')
|
||||
return layer
|
||||
}
|
||||
|
||||
function buildStationLayer(lat: number, lon: number, dark: boolean): VectorLayer<VectorSource> {
|
||||
const center = fromLonLat([lon, lat])
|
||||
const source = new VectorSource()
|
||||
const center = fromLonLat([lon, lat])
|
||||
const source = new VectorSource()
|
||||
const ringStroke = dark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.35)'
|
||||
|
||||
for (const nm of [50, 100, 150, 200]) {
|
||||
const ring = new Feature(new CircleGeom(center, nm * NM))
|
||||
ring.setStyle(new Style({
|
||||
stroke: new Stroke({ color: ringStroke, width: 1, lineDash: [6, 4] }),
|
||||
fill: new Fill({ color: 'transparent' }),
|
||||
fill: new Fill({ color: 'transparent' }),
|
||||
}))
|
||||
source.addFeature(ring)
|
||||
}
|
||||
@@ -168,7 +169,7 @@ function buildStationLayer(lat: number, lon: number, dark: boolean): VectorLayer
|
||||
dot.setStyle(new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 7,
|
||||
fill: new Fill({ color: dark ? '#ffffff' : '#000000' }),
|
||||
fill: new Fill({ color: dark ? '#ffffff' : '#000000' }),
|
||||
stroke: new Stroke({ color: dark ? '#000000' : '#ffffff', width: 2 }),
|
||||
}),
|
||||
}))
|
||||
@@ -178,48 +179,29 @@ function buildStationLayer(lat: number, lon: number, dark: boolean): VectorLayer
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
fetchRadarFrames().then(() => {
|
||||
radarFrameIndex.value = nowcastStartIndex.value - 1
|
||||
});
|
||||
|
||||
const position = await api.position();
|
||||
|
||||
const position = await api.position()
|
||||
const lat = position?.latitude || 0
|
||||
const lon = position?.longitude || 0
|
||||
|
||||
stationLayer = buildStationLayer(lat, lon, props.dark)
|
||||
|
||||
map = new Map({
|
||||
target: mapEl.value!,
|
||||
layers: [buildBaseLayer(props.dark), stationLayer],
|
||||
target: mapEl.value!,
|
||||
layers: [buildBaseLayer(props.dark), stationLayer],
|
||||
interactions: defaultInteractions({ altShiftDragRotate: false, pinchRotate: false }),
|
||||
view: new View({ center: fromLonLat([lon, lat]), zoom: 8, maxZoom: 13 }),
|
||||
view: new View({ center: fromLonLat([lon, lat]), zoom: 8, maxZoom: 13 }),
|
||||
controls: [],
|
||||
})
|
||||
|
||||
aisLayer = new AISLayer(map);
|
||||
aisLayer.show();
|
||||
aisLayer = new AISLayer(map)
|
||||
aisLayer.show()
|
||||
airTraffic = new AirTrafficLayer(map)
|
||||
airTraffic.show()
|
||||
range = new RangeLayer(map);
|
||||
range.show();
|
||||
range = new RangeLayer(map)
|
||||
range.show()
|
||||
|
||||
for (const id of activeOverlays.value) {
|
||||
if (id === 'wind') continue
|
||||
const layer = buildOverlayLayer(id)
|
||||
overlayLayers[id] = layer
|
||||
map.addLayer(layer)
|
||||
}
|
||||
|
||||
map.on('moveend', () => {
|
||||
if (showWind.value) syncWindSrc()
|
||||
})
|
||||
|
||||
radarInterval = setInterval(async () => {
|
||||
await fetchRadarFrames()
|
||||
radarFrameIndex.value = nowcastStartIndex.value - 1
|
||||
seekRadar(radarFrameIndex.value)
|
||||
}, 5 * 60 * 1000)
|
||||
refreshRain()
|
||||
radarInterval = setInterval(refreshRain, 5 * 60 * 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -332,80 +314,12 @@ watch(() => props.dark, dark => {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.radar-timeline {
|
||||
position: absolute;
|
||||
bottom: 72px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--surface);
|
||||
border-radius: 16px;
|
||||
padding: 8px 16px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
width: min(600px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.slider-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.slider-ticks {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
&__line {
|
||||
width: 1px;
|
||||
height: 5px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted, #888);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.nowcast &__line { background: #6ab4ff; }
|
||||
&.nowcast &__label { color: #6ab4ff; }
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100%;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-label {
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
min-width: 42px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop { display: flex; }
|
||||
.mobile { display: none; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.desktop { display: none; }
|
||||
.mobile { display: flex; }
|
||||
.radar-timeline { bottom: 10px; }
|
||||
.overlay-toggles { bottom: 10px; }
|
||||
.layers-menu { bottom: 10px; }
|
||||
}
|
||||
@@ -415,7 +329,6 @@ input[type='range'] {
|
||||
<div class="map-wrap">
|
||||
<div ref="mapEl" class="map-el" />
|
||||
|
||||
<!-- Windy animated wind iframe -->
|
||||
<iframe
|
||||
v-if="showWind"
|
||||
class="wind-iframe"
|
||||
@@ -424,7 +337,6 @@ input[type='range'] {
|
||||
allowfullscreen
|
||||
/>
|
||||
|
||||
<!-- Desktop toggles -->
|
||||
<div class="overlay-toggles desktop">
|
||||
<button class="overlay-btn" :class="{ active: atActive }" @click="toggleAT">
|
||||
✈️ Traffic
|
||||
@@ -439,7 +351,6 @@ input[type='range'] {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile layers button + dropdown -->
|
||||
<div class="layers-menu mobile">
|
||||
<button class="layers-btn" @click="showOverlays = !showOverlays">
|
||||
⚙️ Layers
|
||||
|
||||
Reference in New Issue
Block a user