Anleitung

Welche Ihrer Orte Schnee bekommen

Wenn Sie die Höhe Ihrer Chalets, Haltestellen oder Kunden kennen, sagt Ihnen die Schneefallgrenze der Prognose, welche davon Schnee bekommen. Fragen Sie die Höhen einmal ab; danach ist es jeden Tag ein Vergleich, ohne weitere Anfrage.

Die Karte wird geladen…

Schnee:

Regen:

Der Code unten, mit 18 Dörfern der Jungfrauregion als Orten. Verschieben Sie den Regler: Über der Grenze werden die Orte blau und das Gelände weiss.

Ferienvermietungen können ihren Gästen sagen, für welche Chalets es morgen Schneeketten braucht. Ein Winterdienst sieht, bei welchen Kunden er zuerst räumt, ein Bus- oder Lieferdienst, welche Haltestellen und Adressen Winterausrüstung brauchen.

Drei Schritte

  1. Tragen Sie Ihre Orte in eine Tabelle mit den Spalten name, lat und lon ein, und speichern Sie sie als places.csv. Weitere Spalten dürfen bleiben.
  2. Lassen Sie das Skript unten einmal laufen, mit einem Schlüssel vom Typ «Alle APIs» aus Ihrem Dashboard (erstellen Sie zuerst ein kostenloses Konto). Es schreibt places-heights.csv, dieselbe Tabelle mit einer Spalte height. Lassen Sie es wieder laufen, wenn Orte dazukommen.
  3. Vergleichen Sie jeden Tag die Höhen mit der Schneefallgrenze der Prognose: Jeder Ort auf oder über der Grenze bekommt Schnee. Die Seite unten zeigt das auf einer Karte, mit einem Schlüssel vom Typ «Nur Karten».

Das Skript

import csv, requests

API = "https://api.overpass.ch/v1/IHR_API_SCHLÜSSEL/elevation"
BATCH = 10_000  # Orte pro Anfrage: 1000 im kostenlosen Tarif

# places.csv: eine Kopfzeile mit name, lat und lon, dann eine Zeile pro Ort.
# Kommas und Strichpunkte gehen beide, wie Tabellenprogramme sie speichern.
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), "Orte mit ihren Höhen in places-heights.csv")

Es braucht Python 3 und das Paket requests. Eine Liste von 10'000 Orten braucht eine Anfrage und ein paar Sekunden.

Die Karte

<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">Schneefallgrenze</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>Schnee:</b> <span id="snow"></span></p>
  <p><b>Regen:</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 = "IHR_SCHLÜSSEL";
const input = document.getElementById("line");
// Ihre Orte mit ihren Höhen, vom Skript oben
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=de`,
  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 // die Seite scrollt an der Karte vorbei; Strg + Mausrad oder zwei Finger bewegen sie
});
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("de-CH")} m`;
  return { p, el, marker: new maplibregl.Marker({ element: el }).setLngLat([p.lon, p.lat]).addTo(map) };
});

// Weiss über der Schneefallgrenze: die Geländekacheln färben jedes Pixel nach seiner Höhe
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("de-CH")} 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(", ") || "keine";
  document.getElementById("snow").textContent = names(true);
  document.getElementById("rain").textContent = names(false);
}
input.addEventListener("input", update);
update();
</script>

Legen Sie places-heights.csv neben die Seite, und setzen Sie Ihren Schlüssel «Nur Karten» dort ein, wo IHR_SCHLÜSSEL steht. Auf einer Seite für Gäste oder Mitarbeitende ersetzen Sie den Regler durch die Schneefallgrenze des Tages.

Die Schneefallgrenze der Prognose

Wetterdienste geben die Schneefallgrenze in ihren Prognosen für Bergregionen an; viele Wetter-APIs geben stattdessen die Nullgradgrenze an, die Höhe von 0 °C. Schnee fällt meist bis etwa 300 m unter die Nullgradgrenze. Verwenden Sie den Wert der Prognose, der Sie für Ihre Region vertrauen, und rechnen Sie bei Orten nahe der Grenze mit einem Spielraum von etwa 100 m.

Was es kostet

Eine Höhenabfrage pro Ort, einmal. Die Karte kostet Kacheln: Ihre erste Ansicht lädt auf dem Handy 8 Kacheln und auf einem grossen Bildschirm 10, etwa 22, wenn jemand hineinzoomt und die Karte viermal verschiebt. Das weisse Gelände kommt aus Geländekacheln, die wie die anderen Kacheln zählen. Wir haben das mit MapLibre GL JS 6.13.0 in Fenstern von 1280 × 720 und 390 × 700 Pixeln gemessen.

TarifHöhenabfragen im MonatKartenkacheln im MonatBesuche mit 22 KachelnFür ein Unternehmen
Jedes Konto, kostenlos1 Million100'000etwa 4'500✗ Nein, nur für nicht kommerzielle Nutzung
Elevation, $7 im Monat50 Millionen100'000etwa 4'500✓ Ja
Maps Small, $12 im Monat50 Millionen2 Millionenetwa 90'000✓ Ja

Preise zuzüglich Steuern. Alle Tarife: Preise.

Gut zu wissen

  • Die Höhen kommen aus einem Raster mit einem Punkt alle 90 m. In Dörfern liegen sie meist innerhalb weniger Meter; an einem steilen Hang kann ein Ort mehr abweichen. Höhen, die Sie genauer kennen, zum Beispiel aus einem Bauplan, können Sie stattdessen in die Tabelle schreiben.
  • Das weisse Gelände markiert das Gelände über der Grenze, ob dort heute Schnee liegt oder nicht.
  • Wo Sie die Höhen zeigen, nennen Sie die Quelle der Höhendaten. Die Karte tut das in ihrer Ecke, weil sie Geländekacheln verwendet.
  • Die Seite liest eine einfache Tabelle: keine Kommas oder Strichpunkte in den Namen. Das Skript und die Seite nehmen beide Trennzeichen und Dezimalkommas, wie Tabellenprogramme in vielen Ländern sie speichern.