De Azimuth- en Grayline-tool: van idee tot interactieve kaart

21 August 2026

De Azimuth- en Grayline-tool: van idee tot interactieve kaart

Als radioamateur wil je weten in welke richting je antenne moet draaien, en of het momenteel dag of nacht is aan de andere kant van de wereld — dat bepaalt namelijk sterk hoe de HF-banden zich gedragen. Voor PA2JFX.nl heb ik daarom een eigen azimuthale kaarttool gebouwd: een interactieve wereldkaart die real-time laat zien waar het licht en donker is (de grayline), en die met een simpele klik de peilrichting (azimuth) naar elke locatie op aarde toont.

Waar het idee vandaan komt

De inspiratie kwam van twee bestaande tools van Tom Epperly (NS6T), een bekende naam in de radioamateurwereld: ns6t.net/azimuth (klassieke server-side kaart-generator) en ns6t.net/AzShadowMap (moderne client-side variant met live grayline).

Ik wilde iets dat volledig eigen was: mijn eigen huisstijl (#288592 teal-accent), Nederlandstalig, geintegreerd in WordPress zonder iframes, en met functionaliteit die precies aansluit op wat ik als radioamateur nodig heb.

Belangrijk: de originele NS6T-code is auteursrechtelijk beschermd. In plaats van die code te kopieren is de wiskunde erachter (azimuthale projectie, zonpositie-algoritmes) publiek beschikbare kennis uit GIS-handboeken en het NOAA solar calculator-algoritme — zelf opnieuw geimplementeerd. De landcontouren komen van Natural Earth Data (CC0).

De bronnen op een rij

OnderdeelBron
Inspiratie/referentie-UXNS6T Azimuth & AzShadowMap tools
Landgrenzen/kaartdataNatural Earth Data (CC0, publiek domein)
Zonpositie-algoritmeNOAA Solar Calculator (Jean Meeus-methode)
Satelliet-positiessatellite.js — SGP4/SDP4-propagator (NASA/Celestrak methode)
Live TLE-baangegevensCelestrak.org
Actuele tijd (UTC)worldtimeapi.org (NTP-gesynchroniseerd)
Space weather (SFI/SN/K-index)NOAA Space Weather Prediction Center
PDF-generatiejsPDF (client-side, browser)
KaartrenderingD3.js voor SVG-manipulatie

De techniek: hoe werkt het eigenlijk?

1. Locator vs. coordinaten — Maidenhead Grid

Radioamateurs gebruiken vaak een Maidenhead Locator (bijv. JO22JF, mijn eigen QTH) in plaats van decimale coordinaten. Dat is een compact rastersysteem waarbij de aarde wordt opgedeeld in steeds kleinere vakjes: de eerste 2 letters (A–R) verdelen de aarde in velden van 20° lon x 10° lat, de volgende 2 cijfers (0–9) verdelen dat verder op (2° x 1°), en de laatste 2 letters (a–x) verfijnen tot ongeveer 5x2,5 km nauwkeurig.

// Grid → lat/lon
function parseGrid(grid) {
  var lon = (grid.charCodeAt(0) - 65) * 20 - 180;
  var lat = (grid.charCodeAt(1) - 65) * 10 - 90;
  lon += (grid.charCodeAt(2) - 48) * 2;
  lat += (grid.charCodeAt(3) - 48) * 1;
  if (grid.length >= 6) {
    lon += (grid.charCodeAt(4) - 97) * (2 / 24);
    lat += (grid.charCodeAt(5) - 97) * (1 / 24);
  }
  return { lat: lat, lon: lon };
}

// lat/lon → Grid (omgekeerde richting)
function toGrid(lat, lon) {
  var aLat = lat + 90, aLon = lon + 180;
  var cc = [
    65 + Math.trunc(aLon / 20), 65 + Math.trunc(aLat / 10),
    48 + Math.trunc((aLon % 20) / 2), 48 + Math.trunc(aLat % 10),
    97 + Math.trunc((aLon % 2) * 12), 97 + Math.trunc((aLat % 1) * 24)
  ];
  return String.fromCharCode.apply(null, cc);
}

Het mooie is dat dit bidirectioneel werkt: vul je een grid in, dan verschijnen automatisch de lat/lon-waarden, en omgekeerd — beide invoervelden roepen elkaars berekening aan zodra er iets wijzigt.

2. De azimuthale equidistante projectie

Dit is het hart van de kaart. Anders dan een gewone wereldkaart (Mercator) toont een azimuthale projectie de aarde als een cirkel, gecentreerd op je eigen QTH. De afstand vanaf het middelpunt is recht proportioneel aan de werkelijke afstand op aarde — en dat is precies waarom radioamateurs deze projectie gebruiken: je kunt in een oogopslag de richting aflezen waarin je je antenne moet draaien.

// Great-circle afstand + peilhoek (bearing) vanaf het centrum
function greatCircle(lat1, lon1, lat2, lon2) {
  var toRad = function(d) { return d * Math.PI / 180; };
  var phi1 = toRad(lat1), phi2 = toRad(lat2);
  var dLon = toRad(lon2 - lon1);

  // Hoekafstand (radialen) via de bolvormige cosinusregel
  var cosD = Math.sin(phi1) * Math.sin(phi2) +
             Math.cos(phi1) * Math.cos(phi2) * Math.cos(dLon);
  cosD = Math.max(-1, Math.min(1, cosD)); // clamp tegen floating-point drift
  var dist = Math.acos(cosD);

  // Bearing (richting) vanaf punt 1 naar punt 2
  var y = Math.sin(dLon) * Math.cos(phi2);
  var x = Math.cos(phi1) * Math.sin(phi2) -
          Math.sin(phi1) * Math.cos(phi2) * Math.cos(dLon);
  var bearing = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;

  return { distance: dist, bearing: bearing }; // dist in radialen, bearing in graden
}

// Projectie naar SVG-pixel: afstand→radius (lineair), bearing→hoek
function transformPoint(lat, lon, centerLat, centerLon, mapRadius) {
  var gc = greatCircle(centerLat, centerLon, lat, lon);
  var r = (gc.distance / Math.PI) * mapRadius;      // 180° = mapRadius pixels
  var theta = (gc.bearing - 90) * Math.PI / 180;    // 0° boven, kloksgewijs
  return {
    x: mapCenterX + r * Math.cos(theta),
    y: mapCenterY + r * Math.sin(theta)
  };
}

3. De grayline — dag/nacht-grens

De grayline toont waar het op dit moment schemering is. Deze grens is enorm belangrijk voor kortegolfcommunicatie: op de schemeringszone is de ionosfeer in een overgangstoestand waardoor HF-signalen vaak verrassend ver reiken (het bekende grayline-propagatie-effect). De berekening gebruikt het NOAA-zonpositie-algoritme om de exacte positie van de zon (het subsolaire punt) te bepalen, en tekent daaromheen een cirkel van 90° rond het antipodische punt — de terminator.

// Subsolair punt (waar de zon recht boven staat) op basis van UTC-tijd
function subsolarPoint(date) {
  var toRad = function(d) { return d * Math.PI / 180; };
  var jd = (date.getTime() / 86400000) + 2440587.5; // Juliaanse dag
  var n = jd - 2451545.0;                            // dagen sinds J2000

  var L = (280.460 + 0.9856474 * n) % 360;            // gemiddelde ecliptische lengte
  var g = toRad((357.528 + 0.9856003 * n) % 360);     // gemiddelde anomalie
  var lambda = toRad(L + 1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g));
  var epsilon = toRad(23.439 - 0.0000004 * n);        // schuinte ecliptica

  var decl = Math.asin(Math.sin(epsilon) * Math.sin(lambda)) * 180 / Math.PI; // zon-breedtegraad

  var gmst = (280.46061837 + 360.98564736629 * n) % 360;
  var raSun = Math.atan2(Math.cos(epsilon) * Math.sin(lambda), Math.cos(lambda)) * 180 / Math.PI;
  var lon = ((raSun - gmst) % 360 + 540) % 360 - 180; // zon-lengtegraad (subsolair punt)

  return { lat: decl, lon: lon };
}

// Terminator: alle punten op 90° hoekafstand van het antipode van de zon
function terminatorPoints(sun, stepDeg) {
  var antiLat = -sun.lat, antiLon = (sun.lon + 180 + 360) % 360 - 180;
  var pts = [];
  for (var a = 0; a < 360; a += stepDeg) {
    var angle = a * Math.PI / 180, fp = 90 * Math.PI / 180;
    var lat = Math.asin(Math.sin(toRad(antiLat)) * Math.cos(fp) +
      Math.cos(toRad(antiLat)) * Math.sin(fp) * Math.cos(angle)) * 180 / Math.PI;
    var lon = antiLon + Math.atan2(
      Math.sin(angle) * Math.sin(fp) * Math.cos(toRad(antiLat)),
      Math.cos(fp) - Math.sin(toRad(antiLat)) * Math.sin(toRad(lat))
    ) * 180 / Math.PI;
    pts.push({ lat: lat, lon: lon });
  }
  return pts;
}

4. Point-and-click: klikken om te centreren

Je kunt op de kaart klikken om deze opnieuw te centreren op die locatie. Technisch is dit de inverse van de projectieberekening: vanaf een pixelpositie wordt terugberekend welke lat/lon daarbij hoort. Een subtiele valkuil: een SVG met een viewBox schaalt anders dan de werkelijke pixelgrootte op scherm, dus een klik moet geschaald worden naar SVG-coordinaten om correct te centreren.

function onMapClick(event) {
  var rect = svg.getBoundingClientRect();

  // KRITIEK: schaal display-pixels terug naar SVG-interne coordinaten
  // (1024 = de SVG viewBox-breedte, rect.width = werkelijke schermgrootte)
  var svgX = (event.clientX - rect.left) * (1024 / rect.width);
  var svgY = (event.clientY - rect.top)  * (1024 / rect.height);

  var dx = svgX - mapCenterX, dy = svgY - mapCenterY;
  var r = Math.sqrt(dx * dx + dy * dy);
  var theta = Math.atan2(dy, dx);

  // Inverse van de projectie: pixel-afstand → hoekafstand → lat/lon
  var dist = (r / mapRadius) * Math.PI;      // radialen
  var bearing = theta + Math.PI / 2;          // graden-equivalent terugrekenen

  var lat2 = Math.asin(Math.sin(toRad(centerLat)) * Math.cos(dist) +
    Math.cos(toRad(centerLat)) * Math.sin(dist) * Math.cos(bearing));
  var lon2 = toRad(centerLon) + Math.atan2(
    Math.sin(bearing) * Math.sin(dist) * Math.cos(toRad(centerLat)),
    Math.cos(dist) - Math.sin(toRad(centerLat)) * Math.sin(lat2));

  setLocation(lat2 * 180 / Math.PI, lon2 * 180 / Math.PI);
  rebuildMap(); // kaart hertekenen met nieuw centrum
}

5. Satellietposities — SGP4

Voor de satelliettracking (ISS, QO-100 en andere amateur-satellieten) gebruik ik de echte SGP4/SDP4-baanpropagator — hetzelfde algoritme dat NASA en Celestrak gebruiken. Een versimpeld model (mean anomaly + inclinatie) gaf aantoonbaar foute posities. Baangegevens (TLE’s) worden live opgehaald van Celestrak, want een verouderde TLE drift al na een paar uur meerdere graden af van de werkelijke positie.

// satellite.js library — SGP4 propagatie
function getSatPos(name, date) {
  if (!satRecCache[name]) {
    var tle = tleData[name];
    satRecCache[name] = satellite.twoline2satrec(tle[0], tle[1]);
  }
  var pv = satellite.propagate(satRecCache[name], date);
  if (!pv || !pv.position) return null;

  var gst = satellite.gstime(date);
  var geo = satellite.eciToGeodetic(pv.position, gst);
  return {
    lat: geo.latitude  * 180 / Math.PI,
    lon: geo.longitude * 180 / Math.PI,
    h:   geo.height
  };
}

// Verse TLE ophalen bij elke satelliet-selectie (TLE's verouderen snel)
function fetchTLE(name, norad, callback) {
  var url = 'https://celestrak.org/NORAD/elements/gp.php?CATNR=' + norad + '&FORMAT=TLE';
  fetch(url).then(function(r) { return r.text(); }).then(function(data) {
    var lines = data.split('\\n');
    tleData[name] = [lines[1].trim(), lines[2].trim()];
    delete satRecCache[name]; // forceer her-initialisatie van SGP4
    callback();
  });
}

Het resultaat

Het eindresultaat is een schone, donker-getinte interactieve kaart met locator/coordinaten-invoer, live grayline-overlay, klik-om-te-centreren, satelliettracking, space weather-gegevens en PDF-export in kleur (A4/A3) — volledig client-side, geen server of iframe nodig, direct geintegreerd in WordPress.

← Terug naar blog