Working drone map solution with up to date data
This commit is contained in:
214
nats-worker.js
Normal file
214
nats-worker.js
Normal file
@@ -0,0 +1,214 @@
|
||||
'use strict';
|
||||
|
||||
// ── NATS AIXM 5.1 XML parser Web Worker ──────────────────────────────────────
|
||||
// Runs off the main thread to avoid freezing the UI on the 72 MB file.
|
||||
// Accepts: { buffer: ArrayBuffer }
|
||||
// Returns: { type:'progress', percent } or { type:'done', geojson } or { type:'error', message }
|
||||
|
||||
self.onmessage = function (e) {
|
||||
try {
|
||||
const text = new TextDecoder('utf-8').decode(e.data.buffer);
|
||||
const geojson = parseAIXM(text);
|
||||
self.postMessage({ type: 'done', geojson });
|
||||
} catch (err) {
|
||||
self.postMessage({ type: 'error', message: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// ── Airspace types relevant to drone operations ───────────────────────────────
|
||||
// FIR excluded: it is one giant polygon covering all UK airspace — useless on a drone map.
|
||||
const RELEVANT_TYPES = new Set([
|
||||
'R', // Restricted
|
||||
'P', // Prohibited
|
||||
'CTR', // Control zone (immediately around aerodromes)
|
||||
'CTA', // Control area
|
||||
'TMA', // Terminal manoeuvring area
|
||||
'RAS', // Radar Advisory Service area
|
||||
'OTHER:TMZ', // Transponder Mandatory Zone
|
||||
// D / D_OTHER (Danger) excluded — UK data contains enormous small-arms ranges
|
||||
// that cover huge swathes of the country and swamp the map. Danger areas are
|
||||
// covered separately via the user-loaded GeoJSON layer.
|
||||
]);
|
||||
|
||||
// ── Main parse function ───────────────────────────────────────────────────────
|
||||
function parseAIXM(text) {
|
||||
const features = [];
|
||||
|
||||
// Slice the document into hasMember chunks — avoids running complex regex
|
||||
// over the full 72 MB string all at once.
|
||||
const members = text.split('</message:hasMember>');
|
||||
const total = members.length;
|
||||
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const member = members[i];
|
||||
|
||||
// Progress every 250 chunks
|
||||
if (i % 250 === 0) {
|
||||
self.postMessage({ type: 'progress', percent: Math.round((i / total) * 100) });
|
||||
}
|
||||
|
||||
// Fast bail-out: only process members that contain an Airspace element
|
||||
if (!member.includes('<aixm:Airspace ')) continue;
|
||||
|
||||
// Extract the Airspace element from within this member
|
||||
const start = member.indexOf('<aixm:Airspace ');
|
||||
if (start === -1) continue;
|
||||
|
||||
// Only process BASELINE timeslices
|
||||
if (!member.includes('<aixm:interpretation>BASELINE</aixm:interpretation>')) continue;
|
||||
|
||||
const block = member.substring(start);
|
||||
|
||||
// ── Properties ──────────────────────────────────────────────────────────
|
||||
// type is within AirspaceTimeSlice; take the first match
|
||||
const typeM = block.match(/<aixm:type>([^<]+)<\/aixm:type>/);
|
||||
if (!typeM) continue;
|
||||
const type = typeM[1].trim();
|
||||
if (!RELEVANT_TYPES.has(type)) continue;
|
||||
|
||||
const name = first(block, /<aixm:name>([^<]+)<\/aixm:name>/);
|
||||
const designator = first(block, /<aixm:designator>([^<]+)<\/aixm:designator>/);
|
||||
const localType = first(block, /<aixm:localType>([^<]+)<\/aixm:localType>/);
|
||||
const upperLimit = first(block, /<aixm:upperLimit[^>]*>([^<]+)<\/aixm:upperLimit>/);
|
||||
const upperUom = first(block, /<aixm:upperLimit uom="([^"]+)"/);
|
||||
const upperRef = first(block, /<aixm:upperLimitReference>([^<]+)<\/aixm:upperLimitReference>/);
|
||||
const lowerLimit = first(block, /<aixm:lowerLimit[^>]*>([^<]+)<\/aixm:lowerLimit>/);
|
||||
const lowerUom = first(block, /<aixm:lowerLimit uom="([^"]+)"/);
|
||||
const lowerRef = first(block, /<aixm:lowerLimitReference>([^<]+)<\/aixm:lowerLimitReference>/);
|
||||
|
||||
// ── Geometry ─────────────────────────────────────────────────────────────
|
||||
const geometry = parseGeometry(block);
|
||||
if (!geometry) continue;
|
||||
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
properties: { name, designator, localType, type, upperLimit, upperUom, upperRef, lowerLimit, lowerUom, lowerRef },
|
||||
geometry,
|
||||
});
|
||||
}
|
||||
|
||||
self.postMessage({ type: 'progress', percent: 100 });
|
||||
return { type: 'FeatureCollection', features };
|
||||
}
|
||||
|
||||
// ── Geometry parser ───────────────────────────────────────────────────────────
|
||||
function parseGeometry(block) {
|
||||
// Find the horizontal projection Surface
|
||||
const sStart = block.indexOf('<aixm:Surface ');
|
||||
if (sStart === -1) return null;
|
||||
const sEnd = block.indexOf('</aixm:Surface>', sStart);
|
||||
if (sEnd === -1) return null;
|
||||
const surface = block.substring(sStart, sEnd + 15);
|
||||
|
||||
const ring = [];
|
||||
|
||||
// Walk through each curveMember
|
||||
const cmRe = /<gml:curveMember[^>]*>([\s\S]*?)<\/gml:curveMember>/g;
|
||||
let cm;
|
||||
while ((cm = cmRe.exec(surface)) !== null) {
|
||||
const seg = cm[1];
|
||||
appendSegment(seg, ring);
|
||||
}
|
||||
|
||||
if (ring.length < 3) return null;
|
||||
|
||||
// Close the ring
|
||||
const first0 = ring[0], last = ring[ring.length - 1];
|
||||
if (first0[0] !== last[0] || first0[1] !== last[1]) ring.push([...first0]);
|
||||
|
||||
return { type: 'Polygon', coordinates: [ring] };
|
||||
}
|
||||
|
||||
function appendSegment(seg, ring) {
|
||||
if (seg.includes('<gml:CircleByCenterPoint')) {
|
||||
// ── Full circle ──────────────────────────────────────────────────────────
|
||||
const posM = seg.match(/<gml:pos[^>]*>([\d.\-]+)\s+([\d.\-]+)<\/gml:pos>/);
|
||||
const radM = seg.match(/<gml:radius[^>]*>([\d.\-]+)<\/gml:radius>/);
|
||||
const uomM = seg.match(/<gml:radius uom="([^"]+)"/);
|
||||
if (!posM || !radM) return;
|
||||
const lat = parseFloat(posM[1]), lon = parseFloat(posM[2]);
|
||||
const km = toKm(parseFloat(radM[1]), uomM ? uomM[1] : '[nmi_i]');
|
||||
ring.push(...circleCoords(lon, lat, km, 64));
|
||||
|
||||
} else if (seg.includes('<gml:ArcByCenterPoint')) {
|
||||
// ── Arc segment ──────────────────────────────────────────────────────────
|
||||
// GML angles: counterclockwise from east (math convention).
|
||||
// Bearing = (90 - mathAngle + 360) % 360
|
||||
const posM = seg.match(/<gml:pos[^>]*>([\d.\-]+)\s+([\d.\-]+)<\/gml:pos>/);
|
||||
const radM = seg.match(/<gml:radius[^>]*>([\d.\-]+)<\/gml:radius>/);
|
||||
const uomM = seg.match(/<gml:radius uom="([^"]+)"/);
|
||||
const startM = seg.match(/<gml:startAngle[^>]*>([\d.\-]+)<\/gml:startAngle>/);
|
||||
const endM = seg.match(/<gml:endAngle[^>]*>([\d.\-]+)<\/gml:endAngle>/);
|
||||
if (!posM || !radM || !startM || !endM) return;
|
||||
const lat = parseFloat(posM[1]), lon = parseFloat(posM[2]);
|
||||
const km = toKm(parseFloat(radM[1]), uomM ? uomM[1] : '[nmi_i]');
|
||||
const sa = parseFloat(startM[1]), ea = parseFloat(endM[1]);
|
||||
ring.push(...arcCoords(lon, lat, km, sa, ea, 32));
|
||||
|
||||
} else {
|
||||
// ── GeodesicString / LineStringSegment ───────────────────────────────────
|
||||
// gml:posList: flat sequence "lat1 lon1 lat2 lon2 ..."
|
||||
const plM = seg.match(/<gml:posList[^>]*>([\s\S]*?)<\/gml:posList>/);
|
||||
if (plM) {
|
||||
const vals = plM[1].trim().split(/\s+/);
|
||||
for (let i = 0; i + 1 < vals.length; i += 2) {
|
||||
ring.push([parseFloat(vals[i + 1]), parseFloat(vals[i])]);
|
||||
}
|
||||
} else {
|
||||
// Individual gml:pos elements inside pointProperty/Point
|
||||
const posRe = /<gml:pos[^>]*>([\d.\-]+)\s+([\d.\-]+)<\/gml:pos>/g;
|
||||
let pt;
|
||||
while ((pt = posRe.exec(seg)) !== null) {
|
||||
ring.push([parseFloat(pt[2]), parseFloat(pt[1])]); // flip to [lon, lat]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometry utilities ────────────────────────────────────────────────────────
|
||||
function toKm(value, uom) {
|
||||
if (uom === '[nmi_i]') return value * 1.852;
|
||||
if (uom === 'M') return value / 1000;
|
||||
if (uom === 'KM') return value;
|
||||
return value * 1.852; // assume nmi
|
||||
}
|
||||
|
||||
// Great-circle destination point given start (lat/lon in degrees), bearing (deg CW from N), distance (km)
|
||||
function destPoint(lat, lon, bearingDeg, distKm) {
|
||||
const R = 6371;
|
||||
const d = distKm / R;
|
||||
const b = bearingDeg * Math.PI / 180;
|
||||
const φ1 = lat * Math.PI / 180;
|
||||
const λ1 = lon * Math.PI / 180;
|
||||
const φ2 = Math.asin(Math.sin(φ1) * Math.cos(d) + Math.cos(φ1) * Math.sin(d) * Math.cos(b));
|
||||
const λ2 = λ1 + Math.atan2(Math.sin(b) * Math.sin(d) * Math.cos(φ1), Math.cos(d) - Math.sin(φ1) * Math.sin(φ2));
|
||||
return [λ2 * 180 / Math.PI, φ2 * 180 / Math.PI]; // [lon, lat] for GeoJSON
|
||||
}
|
||||
|
||||
function circleCoords(lon, lat, radiusKm, steps) {
|
||||
const pts = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
pts.push(destPoint(lat, lon, (i / steps) * 360, radiusKm));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
function arcCoords(lon, lat, radiusKm, startMathDeg, endMathDeg, steps) {
|
||||
// GML CCW-from-east → sweep counterclockwise; always positive
|
||||
let sweep = endMathDeg - startMathDeg;
|
||||
if (sweep <= 0) sweep += 360;
|
||||
|
||||
const pts = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const mathAngle = startMathDeg + (sweep * i / steps);
|
||||
const bearing = ((90 - mathAngle) % 360 + 360) % 360; // CW from N
|
||||
pts.push(destPoint(lat, lon, bearing, radiusKm));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// ── Regex helper ──────────────────────────────────────────────────────────────
|
||||
function first(text, re) {
|
||||
const m = text.match(re);
|
||||
return m ? m[1].trim() : '';
|
||||
}
|
||||
Reference in New Issue
Block a user