cloudlog/assets/js/qra-utils.js
Peter Goodhall 0f62a7178a Add client-side QRA utils and use in QSO
Introduce a frontend QRA utilities module and switch QSO locator/bearing/distance logic to run client-side. Added assets/js/qra-utils.js (ported from the PHP Qra library), injected measurement_base and station_gridsquares into the QSO view, and updated application/controllers/Qso.php to provide the measurement_base for the frontend. Footer now loads qra-utils.js before qso.js. Modified assets/js/sections/qso.js to debounce locator input, place map markers from local grid→lat/lng math, and compute bearing/distance via QraUtils (removes several AJAX calls to server endpoints). This reduces server round-trips and keeps frontend behavior consistent with the PHP implementation.
2026-03-29 13:57:36 +01:00

151 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* QRA (Maidenhead grid locator) utilities
* Ported from application/libraries/Qra.php — output is byte-for-byte identical.
*/
(function (global) {
'use strict';
function deg2rad(deg) { return deg * Math.PI / 180; }
function rad2deg(rad) { return rad * 180 / Math.PI; }
/**
* Convert a Maidenhead locator to [lat, lng].
* Supports 4-, 6-, 8-, 10-char grids and comma-separated multi-grids.
* Returns [lat, lng] or false on invalid input.
*/
function qra2latlong(strQRA) {
if (!strQRA || typeof strQRA !== 'string') return false;
strQRA = strQRA.replace(/\s+/g, '');
if (strQRA.indexOf(',') !== -1) {
var parts = strQRA.split(',');
if (parts.length === 4) {
var lengths = parts.map(function (p) { return p.length; });
if (lengths.some(function (l) { return l !== lengths[0]; })) return false;
var coords = [0, 0];
for (var i = 0; i < 4; i++) {
var c = qra2latlong(parts[i]);
if (!c) return false;
coords[0] += c[0];
coords[1] += c[1];
}
return [Math.round(coords[0] / 4), Math.round(coords[1] / 4)];
} else if (parts.length === 2) {
if (parts[0].length !== parts[1].length) return false;
var c0 = qra2latlong(parts[0]);
var c1 = qra2latlong(parts[1]);
if (!c0 || !c1) return false;
var lat = c0[0] !== c1[0]
? Math.round(((c0[0] + c1[0]) / 2) * 10) / 10
: Math.round(c0[0] * 10) / 10;
var lng = c0[1] !== c1[1]
? Math.round((c0[1] + c1[1]) / 2)
: Math.round(c0[1]);
return [lat, lng];
} else {
return false;
}
}
if (strQRA.length % 2 !== 0 || strQRA.length > 10) return [0, 0];
strQRA = strQRA.toUpperCase();
if (strQRA.length === 4) strQRA += 'LL';
if (strQRA.length === 6) strQRA += '55';
if (strQRA.length === 8) strQRA += 'LL';
if (!/^[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}$/.test(strQRA)) return false;
var ch = strQRA.split('');
var A = 'A'.charCodeAt(0);
var Z = '0'.charCodeAt(0);
var a = ch[0].charCodeAt(0) - A;
var b = ch[1].charCodeAt(0) - A;
var c = ch[2].charCodeAt(0) - Z;
var d = ch[3].charCodeAt(0) - Z;
var e = ch[4].charCodeAt(0) - A;
var f = ch[5].charCodeAt(0) - A;
var g = ch[6].charCodeAt(0) - Z;
var h = ch[7].charCodeAt(0) - Z;
var ii = ch[8].charCodeAt(0) - A;
var j = ch[9].charCodeAt(0) - A;
var nLong = (a * 20) + (c * 2) + (e / 12) + (g / 120) + (ii / 2880) - 180;
var nLat = (b * 10) + d + (f / 24) + (h / 240) + (j / 5760) - 90;
return [nLat, nLong];
}
/**
* Distance between two lat/lng points.
* unit: 'M' miles | 'K' kilometers | 'N' nautical miles
* Returns numeric distance rounded to 1 decimal (matches PHP distance()).
*/
function calcDistance(lat1, lon1, lat2, lon2, unit) {
var theta = lon1 - lon2;
var dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));
dist = Math.acos(Math.min(1, Math.max(-1, dist)));
dist = rad2deg(dist);
dist = dist * 60 * 1.1515;
if (unit === 'K') dist *= 1.609344;
else if (unit === 'N') dist *= 0.8684;
return Math.round(dist * 10) / 10;
}
/**
* Bearing in degrees (0359) from point 1 to point 2 (matches PHP get_bearing()).
*/
function getBearing(lat1, lon1, lat2, lon2) {
return (Math.round(rad2deg(Math.atan2(
Math.sin(deg2rad(lon2) - deg2rad(lon1)) * Math.cos(deg2rad(lat2)),
Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) -
Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(lon2) - deg2rad(lon1))
))) + 360) % 360;
}
/**
* Full bearing string matching PHP Qra::bearing() output exactly.
* e.g. "135° SE 245 kilometers"
* myGrid / theirGrid: Maidenhead locator strings
* unit: 'M' | 'K' | 'N'
*/
function bearingString(myGrid, theirGrid, unit) {
var my = qra2latlong(myGrid);
var stn = qra2latlong(theirGrid);
if (!my || !stn) return '';
var bearingDeg = getBearing(my[0], my[1], stn[0], stn[1]);
var dist = Math.round(calcDistance(my[0], my[1], stn[0], stn[1], unit || 'M')); // rounded to 0 d.p. — matches PHP
var dirs = ['N', 'E', 'S', 'W'];
var rounded = Math.round(bearingDeg / 22.5) % 16;
var dir;
if ((rounded % 4) === 0) {
dir = dirs[rounded / 4];
} else {
dir = dirs[2 * Math.floor(((Math.floor(rounded / 4) + 1) % 4) / 2)];
dir += dirs[1 + 2 * Math.floor(rounded / 8)];
}
var unitLabel = unit === 'K' ? ' kilometers' : unit === 'N' ? ' nautic miles' : ' miles';
return Math.round(bearingDeg) + '\u00B0 ' + dir + ' ' + dist + unitLabel;
}
/**
* Distance between two gridsquares in km (matches PHP searchdistance() which hardcodes 'K').
* Returns numeric value or null on invalid input.
*/
function distanceKm(myGrid, theirGrid) {
var my = qra2latlong(myGrid);
var stn = qra2latlong(theirGrid);
if (!my || !stn) return null;
return calcDistance(my[0], my[1], stn[0], stn[1], 'K');
}
global.QraUtils = {
qra2latlong: qra2latlong,
bearingString: bearingString,
distanceKm: distanceKm,
};
}(window));