Skip to content

Commit

Permalink
Merge pull request #1 from ozgend/airports-cache
Browse files Browse the repository at this point in the history
added airport detail, metar info, and runway list
  • Loading branch information
ozgend authored Sep 8, 2021
2 parents b5b2f39 + 62a17fd commit 9263385
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 7,754 deletions.
7,698 changes: 0 additions & 7,698 deletions data/airports.csv

This file was deleted.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"description": "",
"bin": "src/server.js",
"scripts": {
"run": "node ./src/server.js"
"run": "node ./src/server.js",
"get-airports": "curl https://ourairports.com/data/airports.csv -o ./data/airports.csv",
"get-runways": "curl https://ourairports.com/data/runways.csv -o ./data/runways.csv"
},
"author": "ozgend",
"license": "ISC",
Expand All @@ -19,4 +21,4 @@
"mode-s-demodulator": "^1.0.1",
"mongodb": "^4.1.1"
}
}
}
7 changes: 5 additions & 2 deletions src/background-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,17 @@ const aircraftIcaoDetailUpdate = async () => {

};

const _tasks = [aircraftIcaoDetailUpdate];
const _tasks = [
{ handler: aircraftIcaoDetailUpdate, interval: 2000 }
];

let _pids = [];

const start = async () => {
_collection = await _mongoRepository.getCollection(_mongoRepository.schemaList.aircraft_icao);

_pids = _tasks.map(task => {
return setInterval(task, 2000);
return setInterval(task.handler, task.interval);
});
};

Expand Down
3 changes: 2 additions & 1 deletion src/mongo-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class MongoRepository {
this._url = process.env['MONGODB_HOST'];
this.schemaList = {
aircraft_icao: 'adsb_radar.aircraft_icao',
airport_icao: 'adsb_radar.airport_icao'
airport_icao: 'adsb_radar.airport_icao',
runway_icao: 'adsb_radar.runway_icao'
};

MongoRepository._instance = this;
Expand Down
Binary file modified src/public/airport.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/blue_dot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/closedport.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/public/heliport.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/public/icon-airport.png
Binary file not shown.
113 changes: 86 additions & 27 deletions src/public/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,63 @@ const _mapBaseLayers = {
maxZoom: 19,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}),
CartoLight: L.tileLayer('https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}),
CartoBlack: L.tileLayer('https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}),

};
const _airportIcon = L.icon({
iconUrl: '/public/airport.png',
iconSize: [14, 14],
// iconAnchor: [18, 36],
popupAnchor: [0, 0]
});

// const _aircraftIcon = L.icon({
// iconUrl: '/public/aircraft.png',
// iconSize: [24, 24],
// popupAnchor: [0, 0]
// });
const _heliportIcon = L.icon({
iconUrl: '/public/heliport.png',
iconSize: [14, 14],
popupAnchor: [0, 0]
});

let _airportMarkerLayer;
let _aircraftMarkerLayer;
const _closedportIcon = L.icon({
iconUrl: '/public/closedport.png',
iconSize: [14, 14],
popupAnchor: [0, 0]
});

let _airportPopup;
let _aircraftMarkers = {};
let _aircraftMarkerLayer;
let _airportMarkerLayer;
let _heliportMarkerLayer;
let _closedportMarkerLayer;

const init = () => {
console.log('init map');

// map overlay + layers
_map = L.map('map', {
// layers: [_mapBaseLayers.Dark, _mapBaseLayers.Light, _mapBaseLayers.OSM, _mapBaseLayers.OSMBW],
}).setView(_homeLocation, 10);
_map = L.map('map').setView(_homeLocation, 10);

// default marker layers
_mapBaseLayers.OSMBW.addTo(_map);
// control & marker layers
_mapBaseLayers.CartoBlack.addTo(_map);
_aircraftMarkerLayer = L.layerGroup().addTo(_map);
_airportMarkerLayer = L.layerGroup().addTo(_map);
_heliportMarkerLayer = L.layerGroup().addTo(_map);
_closedportMarkerLayer = L.layerGroup().addTo(_map);

L.control.layers(_mapBaseLayers, {
'Airports': _airportMarkerLayer,
'Heliports': _heliportMarkerLayer,
'Closed': _closedportMarkerLayer,
'Aircrafts': _aircraftMarkerLayer
}).addTo(_map);
L.control.scale().addTo(_map);

L.marker(_homeLocation).bindPopup('here').addTo(_map);
L.marker(_homeLocation).bindPopup('rtl-sdr').addTo(_map);

// map events
_map.on('zoomend', () => {
Expand All @@ -81,16 +99,57 @@ const clearLayer = async () => {
_aircraftMarkers = {};
_aircraftMarkerLayer.clearLayers();
_airportMarkerLayer.clearLayers();
_heliportMarkerLayer.clearLayers();
_closedportMarkerLayer.clearLayers();
};

const buildAirportInfoCard = async (airport) => {
const { metar, runways } = await getData(`/airport/detail/${airport.ident}`);

html = `<div class="airport-info-card">`;
html += `<h3>${airport.name}</h3>${airport.ident} - ${airport.iata_code} | ${airport.municipality},${airport.iso_country}`;

if (runways && runways.length > 0) {
html += `<h4>Runways (${runways.length})</h4>`;
runways.forEach(r => { html += `‣ ${r.le_ident}/${r.he_ident} - ${r.surface.toLowerCase()}, ${parseFloat(r.length_ft).toFixed(1)}ft <br>`; });
}
if (metar && metar.MetarId) {
html += `<h4>METAR (${metar.ObservationTimeUtc.replace('T', ' ')} UTC)</h4>
Temp: ${metar.TemperatureCelsius}°C, Vis: ${metar.VisibilityStatuteMiles} mi, Alt:${metar.AltimiterInHG.toFixed(2)} in /hg<br>
Wind: ${metar.WindDirectionAngle}° @${metar.WindSpeedKnots} kts., Gust: ${metar.WindGustKnots || 'none'}`;
}

html += `</div>`;

_airportPopup = L.popup().setLatLng([airport.latitude_deg, airport.longitude_deg]).setContent(html).openOn(_map);
};

const buildAircraftInfoCard = (aircraft) => {
const html = `< div ><b>${aircraft.detail.model || '-'}</b><br>${aircraft.callsign} - ${aircraft.detail.registration || aircraft.icao.toString(16)}<br>${aircraft.detail.operator || '-'}<br>${aircraft.altitude}ft. ${parseInt(aircraft.speed)}kt. ${parseInt(aircraft.heading)}° </div > `;
return html;
};

const getAirports = async () => {
const bounds = _map.getBounds();
_airportMarkerLayer.clearLayers();
const airports = await getData(`/airport/search?start_lat=${bounds._southWest.lat}&start_lng=${bounds._southWest.lng}&end_lat=${bounds._northEast.lat}&end_lng=${bounds._northEast.lng}`) || [];

let airportMarker;

airports.forEach(airport => {

const airports = await getData(`/airports?start_lat=${bounds._southWest.lat}&start_lng=${bounds._southWest.lng}&end_lat=${bounds._northEast.lat}&end_lng=${bounds._northEast.lng}`) || [];
if (airport.type === 'closed') {
airportMarker = L.marker([parseFloat(airport.latitude_deg), parseFloat(airport.longitude_deg)], { icon: _closedportIcon, title: `${airport.name}\n${airport.ident} - ${airport.iata_code} | ${airport.municipality},${airport.iso_country}` }).addTo(_closedportMarkerLayer)
}
else if (airport.type === 'heliport') {
airportMarker = L.marker([parseFloat(airport.latitude_deg), parseFloat(airport.longitude_deg)], { icon: _heliportIcon, title: `${airport.name}\n${airport.ident} - ${airport.iata_code} | ${airport.municipality},${airport.iso_country}` }).addTo(_heliportMarkerLayer);
}
else {
airportMarker = L.marker([parseFloat(airport.latitude_deg), parseFloat(airport.longitude_deg)], { icon: _airportIcon, title: `${airport.name}\n${airport.ident} - ${airport.iata_code} | ${airport.municipality},${airport.iso_country}` }).addTo(_airportMarkerLayer);
}

airports.forEach(item => {
L.marker([parseFloat(item.lat), parseFloat(item.lng)], { icon: _airportIcon, title: `${item.name}\n${item.ICAO} - ${item.city},${item.country}` }).addTo(_airportMarkerLayer);
airportMarker.on('click', (e) => {
buildAirportInfoCard(airport, airportMarker);
})
});
};

Expand All @@ -106,15 +165,15 @@ const createAircraftStream = () => {

_ws.onmessage = (message) => {
const aircrafts = JSON.parse(message.data);
aircrafts.forEach(a => {
const html = `<div><b>${a.detail.model || '-'}</b><br>${a.callsign} - ${a.detail.registration || a.icao.toString(16)}<br>${a.detail.operator || '-'}<br>${a.altitude}ft. ${parseInt(a.speed)}kt. ${parseInt(a.heading)}°s </div > `;
const markerIcon = L.divIcon({ className: 'adsb-radar-aircraft-marker-holder', html: `<img class="adsb-radar-aircraft-icon" style=" transform: rotate(${parseInt(a.heading)}deg)" src="/public/aircraft.png">` });
if (!_aircraftMarkers[a.icao]) {
_aircraftMarkers[a.icao] = L.marker([parseFloat(a.lat), parseFloat(a.lng)], { mmmmmmiii: a.icao, dddddeggg: a.heading }).bindPopup(html).addTo(_aircraftMarkerLayer);
aircrafts.forEach(aircraft => {
const html = buildAircraftInfoCard(aircraft);
const markerIcon = L.divIcon({ className: 'adsb-radar-aircraft-marker-holder', html: `<img class="adsb-radar-aircraft-icon" style=" transform: rotate(${parseInt(aircraft.heading)}deg)" src="/public/aircraft.png">` });
if (!_aircraftMarkers[aircraft.icao]) {
_aircraftMarkers[aircraft.icao] = L.marker([parseFloat(aircraft.lat), parseFloat(aircraft.lng)], { mmmmmmiii: aircraft.icao, dddddeggg: aircraft.heading }).bindPopup(html).addTo(_aircraftMarkerLayer);
}
_aircraftMarkers[a.icao].setPopupContent(html);
_aircraftMarkers[a.icao].setIcon(markerIcon);
_aircraftMarkers[a.icao].setLatLng([parseFloat(a.lat), parseFloat(a.lng)]);
_aircraftMarkers[aircraft.icao].setPopupContent(html);
_aircraftMarkers[aircraft.icao].setIcon(markerIcon);
_aircraftMarkers[aircraft.icao].setLatLng([parseFloat(aircraft.lat), parseFloat(aircraft.lng)]);
});
};
};
Expand All @@ -126,7 +185,7 @@ const createAircraftStream = () => {

// aircrafts?.forEach(a => {
// L.marker([parseFloat(a.lat), parseFloat(a.lng)], {
// icon: L.divIcon({ className: 'adsb-radar-aircraft-info', html: `< div > <img style="transform: rotate(${a.heading}deg)" src="/public/aircraft.png"><b>${a.detail.model}</b><br>${a.callsign} - ${a.detail.registration}<br>${a.detail.operator}<br>${a.altitude}ft. - ${parseInt(a.speed)}kts</div>` }),
// icon: L.divIcon({className: 'adsb-radar-aircraft-info', html: `< div > <img style="transform: rotate(${a.heading}deg)" src="/public/aircraft.png"><b>${a.detail.model}</b><br>${a.callsign} - ${a.detail.registration}<br>${a.detail.operator}<br>${a.altitude}ft. - ${parseInt(a.speed)}kts</div>` }),
// }).addTo(_aircraftMarkerLayer);
// });
// };
Expand Down
7 changes: 7 additions & 0 deletions src/public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ img.adsb-radar-aircraft-icon {
position: absolute;
top: -16px;
left: -16px;
}

div.airport-info-card>h2,
div.airport-info-card>h3,
div.airport-info-card>h4 {
margin: 4px 0;
padding: 0;
}
1 change: 0 additions & 1 deletion src/rtl1090.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const Decoder = require('mode-s-decoder');
const { Int32 } = require('mongodb');
const net = require('net');
const _store = require('./store');

Expand Down
10 changes: 5 additions & 5 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ _app.get('/favicon.ico', (req, reply) => {
reply.sendFile('favicon.ico');
});

_app.get('/aircrafts', async (req, reply) => {
const aircrafts = await service.getAircrafts();
_app.get('/airport/search', async (req, reply) => {
const data = await service.searchAirports(req.query.start_lat, req.query.start_lng, req.query.end_lat, req.query.end_lng);
reply
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
.send(aircrafts);
.send(data);
});

_app.get('/airports', async (req, reply) => {
const data = await service.searchAirports(req.query.start_lat, req.query.start_lng, req.query.end_lat, req.query.end_lng);
_app.get('/airport/detail/:icao', async (req, reply) => {
const data = await service.getAirportDetail(req.params.icao);
reply
.code(200)
.header('Content-Type', 'application/json; charset=utf-8')
Expand Down
65 changes: 47 additions & 18 deletions src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,69 @@

const fs = require('fs');
const path = require('path');
const got = require('got');
const csvParse = require('csv-parse/lib/sync');
const _store = require('./store');
const MongoRepository = require('./mongo-repository');
const _mongoRepository = new MongoRepository();

const _dataHeaders = {
airports: ['id', 'name', 'city', 'country', 'IATA', 'ICAO', 'lat', 'lng', 'altitude', 'utcOffset', 'DST', 'tz', 'type', 'source'],
routes: ['airline', 'airlineId', 'source', 'sourceId', 'dest', 'destId', 'codeshare', 'stops', 'equipment']
airports: ['id', 'ident', 'type', 'name', 'latitude_deg', 'longitude_deg', 'elevation_ft', 'continent', 'iso_country', 'iso_region', 'municipality', 'scheduled_service', 'gps_code', 'iata_code', 'local_code', 'home_link', 'wikipedia_link', 'keywords'],
runways: ['id', 'airport_ref', 'airport_ident', 'length_ft', 'width_ft', 'surface', 'lighted', 'closed', 'le_ident', 'le_latitude_deg', 'le_longitude_deg', 'le_elevation_ft', 'le_heading_degT', 'le_displaced_threshold_ft', 'he_ident', 'he_latitude_deg', 'he_longitude_deg', 'he_elevation_ft', 'he_heading_degT', 'he_displaced_threshold_ft']
};

const _mapDataCache = {
airports: null,
routes: null
};
exports.searchAirports = async (start_lat, start_lng, end_lat, end_lng) => {
const collection = await _mongoRepository.getCollection(_mongoRepository.schemaList.airport_icao);
let airports = [];

airports = await collection.find({ latitude_deg: { $gte: start_lat, $lte: end_lat }, longitude_deg: { $gte: start_lng, $lte: end_lng } }).toArray();

if (airports.length > 0) {
return airports;
}

exports.searchAirports = (start_lat, start_lng, end_lat, end_lng) => {
const data = this.getMapData('airports');
const airports = data.filter(a => a.lat >= start_lat && a.lat <= end_lat && a.lng >= start_lng && a.lng <= end_lng);
const raw = fs.readFileSync(path.join(__dirname, '../data', `airports.csv`), { encoding: 'utf8' });
const data = csvParse(raw, { columns: _dataHeaders.airports, skip_empty_lines: true }).map(d => { d._id = d.id; return d; });
await collection.insertMany(data);

airports = data.filter(a => a.latitude_deg >= start_lat && a.latitude_deg <= end_lat && a.longitude_deg >= start_lng && a.longitude_deg <= end_lng);
return airports;
};

exports.getMapData = (name, search) => {
let data = _mapDataCache[name];
exports.getAirportDetail = async (icao) => {
const detail = {};

if (data) {
return data;
if (icao.includes('-')) {
return detail;
}

const raw = fs.readFileSync(path.join(__dirname, '../data', `${name}.csv`), { encoding: 'utf8' });
data = csvParse(raw, { columns: _dataHeaders[name], skip_empty_lines: true });
_mapDataCache[name] = data;
try {
const metarResponse = await got(`https://sdm.virtualradarserver.co.uk/api/1.00/weather/airport/${icao}?_=${Date.now()}`, { responseType: 'json' });
if (metarResponse.statusCode === 200) {
detail.metar = metarResponse.body || {};
}
}
catch (err) {
console.error(err);
}

return data;
}
const runwayCollection = await _mongoRepository.getCollection(_mongoRepository.schemaList.runway_icao);
detail.runways = await runwayCollection.find({ airport_ident: icao }).toArray();

if (detail.runways.length > 0) {
return detail;
}

const raw = fs.readFileSync(path.join(__dirname, '../data', `runways.csv`), { encoding: 'utf8' });
const data = csvParse(raw, { columns: _dataHeaders.runways, skip_empty_lines: true, fromLine: 1 }).map(d => { d._id = d.id; return d; });
detail.runways = data.filter(r => r.airport_ident === icao);

if (detail.runways.length > 0) {
await runwayCollection.insertMany(data);
}

return detail;
};

exports.getAircrafts = async () => {
const aircrafts = _store.getAircrafts().filter(aircraft => aircraft.lat || aircraft.lon);
Expand Down

0 comments on commit 9263385

Please sign in to comment.