Weather custom popup

Let’s create the custom non-colliding popups using the Marker Layout on top of MapTiler SDK. The example uses the custom Marker Layout because it allows complete freedom of customization, which we can’t do with the original MapLibre popups due to their limited design.

We gather the weather data (including wind, temperature and precipitation) displayed in the popup from various layers of the MapTiler Weather JS module.

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

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.STREETS,
    zoom: 4.5,
    center: [2, 40],
    hash: true,
    projectionControl: true
  });

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

  (async () => {
    await map.onReadyAsync();

    const markerManager = new MarkerLayout(map, {
      layers: ["City labels", "Place labels", "Town labels"],
      markerSize: [140, 80],
      markerAnchor: "top",
      offset: [0, -8], // so that the tip of the marker bottom pin lands on the city dot
      sortingProperty: "rank",
      filter: ((feature) => {
        if (["City labels", "Town labels"].includes(feature.layer.id)) {
          return true;
        } else {
          return ["village"].includes(feature.properties.class)
        }
      }),
    });

    // 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((abstractMarker) => {
        const markerDiv = markerLogicContainer[abstractMarker.id];
        delete markerLogicContainer[abstractMarker.id];
        markerContainer.removeChild(markerDiv);
      });

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

      // Create the div that corresponds to the new markers
      markerStatus.new.forEach((abstractMarker) => {
        const markerDiv = makeMarker(abstractMarker,
          temperatureLayer,
          radarLayer,
          precipitationLayer,
          windLayer,
          new Date()
        );
        markerLogicContainer[abstractMarker.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);
      })
    }

    // While moving the map, this event is triggered many times per seconds
    // so we only perform a soft update (that could be debounced)
    map.on("move", softUpdateMarkers);

    // When done moving, we perform a full update
    map.on("moveend", updateMarkers)

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

  })()


function makeMarker(abstractMarker, 
  temperatureLayer, radarLayer,
  precipitationLayer, windLayer, 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 feature = abstractMarker.features[0];
  const lonLat = feature.geometry.coordinates;
  const temperatureData = temperatureLayer.pickAt(lonLat[0], lonLat[1]);
  const precipitationData = precipitationLayer.pickAt(lonLat[0], lonLat[1]);
  const windData = windLayer.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 windSpeed = windData?.speedKilometersPerHour || 0;
  const compassDirection = windData?.compassDirection || 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";
  
  let windDiv = "";
  if (windSpeed > 5) {
    windDiv = `
    <div
      class="markerWind"
    >
      <img class="markerWindIcon" src="./weather-icons/wind.svg"></img>${windSpeed.toFixed(0)} <span style="font-weight: 600; margin-left: 4px">${compassDirection}</span>
    </div>
    `
  }

  let precipDiv = "";
  if (precipMmH > 0.1) {
    precipDiv = `
    <div
      class="markerPrecipitation"
    >
      ${precipMmH.toFixed(1)} mm/h
    </div>
    `
  }

  marker.innerHTML = `
    <div class="markerPointy"></div>
    <div class="markerBody">

      <div class="markerTop">
        ${feature.properties["name:en"] || feature.properties["name"]}
      </div>

      <div class="markerBottom">
        <div class="markerBottomLeft">
          ${temperature ? `<div class="markerTemperature">${temperature}°</div>`: `<div class="markerTemperature"></div>`}
          ${windDiv}
        </div>

        <div class="markerBottomRight">
          <img class="markerMainWeatherIcon" src=${mainWeatherIconURL}></img>
          ${precipDiv}
        </div>
      </div>
    </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)`);
}

How to create your custom popup?

Check the Marker Layout helper reference to find inspiration on creating your popups and to learn about all the options, methods, etc.

Learn more

Check out the Weather JS module reference

Display a popup

Display a popup

Examples

Add an information popup to the map.

Show polygon information on click

Show polygon information on click

Examples

Display a popup when a user clicks a polygon.

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.

Weather layer switcher

Weather layer switcher

Examples

Switch weather layers and view detailed data under cursors.

Was this helpful?