Weather Radar for LCARS
In continuing with the LCARS sites I maintain, I opted to include a very simple radar map using LeafletJS and RainViewer's API.
My 25th Century LCARS Site (Picard) - Just because I love the color scheme
My 24th Century LCARS Site (TNG) - This "display" site is used on an old touchscreen computer in our common area
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors. Radar by RainViewer. ',
maxZoom: 19
});
var map = L.map('map', {
center: [33.5028, -112.0386],
zoom: 10,
fullscreenControl: true,
layers: osm // Directly add the osm layer to the map
});
function onLocationFound(e) {
var radius = e.accuracy;
var geolocationIcon = L.icon({
iconUrl: 'assets/icons/user.png',
iconSize: [32, 32], // Adjust size as needed
iconAnchor: [16, 16] // Optional: Set anchor point
});
L.marker(e.latlng, { icon: geolocationIcon })
.addTo(map)
.bindPopup("YOU ARE WITHIN " + radius + " METERS OF THIS POINT")
.openPopup();
L.circle(e.latlng, radius).addTo(map);
map.setView(e.latlng, 12); // Zoom to a more detailed level (adjust as needed)
}
function onLocationError(e) {
alert(e.message);
}
map.on('locationfound', onLocationFound);
map.on('locationerror', onLocationError);
map.locate({ setView: true, maxZoom: 12 }); // Attempt to locate user and zoom on success
var clickIcon = L.icon({
iconUrl: 'assets/icons/user.png',
iconSize: [32, 32], // Adjust size as needed
iconAnchor: [16, 16] // Optional: Set anchor point
});
var clickMarker;
map.on('click', function(e) {
var latlng = e.latlng;
var coordinatesDiv = document.getElementById('coordinates');
coordinatesDiv.innerHTML = 'POINT ' + latlng.lat + ', ' + latlng.lng;
if (clickMarker) {
map.removeLayer(clickMarker); // Remove the previous marker
}
clickMarker = L.marker(latlng, { icon: clickIcon }).addTo(map); // Add a new marker at the clicked location
});
// RAINVIEWER
var apiData = {};
var mapFrames = [];
var lastPastFramePosition = -1;
var radarLayers = [];
var optionKind = 'radar'; // can be 'radar' or 'satellite'
var optionTileSize = 256; // can be 256 or 512.
var optionColorScheme = 6; // from 0 to 8. Check the https://rainviewer.com/api/color-schemes.html for additional information
var optionSmoothData = 1; // 0 - not smooth, 1 - smooth
var optionSnowColors = 1; // 0 - do not show snow colors, 1 - show snow colors
var animationPosition = 0;
var animationTimer = false;
var loadingTilesCount = 0;
var loadedTilesCount = 0;
function startLoadingTile() {
loadingTilesCount++;
}
function finishLoadingTile() {
// Delayed increase loaded count to prevent changing the layer before
// it will be replaced by next
setTimeout(function() { loadedTilesCount++; }, 250);
}
function isTilesLoading() {
return loadingTilesCount > loadedTilesCount;
}
/**
* Load all the available maps frames from RainViewer API
*/
var apiRequest = new XMLHttpRequest();
apiRequest.open("GET", "https://api.rainviewer.com/public/weather-maps.json", true);
apiRequest.onload = function(e) {
// store the API response for re-use purposes in memory
apiData = JSON.parse(apiRequest.response);
initialize(apiData, optionKind);
};
apiRequest.send();
/**
* Initialize internal data from the API response and options
*/
function initialize(api, kind) {
// remove all already added tiled layers
for (var i in radarLayers) {
map.removeLayer(radarLayers[i]);
}
mapFrames = [];
radarLayers = [];
animationPosition = 0;
if (!api) {
return;
}
if (kind == 'satellite' && api.satellite && api.satellite.infrared) {
mapFrames = api.satellite.infrared;
lastPastFramePosition = api.satellite.infrared.length - 1;
showFrame(lastPastFramePosition, true);
}
else if (api.radar && api.radar.past) {
mapFrames = api.radar.past;
if (api.radar.nowcast) {
mapFrames = mapFrames.concat(api.radar.nowcast);
}
// show the last "past" frame
lastPastFramePosition = api.radar.past.length - 1;
showFrame(lastPastFramePosition, true);
}
}
/**
* Animation functions
* @param path - Path to the XYZ tile
*/
function addLayer(frame) {
if (!radarLayers[frame.path]) {
var colorScheme = optionKind == 'satellite' ? 0 : optionColorScheme;
var smooth = optionKind == 'satellite' ? 0 : optionSmoothData;
var snow = optionKind == 'satellite' ? 0 : optionSnowColors;
var source = new L.TileLayer(apiData.host + frame.path + '/' + optionTileSize + '/{z}/{x}/{y}/' + colorScheme + '/' + smooth + '_' + snow + '.png', {
tileSize: 256,
opacity: 0.01,
zIndex: frame.time
});
// Track layer loading state to not display the overlay
// before it will completelly loads
source.on('loading', startLoadingTile);
source.on('load', finishLoadingTile);
source.on('remove', finishLoadingTile);
radarLayers[frame.path] = source;
}
if (!map.hasLayer(radarLayers[frame.path])) {
map.addLayer(radarLayers[frame.path]);
}
}
/**
* Display particular frame of animation for the @position
* If preloadOnly parameter is set to true, the frame layer only adds for the tiles preloading purpose
* @param position
* @param preloadOnly
* @param force - display layer immediatelly
*/
function changeRadarPosition(position, preloadOnly, force) {
while (position >= mapFrames.length) {
position -= mapFrames.length;
}
while (position < 0) {
position += mapFrames.length;
}
var currentFrame = mapFrames[animationPosition];
var nextFrame = mapFrames[position];
addLayer(nextFrame);
// Quit if this call is for preloading only by design
// or some times still loading in background
if (preloadOnly || (isTilesLoading() && !force)) {
return;
}
animationPosition = position;
if (radarLayers[currentFrame.path]) {
radarLayers[currentFrame.path].setOpacity(0);
}
radarLayers[nextFrame.path].setOpacity(100);
var pastOrForecast = nextFrame.time > Date.now() / 1000 ? 'FORECAST' : 'PAST RADAR';
document.getElementById("timestamp").innerHTML = pastOrForecast + ': ' + (new Date(nextFrame.time * 1000)).toLocaleString("en-US", {timeZoneName: "short", hour12: false});
/**document.getElementById("timestamp").innerHTML = pastOrForecast + ': ' + (new Date(nextFrame.time * 1000)).toString();**/
}
/**
* Check avialability and show particular frame position from the timestamps list
*/
function showFrame(nextPosition, force) {
var preloadingDirection = nextPosition - animationPosition > 0 ? 1 : -1;
changeRadarPosition(nextPosition, false, force);
// preload next next frame (typically, +1 frame)
// if don't do that, the animation will be blinking at the first loop
changeRadarPosition(nextPosition + preloadingDirection, true);
}
/**
* Stop the animation
* Check if the animation timeout is set and clear it.
*/
function stop() {
if (animationTimer) {
clearTimeout(animationTimer);
animationTimer = false;
return true;
}
return false;
}
function play() {
showFrame(animationPosition + 1);
// Main animation driver. Run this function every 500 ms
animationTimer = setTimeout(play, 500);
}
function playStop() {
if (!stop()) {
play();
}
}
/**
* Change map options
*/
function setKind(kind) {
optionKind = kind;
initialize(apiData, optionKind);
}
function setColors() {
var e = document.getElementById('colors');
optionColorScheme = e.options[e.selectedIndex].value;
initialize(apiData, optionKind);
}
/**
* Handle arrow keys for navigation between next \ prev frames
*/
document.onkeydown = function (e) {
e = e || window.event;
switch (e.which || e.keyCode) {
case 37: // left
stop();
showFrame(animationPosition - 1, true);
break;
case 39: // right
stop();
showFrame(animationPosition + 1, true);
break;
default:
return; // exit this handler for other keys
}
e.preventDefault();
return false;
}