Weather map with custom icons, animated SVGs or Lotties via Marker Layout

Create a weather map using the Marker Layout to show your custom weather markers icons, animated SVGs or Lotties. In this example, we use the MapTiler SDK together with the MapTiler Weather library.

Lottie is a file format for vector graphics animation. They can be scaled without pixelation or loss of quality, just like an SVG file. It is intended as a lighter alternative to animated GIFs and APNG files for use in the web and mobile and desktop applications. Read more about Lottie file format.

This example shows how to obtain the information from the MapTiler Weather layers to create a weather map where the icons corresponding to the current state of the weather in the main cities are shown. This example demonstrates the use of custom markers with content sourced from external data.

For this demo, we used Meteocons, a weather icon pack designed by Bas Milius also available on GitHub.

Note

If you prefer to follow this example using TypeScript and ES modules, check out the demo that we have prepared in the Marker Layout library’s GitHub repository.

NPM module setup


npm install --save @maptiler/sdk @maptiler/marker-layout @maptiler/weather
import { Map, MapStyle, config } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import { MarkerLayout } from '@maptiler/marker-layout';
import { WindLayer, PrecipitationLayer, TemperatureLayer, RadarLayer, ColorRamp } from '@maptiler/weather';

const appContainer = document.getElementById('map');

config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';

 // Creating a map
 const map = new Map({
  container: appContainer,
  style: MapStyle.BASE,
  geolocate: true,
});

// Creating the div that will contain all the markers
const markerContainer = document.createElement("div");
appContainer.appendChild(markerContainer);

(async () => {
  // Waiting that the map is "loaded"
  // (this is equivalent to putting the rest of the code the "load" event callback)
  await map.onReadyAsync();

  // The marker manager is in charge of computing the positions where markers
  // should be, sort them by POI rank and select non-overlaping places.
  // (it does not actually create DOM elements, it just uses logical points and bounding boxes)
  const markerManager = new MarkerLayout(map, {
    layers: ["City labels", "Town labels"],
    markerSize: [40, 70],
    offset: [0, -10],
    markerAnchor: "center",
  });

  // Creating the weather layers...
  // Temperature will be used as the main overlay
  const temperatureLayer = new TemperatureLayer({
    opacity: 0.7,
  });

  // Radar will be using the cloud color ramp and used as a cloud overlay
  const radarLayer = new RadarLayer({
    colorramp: ColorRamp.builtin.RADAR_CLOUD,
  });

  // From the wind layer, we only display the particles (the background is using the NULL color ramp, which is transparent).
  // The slower particles are transparent, the fastest are opaque white
  const windLayer = new WindLayer({
    colorramp: ColorRamp.builtin.NULL,
    color: [255, 255, 255, 0],
    fastColor: [255, 255, 255, 100],
  });

  // The precispitation layer is created but actually not displayed.
  // It will only be used for picking precipitation metrics at the locations of the markers
  const precipitationLayer = new PrecipitationLayer({
    colorramp: ColorRamp.builtin.NULL,
  });

  // Setting the water layer partially transparent to increase the visual separation between land and water
  map.setPaintProperty("Water", "fill-color", "rgba(0, 0, 0, 0.7)");
  map.addLayer(temperatureLayer, "Place labels");
  map.addLayer(windLayer);
  map.addLayer(radarLayer);
  map.addLayer(precipitationLayer);

  // Waiting for weather data readyness
  await temperatureLayer.onSourceReadyAsync();
  await radarLayer.onSourceReadyAsync();
  await windLayer.onSourceReadyAsync();
  await precipitationLayer.onSourceReadyAsync();

  // This object contains the marker DIV so that they can be updated rather than fully recreated every time
  const markerLogicContainer = {};

  let markerStatus = null;
  // This function will be used as the callback for some map events
  const updateMarkers = () => {
    markerStatus = markerManager.update();

    if (!markerStatus) return;

    // Remove the div that corresponds to removed markers
    markerStatus.removed.forEach((pb) => {
      const markerDiv = markerLogicContainer[pb.id];
      delete markerLogicContainer[pb.id];
      markerContainer.removeChild(markerDiv);
    });

    // Update the div that corresponds to updated markers
    markerStatus.updated.forEach((pb) => {
      const markerDiv = markerLogicContainer[pb.id];
      updateMarkerDiv(pb, markerDiv);
    });

    // Create the div that corresponds to the new markers
    markerStatus.new.forEach((pb) => {
      const markerDiv = makeMarker(
        pb,
        temperatureLayer,
        radarLayer,
        precipitationLayer,
        new Date()
      );
      markerLogicContainer[pb.id] = markerDiv;
      markerContainer.appendChild(markerDiv);
    });
  };

  const softUpdateMarkers = () => {
    // A previous run of .update() yieding no result or not being ran at all
    // would stop the soft update
    if (!markerStatus) return;

    markerStatus.updated.forEach((abstractMarker) => {
      markerManager.softUpdateAbstractMarker(abstractMarker);
      const markerDiv = markerLogicContainer[abstractMarker.id];
      updateMarkerDiv(abstractMarker, markerDiv);
    });

    markerStatus.new.forEach((abstractMarker) => {
      markerManager.softUpdateAbstractMarker(abstractMarker);
      const markerDiv = markerLogicContainer[abstractMarker.id];
      updateMarkerDiv(abstractMarker, markerDiv);
    });
  };

  // The "idle" event is triggered every second because of the particle layer being refreshed,
  // even though their is no new data loaded, so this approach proved to be the best for this scenario
  map.on("move", softUpdateMarkers);

  map.on("moveend", updateMarkers);

  map.once("idle", () => {
    updateMarkers();
  });
})();

function makeMarker(
  abstractMarker,
  temperatureLayer,
  radarLayer,
  precipitationLayer,
  date
) {
  const marker = document.createElement("div");
  marker.classList.add("marker");
  marker.classList.add("fade-in-animation");
  marker.style.setProperty("width", `${abstractMarker.size[0]}px`);
  marker.style.setProperty("height", `${abstractMarker.size[1]}px`);
  marker.style.setProperty(
    "transform",
    `translate(${abstractMarker.position[0]}px, ${abstractMarker.position[1]}px)`
  );

  const lonLat = abstractMarker.features[0].geometry.coordinates;
  const temperatureData = temperatureLayer.pickAt(lonLat[0], lonLat[1]);
  const precipitationData = precipitationLayer.pickAt(
    lonLat[0],
    lonLat[1]
  );

  const radarData = radarLayer.pickAt(lonLat[0], lonLat[1]);

  let mainWeatherIconURL = "./weather-icons/";
  const radarDBz = radarData?.value || -20;
  const precipMmH = precipitationData?.value || 0;
  const temperatureDeg = temperatureData?.value || 0;
  const temperature = temperatureData?.value.toFixed(1);

  const sunPosition = SunCalc.getPosition(date, lonLat[1], lonLat[0]);

  if (sunPosition.altitude < 0) {
    mainWeatherIconURL += "night-";
  } else {
    mainWeatherIconURL += "day-";
  }

  if (radarDBz < 0) {
    if (precipMmH > 0.2) {
      mainWeatherIconURL += "cloudy-";
    } else {
      mainWeatherIconURL += "clear-";
    }
  } else if (radarDBz < 10) {
    mainWeatherIconURL += "cloudy-";
  } else if (radarDBz < 20) {
    mainWeatherIconURL += "overcast-";
  } else {
    mainWeatherIconURL += "extreme-";
  }

  if (precipMmH > 5) {
    mainWeatherIconURL += temperatureDeg < -1 ? "snow" : "rain";
  } else if (precipMmH > 0.2) {
    mainWeatherIconURL += temperatureDeg < -1 ? "snow" : "drizzle";
  } else {
    mainWeatherIconURL += "none";
  }

  mainWeatherIconURL += ".svg";

  marker.innerHTML = `
  <img class="markerMainWeatherIcon" src=${mainWeatherIconURL}></img>
  <div class="markerTemperature">${temperature ? `${temperature}°` : ''}</div>
`;
  return marker;
}

function updateMarkerDiv(abstractMarker, marker) {
  marker.style.setProperty("width", `${abstractMarker.size[0]}px`);
  marker.style.setProperty("height", `${abstractMarker.size[1]}px`);
  marker.style.setProperty(
    "transform",
    `translate(${abstractMarker.position[0]}px, ${abstractMarker.position[1]}px)`
  );
}

Learn more

Check out the Marker Layout JS module reference

Consult the MapTiler Weather JS module reference

Weather custom popup

Weather custom popup

Examples

Create highly customizable popups for weather maps using MarkerLayout.

How to create non-colliding marker overlays

Filtered Marker Layout

Examples

Create non-colliding marker overlays to display the information from the city and town label layers.

Create custom marker overlays using the Marker Layout

Non filtered Marker Layout

Examples

Create custom non-colliding marker overlays on your map using the Marker Layout on top of MapTiler SDK. .

Add interaction to landmarks

Add interaction to landmarks

Examples

Create a map with custom landmarks and add interactions like popups to display additional information.

Was this helpful?