3D globe with custom weather markers

Create a weather globe 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.

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.

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.

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

config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
const appContainer = document.getElementById('map');
const map = new Map({
  container: appContainer, // container's id or the HTML element to render the map
  style:  MapStyle.BASE,
  center: [45, 25], // starting position [lng, lat]
  zoom: 2.5, // starting zoom
  projection: 'globe' //enable globe projection
});

// 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: ["Country labels", "City labels"],
    markerSize: [40, 70],
    offset: [0, -10],
    markerAnchor: "center",
  });

  // Creating the weather layers...
  // Temperature will be using the NULL color ramp
  const temperatureLayer = new TemperatureLayer({
    colorramp: ColorRamp.builtin.NULL,
  });

  // Radar will be using the NULL color ramp
  const radarLayer = new RadarLayer({
    colorramp: ColorRamp.builtin.NULL,
  });

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

  // 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", () => {
    setTimeout(() => {
      updateMarkers();
    }, 300);
  });
})();

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 = "https://docs.maptiler.com/sdk-js/examples/marker-layout-weather/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)`
  );
}

Warning

The Weather JS module can only be used with the MapTiler SDK. To visualize the layers of the Weather JS module, you must use the MapTiler SDK v2.x. We are working on adapting the weather module to work with v3 of the SDK and to be able to have weather visualizations on a 3D globe.

Projection control how to toggle the map between mercator and globe projection

Projection control (globe)

Tutorials

Toggle between Mercator and globe projections using map controls.

Add a 3D model to globe using MapTiler 3D JS

Add a 3D model to globe using MapTiler 3D JS

Examples

Integrate 3D models into your interactive globe map surface.

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.

3D globe with custom marker overlays

3D globe with custom marker overlays

Examples

Create non-colliding marker overlays on interactive 3D globe maps.

Was this helpful?