/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2021, 2023 Jon Beniston, M7RCE // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include "openaip.h" QList Airspace::readXML(const QString &filename) { QList airspaces; QFile file(filename); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QXmlStreamReader xmlReader(&file); while(!xmlReader.atEnd() && !xmlReader.hasError()) { if (xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("ASP")) { Airspace *airspace = new Airspace(); airspace->m_category = xmlReader.attributes().value("CATEGORY").toString(); while(xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("COUNTRY")) { airspace->m_country = xmlReader.readElementText(); } else if (xmlReader.name() == QLatin1String("NAME")) { airspace->m_name = xmlReader.readElementText(); } else if (xmlReader.name() == QLatin1String("ALTLIMIT_TOP")) { while(xmlReader.readNextStartElement()) { airspace->m_top.m_reference = xmlReader.attributes().value("REFERENCE").toString(); airspace->m_top.m_altUnit = xmlReader.attributes().value("UNIT").toString(); if (xmlReader.name() == QLatin1String("ALT")) { airspace->m_top.m_alt = xmlReader.readElementText().toInt(); } else { xmlReader.skipCurrentElement(); } } } else if (xmlReader.name() == QLatin1String("ALTLIMIT_BOTTOM")) { while(xmlReader.readNextStartElement()) { airspace->m_bottom.m_reference = xmlReader.attributes().value("REFERENCE").toString(); airspace->m_bottom.m_altUnit = xmlReader.attributes().value("UNIT").toString(); if (xmlReader.name() == QLatin1String("ALT")) { airspace->m_bottom.m_alt = xmlReader.readElementText().toInt(); } else { xmlReader.skipCurrentElement(); } } } else if (xmlReader.name() == QLatin1String("GEOMETRY")) { while(xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("POLYGON")) { QString pointsString = xmlReader.readElementText(); QStringList points = pointsString.split(","); for (const auto& ps : points) { QStringList split = ps.trimmed().split(" "); if (split.size() == 2) { QPointF pf(split[0].toDouble(), split[1].toDouble()); airspace->m_polygon.append(pf); } else { qDebug() << "Airspace::readXML - Unexpected polygon point format: " << ps; } } } else { xmlReader.skipCurrentElement(); } } } else { xmlReader.skipCurrentElement(); } } airspace->calculatePosition(); //qDebug() << "Adding airspace: " << airspace->m_name << " " << airspace->m_category; airspaces.append(airspace); } } } file.close(); } else { // Don't warn, as many countries don't have files //qDebug() << "Airspace::readXML: Could not open " << filename << " for reading."; } return airspaces; } QList NavAid::readXML(const QString &filename) { int uniqueId = 1; QList navAidInfo; QFile file(filename); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QXmlStreamReader xmlReader(&file); while(!xmlReader.atEnd() && !xmlReader.hasError()) { if (xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("NAVAID")) { QStringView typeRef = xmlReader.attributes().value("TYPE"); if ((typeRef == QLatin1String("NDB")) || (typeRef == QLatin1String("DME")) || (typeRef == QLatin1String("VOR")) || (typeRef == QLatin1String("VOR-DME")) || (typeRef == QLatin1String("VORTAC")) || (typeRef == QLatin1String("DVOR")) || (typeRef == QLatin1String("DVOR-DME")) || (typeRef == QLatin1String("DVORTAC"))) { QString type = typeRef.toString(); QString name; QString id; float lat = 0.0f; float lon = 0.0f; float elevation = 0.0f; float frequency = 0.0f; QString channel; int range = 25; float declination = 0.0f; bool alignedTrueNorth = false; while(xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("NAME")) { name = xmlReader.readElementText(); } else if (xmlReader.name() == QLatin1String("ID")) { id = xmlReader.readElementText(); } else if (xmlReader.name() == QLatin1String("GEOLOCATION")) { while(xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("LAT")) { lat = xmlReader.readElementText().toFloat(); } else if (xmlReader.name() == QLatin1String("LON")) { lon = xmlReader.readElementText().toFloat(); } else if (xmlReader.name() == QLatin1String("ELEV")) { elevation = xmlReader.readElementText().toFloat(); } else { xmlReader.skipCurrentElement(); } } } else if (xmlReader.name() == QLatin1String("RADIO")) { while(xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("FREQUENCY")) { if (type == "NDB") { frequency = xmlReader.readElementText().toFloat(); } else { frequency = xmlReader.readElementText().toFloat() * 1000.0; } } else if (xmlReader.name() == QLatin1String("CHANNEL")) { channel = xmlReader.readElementText(); } else { xmlReader.skipCurrentElement(); } } } else if (xmlReader.name() == QLatin1String("PARAMS")) { while(xmlReader.readNextStartElement()) { if (xmlReader.name() == QLatin1String("RANGE")) { range = xmlReader.readElementText().toInt(); } else if (xmlReader.name() == QLatin1String("DECLINATION")) { declination = xmlReader.readElementText().toFloat(); } else if (xmlReader.name() == QLatin1String("ALIGNEDTOTRUENORTH")) { alignedTrueNorth = xmlReader.readElementText() == "TRUE"; } else { xmlReader.skipCurrentElement(); } } } else { xmlReader.skipCurrentElement(); } } NavAid *navAid = new NavAid(); navAid->m_id = uniqueId++; navAid->m_ident = id; // Check idents conform to our filtering rules if (navAid->m_ident.size() < 2) { qDebug() << "NavAid::readXML: Ident less than 2 characters: " << navAid->m_ident; } else if (navAid->m_ident.size() > 3) { qDebug() << "NavAid::readXML: Ident greater than 3 characters: " << navAid->m_ident; } navAid->m_type = type; navAid->m_name = name; navAid->m_frequencykHz = frequency; navAid->m_channel = channel; navAid->m_latitude = lat; navAid->m_longitude = lon; navAid->m_elevation = elevation; navAid->m_range = range; navAid->m_magneticDeclination = declination; navAid->m_alignedTrueNorth = alignedTrueNorth; navAidInfo.append(navAid); } } } } file.close(); } else { // Don't warn, as many countries don't have files //qDebug() << "NavAid::readNavAidsXML: Could not open " << filename << " for reading."; } return navAidInfo; } const QStringList OpenAIP::m_countryCodes = { "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "zw" }; QSharedPointer> OpenAIP::m_airspaces; QSharedPointer> OpenAIP::m_navAids; QDateTime OpenAIP::m_airspacesModifiedDateTime; QDateTime OpenAIP::m_navAidsModifiedDateTime; OpenAIP::OpenAIP(QObject *parent) : QObject(parent) { connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OpenAIP::downloadFinished); } OpenAIP::~OpenAIP() { disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OpenAIP::downloadFinished); } QString OpenAIP::getDataDir() { // Get directory to store app data in QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); // First dir is writable return locations[0]; } QString OpenAIP::getAirspaceFilename(int i) { return getAirspaceFilename(m_countryCodes[i]); } QString OpenAIP::getAirspaceFilename(const QString& countryCode) { return getDataDir() + "/" + countryCode + "_asp.xml"; } QString OpenAIP::getAirspaceURL(int i) { if (i < m_countryCodes.size()) { return QString(OPENAIP_AIRSPACE_URL).arg(m_countryCodes[i]); } else { return QString(); } } void OpenAIP::downloadAirspaces() { m_countryIndex = 0; downloadAirspace(); } void OpenAIP::downloadAirspace() { QString filename = getAirspaceFilename(m_countryIndex); QString urlString = getAirspaceURL(m_countryIndex); QUrl dbURL(urlString); qDebug() << "OpenAIP::downloadAirspace: Downloading " << urlString; emit downloadingURL(urlString); m_dlm.download(dbURL, filename); } QString OpenAIP::getNavAidsFilename(int i) { return getNavAidsFilename(m_countryCodes[i]); } QString OpenAIP::getNavAidsFilename(const QString& countryCode) { return getDataDir() + "/" + countryCode + "_nav.xml"; } QString OpenAIP::getNavAidsURL(int i) { if (i < m_countryCodes.size()) { return QString(OPENAIP_NAVAIDS_URL).arg(m_countryCodes[i]); } else { return QString(); } } void OpenAIP::downloadNavAids() { m_countryIndex = 0; downloadNavAid(); } void OpenAIP::downloadNavAid() { QString filename = getNavAidsFilename(m_countryIndex); QString urlString = getNavAidsURL(m_countryIndex); QUrl dbURL(urlString); qDebug() << "OpenAIP::downloadNavAid: Downloading " << urlString; emit downloadingURL(urlString); m_dlm.download(dbURL, filename); } void OpenAIP::downloadFinished(const QString& filename, bool success) { // Not all countries have corresponding files, so we should expect some errors if (!success) { qDebug() << "OpenAIP::downloadFinished: Failed: " << filename; } if (filename == getNavAidsFilename(m_countryIndex)) { m_countryIndex++; if (m_countryIndex < m_countryCodes.size()) { downloadNavAid(); } else { emit downloadNavAidsFinished(); } } else if (filename == getAirspaceFilename(m_countryIndex)) { m_countryIndex++; if (m_countryIndex < m_countryCodes.size()) { downloadAirspace(); } else { emit downloadAirspaceFinished(); } } else { qDebug() << "OpenAIP::downloadFinished: Unexpected filename: " << filename; emit downloadError(QString("Unexpected filename: %1").arg(filename)); } } // Read airspaces for all countries QList *OpenAIP::readAirspaces() { QList *airspaces = new QList(); for (const auto& countryCode : m_countryCodes) { airspaces->append(readAirspaces(countryCode)); } return airspaces; } // Read airspaces for a single country QList OpenAIP::readAirspaces(const QString& countryCode) { return Airspace::readXML(getAirspaceFilename(countryCode)); } // Read NavAids for all countries QList *OpenAIP::readNavAids() { QList *navAids = new QList(); for (const auto& countryCode : m_countryCodes) { navAids->append(readNavAids(countryCode)); } return navAids; } // Read NavAids for a single country QList OpenAIP::readNavAids(const QString& countryCode) { return NavAid::readXML(getNavAidsFilename(countryCode)); } QSharedPointer> OpenAIP::getAirspaces() { QDateTime filesDateTime = getAirspacesModifiedDateTime(); if (!m_airspaces || (filesDateTime > m_airspacesModifiedDateTime)) { // Using shared pointer, so old object, if it exists, will be deleted, when no longer user m_airspaces = QSharedPointer>(readAirspaces()); m_airspacesModifiedDateTime = filesDateTime; } return m_airspaces; } QSharedPointer> OpenAIP::getNavAids() { QDateTime filesDateTime = getNavAidsModifiedDateTime(); if (!m_navAids || (filesDateTime > m_navAidsModifiedDateTime)) { // Using shared pointer, so old object, if it exists, will be deleted, when no longer user m_navAids = QSharedPointer>(readNavAids()); m_navAidsModifiedDateTime = filesDateTime; } return m_navAids; } // Gets the date and time the airspaces files were most recently modified QDateTime OpenAIP::getAirspacesModifiedDateTime() { QDateTime dateTime; for (const auto& countryCode : m_countryCodes) { QFileInfo fileInfo(getAirspaceFilename(countryCode)); QDateTime fileModifiedDateTime = fileInfo.lastModified(); if (fileModifiedDateTime > dateTime) { dateTime = fileModifiedDateTime; } } return dateTime; } // Gets the date and time the navaid files were most recently modified QDateTime OpenAIP::getNavAidsModifiedDateTime() { QDateTime dateTime; for (const auto& countryCode : m_countryCodes) { QFileInfo fileInfo(getNavAidsFilename(countryCode)); QDateTime fileModifiedDateTime = fileInfo.lastModified(); if (fileModifiedDateTime > dateTime) { dateTime = fileModifiedDateTime; } } return dateTime; }