Skip to content

Commit

Permalink
Initial online ATC with extended sectors in atc online API
Browse files Browse the repository at this point in the history
  • Loading branch information
Kahn committed Oct 14, 2021
1 parent 2d82c52 commit 5bc9864
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 157 deletions.
244 changes: 153 additions & 91 deletions atc.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { getLineFeatures, getXMLtoJS } from './client.js';
import { getLineFeatures, getXMLtoJS, getVatsimAFV } from './client.js';
import { polygon, featureCollection } from '@turf/helpers';
import bunyan from 'bunyan';
import config from 'config';
import { lineToPolygon, lineString } from '@turf/turf';
import {
lineToPolygon,
lineString,
booleanIntersects,
buffer,
featureEach,
union
} from '@turf/turf';
import { iso2dec } from './iso2dec.js';
import rgb2hex from 'rgb2hex';

Expand Down Expand Up @@ -84,94 +91,6 @@ export async function getATCSectors() {
}
sectors.push(sector);
});


// // Iterate sectors to create objects
// let volume;
// let sector;
// sectorsXml.Sectors.Sector.forEach(function(s){
// var sector = {
// ...s._attributes,
// standard_position: false,
// volumes: [],
// responsibleSectors: ("ResponsibleSectors" in s ? s.ResponsibleSectors._text.split(',') : [])
// };
// // console.log({sector: sector})
// // Iterate all Volumes to join with Sector
// var volumes = volumesXml.Volumes.Volume.filter(function(volume){
// // TODO - Iterate sector.Volumes._text.split(',') to match
// if(sector.Name === volume._attributes.Name){
// // console.log("Matched Sector to Volume")
// // console.log({volume: volume});
// var newVol = {
// ...volume._attributes,
// boundaries: []
// };
// // Get feature
// var volumeBoundaries = volume.Boundaries._text.split(','); // ARAFURA
// // Iterate volume boundaries to join Features to Sector volumes
// volumeBoundaries.forEach(function(volumeBoundary){
// // console.log({volumeBoundaries: volumeBoundary});
// var feature = volumeBoundaryFeatures.filter(function(feature){
// if(feature.properties.Name === volumeBoundary){
// // console.log("Matched feature to Boundary")
// // console.log({feature: volumeBoundary});
// newVol.boundaries.push(feature);
// };
// })
// });
// // Attach volume attributes to feature properties
// // Push feature
// sector.volumes.push(newVol);
// }
// // return volume._attributes.Name == sector.Name;
// })
// // var volumes = volumesXml.Volumes.Volume.forEach(function(volume){
// // console.log({volume: volume})
// // var features = [];
// // // Boundaries are CSV
// // var volumeBoundaries = volume.Boundaries._text.split(','); // ARAFURA
// // volumeBoundaries.forEach(function(volumeBoundary){
// // // Get matching Feature for boundary.
// // var feature = volumeBoundaryFeatures.find(obj => {
// // if (obj.properties.Name === volumeBoundary){
// // console.log('Match obj.properties.Name === volumeBoundary')
// // }
// // return false;
// // });
// // // sector.volumes.push(feature);
// // });
// // })
// if (config.get("map.sectors.standard").includes(sector.Callsign)){
// // console.log(`Standard pos ${element._attributes.Callsign}`);
// sector.standard_position = true;
// }
// sectors.push(sector);
// })
// // Iterate volume boundaries, add volume details to GeoJSON
// // volumes.Volumes.Volume.forEach(function(volume){
// // var vol = {

// // }
// // console.log(volume)
// // });

// // var match = sectors.Sectors.Sector.find((element) => {
// // // Attach sector details to sectors
// // if (element._attributes.FullName.toUpperCase() == volume.properties.Name){
// // volume.properties = element._attributes;
// // }
// // // Flag standard sectors
// // if (config.get("map.sectors.standard").includes(element._attributes.Callsign)){
// // // console.log(`Standard pos ${element._attributes.Callsign}`);
// // volume.properties.standard_position = true;
// // }
// // });

// // Union
// // https://github.com/Turfjs/turf/tree/master/packages/turf-union

// // Return FC
return sectors;
}

Expand Down Expand Up @@ -210,4 +129,147 @@ function getVolume(volumeName, volumesXml){
})
newVol.Boundaries = boundaryFeatures;
return newVol;
};
};

function uniq(a) {
return Array.from(new Set(a));
}

function mergeSectors(sector, sectors, json){
let mergedSector;
// FSS don't have sector volumes, only responsible sectors.
if(sector.volumes.length > 0){
mergedSector = mergeBoundaries(sector);
}
if(sectors.length > 0){
var sectorVolumes = [];
// Union all sector volumes into a polygon
sectors.forEach(function(e){
var sector = getSectorByName(e, json);
var poly = mergeBoundaries(sector);
sectorVolumes.push(poly);
});
// Add self and union all sector volumes
sectorVolumes.push(mergeBoundaries(sector));
var mergedPoly = unionArray(sectorVolumes);
mergedSector = mergedPoly;
}
if(mergedSector != undefined){
mergedSector.properties = {...sector};
delete mergedSector.properties.responsibleSectors
delete mergedSector.properties.volumes
return mergedSector;
}
}

function mergeBoundaries(sector){
var features = sector.volumes.map((volume) => volume.Boundaries.map((boundary) => boundary)).flat();
if(features.length > 1){
var union = unionArray(features);
union.properties = { ...sector };
delete union.properties.responsibleSectors;
delete union.properties.volumes;
return union;
}else if (features.length == 1){
features[0].properties = { ...sector };
delete features[0].properties.responsibleSectors;
delete features[0].properties.volumes;
return features[0];
}else{
return false;
}
}

function unionArray(array){
const bufferKm = 0.1;
var fc = featureCollection(array);
if (fc.length === 0) {
return null
}
// buffer is a dirty dirty hack to close up some holes in datasets
let ret = buffer(fc.features[0],bufferKm, {units: 'kilometers'});
// let ret = featureCollection.features[0];

featureEach(fc, function (currentFeature, featureIndex) {
if (featureIndex > 0) {
ret = union(ret, buffer(currentFeature,bufferKm, {units: 'kilometers'}))
// ret = turf.union(ret, currentFeature);
}
});

// Remove any holes added in union
ret.geometry.coordinates.length = 1;

return ret;
};

function isAdjacentSector(sectorFrequency, primarySector, sectors){
var adjacentSector = false;
try{
// Get sectors with matching frequency
var sectorsWithFreq = sectors.filter(function cb(sector){
if(parseFloat(sector.Frequency) === parseFloat(sectorFrequency)){
return sector;
};
});
sectorsWithFreq.forEach(function(sectorFrequencyMatch){
var sfmFreq = parseFloat(sectorFrequencyMatch.Frequency)
var sfFreq = parseFloat(sectorFrequency)
// Match freqs where not own sector freq
if(sfmFreq === sfFreq && sectorFrequencyMatch.Callsign != primarySector.properties.Callsign){
var sectorsIntersect = booleanIntersects(primarySector, mergeBoundaries(sectorFrequencyMatch));
if(sectorsIntersect){
// Return only sectors sharing a border
adjacentSector = sectorFrequencyMatch;
}
}
});
}finally{
return adjacentSector;
}
};

export async function getOnlinePositions() {
var onlineSectors = [];
var sectors = await getATCSectors();
var stations = await getVatsimAFV();

// SY_APP 124.400 AFV 124400000
// iterate txvrs.element.transceivers.element frequency/1000000
stations.forEach(function(station, index){
var activePosition = {};
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 = [];
station.transceivers.forEach(function(element){
// Hertz to Megahurts
element.frequency = element.frequency/1000000;
txvrs.push(element);
})
frequency = uniq(txvrs);
// Join sectors by callsign
var sector = sectors.find(function cb(element){
if(element.Callsign === station.callsign){
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))
}
})
}
})


return featureCollection(uniq(onlineSectors));
}
71 changes: 59 additions & 12 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,23 +229,70 @@ export async function getVatsimData () {
return data;
}

export async function getVatsimAFV (url) {
var data = cache.get("vatsimAFV");
if (data == undefined) {
const res = await fetch(url)
.then(res => res.json())
.then( data => {
return data;
})
.catch(err => log.error(err));
data = res;
export async function getVatsimAFV () {
const vatsimServers = await getVatsimServers();
var getUrl = uniqueRandomArray(vatsimServers.data.transceivers);
var url = getUrl();
log.debug(`VATSIM data URL: ${url}`);
var ttlMs = cache.getTtl('getVatsimAFV');
let data;
// VATSIM data is refreshed every 15s. Check 10s out from expiry.
if (ttlMs == undefined || ttlMs - Date.now() <= 10000) {
try{
// Download fresh VATSIM data
if(ttlMs == undefined){
// If there is nothing cached - retry forever.
const res = await fetch(url, {
retryOptions: {
retryMaxDuration: 30000, // Max 30s retrying
retryInitialDelay: 1000, // 1s initial wait
retryBackoff: 500 // 0.5s backoff
},
headers: {
'User-Agent': userAgent
}
})
.then(res => res.json())
.then( data => {
return data;
})
log.trace({res: res});
data = res;
}else{
// If there is an old cache, timeout quickly.
const res = await fetch(url, {
retryOptions: {
retryMaxDuration: 2000,
retryInitialDelay: 500,
retryBackoff: 1.0 // no backoff
},
headers: {
'User-Agent': userAgent
}
})
.then(res => res.json())
.then( data => {
return data;
})
log.trace({res: res});
data = res;
}
log.info({
cache: 'set',
url: url,
keys: Object.keys(data).length
});
cache.set("vatsimAFV", data, 15);
})
cache.set('getVatsimAFV', data, 30);
}catch(err){
if ( err instanceof FetchError) {
// Failed to download - load from cache
data = cache.get(url);
} else {
log.error(err);
}
};
}else{
data = cache.get('getVatsimAFV');
log.debug({
cache: 'get',
url: url,
Expand Down
2 changes: 1 addition & 1 deletion public/map.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
</style>
</head>
<body>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
<div id='map'></div>
<!-- <nav id="listing-group" class="listing-group">
<input type="checkbox" id="declutter" checked="checked">
<label for="declutter">Declutter</label>
</nav> -->
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
<!-- Load the `mapbox-gl-geocoder` plugin. -->
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.min.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.css" type="text/css">
Expand Down
Loading

0 comments on commit 5bc9864

Please sign in to comment.