From c5d252e96234fdf71c964b0e9fc41b2c07892e87 Mon Sep 17 00:00:00 2001 From: Sam Wilson Date: Thu, 7 Oct 2021 23:28:48 +1100 Subject: [PATCH] Adds AIRAC from profile #1 Reformat sectors for better exploration and search Adds correct VATSIM data API discovery --- client.js | 83 ++++++++++++++++- config/default.json | 35 +++++-- config/production.json | 2 +- package.json | 1 + pilots.js | 2 +- public/map.js | 20 +++- public/sectormap.html | 17 ++++ public/sectormap.js | 202 +++++++++++++++++++++++++++++++++++------ server.js | 11 ++- 9 files changed, 324 insertions(+), 49 deletions(-) diff --git a/client.js b/client.js index 243abd7..8f116da 100644 --- a/client.js +++ b/client.js @@ -8,6 +8,7 @@ import query_overpass from 'query-overpass'; import config from 'config'; import { iso2dec } from './iso2dec.js'; import {Mutex, Semaphore, withTimeout} from 'async-mutex'; +import uniqueRandomArray from 'unique-random-array'; var log = bunyan.createLogger({name: config.get('app.name'), level: config.get('app.log_level')}); @@ -35,6 +36,76 @@ export function cacheStats(){ return stats; } +async function getVatsimServers(){ + const url = config.get('data.vatsim.statusUrl'); + var ttlMs = cache.getTtl(url); + 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(url, 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(url); + log.debug({ + cache: 'get', + url: url, + keys: Object.keys(data).length + }) + } + return data; +} + export async function getOSMAerodromeData (areaName) { log.info(`getOSMAerodromeData`); var data = await mutex.runExclusive(async () => { @@ -85,8 +156,12 @@ export async function getOSMAerodromeData (areaName) { return data; }; -export async function getVatsimData (url) { - var ttlMs = cache.getTtl(url); +export async function getVatsimData () { + const vatsimServers = await getVatsimServers(); + var getUrl = uniqueRandomArray(vatsimServers.data.v3); + var url = getUrl(); + log.debug(`VATSIM data URL: ${url}`); + var ttlMs = cache.getTtl('getVatsimData'); let data; // VATSIM data is refreshed every 15s. Check 10s out from expiry. if (ttlMs == undefined || ttlMs - Date.now() <= 10000) { @@ -134,7 +209,7 @@ export async function getVatsimData (url) { url: url, keys: Object.keys(data).length }) - cache.set(url, data, 30); + cache.set('getVatsimData', data, 30); }catch(err){ if ( err instanceof FetchError) { // Failed to download - load from cache @@ -144,7 +219,7 @@ export async function getVatsimData (url) { } }; }else{ - data = cache.get(url); + data = cache.get('getVatsimData'); log.debug({ cache: 'get', url: url, diff --git a/config/default.json b/config/default.json index e447e89..50290b9 100644 --- a/config/default.json +++ b/config/default.json @@ -21,37 +21,52 @@ ], "sectors": { "standard": [ + "BN-TRT_CTR", "BN-ARA_CTR", - "BN-ISA_CTR", "BN-KEN_CTR", + "BN-ISA_CTR", "BN-INL_CTR", "BN-ARM_CTR", - "BN-TRT_CTR", + "ML-OLW_CTR", "ML-ASP_CTR", "ML-ARM_CTR", - "ML-OLW_CTR", - "ML-PIY_CTR", "ML-TBD_CTR", - "ML-TAS_CTR", + "ML-PIY_CTR", + "ML-BIK_CTR", "ML-ELW_CTR", - "ML-BIK_CTR" + "ML-TAS_CTR", + "BN-TSN_FSS", + "ML-IND_FSS" ] - } + }, + "majorAerodromes": [ + "YSSY", + "YMML", + "YBBN", + "YPPH", + "YPAD", + "YBCG", + "YBCS", + "YSCB", + "YMHB", + "YPDN" + ] }, "data": { "vatsim": { - "dataUrl": "https://data.vatsim.net/v3/vatsim-data.json" + "statusUrl": "https://status.vatsim.net/status.json" }, "vatsys": { "fir_boundariesUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Maps/FIR_BOUNDARIES.xml", "volumesUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Volumes.xml", "sectorsUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Sectors.xml", "coastlineUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Maps/COAST_ALL.xml", - "coloursUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Colours.xml" + "coloursUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Colours.xml", + "profileUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Profile.xml" }, "osm": { "overpassUrl": "https://lz4.overpass-api.de/api/interpreter", - "aerodromesArea": "New South Wales" + "aerodromesArea": "Australia" } } } \ No newline at end of file diff --git a/config/production.json b/config/production.json index 0112017..df5ee17 100644 --- a/config/production.json +++ b/config/production.json @@ -54,7 +54,7 @@ }, "data": { "vatsim": { - "dataUrl": "https://data.vatsim.net/v3/vatsim-data.json" + "statusUrl": "https://status.vatsim.net/status.json" }, "vatsys": { "fir_boundariesUrl": "https://raw.githubusercontent.com/vatSys/australia-dataset/master/Maps/FIR_BOUNDARIES.xml", diff --git a/package.json b/package.json index ba9101f..80e7785 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "node-fetch": "^2.6.1", "query-overpass": "https://github.com/Kahn/query-overpass.git", "rgb2hex": "^0.2.5", + "unique-random-array": "^3.0.0", "xml-js": "^1.6.11" }, "devDependencies": { diff --git a/pilots.js b/pilots.js index 8cd5202..149bb79 100644 --- a/pilots.js +++ b/pilots.js @@ -9,7 +9,7 @@ var log = bunyan.createLogger({name: config.get('app.name'), level: config.get(' export async function getPilots(){ try{ var firs = await getLineFeatures(config.get('data.vatsys.fir_boundariesUrl')); - var vatsimData = await getVatsimData(config.get('data.vatsim.dataUrl')); + var vatsimData = await getVatsimData(); var aerodromes = await getOSMAerodromeData(config.get('data.osm.aerodromesArea')); }catch(err){ log.error(err) diff --git a/public/map.js b/public/map.js index b796991..545880b 100644 --- a/public/map.js +++ b/public/map.js @@ -155,16 +155,19 @@ var map = new mapboxgl.Map({ map.dragRotate.disable(); map.touchZoomRotate.disableRotation(); -map.addControl(new mapboxgl.AttributionControl({ - customAttribution: 'vatsim-map' -})) - // Light / Dark switch var theme = findGetParameter('theme') || 'light'; if(theme == 'dark'){ map.setStyle(styleDark); } +async function getDataset() { + var response = await fetch(`${window.location.protocol}//${window.location.hostname}:${window.location.port}/v1/dataset`); + var json = await response.json(); + dataset = json; + return json; +}; + async function getPilots() { var dataApi = findGetParameter('dataApi'); if(dataApi != false){ @@ -490,8 +493,15 @@ async function setPilotMarkers () { getPilots(); -map.on('load', function () { +(async () => { +var dataset = await getDataset(); +console.log(dataset); +map.addControl(new mapboxgl.AttributionControl({ + customAttribution: `vatSys ${dataset.Profile._attributes.Name} dataset AIRAC ${dataset.Profile.Version._attributes.AIRAC}${dataset.Profile.Version._attributes.Revision} | vatsim-map` +})) +})(); +map.on('load', function () { map.addSource('aircraftMarkersSource', { 'type': 'geojson', 'data': null diff --git a/public/sectormap.html b/public/sectormap.html index b373c10..aea38ec 100644 --- a/public/sectormap.html +++ b/public/sectormap.html @@ -14,6 +14,7 @@ top: 0; bottom: 0; width: 100%; + z-index: 0; } .mapboxgl-popup-content { @@ -25,6 +26,13 @@ /* line-height: 1.4; */ white-space: pre-line; } + #mgl-map-overlay { + position: absolute; + top: 10px; + left: 10px; + z-index: 1; + background-color: #fff; + } @@ -43,6 +51,15 @@ +
+ Map key + +
\ No newline at end of file diff --git a/public/sectormap.js b/public/sectormap.js index a39ab08..2486740 100644 --- a/public/sectormap.js +++ b/public/sectormap.js @@ -115,34 +115,82 @@ async function getATCSectors() { stdSectors.push(mergedSector); } }); - stdSectors = turf.featureCollection(stdSectors); - var allSectors = []; + var upperSectors = []; sectorsJson.forEach(sector => { - if(sector.Callsign.includes("CTR")){ + // // Do not filter sub sector volumes + // var sector = mergeBoundaries(sector); + // if(sector != false){ + // allSectors.push(sector); + // } + + if(sector.Callsign.includes("FSS") ||sector.Callsign.includes("CTR")){ + var sector = mergeBoundaries(sector); + if(sector != false){ + upperSectors.push(sector); + } + } + }); + + var tmaSectors = []; + sectorsJson.forEach(sector => { + // // Do not filter sub sector volumes + // var sector = mergeBoundaries(sector); + // if(sector != false){ + // allSectors.push(sector); + // } + + if(sector.Callsign.includes("APP")){ + var sector = mergeBoundaries(sector); + if(sector != false){ + tmaSectors.push(sector); + } + } + }); + + var twrSectors = []; + sectorsJson.forEach(sector => { + // // Do not filter sub sector volumes + // var sector = mergeBoundaries(sector); + // if(sector != false){ + // allSectors.push(sector); + // } + + if(sector.Callsign.includes("TWR")){ var sector = mergeBoundaries(sector); if(sector != false){ - allSectors.push(sector); + twrSectors.push(sector); } } }); - allSectors = turf.featureCollection(allSectors); - SECTORS = allSectors; - console.log(`Complete geojson`) + SECTORS = turf.featureCollection(upperSectors.concat(tmaSectors,twrSectors)); + console.log(`debug geojson`) console.log(SECTORS); map.addSource('std', { 'type': 'geojson', - 'data': stdSectors + 'data': turf.featureCollection(stdSectors) }); - map.addSource('nonstd', { + map.addSource('upper', { 'type': 'geojson', - 'data': allSectors + 'data': turf.featureCollection(upperSectors) + }); + + map.addSource('tma', { + 'type': 'geojson', + 'data': turf.featureCollection(tmaSectors) + }); + + map.addSource('twr', { + 'type': 'geojson', + 'data': turf.featureCollection(twrSectors) }); const stdLayout = { 'text-field': ['format', + ['get', 'FullName'], {}, + "\n", {}, ['get', 'Callsign'], {}, "\n", {}, ['get', 'Frequency'], {}, @@ -158,8 +206,29 @@ async function getATCSectors() { 'text-ignore-placement': false }; + const twrTmaLayout = { + 'text-field': ['format', + ['get', 'FullName'], {}, + "\n", {}, + ['get', 'Callsign'], {}, + "\n", {}, + ['get', 'Frequency'], {}, + ], + 'text-font': [ + 'Open Sans Semibold', + 'Arial Unicode MS Bold' + ], + 'text-size': 12, + 'text-offset': [0, 0], + 'text-anchor': 'center', + 'text-allow-overlap': false, + 'text-ignore-placement': false + }; + const nonstdLayout = { 'text-field': ['format', + ['get', 'FullName'], {}, + "\n", {}, ['get', 'Callsign'], {}, "\n", {}, ['get', 'Frequency'], {}, @@ -176,19 +245,53 @@ async function getATCSectors() { }; // map.addLayer({ - // 'id': 'stdPoly', + // 'id': 'tmaPoly', + // 'type': 'fill', + // 'source': 'twr', // reference the data source + // 'layout': {}, + // 'paint': { + // 'fill-color': '#23d922', + // 'fill-opacity': 0.2 + // } + // }); + // map.addLayer({ + // 'id': 'twrPoly', // 'type': 'fill', - // 'source': 'std', // reference the data source + // 'source': 'tma', // reference the data source // 'layout': {}, // 'paint': { - // 'fill-color': await getColourHex('Infill'), - // 'fill-opacity': 0.6 + // 'fill-color': '#ab0dde', + // 'fill-opacity': 0.1 // } // }); map.addLayer({ - 'id': 'nonstdLine', + 'id': 'tmaLine', + 'type': 'line', + 'source': 'tma', + 'layout': {}, + 'minzoom': 5, + 'paint': { + 'line-color': "#949494", + 'line-width': 3, + 'line-dasharray': [10, 8] + } + }); + map.addLayer({ + 'id': 'twrLine', 'type': 'line', - 'source': 'nonstd', + 'source': 'twr', + 'layout': {}, + 'minzoom': 5, + 'paint': { + 'line-color': "#949494", + 'line-width': 1, + 'line-dasharray': [2, 2] + } + }); + map.addLayer({ + 'id': 'upperLine', + 'type': 'line', + 'source': 'upper', 'layout': {}, 'minzoom': 5, 'paint': { @@ -209,9 +312,9 @@ async function getATCSectors() { } }); map.addLayer({ - 'id': 'nonstdText', + 'id': 'upperText', 'type': 'symbol', - 'source': 'nonstd', // reference the data source + 'source': 'upper', // reference the data source 'minzoom': 5, 'layout': nonstdLayout, 'paint': { @@ -219,6 +322,27 @@ async function getATCSectors() { } }); + map.addLayer({ + 'id': 'tmaText', + 'type': 'symbol', + 'source': 'tma', // reference the data source + 'minzoom': 8, + 'layout': twrTmaLayout, + 'paint': { + 'text-color': "#646464", + } + }); + + map.addLayer({ + 'id': 'twrText', + 'type': 'symbol', + 'source': 'twr', // reference the data source + 'minzoom': 8, + 'layout': twrTmaLayout, + 'paint': { + 'text-color': "#646464", + } + }); map.addLayer({ 'id': 'stdText', @@ -237,35 +361,49 @@ async function getATCSectors() { }; // Map BG ASDBackground -var SECTORS = false; +// Define global SECTORS to enable geocoder +var SECTORS = [false]; mapboxgl.accessToken = 'pk.eyJ1IjoiY3ljbG9wdGl2aXR5IiwiYSI6ImNqcDY0NnZnYzBmYjYzd284dzZudmdvZmUifQ.RyR4jd1HRggrbeZRvkv0xg'; var map = new mapboxgl.Map({ container: 'map', // container ID - style: 'mapbox://styles/cycloptivity/cksqo94ovrrk517lyn947vqw2', + // style: 'mapbox://styles/cycloptivity/cksqo94ovrrk517lyn947vqw2', + style: 'mapbox://styles/cycloptivity/ckrai7rg601cw18p5zu4ntq27', center: [134.9, -28.2 ], zoom: 4.3, - maxZoom: 7, + // maxZoom: 7, attributionControl: false }); map.dragRotate.disable(); map.touchZoomRotate.disableRotation(); -map.addControl(new mapboxgl.AttributionControl({ -customAttribution: 'vatsim-map' -})) +async function getDataset() { + var response = await fetch(`${window.location.protocol}//${window.location.hostname}:${window.location.port}/v1/dataset`); + var json = await response.json(); + dataset = json; + return json; +}; + +(async () => { + var dataset = await getDataset(); + console.log(dataset); + map.addControl(new mapboxgl.AttributionControl({ + customAttribution: `vatSys ${dataset.Profile._attributes.Name} dataset AIRAC ${dataset.Profile.Version._attributes.AIRAC}${dataset.Profile.Version._attributes.Revision} | vatsim-map` + })) +})(); //options.localGeocoder Function? A function accepting the query string which performs local geocoding to supplement results from the Mapbox Geocoding API. Expected to return an Array of GeoJSON Features in the Carmen GeoJSON format. function forwardGeocoder(query) { const matchingFeatures = []; for (const feature of SECTORS.features) { + console.log(feature) // Search by callsign if ( feature.properties.Callsign .toLowerCase() .includes(query.toLowerCase()) ) { - feature['place_name'] = `${feature.properties.Callsign} (${feature.properties.Frequency})`; + feature['place_name'] = `${feature.properties.FullName} ${feature.properties.Callsign} (${feature.properties.Frequency})`; feature['center'] = turf.centroid(feature).geometry.coordinates; matchingFeatures.push(feature); } @@ -275,11 +413,21 @@ function forwardGeocoder(query) { .toLowerCase() .includes(query.toLowerCase()) ) { - feature['place_name'] = `${feature.properties.Callsign} (${feature.properties.Frequency})`; + feature['place_name'] = `${feature.properties.FullName} ${feature.properties.Callsign} (${feature.properties.Frequency})`; feature['center'] = turf.centroid(feature).geometry.coordinates; matchingFeatures.push(feature); } - } + // Search by Name + if ( + feature.properties.FullName + .toLowerCase() + .includes(query.toLowerCase()) + ) { + feature['place_name'] = `${feature.properties.FullName} ${feature.properties.Callsign} (${feature.properties.Frequency})`; + feature['center'] = turf.centroid(feature).geometry.coordinates; + matchingFeatures.push(feature); + } + }; return matchingFeatures; } diff --git a/server.js b/server.js index d26e2a5..f7a5d8f 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ import { getATCSectors, getCoastline, getColours } from './atc.js'; import { getAerodromes, getMajorAerodromes } from './aerodrome.js'; import config from 'config'; import { getOSMAerodromeData } from './client.js'; +import { getDataset } from './dataset.js'; const app = express() const PORT = config.get('app.http.port'); @@ -32,6 +33,15 @@ app.use('/favicon.ico', express.static('public/favicon.ico')); app.use('/testdata', express.static('data')); +app.get('/v1/dataset', cors(), async (req, res) => { + var dataset = await getDataset(); + if(dataset == false){ + res.sendStatus(500); + }else{ + res.send(dataset) + } +}); + app.get('/v1/pilots', cors(), async (req, res) => { var pilots = await getPilots(); if(pilots == false){ @@ -49,7 +59,6 @@ app.get('/v1/atc/sectors', cors(), async (req, res) => { return sector.standard_position===true; }); } - // console.log(sectors); if(sectors == false){ res.sendStatus(500); }else{