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)`
);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title>Weather map with custom icons, animated SVGs or Lotties via Marker Layout | NPM example</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="map"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
html,
body {
margin: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.marker {
position: absolute;
pointer-events: none;
}
.markerMainWeatherIcon {
width: 100%;
height: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.8));
}
.markerTemperature {
width: 100%;
height: auto;
position: absolute;
bottom: 0px;
left: 0;
right: 0;
margin: auto;
text-align: center;
font-size: 15px;
color: white;
text-shadow: 0px 0px 4px black;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in-animation {
animation: fadeIn 0.5s ease forwards;
}
Learn more
Check out the Marker Layout JS module reference
Consult the MapTiler Weather JS module reference
Related examples
Weather custom popup
ExamplesCreate highly customizable popups for weather maps using MarkerLayout.
Filtered Marker Layout
ExamplesCreate non-colliding marker overlays to display the information from the city and town label layers.
Non filtered Marker Layout
ExamplesCreate custom non-colliding marker overlays on your map using the Marker Layout on top of MapTiler SDK. .
Add interaction to landmarks
ExamplesCreate a map with custom landmarks and add interactions like popups to display additional information.