From ecb4d57c6e83610c5def82c6207012c84bc173ee Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Sat, 13 Nov 2021 16:48:33 +1100 Subject: [PATCH] Aerodrome and bay APIs --- TODO.md | 85 +++++++++++++++++++++++++++++++++++++++++++-- aerodrome.js | 26 +++++++++++--- atc.js | 64 ++++++++++++++++++++++++++-------- client.js | 53 ++++++++++++++++++++-------- config/default.json | 3 +- package.json | 3 ++ public/map.js | 84 +++++++++++++++++++++++++++++++++++--------- server.js | 11 +++++- 8 files changed, 273 insertions(+), 56 deletions(-) diff --git a/TODO.md b/TODO.md index bae9642..b9ea2ca 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,35 @@ ## TODO -### Feedback +* Map the on the ground users. +* Make major AD's permanently shown. +* Join AFV freqs with sectors to show online. +* Fix the on the ground altitude maths +* Make light theme AD parts darker +* Markers and labels are too small on the ground. +* Query OSM via Overpass API for aeroway=parking_position +// Get aerodrome polygon +// if aeroway=parking_position not undefined +// bay = aeroway=parking_position ref +Nah - Use https://overpass-turbo.eu/# to pull the features geojson into a mapbox tileset for a quick and dirty option. +Expand client.js to enrich pilots API + +// Boarding - On aerodrome, on apron poly. 0 GS +// Departing - On aerodrome +// Enroute - Off aerodrome +// Arriving - On aerodrome +// Arrived - On aerodrome, on apron poly. 0 GS + -* Add FL and GS to labels aka datatag +Ideas +Airspace map - sector map for Zach. +https://vatpac.org/controllers/airspace/ + +Add CID search to local + +### Feedback + +* Add FL and GS to labels aka datatag to labels * Instrument API response times and request times from VATSIM to track down loading delays @@ -13,6 +39,22 @@ ## Future +### OSM mapping + +Aerodromes missing aeroway=parking_positions: + +Major +* YPDN - Done +* YBCS +* YPAD +* YPPH + +Metro +* YBAF +* YSBK +* YMAV +* YPPF + ### Features * Theme switch light / dark @@ -31,9 +73,33 @@ * Use nav API for progressive taxi or draw on ground map routes * Use GS and HDG to animate markers between refresh +Query tilesets to get "in poly" for ATC on aerodromes.https://docs.mapbox.com/help/tutorials/find-elevations-with-tilequery-api/ +https://docs.mapbox.com/mapbox-gl-js/api/map/#map#querysourcefeatures +https://docs.mapbox.com/mapbox-gl-js/example/filter-features-within-map-view/ +https://docs.mapbox.com/mapbox-gl-js/example/query-similar-features/ + +Stretch goal enrich the pilot locations API for aerodrome reporting board with where the acft is +WAT499 YSSY YMML Boarding gate 54 +WAT499 YSSY YMML Taxiing on C GS 15 +WAT499 YSSY YMML Departed + +* Use mapbox 3D elements for dynamic ATC poly visualizations https://blog.mapbox.com/dive-into-large-datasets-with-3d-shapes-in-mapbox-gl-c89023ef291 + + ### Tech debt +* What is going wrong with test imports? * Retest the xmlToPoly client with a single Line XML file +* Retest text and icon scaling on low res PC, mobile +* Add GS in hundreds via API for display +* Expose alt format via API for display +* Add instrumentation to capture iteration cost https://dev.to/typescripttv/measure-execution-times-in-browsers-node-js-js-ts-1kik +* Implement URL discovery via https://status.vatsim.net/status.json for https://github.com/vatsimnetwork/developer-info/wiki/Data-Feeds + +### Feedback + +Possible to see C steps. Import LL labels markers. +TMA splits ### Current stats In FIR now @@ -41,6 +107,7 @@ Top types Arr / dep counts ### Stored stats +Is using a DB going to be worth it? Start tracking what needs it. Eg ENR time and track. Persist objects in memory for a few hours? Traffic heatmap (DB needed) ## Credits @@ -50,6 +117,9 @@ https://commons.wikimedia.org/wiki/File:Plane_font_awesome.svg public/flaticon.com/ga-*.png
Icons made by Freepik from www.flaticon.com
+Openstreet map data +The data included in this document is from www.openstreetmap.org. The data is made available under ODbL + ## Theme Green 33cc99 A10 @@ -69,4 +139,13 @@ https://docs.mapbox.com/mapbox-gl-js/example/measure/ https://docs.mapbox.com/mapbox-gl-js/example/set-popup/ Change markers on zoom level. Would require iterating markers -https://docs.mapbox.com/mapbox-gl-js/example/updating-choropleth/ \ No newline at end of file +https://docs.mapbox.com/mapbox-gl-js/example/updating-choropleth/ + +Sectors.xml +Meta -> Volumes +Sector -> Volumes(VolumeName) + +Volumes.xml +Meta -> Line +Volume(Name) -> Boundaries(Name) +Volume(ARA) -> Boundaries(ARAFURA) \ No newline at end of file diff --git a/aerodrome.js b/aerodrome.js index fd699fd..72c0571 100644 --- a/aerodrome.js +++ b/aerodrome.js @@ -1,4 +1,4 @@ -import { getOSMAerodromeData } from './client.js'; +import { getOSMAerodromeData, getOSMParkingPositionData } from './client.js'; import { point, featureCollection } from '@turf/helpers'; import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; import bunyan from 'bunyan'; @@ -8,10 +8,10 @@ var log = bunyan.createLogger({name: config.get('app.name'), level: config.get(' export async function getAerodromes(){ try{ - var aerodromes = await getOSMAerodromeData(config.get('data.osm.aerodromesArea')); - if(aerodromes){ - if(aerodromes.type === "FeatureCollection"){ - return aerodromes; + var data = await getOSMAerodromeData(config.get('data.osm.aerodromesArea')); + if(data){ + if(data.type === "FeatureCollection"){ + return data; }else{ return false; } @@ -34,4 +34,20 @@ export async function getMajorAerodromes(){ } }); }); +} + +export async function getAerodromeBays(){ + try{ + var data = await getOSMParkingPositionData(config.get('data.osm.aerodromesArea')); + if(data){ + if(data.type === "FeatureCollection"){ + return data; + }else{ + return false; + } + }; + }catch(err){ + log.error(err) + return false; + } } \ No newline at end of file diff --git a/atc.js b/atc.js index d489089..1d66a54 100644 --- a/atc.js +++ b/atc.js @@ -135,6 +135,24 @@ function uniq(a) { return Array.from(new Set(a)); } +function getSectorByName(sectorName, sectors){ + var sector = sectors.find(e => { + if(e.Name === sectorName){ + return e; + }; + }); + return sector; +} + +function getSectorByCallsign(sectorName, sectors){ + var sector = sectors.find(e => { + if(e.Callsign === sectorName){ + return e; + }; + }); + return sector; +} + function mergeSectors(sector, sectors, json){ let mergedSector; // FSS don't have sector volumes, only responsible sectors. @@ -237,36 +255,54 @@ export async function getOnlinePositions() { // SY_APP 124.400 AFV 124400000 // iterate txvrs.element.transceivers.element frequency/1000000 stations.forEach(function(station, index){ - var activePosition = {}; + var activePosition = false; + // Keep only CTR, APP, and TWR if(station.callsign.toUpperCase().includes("CTR") === false && station.callsign.toUpperCase().includes("APP") === false && station.callsign.toUpperCase().includes("TWR") === false){ delete stations[index]; }else{ - var txvrs = []; - var frequency = []; + var activeFrequncies = []; + // Transform frequencies array station.transceivers.forEach(function(element){ // Hertz to Megahurts element.frequency = element.frequency/1000000; - txvrs.push(element); + activeFrequncies.push(element.frequency); }) - frequency = uniq(txvrs); + activeFrequncies = uniq(activeFrequncies); + // Join sectors by callsign var sector = sectors.find(function cb(element){ if(element.Callsign === station.callsign){ - return element; + // Check std sectors and load sub sectors. + if(element.standard_position === true){ + var sectorWithSubsectors = mergeSectors(element, element.responsibleSectors,sectors); + return sectorWithSubsectors; + }else{ + return element; + } }; }); if(sector !== undefined){ onlineSectors.push(mergeBoundaries(sector)); activePosition = mergeBoundaries(sector); } - // Join sectors by frequency - // TODO - Only if adjacent to match vatpac extending policy. - txvrs.forEach(function(element){ - var adjacentSector = isAdjacentSector(element.frequency, activePosition, sectors); - if(adjacentSector !== false){ - onlineSectors.push(mergeBoundaries(adjacentSector)) - } - }) + + if(activePosition !== false){ + // Join sectors by frequency + // TODO - How to incrementally add sectors working outwards from the logged on sector? + var extendedPoly = activePosition; + activeFrequncies.forEach(function(element){ + var adjacentSector = isAdjacentSector(element, extendedPoly, sectors); + if(adjacentSector !== false){ + extendedPoly = unionArray([extendedPoly, sectorWithSubsectors]) + if(adjacentSector.standard_position === true){ + var sectorWithSubsectors = mergeSectors(adjacentSector, adjacentSector.responsibleSectors,sectors); + onlineSectors.push(sectorWithSubsectors); + }else{ + onlineSectors.push(mergeBoundaries(adjacentSector)); + } + } + }) + } } }) diff --git a/client.js b/client.js index c333873..418b1e2 100644 --- a/client.js +++ b/client.js @@ -9,6 +9,7 @@ import config from 'config'; import { iso2dec } from './iso2dec.js'; import {Mutex, Semaphore, withTimeout} from 'async-mutex'; import uniqueRandomArray from 'unique-random-array'; +import sha1 from 'sha1'; var log = bunyan.createLogger({name: config.get('app.name'), level: config.get('app.log_level')}); @@ -108,31 +109,55 @@ async function getVatsimServers(){ export async function getOSMAerodromeData (areaName) { log.info(`getOSMAerodromeData`); + var data = await queryOverpass( + `area["name"="${areaName}"]->.boundaryarea; + ( + nwr(area.boundaryarea)["aeroway"="aerodrome"]; + ); + out body; + >; + out skel qt;` + ) + return data; +} + +export async function getOSMParkingPositionData (areaName) { + log.info(`getOSMParkingPositionData`); + var data = await queryOverpass( + `area["name"="${areaName}"]->.boundaryarea; + ( + nwr(area.boundaryarea)["aeroway"="parking_position"]; + ); + out body; + >; + out skel qt;` + ) + return data; +} + +export async function queryOverpass (query) { + var cachekey = sha1(query); + log.info(`queryOverpass ${cachekey}`); + log.info(query); var data = await mutex.runExclusive(async () => { - log.info(`mutex locked`); - var ttlMs = cache.getTtl(areaName); + log.info(`mutex locked ${cachekey}`); + var ttlMs = cache.getTtl(cachekey); let data; if (ttlMs == undefined || ttlMs - Date.now() <= 120000) { - log.info(`Querying OSM`); + log.info(`Querying OSM ${cachekey}`); try{ data = await query_overpass( - `area["name"="${areaName}"]->.boundaryarea; - ( - nwr(area.boundaryarea)["aeroway"="aerodrome"]; - ); - out body; - >; - out skel qt;`, + query, function(err, data){ if(err){ log.error(err); }else{ log.info({ cache: 'set', - area: areaName, + cachekey: cachekey, keys: Object.keys(data).length }) - cache.set(areaName, data, 86400); + cache.set(cachekey, data, 86400); } }, { overpassUrl: config.get('data.osm.overpassUrl'), userAgent: `${config.get('app.name')}/${config.get('app.version')}` } @@ -144,10 +169,10 @@ export async function getOSMAerodromeData (areaName) { } }else{ log.info(`Return cached OSM response`); - data = cache.get(areaName); + data = cache.get(cachekey); log.info({ cache: 'get', - area: areaName, + cachekey: cachekey, keys: Object.keys(data).length }) } diff --git a/config/default.json b/config/default.json index 50290b9..1b65131 100644 --- a/config/default.json +++ b/config/default.json @@ -26,10 +26,9 @@ "BN-KEN_CTR", "BN-ISA_CTR", "BN-INL_CTR", - "BN-ARM_CTR", + "BN-ARL_CTR", "ML-OLW_CTR", "ML-ASP_CTR", - "ML-ARM_CTR", "ML-TBD_CTR", "ML-PIY_CTR", "ML-BIK_CTR", diff --git a/package.json b/package.json index 80e7785..61105c6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "private": true, "dependencies": { "@adobe/node-fetch-retry": "^1.1.1", + "@mapbox/polylabel": "^1.0.2", + "@turf/boolean-intersects": "^6.5.0", "@turf/turf": "^6.5.0", "async-mutex": "^0.3.2", "bunyan": "^1.8.15", @@ -17,6 +19,7 @@ "node-fetch": "^2.6.1", "query-overpass": "https://github.com/Kahn/query-overpass.git", "rgb2hex": "^0.2.5", + "sha1": "^1.1.1", "unique-random-array": "^3.0.0", "xml-js": "^1.6.11" }, diff --git a/public/map.js b/public/map.js index fd80fa9..82e0e1e 100644 --- a/public/map.js +++ b/public/map.js @@ -270,38 +270,88 @@ async function getATCSectors() { try{ var response = await fetch(`${window.location.protocol}//${window.location.hostname}:${window.location.port}/v1/atc/online`); var json = await response.json(); - + var ctrs = []; + var tmas = []; + var twrs = []; - map.addSource('atcSectors', { - 'type': 'geojson', - 'data': json - }); - // Add a new layer to visualize the polygon. - map.addLayer({ - 'id': 'atcSectors', - 'type': 'fill', - 'source': 'atcSectors', // reference the data source - 'layout': {}, - 'paint': { - 'fill-color': '#3b8df9', // blue color fill - 'fill-opacity': 0.1 - } + // Split CTR, TMA, and TWRs + json.features.forEach(function(e){ + console.log(e.properties.Callsign) + if(e.properties.Callsign.includes("CTR")){ + console.log(e) + ctrs.push(e); + } + if(e.properties.Callsign.includes("APP")){ + console.log(e) + tmas.push(e); + } + if(e.properties.Callsign.includes("TWR")){ + twrs.push(e); + } }); + + map.addSource('atcCtrs', { + 'type': 'geojson', + 'data': turf.featureCollection(ctrs) + }); + map.addSource('atcTmas', { + 'type': 'geojson', + 'data': turf.featureCollection(tmas) + }); + map.addSource('atcTwrs', { + 'type': 'geojson', + 'data': turf.featureCollection(twrs) + }); + // Add a new layer to visualize the polygon. + // map.addLayer({ + // 'id': 'atcSectors', + // 'type': 'fill', + // 'source': 'atcCtrs', // reference the data source + // 'layout': {}, + // 'paint': { + // 'fill-color': '#3b8df9', // blue color fill + // 'fill-opacity': 0.1 + // } + // }); // Add a black outline around the polygon. map.addLayer({ 'id': 'atcOutline', 'type': 'line', - 'source': 'atcSectors', + 'source': 'atcCtrs', 'layout': {}, 'paint': { 'line-color': '#3b8df9', 'line-width': 2 } }); + map.addLayer({ + 'id': 'tmaLine', + 'type': 'line', + 'source': 'atcTmas', + 'layout': {}, + 'minzoom': 5, + 'paint': { + 'line-color': "#33cc99", + 'line-width': 3, + 'line-dasharray': [5, 5] + } + }); + map.addLayer({ + 'id': 'twrLine', + 'type': 'line', + 'source': 'atcTwrs', + 'layout': {}, + 'minzoom': 5, + 'paint': { + 'line-color': "#3b8df9", + 'line-width': 3, + 'line-dasharray': [1, 1] + } + }); // // Add sector labels var atcLabelPoints = []; json.features.forEach(function(e){ - console.log(e) + // console.log(e) atcLabelPoints.push(turf.centroid(e)); }); console.log(atcLabelPoints); diff --git a/server.js b/server.js index 1b17480..e918ab3 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,7 @@ import cors from 'cors'; import { clearCache, cacheStats } from './client.js'; import {getPilots} from './pilots.js'; import { getATCSectors, getCoastline, getColours, getOnlinePositions } from './atc.js'; -import { getAerodromes, getMajorAerodromes } from './aerodrome.js'; +import { getAerodromes, getMajorAerodromes, getAerodromeBays } from './aerodrome.js'; import config from 'config'; import { getOSMAerodromeData } from './client.js'; import { getDataset } from './dataset.js'; @@ -84,6 +84,15 @@ app.get('/v1/aerodromes', cors(), async (req, res) => { } }); +app.get('/v1/aerodromes/bays', cors(), async (req, res) => { + var data = await getAerodromeBays(); + if(data == false){ + res.sendStatus(500); + }else{ + res.send(data) + } +}); + app.get('/v1/aerodromes/major', cors(), async (req, res) => { var data = await getMajorAerodromes(); if(data == false){