├── .gitattributes ├── .mocharc.json ├── .gitignore ├── .travis.yml ├── .editorconfig ├── example.js ├── license ├── test.js ├── package.json ├── index.js └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | 4 | coverage/ 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'stable' 5 | script: 'npm test' 6 | after_script: 7 | - 'cat coverage/lcov.info | ./node_modules/.bin/coveralls' 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const terminalProcedures = require('./') 2 | 3 | terminalProcedures.fetchCurrentCycle().then(r => console.log(r)) 4 | 5 | terminalProcedures.list('PANC').then(results => { 6 | console.log(results) 7 | const out = results.map(tp => { 8 | return { 9 | name: tp.procedure.name, 10 | type: tp.type, 11 | url: tp.procedure.url 12 | } 13 | }) 14 | console.log( 15 | JSON.stringify( 16 | { 17 | documents: { 18 | terminalProcedures: [ out ] 19 | } 20 | }, 21 | null, 22 | 2 23 | ) 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Forrest Desjardins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | const assert = require('assert') 4 | const terminalProcedures = require('./index') 5 | 6 | describe('terminalProcedures', () => { 7 | it('should fetch terminal procedures for a single ICAO', () => { 8 | return terminalProcedures('PANC').then(procedures => { 9 | assert(procedures) 10 | }) 11 | }) 12 | 13 | it('should fetch terminal procedures for an array of ICAOs', () => { 14 | return terminalProcedures([ 'PANC', 'KSFO' ]).then(procedures => { 15 | assert(procedures.length === 2) 16 | procedures.map(assert) 17 | }) 18 | }) 19 | 20 | it('should expose the list method', () => { 21 | return terminalProcedures.list('PANC') 22 | }) 23 | 24 | it('should expose the fetchCurrentCycle method', () => { 25 | return terminalProcedures.fetchCurrentCycle().then(cycle => { 26 | assert(parseInt(cycle)) 27 | }) 28 | }) 29 | 30 | it('should fetch terminal procedures for an array of ICAOs using the list method', () => { 31 | return terminalProcedures.list([ 'PANC', 'KSFO' ]).then(procedures => { 32 | assert(procedures.length === 2) 33 | procedures.map(assert) 34 | }) 35 | }) 36 | 37 | it('should fetch all terminal procedures on multiple pages', () => { 38 | return terminalProcedures.list('KIAH').then(procedures => { 39 | assert(procedures.length > 50) 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terminal-procedures", 3 | "description": "Fetch terminal procedures from https://www.faa.gov/", 4 | "version": "1.0.5", 5 | "author": { 6 | "name": "Forrest Desjardins", 7 | "email": "desjardinsfg@gmail.com", 8 | "url": "github.com/fdesjardins" 9 | }, 10 | "dependencies": { 11 | "cheerio": "^1.0.0-rc.3", 12 | "superagent": "^6.1.0" 13 | }, 14 | "devDependencies": { 15 | "coveralls": "^3.1.0", 16 | "mocha": "^8.1.3", 17 | "nyc": "^15.1.0", 18 | "standard": "^14.3.4" 19 | }, 20 | "engines": { 21 | "node": ">=8" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "standard" 26 | ], 27 | "rules": { 28 | "object-curly-spacing": [ 29 | 2, 30 | "always" 31 | ], 32 | "array-bracket-spacing": [ 33 | 2, 34 | "always" 35 | ], 36 | "func-style": [ 37 | 2, 38 | "expression" 39 | ], 40 | "prefer-arrow-callback": [ 41 | 2, 42 | { 43 | "allowNamedFunctions": false 44 | } 45 | ] 46 | } 47 | }, 48 | "files": [ 49 | "index.js" 50 | ], 51 | "keywords": [ 52 | "airports", 53 | "api", 54 | "aviation", 55 | "charts", 56 | "diagrams", 57 | "documents", 58 | "faa", 59 | "notices", 60 | "procedures", 61 | "terminal" 62 | ], 63 | "license": "MIT", 64 | "main": "index.js", 65 | "nyc": { 66 | "reporter": [ 67 | "lcov", 68 | "text" 69 | ] 70 | }, 71 | "repository": "fdesjardins/terminal-procedures", 72 | "scripts": { 73 | "test": "eslint *.js && nyc mocha test.js" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio') 2 | const superagent = require('superagent') 3 | 4 | const BASE_URL = 5 | 'https://www.faa.gov/air_traffic/flight_info/aeronav/digital_products/dtpp/search/' 6 | 7 | // For some reason the server takes forever to respond without this request header 8 | const ACCEPT = 'text/html' 9 | 10 | /** 11 | * A shortcut to the list() method 12 | */ 13 | const terminalProcedures = (module.exports = (icaos, options = {}) => { 14 | return terminalProcedures.list(icaos, options) 15 | }) 16 | 17 | /** 18 | * Main fetching method; accepts one or more ICAO codes 19 | */ 20 | terminalProcedures.list = (icaos, options = {}) => { 21 | if (Array.isArray(icaos)) { 22 | return Promise.all(icaos.map(listOne)) 23 | } 24 | return listOne(icaos) 25 | } 26 | 27 | /** 28 | * Fetch the current diagrams distribution cycle numbers (.e.g, 1813) 29 | */ 30 | const fetchCurrentCycle = (terminalProcedures.fetchCurrentCycle = async () => { 31 | const response = await superagent 32 | .get(BASE_URL) 33 | .set('Accept', ACCEPT) 34 | 35 | const $ = cheerio.load(response.text) 36 | return $('select#cycle > option:contains(Current)').val() 37 | }) 38 | 39 | /** 40 | * Using the current cycle, fetch the terminal procedures for a single ICAO code 41 | */ 42 | const listOne = async icao => { 43 | const searchCycle = await fetchCurrentCycle() 44 | let procedures = [] 45 | let lastPageFetched = 0 46 | let lastNumFetched = 1 47 | while (lastNumFetched > 0) { 48 | const page = await superagent 49 | .get( 50 | `${BASE_URL}/results/?cycle=${searchCycle}&ident=${icao}&sort=type&dir=asc&page=${lastPageFetched + 51 | 1}` 52 | ) 53 | .set('Accept', ACCEPT) 54 | .then(res => parse(res.text)) 55 | if (page) { 56 | lastNumFetched = page.length 57 | lastPageFetched += 1 58 | procedures = procedures.concat(page) 59 | } else { 60 | break 61 | } 62 | } 63 | return procedures 64 | } 65 | 66 | /** 67 | * Parsing helper methods 68 | */ 69 | const text = ($row, columnIndex) => 70 | $row 71 | .find(`td:nth-child(${columnIndex})`) 72 | .text() 73 | .trim() 74 | 75 | const link = ($row, columnIndex) => 76 | $row 77 | .find(`td:nth-child(${columnIndex})`) 78 | .find('a') 79 | .attr('href') 80 | 81 | const extractRow = $row => { 82 | const type = text($row, 7) 83 | 84 | if (!type) { 85 | return null 86 | } 87 | 88 | return { 89 | state: text($row, 1), 90 | city: text($row, 2), 91 | airport: text($row, 3), 92 | ident: text($row, 4), 93 | vol: text($row, 5), 94 | flag: text($row, 6), 95 | type, 96 | procedure: { 97 | name: text($row, 8), 98 | url: link($row, 8) 99 | }, 100 | compare: { 101 | name: text($row, 9), 102 | url: link($row, 9) 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Parse the response HTML into JSON 109 | */ 110 | const parse = html => { 111 | const $ = cheerio.load(html) 112 | const $resultsTable = $('#resultsTable') 113 | 114 | if (!$resultsTable.html()) { 115 | console.error('Unable to parse the #resultsTable page element') 116 | return null 117 | } 118 | 119 | const results = $resultsTable 120 | .find('tr') 121 | .toArray() 122 | .map(row => extractRow($(row))) 123 | .filter(x => !!x) 124 | 125 | if (results.length > 0) { 126 | return results 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # terminal-procedures 2 | 3 | Fetch the latest terminal procedures information from https://www.faa.gov/ 4 | 5 | [![NPM Version][npm-image]][npm-url] 6 | [![Build Status][travis-image]][travis-url] 7 | [![Coverage][coveralls-image]][coveralls-url] 8 | [![Maintainability][code-climate-image]][code-climate-url] 9 | 10 | ## Installation 11 | 12 | ``` 13 | $ npm install --save terminal-procedures 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```js 19 | const terminalProcedures = require('./') 20 | 21 | terminalProcedures.list('KBRO').then(results => { 22 | console.log(JSON.stringify(results, null, 2)) 23 | }) 24 | ``` 25 | 26 | ### Output 27 | 28 | ```json 29 | { 30 | "documents": { 31 | "terminalProcedures": [ 32 | [ 33 | { 34 | "name": "AIRPORT DIAGRAM (PDF)", 35 | "type": "APD", 36 | "url": "http://aeronav.faa.gov/d-tpp/1707/00061ad.pdf#nameddest=(BRO)" 37 | }, 38 | { 39 | "name": "ILS OR LOC RWY 13 (PDF)", 40 | "type": "IAP", 41 | "url": "http://aeronav.faa.gov/d-tpp/1707/00061il13.pdf#nameddest=(BRO)" 42 | }, 43 | { 44 | "name": "RNAV (GPS) RWY 13 (PDF)", 45 | "type": "IAP", 46 | "url": "http://aeronav.faa.gov/d-tpp/1707/00061r13.pdf#nameddest=(BRO)" 47 | }, 48 | { 49 | "name": "RNAV (GPS) RWY 18 (PDF)", 50 | "type": "IAP", 51 | "url": "http://aeronav.faa.gov/d-tpp/1707/00061r18.pdf#nameddest=(BRO)" 52 | }, 53 | { 54 | "name": "LOC BC RWY 31 (PDF)", 55 | "type": "IAP", 56 | "url": "http://aeronav.faa.gov/d-tpp/1707/00061lbc31.pdf#nameddest=(BRO)" 57 | }, 58 | { 59 | "name": "VOR OR TACAN-A (PDF)", 60 | "type": "IAP", 61 | "url": "http://aeronav.faa.gov/d-tpp/1707/00061vta.pdf#nameddest=(BRO)" 62 | }, 63 | { 64 | "name": "LAHSO (PDF)", 65 | "type": "LAH", 66 | "url": "http://aeronav.faa.gov/d-tpp/1707/sc3lahso.pdf#nameddest=(BRO)" 67 | }, 68 | { 69 | "name": "TAKEOFF MINIMUMS (PDF)", 70 | "type": "MIN", 71 | "url": "http://aeronav.faa.gov/d-tpp/1707/sc3to.pdf#nameddest=(BRO)" 72 | }, 73 | { 74 | "name": "ALTERNATE MINIMUMS (PDF)", 75 | "type": "MIN", 76 | "url": "http://aeronav.faa.gov/d-tpp/1707/sc3alt.pdf#nameddest=(BRO)" 77 | } 78 | ] 79 | ] 80 | } 81 | } 82 | ``` 83 | 84 | ## API 85 | 86 | ### `terminalProcedures(icaos)` 87 | 88 | ### `terminalProcedures.list(icaos)` 89 | 90 | #### `icaos` 91 | 92 | Type: `string` or `array` 93 | 94 | One of the following: 95 | 96 | - a single ICAO code 97 | - an array of ICAO codes 98 | 99 | ### `terminalProcedures.fetchCurrentCycle()` 100 | 101 | Fetch the current diagrams distribution cycle numbers (.e.g, 1813) 102 | 103 | ## License 104 | 105 | MIT © [Forrest Desjardins](https://github.com/fdesjardins) 106 | 107 | [npm-url]: https://www.npmjs.com/package/terminal-procedures 108 | [npm-image]: https://img.shields.io/npm/v/terminal-procedures.svg?style=flat 109 | [travis-url]: https://travis-ci.org/fdesjardins/terminal-procedures 110 | [travis-image]: https://img.shields.io/travis/fdesjardins/terminal-procedures.svg?style=flat 111 | [coveralls-url]: https://coveralls.io/r/fdesjardins/terminal-procedures 112 | [coveralls-image]: https://img.shields.io/coveralls/fdesjardins/terminal-procedures.svg?style=flat 113 | [code-climate-url]: https://codeclimate.com/github/fdesjardins/terminal-procedures/maintainability 114 | [code-climate-image]: https://api.codeclimate.com/v1/badges/62235bf632b6f023b461/maintainability 115 | --------------------------------------------------------------------------------