Legg et levende pollenkart på nettsiden eller appen din

•10 min lesing
Kartfliser med pollenvarsel over Europa og Afrika, vippet i 3D, hver merket med sin z/x/y-adresse

Atmospores pollenvarsel finnes nå som kartfliser. Med rundt 150 linjer kode, inkludert en liten server, kan du legge et globalt pollenlag dag for dag på ditt eget kart: på en nettside, et dashbord eller i en mobilapp.

Guiden forutsetter ingen erfaring med kart. Den forklarer hva kartfliser er og hvordan tilgangen fungerer, og bygger så et fungerende kart steg for steg med kode du kan kopiere. Prøv den levende versjonen først: bytt mellom gress, trær og ugress, velg en dag, og klikk hvor som helst på land.

ModeratHøySvært høy
Live: det gjeldende varselet, tegnet fra de samme flisene appen din vil bruke. Klikk på en celle for å se verdien.

Dette får du

Alle landområder på jorden

Ett varselrutenett som dekker alle kontinenter, i celler på 0,25° (rundt 28 km).

25 arter pluss totaler

Bjørk, eik, ambrosia, timotei og 21 til, pluss samlede totaler for trær, gress og ugress. Hver flis inneholder alle sammen.

Dine farger, ditt kart

Flisene inneholder tall, ikke bilder. Du velger farger, gjennomsiktighet og hvilke arter som vises, på hvilket bakgrunnskart du vil.

7 dager frem, oppdatert daglig

I dag og de neste seks dagene, beregnet på nytt hver dag fra det nyeste værvarselet.

Kartfliser på to minutter

Et nettkart er ikke ett stort bilde. Det er en mosaikk av små ruter som kalles fliser. Helt utzoomet viser én enkelt flis hele verden. Hvert steg du zoomer inn deler hver flis i fire, slik at kartet kan vise flere detaljer og likevel bare laster det som er på skjermen.

0/0/0
Zoom 0 · 1 flis
1/0/0
1/1/0
1/0/1
1/1/1
Zoom 1 · 4 fliser
2/0/0
2/1/0
2/2/0
2/3/0
2/0/1
2/1/1
2/2/1
2/3/1
2/0/2
2/1/2
2/2/2
2/3/2
2/0/3
2/1/3
2/2/3
2/3/3
Zoom 2 · 16 fliser
Det samme pollenkartet på zoomnivå 0, 1 og 2. Hver flis har en adresse: zoom / kolonne / rad.

Adressen til en flis er tre tall: zoomnivået z, kolonnen x (telt fra venstre) og raden y (telt ovenfra). En flis-URL er en mal med plassholdere for disse tallene:

https://pollenapi.com/tiles/v1/…/2026-10-25/{z}/{x}/{y}.mvt?…

Du regner aldri ut disse tallene selv. Du gir malen til et kartbibliotek som MapLibre, og det fyller inn tallene for flisene på skjermen, laster dem ned og tegner dem. Når du panorerer eller zoomer, henter det de nye.

Flisene våre er vektorfliser (.mvt-filene). I stedet for et ferdig bilde inneholder hver flis cellene i varselrutenettet som former, med pollenverdiene som tall. Derfor kan du fargelegge kartet på nytt, bytte art eller lese den nøyaktige verdien under musen, alt i nettleseren og uten å laste ned noe nytt.

Slik fungerer tilgangen

Kartfliser er inkludert i betalte planer. API-nøkkelen din må holdes hemmelig, så den havner aldri på nettsiden. I stedet ber serveren din om flislenkene, og nettleseren bruker dem:

1. Serveren din

kaller GET /v1/map-tiles med API-nøkkelen din. Det er ett API-kall.

2. Nettsiden din

får et lite JSON-svar (TileJSON) fra serveren din, med en flis-URL for hver varseldag.

3. Atmospores CDN

sender flisene rett til nettleseren. Nedlasting av fliser er gratis og teller ikke mot kvoten din.

Flis-URL-ene er signerte: de virker på alle nettsider i omtrent 6 timer (feltet expires sier nøyaktig når) og er knyttet til kontoen din, så det er ikke noe annet å holde hemmelig.

Bygg det: et pollenkart i tre steg

Du trenger Node.js 18 eller nyere (eller Python 3), en Atmospore API-nøkkel på en betalt plan og en teksteditor. Lag en ny mappe til to filer: en liten server og en nettside.

Vil du heller klone det? Det komplette, testede eksempelet ligger på GitHub: github.com/atmospore/examples

Steg 1: en liten backend

Serveren har én jobb: hente flislenkene med nøkkelen din og gi dem videre til siden. Den husker svaret i en time, så selv en travel side gjør høyst 24 API-kall i døgnet. Velg språk:

server.mjs
// server.mjs: run with  ATMOSPORE_API_KEY=your-key node server.mjs
import http from "node:http";
import { readFile } from "node:fs/promises";

let cached = null;
let fetchedAt = 0;

// Ask Atmospore for fresh tile links at most once an hour.
// Each call counts as one API request; the tiles themselves are free.
async function pollenTiles() {
  if (cached && Date.now() - fetchedAt < 60 * 60 * 1000) return cached;
  const res = await fetch("https://pollenapi.com/v1/map-tiles", {
    headers: { "x-api-key": process.env.ATMOSPORE_API_KEY },
  });
  if (!res.ok) throw new Error(`Atmospore answered ${res.status}`);
  cached = await res.json();
  fetchedAt = Date.now();
  return cached;
}

http.createServer(async (req, res) => {
  try {
    if (req.url === "/pollen-tiles") {
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify(await pollenTiles()));
    } else {
      res.setHeader("Content-Type", "text/html");
      res.end(await readFile("index.html"));
    }
  } catch (err) {
    res.statusCode = 502;
    res.end(String(err));
  }
}).listen(3000, () => console.log("Open http://localhost:3000"));

Har du allerede en backend, legger du heller til /pollen-tiles-ruten der. Ingenting annet i guiden endres.

Steg 2: kartsiden

Lagre dette som index.html i samme mappe. Den laster MapLibre GL JS, et gratis kartbibliotek med åpen kildekode, og et gratis bakgrunnskart fra OpenFreeMap, så du trenger ingen andre kontoer.

index.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pollen map</title>
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css">
  <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>
  <style>
    html, body, #map { margin: 0; height: 100%; }
    #controls { position: absolute; top: 10px; left: 10px; z-index: 1;
                background: white; padding: 8px; border-radius: 8px; font: 14px sans-serif; }
  </style>
</head>
<body>
  <div id="controls">
    <select id="species">
      <option value="grass_tot">Grass</option>
      <option value="tree_tot">Trees</option>
      <option value="weed_tot">Weeds</option>
      <option value="birch">Birch</option>
    </select>
    <select id="day"></select>
  </div>
  <div id="map"></div>

  <script>
    // Risk levels in grains/m³: moderate, high and very high start at these values.
    const THRESHOLDS = {
      tree: [15, 90, 1500],
      grass: [5, 20, 200],
      weed: [10, 50, 500],
    };
    const CATEGORY = { grass_tot: "grass", tree_tot: "tree", weed_tot: "weed", birch: "tree" };

    // Colour each cell by the risk level of one species.
    function fillColor(species) {
      const [moderate, high, veryHigh] = THRESHOLDS[CATEGORY[species]];
      return [
        "step", ["coalesce", ["get", species], 0], // missing value = 0
        "rgba(0, 0, 0, 0)",   // low: transparent
        moderate, "#fde047",  // moderate: yellow
        high, "#f97316",      // high: orange
        veryHigh, "#dc2626",  // very high: red
      ];
    }

    const map = new maplibregl.Map({
      container: "map",
      style: "https://tiles.openfreemap.org/styles/positron", // free basemap, no key
      center: [10, 50],
      zoom: 3,
    });

    map.on("load", async () => {
      const pollen = await (await fetch("/pollen-tiles")).json();

      map.addSource("pollen", {
        type: "vector",
        tiles: pollen.tiles,       // today's {z}/{x}/{y} URL
        minzoom: pollen.minzoom,
        maxzoom: pollen.maxzoom,   // MapLibre zooms in past 5 on its own
        attribution: pollen.attribution,
      });

      // Draw the pollen under the place names so labels stay readable.
      const firstLabel = map.getStyle().layers.find((l) => l.type === "symbol");
      map.addLayer({
        id: "pollen",
        type: "fill",
        source: "pollen",
        "source-layer": "pollen",
        paint: {
          "fill-color": fillColor("grass_tot"),
          "fill-opacity": 0.6,
          "fill-antialias": false, // no hairlines between neighbouring cells
        },
      }, firstLabel?.id);

      // Pick a species: only the colours change, no new download.
      const speciesSelect = document.getElementById("species");
      speciesSelect.onchange = () =>
        map.setPaintProperty("pollen", "fill-color", fillColor(speciesSelect.value));

      // Pick a day: swap in that day's tile URL from steps[].
      const daySelect = document.getElementById("day");
      for (const s of pollen.steps) daySelect.add(new Option(s.step, s.step));
      daySelect.onchange = () => {
        const step = pollen.steps.find((s) => s.step === daySelect.value);
        map.getSource("pollen").setTiles(step.tiles);
      };

      // Click a cell to see the number behind the colour.
      map.on("click", "pollen", (e) => {
        const value = e.features[0].properties[speciesSelect.value] ?? 0;
        new maplibregl.Popup().setLngLat(e.lngLat)
          .setHTML(`${value.toFixed(0)} grains/m³`).addTo(map);
      });
    });
  </script>
</body>
</html>

Steg 3: kjør det

Start serveren med API-nøkkelen din:

terminal
ATMOSPORE_API_KEY=your-key node server.mjs

På Windows (PowerShell): $env:ATMOSPORE_API_KEY="your-key"; node server.mjs

Åpne http://localhost:3000 i nettleseren. Du skal se Europa med dagens varsel for gresspollen. Bruk menyene til å bytte art og dag, og klikk på en farget celle for å se verdien.

Det ferdige eksempelet: dagens risiko for gresspollen over Europa på et lyst bakgrunnskart
Resultatet: dagens risiko for gresspollen, tegnet under stedsnavnene.

Slik fungerer koden

Kilden og laget

map.addSource forteller hvor dataene kommer fra: flis-URL-malen fra serveren din, og zoomområdet 0–5 som det finnes fliser for. Zoomer du forbi 5, fortsetter MapLibre å forstørre flisene fra zoom 5, så du kan fortsatt zoome helt ned på gatenivå. map.addLayer forteller hvordan det skal tegnes: "source-layer": "pollen" er navnet på dataene i hver flis, og typen "fill" maler hver varselcelle som en farget rute. Laget legges inn før det første etikettlaget, så stedsnavnene blir liggende lesbare øverst.

Farger etter risikonivå

Hver celle har en verdi i pollenkorn per kubikkmeter luft for hver art. Step-uttrykket i fillColor gjør tallet om til en farge, med de samme risikonivåene som API-et og nettsiden vår:

PollenModeratHøySvært høy
Trær≥ 15≥ 90≥ 1500
Gress≥ 5≥ 20≥ 200
Ugress≥ 10≥ 50≥ 500

Verdier i korn/m³. Celler under moderat nivå er gjennomsiktige.

Har en celle ingen pollen av en art, er verdien utelatt helt, noe som holder flisene små. Derfor starter uttrykket med coalesce(…, 0): en manglende verdi teller som null. Endre fargene, legg til en blek farge for lave nivåer eller bruk egne terskler; alt ligger i denne ene funksjonen.

Bytte art og dag

Hver flis inneholder allerede alle arter, så å bytte art er bare å bytte fargeregel med setPaintProperty, og ingenting lastes ned. Hver varseldag har sin egen flis-URL i steps-listen serveren din sender videre; setTiles bytter den inn, og MapLibre henter flisene for den dagen. Ingen av delene krever et nytt kall til Atmospore-API-et.

Lese en verdi

map.on("click", "pollen", …) gir deg cellen under musen, med alle verdiene i features[0].properties. Samme idé fungerer for verktøytips, eller for et sidepanel som viser alle arter på stedet noen klikket.

Holde kartet oppdatert

Vi publiserer et nytt varsel hver dag, og flislenkene utløper etter omtrent 6 timer. Backenden over tar seg av begge deler: den henter nye lenker hver time, og innenfor samme time gir Atmospore identiske URL-er, så nettlesere beholder flisene de har mellomlagret.

Har folk siden din åpen i flere timer, kan du be backenden om lenkene på nytt innimellom (én gang i timen er rikelig) og sende den nye URL-en til setTiles. Hvert kall til /v1/map-tiles teller som ett API-kall; nedlasting av fliser teller aldri.

Med et annet kartbibliotek

  • Mapbox GL JS: Koden er den samme: bytt maplibregl med mapboxgl og angi Mapbox-tilgangstokenet ditt. Flisene våre følger Mapbox Vector Tile-standarden.
  • Leaflet: Leaflet tegner bare bildefliser i utgangspunktet. Legg til utvidelsen Leaflet.VectorGrid, gi den flis-URL-malen, og stil "pollen"-laget med en funksjon som returnerer en farge for hver celle.
  • OpenLayers: Bruk et VectorTile-lag med en VectorTile-kilde, MVT-formatet og flis-URL-malen, pluss en stilfunksjon for fargene.
  • iOS og Android: MapLibre Native og Mapbox Maps SDK-ene leser samme TileJSON og samme stilregler, så fargeuttrykket fra denne guiden virker der også.

Greit å vite

Hvor detaljerte er dataene?

Varselrutenettet er 0,25° (rundt 28 km ved ekvator, smalere mot polene). Det finnes fliser for zoom 0–5; zoomer du videre, forstørres flisene fra zoom 5, så på bynivå ser du tydelige ruter. Det er varselets faktiske oppløsning.

Hvorfor ser cellene større ut når jeg zoomer ut?

På zoom 0–2 slås nabocellene sammen til celler på 2°, 1° og 0,5° for å holde verdensvisningen lett. Hver sammenslått celle får den høyeste verdien inni seg, så pollentoppene forsvinner aldri når du zoomer ut.

Hvilke verdier finnes i hver flis?

Én verdi per art, med samme navn som i resten av API-et (birch, oak, timothy, ambrosia og så videre), pluss totalene tree_tot, grass_tot og weed_tot. Feltet vector_layers i svaret fra /v1/map-tiles lister alle, og API-dokumentasjonen har den fullstendige referansen.

Hvorfor mangler noen verdier?

En art uten pollen i en celle utelates fra cellen for å holde flisene små, så regn en manglende verdi som 0. Åpent hav har ingen celler, og fliser som bare ville vært hav finnes ikke: serveren svarer 404, og MapLibre tegner rett og slett ingenting der.

Hva teller mot kvoten min?

Bare kall til /v1/map-tiles, ett API-kall hver. Nedlasting av fliser er gratis og ubegrenset. Med mellomlagringen på én time fra denne guiden blir det høyst 24 kall i døgnet, uansett hvor mange som bruker kartet ditt.

Kan jeg bruke flisene på mitt eget domene og i apper?

Ja, flisene kan lastes fra alle nettsider og apper. Hold API-nøkkelen på serveren din, og vis kildehenvisningen: "© Atmospore" følger med i TileJSON, og MapLibre viser den automatisk.

Sett pollen på kartet ditt

Kartfliser er inkludert i alle betalte planer. Skaff en nøkkel, følg de tre stegene over, og du har et levende pollenkart i gang på få minutter.