De Azimuth- en Grayline-tool: van idee tot interactieve kaart
21 August 2026De 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
| Onderdeel | Bron |
| Inspiratie/referentie-UX | NS6T Azimuth & AzShadowMap tools |
| Landgrenzen/kaartdata | Natural Earth Data (CC0, publiek domein) |
| Zonpositie-algoritme | NOAA Solar Calculator (Jean Meeus-methode) |
| Satelliet-posities | satellite.js — SGP4/SDP4-propagator (NASA/Celestrak methode) |
| Live TLE-baangegevens | Celestrak.org |
| Actuele tijd (UTC) | worldtimeapi.org (NTP-gesynchroniseerd) |
| Space weather (SFI/SN/K-index) | NOAA Space Weather Prediction Center |
| PDF-generatie | jsPDF (client-side, browser) |
| Kaartrendering | D3.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.