2022-11-22 16:19:10 -06:00
|
|
|
import STATUS from './status.mjs';
|
|
|
|
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
2026-04-07 15:16:52 -04:00
|
|
|
import { safeJson } from './utils/fetch.mjs';
|
2022-11-22 16:29:10 -06:00
|
|
|
import WeatherDisplay from './weatherdisplay.mjs';
|
2026-04-07 15:16:52 -04:00
|
|
|
import { registerDisplay } from './navigation.mjs';
|
2026-04-07 18:08:38 -04:00
|
|
|
import {
|
|
|
|
|
createMap,
|
|
|
|
|
addBaseLayers,
|
|
|
|
|
setPrimaryLocationMarker,
|
|
|
|
|
loadNearbyObservationMarkers,
|
|
|
|
|
clearMarkers,
|
|
|
|
|
} from './utils/leaflet-weather-map.mjs';
|
2025-05-31 13:30:21 -05:00
|
|
|
|
2026-04-07 18:32:07 -04:00
|
|
|
const RADAR_METADATA_URL = 'https://api.rainviewer.com/public/weather-maps.json';
|
|
|
|
|
const RADAR_METADATA_CACHE_TTL_MS = 2 * 60 * 1000;
|
|
|
|
|
let radarMetadataCache = null;
|
|
|
|
|
|
|
|
|
|
const getRadarMetadataCached = async (stillWaiting) => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (radarMetadataCache && (now - radarMetadataCache.fetchedAt) < RADAR_METADATA_CACHE_TTL_MS) {
|
|
|
|
|
return radarMetadataCache.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const radarMetadata = await safeJson(RADAR_METADATA_URL, {
|
|
|
|
|
retryCount: 2,
|
|
|
|
|
stillWaiting,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (radarMetadata?.host && radarMetadata?.radar?.past?.length) {
|
|
|
|
|
radarMetadataCache = {
|
|
|
|
|
data: radarMetadata,
|
|
|
|
|
fetchedAt: now,
|
|
|
|
|
};
|
|
|
|
|
return radarMetadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (radarMetadataCache) {
|
|
|
|
|
return radarMetadataCache.data;
|
|
|
|
|
}
|
2026-04-07 15:16:52 -04:00
|
|
|
|
2026-04-07 18:32:07 -04:00
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class Radar extends WeatherDisplay {
|
2020-10-29 16:44:28 -05:00
|
|
|
constructor(navId, elemId) {
|
2025-06-20 22:04:00 -05:00
|
|
|
super(navId, elemId, 'Local Radar');
|
2020-09-04 17:03:03 -05:00
|
|
|
|
2022-12-06 16:14:56 -06:00
|
|
|
this.okToDrawCurrentConditions = false;
|
|
|
|
|
this.okToDrawCurrentDateTime = false;
|
|
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
this.map = null;
|
|
|
|
|
this.baseLayer = null;
|
2026-04-07 16:43:51 -04:00
|
|
|
this.boundaryLayer = null;
|
2026-04-07 15:16:52 -04:00
|
|
|
this.locationMarker = null;
|
2026-04-07 18:08:38 -04:00
|
|
|
this.nearbyMarkers = [];
|
2026-04-07 15:16:52 -04:00
|
|
|
this.radarLayers = [];
|
|
|
|
|
this.mapFrames = [];
|
|
|
|
|
this.radarHost = '';
|
|
|
|
|
|
|
|
|
|
this.timing.baseDelay = 500;
|
|
|
|
|
this.timing.delay = 1;
|
|
|
|
|
this.maxFrames = 6;
|
2020-09-04 17:03:03 -05:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 20:58:53 -05:00
|
|
|
async getData(weatherParameters, refresh) {
|
|
|
|
|
if (!super.getData(weatherParameters, refresh)) return;
|
2026-04-07 15:16:52 -04:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!window.L) {
|
|
|
|
|
throw new Error('Leaflet is not available');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.ensureMap();
|
|
|
|
|
this.map.invalidateSize();
|
|
|
|
|
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 7);
|
|
|
|
|
this.updateLocationMarker();
|
2026-04-07 18:08:38 -04:00
|
|
|
await this.updateNearbyMarkers();
|
2026-04-07 15:16:52 -04:00
|
|
|
|
2026-04-07 18:32:07 -04:00
|
|
|
const radarMetadata = await getRadarMetadataCached(() => this.stillWaiting());
|
2026-04-07 15:16:52 -04:00
|
|
|
|
|
|
|
|
const frames = radarMetadata?.radar?.past?.slice(-this.maxFrames) ?? [];
|
|
|
|
|
if (!frames.length || !radarMetadata?.host) {
|
|
|
|
|
this.clearRadarLayers();
|
|
|
|
|
this.timing.totalScreens = 0;
|
|
|
|
|
this.setStatus(STATUS.noData);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.radarHost = radarMetadata.host;
|
|
|
|
|
this.mapFrames = frames;
|
|
|
|
|
this.resetRadarLayers();
|
|
|
|
|
this.timing.delay = this.buildTiming();
|
|
|
|
|
this.calcNavTiming();
|
|
|
|
|
this.resetNavBaseCount();
|
|
|
|
|
this.showFrame(this.mapFrames.length - 1);
|
2026-04-07 14:57:23 -04:00
|
|
|
this.setStatus(STATUS.loaded);
|
2026-04-07 15:16:52 -04:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Failed to initialize radar: ${error.message}`);
|
|
|
|
|
this.clearRadarLayers();
|
2026-04-07 18:08:38 -04:00
|
|
|
this.clearNearbyMarkers();
|
2026-04-07 15:16:52 -04:00
|
|
|
this.timing.totalScreens = 0;
|
|
|
|
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
2026-04-07 14:57:23 -04:00
|
|
|
}
|
2026-04-07 15:16:52 -04:00
|
|
|
}
|
2020-09-04 17:03:03 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
async ensureMap() {
|
|
|
|
|
if (this.map) return;
|
2020-09-04 17:03:03 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
const mapElement = this.elem.querySelector('.leaflet-map');
|
|
|
|
|
if (!mapElement) {
|
|
|
|
|
throw new Error('Radar map container not found');
|
2020-09-04 17:03:03 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 18:08:38 -04:00
|
|
|
this.map = createMap(mapElement);
|
|
|
|
|
({ baseLayer: this.baseLayer, boundaryLayer: this.boundaryLayer } = addBaseLayers(this.map));
|
2026-04-07 15:16:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetRadarLayers() {
|
|
|
|
|
this.clearRadarLayers();
|
|
|
|
|
this.radarLayers = this.mapFrames.map((frame) => this.createRadarLayer(frame));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearRadarLayers() {
|
|
|
|
|
if (!this.map || !this.radarLayers.length) {
|
|
|
|
|
this.radarLayers = [];
|
|
|
|
|
return;
|
2025-06-24 23:35:41 -04:00
|
|
|
}
|
2020-12-29 10:22:20 -06:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
this.radarLayers.forEach((layer) => {
|
|
|
|
|
if (this.map.hasLayer(layer)) {
|
|
|
|
|
this.map.removeLayer(layer);
|
2025-06-24 23:35:41 -04:00
|
|
|
}
|
2023-01-06 14:39:39 -06:00
|
|
|
});
|
2026-04-07 15:16:52 -04:00
|
|
|
this.radarLayers = [];
|
|
|
|
|
}
|
2020-09-04 17:03:03 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
createRadarLayer(frame) {
|
2026-04-07 15:57:08 -04:00
|
|
|
const tileUrl = `${this.radarHost}${frame.path}/256/{z}/{x}/{y}/4/1_0.png`;
|
2026-04-07 15:16:52 -04:00
|
|
|
const layer = window.L.tileLayer(tileUrl, {
|
|
|
|
|
tileSize: 256,
|
|
|
|
|
opacity: 0,
|
|
|
|
|
zIndex: frame.time,
|
|
|
|
|
crossOrigin: true,
|
|
|
|
|
updateWhenIdle: false,
|
|
|
|
|
keepBuffer: 2,
|
2026-04-07 16:12:43 -04:00
|
|
|
className: 'radar-precip-layer',
|
2025-06-13 16:44:53 -05:00
|
|
|
});
|
|
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
layer.addTo(this.map);
|
|
|
|
|
return layer;
|
|
|
|
|
}
|
2025-06-27 15:29:20 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
buildTiming() {
|
|
|
|
|
const latestFrameIndex = this.mapFrames.length - 1;
|
|
|
|
|
const sequence = [latestFrameIndex, ...this.mapFrames.map((_, index) => index), latestFrameIndex];
|
|
|
|
|
return sequence.map((screenIndex, index) => {
|
|
|
|
|
let time = 1;
|
|
|
|
|
if (screenIndex === latestFrameIndex) {
|
|
|
|
|
time = index === sequence.length - 1 ? 12 : 4;
|
|
|
|
|
}
|
|
|
|
|
return { si: screenIndex, time };
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-27 15:29:20 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
updateLocationMarker() {
|
|
|
|
|
if (!this.map) return;
|
2026-04-07 18:08:38 -04:00
|
|
|
this.locationMarker = setPrimaryLocationMarker(
|
|
|
|
|
this.map,
|
|
|
|
|
this.locationMarker,
|
2026-04-07 15:16:52 -04:00
|
|
|
this.weatherParameters.latitude,
|
|
|
|
|
this.weatherParameters.longitude,
|
2026-04-07 18:08:38 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearNearbyMarkers() {
|
|
|
|
|
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateNearbyMarkers() {
|
|
|
|
|
if (!this.map) return;
|
|
|
|
|
|
|
|
|
|
this.clearNearbyMarkers();
|
|
|
|
|
this.nearbyMarkers = await loadNearbyObservationMarkers(this.map, {
|
|
|
|
|
latitude: this.weatherParameters.latitude,
|
|
|
|
|
longitude: this.weatherParameters.longitude,
|
|
|
|
|
});
|
|
|
|
|
this.nearbyMarkers.forEach((marker) => marker.addTo(this.map));
|
2020-09-04 17:03:03 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
showFrame(screenIndex) {
|
|
|
|
|
if (!this.radarLayers.length || !this.mapFrames.length) return;
|
2020-09-04 17:03:03 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
const frameIndex = Math.max(0, Math.min(screenIndex, this.radarLayers.length - 1));
|
|
|
|
|
this.radarLayers.forEach((layer, index) => {
|
|
|
|
|
layer.setOpacity(index === frameIndex ? 0.8 : 0);
|
|
|
|
|
});
|
2023-01-10 14:12:22 -06:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
const time = DateTime.fromSeconds(this.mapFrames[frameIndex].time)
|
|
|
|
|
.setZone(this.weatherParameters.timeZone)
|
|
|
|
|
.toLocaleString(DateTime.TIME_SIMPLE);
|
|
|
|
|
this.elem.querySelector('.header .right .time').innerHTML = time.length >= 8 ? time : ` ${time} `;
|
|
|
|
|
}
|
2022-08-05 16:23:22 -05:00
|
|
|
|
2026-04-07 15:16:52 -04:00
|
|
|
async drawCanvas() {
|
|
|
|
|
super.drawCanvas();
|
|
|
|
|
if (this.map) {
|
|
|
|
|
this.map.invalidateSize(false);
|
|
|
|
|
this.showFrame(this.screenIndex);
|
|
|
|
|
}
|
2020-09-04 17:03:03 -05:00
|
|
|
this.finishDraw();
|
|
|
|
|
}
|
2020-10-29 16:44:28 -05:00
|
|
|
}
|
2022-11-22 16:19:10 -06:00
|
|
|
|
2025-06-20 22:04:00 -05:00
|
|
|
registerDisplay(new Radar(11, 'radar'));
|