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
+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){