Skip to main content

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;
    }