Which of your places get snow
When you know the height of each of your chalets, bus stops or customers, the snow line of the forecast tells you which of them get snow. Look up the heights once; after that, each day is a comparison, without another request.
Snow:
Rain:
Holiday rentals can tell guests which chalets need snow chains tomorrow. A winter service sees which customers to clear first, a bus or delivery company which stops and addresses need winter equipment.
Three steps
- Put your places in a spreadsheet with the columns
name,latandlon, and save it asplaces.csv. Other columns can stay. - Run the script below once, with a key of the type “All APIs” from your dashboard (create a free account first). It writes
places-heights.csv, the same table with a columnheight. Run it again when you add places. - Each day, compare the heights with the snow line of the forecast: every place at or above it gets snow. The page below shows this on a map, with a key of the type “Maps only”.
The script
import csv, requests
API = "https://api.overpass.ch/v1/YOUR_API_KEY/elevation"
BATCH = 10_000 # places per request: 1,000 on the free plan
# places.csv: a header line with name, lat and lon, then a line per place.
# Commas or semicolons both work, as spreadsheets save them.
with open("places.csv", newline="", encoding="utf-8-sig") as f:
sep = ";" if ";" in f.readline() else ","
f.seek(0)
places = list(csv.DictReader(f, delimiter=sep))
for i in range(0, len(places), BATCH):
batch = places[i:i + BATCH]
where = [[float(p["lat"].replace(",", ".")), float(p["lon"].replace(",", "."))] for p in batch]
r = requests.post(API, json={"locations": where}, timeout=120)
r.raise_for_status()
for p, res in zip(batch, r.json()["results"]):
p["height"] = round(res["elevation"])
with open("places-heights.csv", "w", newline="", encoding="utf-8") as f:
out = csv.DictWriter(f, fieldnames=list(places[0]), delimiter=sep)
out.writeheader()
out.writerows(places)
print(len(places), "places with their heights in places-heights.csv")
It needs Python 3 and the package requests. A list of 10,000 places takes one request and a few seconds.
The map
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.css">
<style>
#map { height: 440px; border-radius: 8px; }
.snow-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: 0 0 10px; }
.snow-bar input { flex: 1 1 200px; }
.place { padding: 2px 7px; border-radius: 10px; font: 600 12px system-ui, sans-serif; white-space: nowrap; background: #fff; color: #333; box-shadow: 0 1px 3px #0005; }
.place.snow { background: #1864ab; color: #fff; }
#lists p { margin: 6px 0; }
</style>
<div class="snow-bar">
<label for="line">Snow line</label>
<input id="line" type="range" min="400" max="2600" step="100" value="1200">
<b id="line-value"></b>
</div>
<div id="map"></div>
<div id="lists">
<p><b>Snow:</b> <span id="snow"></span></p>
<p><b>Rain:</b> <span id="rain"></span></p>
</div>
<script type="module">
import * as maplibregl from "https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.mjs";
const KEY = "YOUR_KEY";
const input = document.getElementById("line");
// Your places with their heights, from the script above
const csv = (await (await fetch("places-heights.csv")).text()).trim();
const sep = csv.split("\n", 1)[0].includes(";") ? ";" : ",";
const [head, ...rows] = csv.split(/\r?\n/).map((line) => line.split(sep));
const get = (row, column) => row[head.indexOf(column)].trim();
const num = (row, column) => +get(row, column).replace(",", ".");
const places = rows.map((r) => ({ name: get(r, "name"), lat: num(r, "lat"), lon: num(r, "lon"), height: num(r, "height") }));
const map = new maplibregl.Map({
container: "map",
style: `https://api.overpass.ch/v1/${KEY}/maps/styles/outdoor.json?lang=en`,
bounds: places.reduce((b, p) => b.extend([p.lon, p.lat]), new maplibregl.LngLatBounds([places[0].lon, places[0].lat], [places[0].lon, places[0].lat])),
fitBoundsOptions: { padding: 50 },
cooperativeGestures: true // the page scrolls past the map; Ctrl + wheel or two fingers move it
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }));
const marks = places.map((p) => {
const el = document.createElement("div");
el.className = "place";
el.textContent = `${p.name} ${p.height.toLocaleString("en")} m`;
return { p, el, marker: new maplibregl.Marker({ element: el }).setLngLat([p.lon, p.lat]).addTo(map) };
});
// White above the snow line: the terrain tiles colour every pixel by its height
const snowColour = (line) => ["interpolate", ["linear"], ["elevation"], line - 1, "rgba(255,255,255,0)", line, "rgba(255,255,255,0.8)"];
map.on("load", () => {
map.addSource("dem", { type: "raster-dem", url: `https://api.overpass.ch/v1/${KEY}/maps/terrain.json` });
const firstLabels = map.getStyle().layers.find((l) => l.type === "symbol");
map.addLayer({ id: "snow", type: "color-relief", source: "dem", paint: { "color-relief-color": snowColour(+input.value) } }, firstLabels && firstLabels.id);
});
function update() {
const line = +input.value;
document.getElementById("line-value").textContent = `${line.toLocaleString("en")} m`;
if (map.getLayer("snow")) map.setPaintProperty("snow", "color-relief-color", snowColour(line));
for (const m of marks) m.el.classList.toggle("snow", m.p.height >= line);
const names = (snow) => places.filter((p) => (p.height >= line) === snow).sort((a, b) => b.height - a.height).map((p) => p.name).join(", ") || "none";
document.getElementById("snow").textContent = names(true);
document.getElementById("rain").textContent = names(false);
}
input.addEventListener("input", update);
update();
</script>
Put places-heights.csv next to the page and your Maps only key where it says YOUR_KEY. On a page for guests or staff, replace the slider with the snow line of the day.
The snow line of the forecast
Weather services give the snow line, also called snowfall limit, in their forecasts for mountain regions; many weather APIs give the freezing level instead, the height of 0 °C. Snow usually falls down to about 300 m below the freezing level. Use the value of the forecast you trust for your region, and keep a margin of 100 m or so for the places near the line.
What it costs
One lookup per place, once. The map costs tiles: its first view loads 8 tiles on a phone and 10 on a large screen, about 22 when a visitor zooms in and moves the map four times. The white ground comes from terrain tiles, which count like the other tiles. We measured this with MapLibre GL JS 6.13.0 in windows of 1280 × 720 and 390 × 700 pixels.
| Plan | Lookups a month | Map tiles a month | Visits at 22 tiles | For a business |
|---|---|---|---|---|
| Every account, free | 1 million | 100'000 | about 4'500 | ✗ No, for non-commercial use only |
| Elevation, CHF 5 a month | 50 million | 100'000 | about 4'500 | ✓ Yes |
| Maps Small, CHF 9 a month | 50 million | 2 million | about 90'000 | ✓ Yes |
Prices plus VAT. All plans: pricing.
Good to know
- The heights come from a grid with a point every 90 m. In villages they are typically within a few metres; on a steep slope a place can be off by more. Heights you know better, for example from a building plan, can go into the table instead.
- The white ground marks the terrain above the line, whether or not snow lies there today.
- Where you show the heights, credit the elevation data. The map does this in its corner, since it uses terrain tiles.
- The page reads a simple table: no commas or semicolons inside the names. The script and the page take both separators, and decimal commas, as spreadsheets in many countries save them.