#!/usr/bin/env node const csUrl = "https://aeronav.faa.gov/upload_313-d/supplements/"; const tppUrl = "https://aeronav.faa.gov/upload_313-d/terminal/"; const ifrEnrouteUrl = "https://aeronav.faa.gov/enroute/"; const vfrUrl = "https://aeronav.faa.gov/visual/"; const output = "."; import { JSDOM } from "jsdom"; import moment from "moment"; import fs from "node:fs/promises"; import decompress from "decompress"; import { waitForDebugger } from "node:inspector"; const csRegions = ["SW"]; const tppRegions = ["SW1", "SW2", "SW3", "SW4"]; const ifrEnrouteLow = [2, 3, 4]; const ifrEnrouteHigh = []; const vfrSectional = ["San Francisco", "Los Angeles"]; const vfrTerminal = ["San Francisco", "Los Angeles"]; const getChartSupplementDiretory = async () => { const dom = await JSDOM.fromURL(csUrl, {}); let chd = dom.window.document.querySelector("pre").children; const files = {}; for (let region of csRegions) { files[region] = []; files[region].current = { date: moment(0) }; } const pattern = /CS_([A-Z]+)_([0-9]{8}).pdf/; for (let i = 0; i < chd.length; i++) { if (chd[i] instanceof dom.window.HTMLAnchorElement) { const file = chd[i].textContent; const m = file.match(pattern); if (m) { const region = m[1]; const date = moment(m[2], "YYYYMMDD"); const url = `${csUrl}${file}`; if (files[region]) { files[region].push({ url, date }); if (moment() >= date && date > files[region].current.date) { files[region].current = { url, date, }; } } } } } return files; }; const getTerminalProcedurePublication = async () => { const dom = await JSDOM.fromURL(tppUrl, {}); let chd = dom.window.document.querySelector("pre").children; const files = {}; for (let region of tppRegions) { files[region] = []; files[region].current = { date: moment(0) }; } const pattern = /([0-9]{4}-[0-9]{2}-[0-9]{2})/; for (let i = 0; i < chd.length; i++) { if (chd[i] instanceof dom.window.HTMLAnchorElement) { const folder = chd[i].textContent; const m = folder.match(pattern); if (m) { const dom = await JSDOM.fromURL(`${tppUrl}${m[1]}`, {}); const date = moment(m[1], "YYYY-MM-DD"); const items = dom.window.document.querySelector("pre").children; const pattern = /([A-Z1-9]+).pdf/; if (moment() >= date) { for (let j = 0; j < items.length; j++) { const file = items[j].textContent; const m = file.match(pattern); const url = `${tppUrl}/${folder}/${file}`; if (m) { const region = m[1]; if (files[region]) { files[region].push({ url, date }); if (date > files[region].current.date) { files[region].current = { url, date, }; } } } } } else { console.log(`skipped ${folder}: in the future`); } } } } return files; }; const getIFREnroute = async () => { const dom = await JSDOM.fromURL(ifrEnrouteUrl, {}); let chd = dom.window.document.querySelector("pre").children; const files = {}; for (const i of ifrEnrouteLow) { const region = `L${i.toFixed(0).padStart(2, "0")}`; files[region] = []; files[region].current = { date: moment(0) }; } for (const i of ifrEnrouteHigh) { const region = `H${i.toFixed(0).padStart(2, "0")}`; files[region] = []; files[region].current = { date: moment(0) }; } const pattern = /([0-9]{2}-[0-9]{2}-[0-9]{4})/; for (let i = 0; i < chd.length; i++) { if (chd[i] instanceof dom.window.HTMLAnchorElement) { const folder = chd[i].textContent; const m = folder.match(pattern); if (m) { const dom = await JSDOM.fromURL(`${ifrEnrouteUrl}${m[1]}`, {}); const date = moment(m[1], "MM-DD-YYYY"); const items = dom.window.document.querySelector("pre").children; const pattern = /ENR_([A-Z0-9]+).zip/; if (moment() >= date) { for (let j = 0; j < items.length; j++) { const file = items[j].textContent; const m = file.match(pattern); const url = `${ifrEnrouteUrl}/${folder}/${file}`; if (m) { const region = m[1]; const unzip = {}; unzip[`ENR_${region}.tif`] = ".tif"; if (files[region]) { files[region].push({ url, date }); if (date > files[region].current.date) { files[region].current = { url, date, unzip, }; } } } } } else { console.log(`skipped ${folder}: in the future`); } } } } return files; }; const getVFR = async () => { const secFiles = {}; for (const i of vfrSectional) { const _region = i.replace(" ", "_"); secFiles[_region] = []; secFiles[_region].current = { date: moment(0) }; } const tacFiles = {}; for (const i of vfrTerminal) { const _region = i.replace(" ", "_"); tacFiles[_region] = []; tacFiles[_region].current = { date: moment(0) }; } const dom = await JSDOM.fromURL(`${vfrUrl}`, {}); let chd = dom.window.document.querySelector("pre").children; const pattern = /([0-9]{2}-[0-9]{2}-[0-9]{4})/; for (let i = 0; i < chd.length; i++) { if (chd[i] instanceof dom.window.HTMLAnchorElement) { const folder = chd[i].textContent; const m = folder.match(pattern); if (m) { const date = moment(m[1], "MM-DD-YYYY"); if (moment() >= date) { for (const region of vfrSectional) { const unzip = {}; const _region = region.replace(" ", "_"); const url = `${vfrUrl}${m[1]}/sectional-files/${_region}.zip`; unzip[`${region} SEC.tif`] = ".tif"; secFiles[_region].push({ url, date }); if (date > secFiles[_region].current.date) { secFiles[_region].current = { url, date, unzip, }; } } for (const region of vfrTerminal) { const unzip = {}; const _region = region.replace(" ", "_"); const url = `${vfrUrl}${m[1]}/tac-files/${_region}_TAC.zip`; unzip[`${region} TAC.tif`] = ".tif"; unzip[`${region} FLY.tif`] = "-flyway.tif"; tacFiles[_region].push({ url, date }); if (date > tacFiles[_region].current.date) { tacFiles[_region].current = { url, date, unzip, }; } } } else { console.log(`skipped ${folder}: in the future`); } } } } return { secFiles, tacFiles }; }; const vfrCharts = await getVFR(); const res = [ { prefix: "cs", files: await getChartSupplementDiretory() }, { prefix: "tpp", files: await getTerminalProcedurePublication() }, { prefix: "ifr-enroute-low", files: await getIFREnroute() }, { prefix: "vfr-sectional", files: vfrCharts.secFiles }, { prefix: "vfr-terminal", files: vfrCharts.tacFiles }, ]; const zip_path = `${output}/zips`; const chart_path = `${output}/charts`; try { await fs.mkdir(zip_path); await fs.mkdir(chart_path); } catch (_) {} for (let r of res) { for (let region of Object.keys(r.files)) { const cur = r.files[region].current; if (!cur.url) { continue; } const date = cur.date.format("YYYY-MM-DD"); const file = `${cur.unzip ? zip_path : chart_path}/${r.prefix}-${region.toLowerCase()}-${date}.${cur.unzip ? "zip" : "pdf"}`; const fileExists = (path) => fs.stat(path).then( () => true, () => false, ); if (await fileExists(file)) { console.log(`file "${file}" already exists`); } else { console.log(`downloading "${cur.url}"`); await fetch(cur.url) .then((r) => r.arrayBuffer()) .then((bytes) => fs.writeFile(file, new Uint8Array(bytes))); } if (cur.unzip) { console.log(`extracting "${file}"`); await decompress(file, "./", { filter: (f) => cur.unzip[f.path] != null, map: (f) => { f.path = `${chart_path}/${r.prefix}-${region.toLowerCase()}-${date}${cur.unzip[f.path]}`; return f; }, }); } } }