From 9c7aa8b3336216c5d9ac0b0024dca01174bb0a34 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Tue, 14 Feb 2023 14:46:08 +0000 Subject: [PATCH] Map Updates Allow OpenSkyNetwork DB, OpenAIP and OurAirports DB stuctures to be shared by different plugins, to speed up loading. Perform map anti-aliasing on the whole map, rather than just info boxes, to improve rendering speed when there are many items. Add map multisampling as a preference. Add plotting of airspaces, airports, navaids on Map feature. Add support for polylines and polygons to be plotted on Map feature. Add support for images to 2D Map feature. Add distance and name filters to Map feature. Filter map items when zoomed out or if off screen, to improve rendering performance. Add UK DAB, FM and AM transmitters to Map feature. Use labelless maps for 2D transmit maps in Map feature (same as in ADS-B demod). --- plugins/channelrx/demodadsb/CMakeLists.txt | 1 - plugins/channelrx/demodadsb/adsbdemodgui.cpp | 398 +- plugins/channelrx/demodadsb/adsbdemodgui.h | 56 +- plugins/channelrx/demodadsb/adsbdemodgui.ui | 3 + plugins/channelrx/demodadsb/map/map.qml | 16 +- plugins/channelrx/demodadsb/ourairportsdb.h | 303 -- plugins/feature/map/CMakeLists.txt | 3 + plugins/feature/map/cesiuminterface.cpp | 19 +- plugins/feature/map/cesiuminterface.h | 9 +- plugins/feature/map/czml.cpp | 185 +- plugins/feature/map/czml.h | 11 +- plugins/feature/map/data.qrc | 5 + plugins/feature/map/data/transmitters.csv | 3478 +++++++++++++++++ plugins/feature/map/icons.qrc | 2 + plugins/feature/map/icons/controltower.png | Bin 0 -> 308 bytes plugins/feature/map/icons/vor.png | Bin 0 -> 365 bytes plugins/feature/map/map.qrc | 12 + plugins/feature/map/map/DME.png | Bin 0 -> 10606 bytes plugins/feature/map/map/DVOR-DME.png | Bin 0 -> 652 bytes plugins/feature/map/map/DVOR.png | Bin 0 -> 415 bytes plugins/feature/map/map/DVORTAC.png | Bin 0 -> 847 bytes plugins/feature/map/map/NDB.png | Bin 0 -> 674 bytes plugins/feature/map/map/VOR-DME.png | Bin 0 -> 652 bytes plugins/feature/map/map/VOR.png | Bin 0 -> 415 bytes plugins/feature/map/map/VORTAC.png | Bin 0 -> 847 bytes plugins/feature/map/map/airport_large.png | Bin 0 -> 1261 bytes plugins/feature/map/map/airport_medium.png | Bin 0 -> 1015 bytes plugins/feature/map/map/airport_small.png | Bin 0 -> 1084 bytes plugins/feature/map/map/heliport.png | Bin 0 -> 1131 bytes plugins/feature/map/map/map.qml | 146 +- plugins/feature/map/map/map3d.html | 27 +- plugins/feature/map/mapgui.cpp | 495 ++- plugins/feature/map/mapgui.h | 25 +- plugins/feature/map/mapitem.cpp | 288 ++ plugins/feature/map/mapitem.h | 184 + plugins/feature/map/mapmodel.cpp | 1068 +++-- plugins/feature/map/mapmodel.h | 452 ++- plugins/feature/map/mapsettings.cpp | 128 +- plugins/feature/map/mapsettings.h | 22 +- plugins/feature/map/mapsettingsdialog.cpp | 130 +- plugins/feature/map/mapsettingsdialog.h | 23 +- plugins/feature/map/mapsettingsdialog.ui | 237 +- plugins/feature/map/osmtemplateserver.h | 19 + plugins/feature/map/readme.md | 7 +- sdrbase/CMakeLists.txt | 3 + sdrbase/settings/mainsettings.h | 7 + sdrbase/settings/preferences.cpp | 3 + sdrbase/settings/preferences.h | 9 +- sdrbase/util/openaip.cpp | 320 +- sdrbase/util/openaip.h | 268 +- sdrbase/util/osndb.cpp | 546 +++ sdrbase/util/osndb.h | 501 +-- sdrbase/util/ourairportsdb.cpp | 419 ++ sdrbase/util/ourairportsdb.h | 119 + sdrgui/gui/graphicsdialog.cpp | 7 + sdrgui/gui/graphicsdialog.ui | 145 +- swagger/sdrangel/api/swagger/include/Map.yaml | 20 +- .../sdrangel/code/qt5/client/SWGMapItem.cpp | 100 + swagger/sdrangel/code/qt5/client/SWGMapItem.h | 24 + .../sdrangel/code/qt5/client/SWGMapItem_2.cpp | 100 + .../sdrangel/code/qt5/client/SWGMapItem_2.h | 24 + 61 files changed, 8373 insertions(+), 1994 deletions(-) delete mode 100644 plugins/channelrx/demodadsb/ourairportsdb.h create mode 100644 plugins/feature/map/data.qrc create mode 100644 plugins/feature/map/data/transmitters.csv create mode 100644 plugins/feature/map/icons/controltower.png create mode 100644 plugins/feature/map/icons/vor.png create mode 100644 plugins/feature/map/map/DME.png create mode 100644 plugins/feature/map/map/DVOR-DME.png create mode 100644 plugins/feature/map/map/DVOR.png create mode 100644 plugins/feature/map/map/DVORTAC.png create mode 100644 plugins/feature/map/map/NDB.png create mode 100644 plugins/feature/map/map/VOR-DME.png create mode 100644 plugins/feature/map/map/VOR.png create mode 100644 plugins/feature/map/map/VORTAC.png create mode 100644 plugins/feature/map/map/airport_large.png create mode 100644 plugins/feature/map/map/airport_medium.png create mode 100644 plugins/feature/map/map/airport_small.png create mode 100644 plugins/feature/map/map/heliport.png create mode 100644 plugins/feature/map/mapitem.cpp create mode 100644 plugins/feature/map/mapitem.h create mode 100644 sdrbase/util/ourairportsdb.cpp create mode 100644 sdrbase/util/ourairportsdb.h diff --git a/plugins/channelrx/demodadsb/CMakeLists.txt b/plugins/channelrx/demodadsb/CMakeLists.txt index 02078f24a..6d2e72368 100644 --- a/plugins/channelrx/demodadsb/CMakeLists.txt +++ b/plugins/channelrx/demodadsb/CMakeLists.txt @@ -29,7 +29,6 @@ set(adsb_HEADERS include_directories( ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client ${Boost_INCLUDE_DIRS} - ${Qt${QT_DEFAULT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS} ) if(NOT SERVER_MODE) diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index 3b404e035..446ce3f46 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -35,8 +35,6 @@ #include #include -#include - #include "ui_adsbdemodgui.h" #include "device/deviceapi.h" #include "channel/channelwebapiutils.h" @@ -531,14 +529,7 @@ QVariant AirportModel::data(const QModelIndex &index, int role) const else if (role == AirportModel::airportImageRole) { // Select an image to use for the airport - if (m_airports[row]->m_type == ADSBDemodSettings::AirportType::Large) - return QVariant::fromValue(QString("airport_large.png")); - else if (m_airports[row]->m_type == ADSBDemodSettings::AirportType::Medium) - return QVariant::fromValue(QString("airport_medium.png")); - else if (m_airports[row]->m_type == ADSBDemodSettings::AirportType::Heliport) - return QVariant::fromValue(QString("heliport.png")); - else - return QVariant::fromValue(QString("airport_small.png")); + return QVariant::fromValue(m_airports[row]->getImageName()); } else if (role == AirportModel::bubbleColourRole) { @@ -618,23 +609,17 @@ QVariant AirspaceModel::data(const QModelIndex &index, int role) const } else if (role == AirspaceModel::airspaceBorderColorRole) { - if (m_airspaces[row]->m_category == "D") - { - return QVariant::fromValue(QColor("blue")); - } - else - { - return QVariant::fromValue(QColor("red")); + if (m_airspaces[row]->m_category == "D") { + return QVariant::fromValue(QColor(0x00, 0x00, 0xff, 0x00)); + } else { + return QVariant::fromValue(QColor(0xff, 0x00, 0x00, 0x00)); } } else if (role == AirspaceModel::airspaceFillColorRole) { - if (m_airspaces[row]->m_category == "D") - { + if (m_airspaces[row]->m_category == "D") { return QVariant::fromValue(QColor(0x00, 0x00, 0xff, 0x10)); - } - else - { + } else { return QVariant::fromValue(QColor(0xff, 0x00, 0x00, 0x10)); } } @@ -789,6 +774,22 @@ bool ADSBDemodGUI::updateLocalPosition(Aircraft *aircraft, double latitude, doub } } +void ADSBDemodGUI::clearFromMap(const QString& name) +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_adsbDemod, "mapitems", mapPipes); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString("")); + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem); + messageQueue->push(msg); + } +} + void ADSBDemodGUI::sendToMap(Aircraft *aircraft, QList *animations) { // Send to Map feature @@ -817,6 +818,7 @@ void ADSBDemodGUI::sendToMap(Aircraft *aircraft, QListsetAltitude(altitudeM); swgMapItem->setPositionDateTime(new QString(aircraft->m_positionDateTime.toString(Qt::ISODateWithMs))); swgMapItem->setFixedPosition(false); + swgMapItem->setAvailableUntil(new QString(aircraft->m_positionDateTime.addSecs(60).toString(Qt::ISODateWithMs))); swgMapItem->setImage(new QString(QString("qrc:///map/%1").arg(aircraft->getImage()))); swgMapItem->setImageRotation(aircraft->m_heading); swgMapItem->setText(new QString(aircraft->getText(true))); @@ -996,9 +998,12 @@ Aircraft *ADSBDemodGUI::getAircraft(int icao, bool &newAircraft) } } - if (m_settings.m_autoResizeTableColumns) - ui->adsbData->resizeColumnsToContents(); - ui->adsbData->setSortingEnabled(true); + if (!m_loadingData) + { + if (m_settings.m_autoResizeTableColumns) + ui->adsbData->resizeColumnsToContents(); + ui->adsbData->setSortingEnabled(true); + } // Check to see if we need to emit a notification about this new aircraft checkStaticNotification(aircraft); } @@ -3586,17 +3591,10 @@ void ADSBDemodGUI::on_getOSNDB_clicked() // Don't try to download while already in progress if (m_progressDialog == nullptr) { - QString osnDBFilename = AircraftInformation::getOSNDBZipFilename(); - if (confirmDownload(osnDBFilename)) - { - // Download Opensky network database to a file - QUrl dbURL(QString(OSNDB_URL)); - m_progressDialog = new QProgressDialog(this); - m_progressDialog->setCancelButton(nullptr); - m_progressDialog->setLabelText(QString("Downloading %1.").arg(OSNDB_URL)); - QNetworkReply *reply = m_dlm.download(dbURL, osnDBFilename); - connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64))); - } + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); + m_osnDB.downloadAircraftInformation(); } } @@ -3605,17 +3603,10 @@ void ADSBDemodGUI::on_getAirportDB_clicked() // Don't try to download while already in progress if (m_progressDialog == nullptr) { - QString airportDBFile = getAirportDBFilename(); - if (confirmDownload(airportDBFile)) - { - // Download Opensky network database to a file - QUrl dbURL(QString(AIRPORTS_URL)); - m_progressDialog = new QProgressDialog(this); - m_progressDialog->setCancelButton(nullptr); - m_progressDialog->setLabelText(QString("Downloading %1.").arg(AIRPORTS_URL)); - QNetworkReply *reply = m_dlm.download(dbURL, airportDBFile); - connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64))); - } + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); + m_ourAirportsDB.downloadAirportInformation(); } } @@ -3627,6 +3618,7 @@ void ADSBDemodGUI::on_getAirspacesDB_clicked() m_progressDialog = new QProgressDialog(this); m_progressDialog->setMaximum(OpenAIP::m_countryCodes.size()); m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); m_openAIP.downloadAirspaces(); } } @@ -3651,136 +3643,6 @@ QString ADSBDemodGUI::getDataDir() return locations[0]; } -QString ADSBDemodGUI::getAirportDBFilename() -{ - return getDataDir() + "/airportDatabase.csv"; -} - -QString ADSBDemodGUI::getAirportFrequenciesDBFilename() -{ - return getDataDir() + "/airportFrequenciesDatabase.csv"; -} - -qint64 ADSBDemodGUI::fileAgeInDays(QString filename) -{ - QFile file(filename); - if (file.exists()) - { - QDateTime modified = file.fileTime(QFileDevice::FileModificationTime); - if (modified.isValid()) - return modified.daysTo(QDateTime::currentDateTime()); - else - return -1; - } - return -1; -} - -bool ADSBDemodGUI::confirmDownload(QString filename) -{ - qint64 age = fileAgeInDays(filename); - if ((age == -1) || (age > 100)) - return true; - else - { - QMessageBox::StandardButton reply; - if (age == 0) - reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded today. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); - else if (age == 1) - reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded yesterday. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); - else - reply = QMessageBox::question(this, "Confirm download", QString("This file was last downloaded %1 days ago. Are you sure you wish to redownload this file?").arg(age), QMessageBox::Yes|QMessageBox::No); - return reply == QMessageBox::Yes; - } -} - -bool ADSBDemodGUI::readOSNDB(const QString& filename) -{ - m_aircraftInfo = AircraftInformation::readOSNDB(filename); - return m_aircraftInfo != nullptr; -} - -bool ADSBDemodGUI::readFastDB(const QString& filename) -{ - m_aircraftInfo = AircraftInformation::readFastDB(filename); - return m_aircraftInfo != nullptr; -} - -void ADSBDemodGUI::updateDownloadProgress(qint64 bytesRead, qint64 totalBytes) -{ - if (m_progressDialog) - { - m_progressDialog->setMaximum(totalBytes); - m_progressDialog->setValue(bytesRead); - } -} - -void ADSBDemodGUI::downloadFinished(const QString& filename, bool success) -{ - bool closeDialog = true; - if (success) - { - if (filename == AircraftInformation::getOSNDBZipFilename()) - { - // Extract .csv file from .zip file - QZipReader reader(filename); - QByteArray database = reader.fileData("media/data/samples/metadata/aircraftDatabase.csv"); - if (database.size() > 0) - { - QFile file(AircraftInformation::getOSNDBFilename()); - if (file.open(QIODevice::WriteOnly)) - { - file.write(database); - file.close(); - } - else - { - qWarning() << "ADSBDemodGUI::downloadFinished - Failed to open " << file.fileName() << " for writing"; - } - } - else - { - qWarning() << "ADSBDemodGUI::downloadFinished - aircraftDatabase.csv not in expected dir. Extracting all."; - if (!reader.extractAll(getDataDir())) { - qWarning() << "ADSBDemodGUI::downloadFinished - Failed to extract files from " << filename; - } - } - readOSNDB(AircraftInformation::getOSNDBFilename()); - // Convert to condensed format for faster loading later - m_progressDialog->setLabelText("Processing."); - AircraftInformation::writeFastDB(AircraftInformation::getFastDBFilename(), m_aircraftInfo); - } - else if (filename == getAirportDBFilename()) - { - m_airportInfo = AirportInformation::readAirportsDB(filename); - // Now download airport frequencies - QUrl dbURL(QString(AIRPORT_FREQUENCIES_URL)); - m_progressDialog->setLabelText(QString("Downloading %1.").arg(AIRPORT_FREQUENCIES_URL)); - QNetworkReply *reply = m_dlm.download(dbURL, getAirportFrequenciesDBFilename()); - connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64))); - closeDialog = false; - } - else if (filename == getAirportFrequenciesDBFilename()) - { - if (m_airportInfo != nullptr) - { - AirportInformation::readFrequenciesDB(filename, m_airportInfo); - // Update airports on map - updateAirports(); - } - } - else - { - qDebug() << "ADSBDemodGUI::downloadFinished: Unexpected filename: " << filename; - } - } - if (closeDialog && m_progressDialog) - { - m_progressDialog->close(); - delete m_progressDialog; - m_progressDialog = nullptr; - } -} - void ADSBDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) { (void) widget; @@ -4335,12 +4197,12 @@ void ADSBDemodGUI::updateAirports() } m_airportModel.removeAllAirports(); - QHash::iterator i = m_airportInfo->begin(); + QHash::const_iterator i = m_airportInfo->begin(); AzEl azEl = m_azEl; while (i != m_airportInfo->end()) { - AirportInformation *airportInfo = i.value(); + const AirportInformation *airportInfo = i.value(); // Calculate distance and az/el to airport from My Position azEl.setTarget(airportInfo->m_latitude, airportInfo->m_longitude, Units::feetToMetres(airportInfo->m_elevation)); @@ -4370,7 +4232,7 @@ void ADSBDemodGUI::updateAirspaces() { AzEl azEl = m_azEl; m_airspaceModel.removeAllAirspaces(); - for (const auto& airspace: m_airspaces) + for (const auto airspace: *m_airspaces) { if (m_settings.m_airspaces.contains(airspace->m_category)) { @@ -4392,7 +4254,7 @@ void ADSBDemodGUI::updateNavAids() m_navAidModel.removeAllNavAids(); if (m_settings.m_displayNavAids) { - for (const auto& navAid: m_navAids) + for (const auto navAid: *m_navAids) { // Calculate distance to NavAid from My Position azEl.setTarget(navAid->m_latitude, navAid->m_longitude, Units::feetToMetres(navAid->m_elevation)); @@ -4745,7 +4607,8 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb m_airspaceModel(this), m_trackAircraft(nullptr), m_highlightAircraft(nullptr), - m_progressDialog(nullptr) + m_progressDialog(nullptr), + m_loadingData(false) { setAttribute(Qt::WA_DeleteOnClose, true); m_helpURL = "plugins/channelrx/demodadsb/readme.md"; @@ -4755,7 +4618,17 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb rollupContents->arrangeRollups(); connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); - m_osmPort = 0; // Pick a free port + // Enable MSAA antialiasing on 2D map + // This is much faster than using layer.smooth in the QML, when there are many items + int multisamples = MainCore::instance()->getSettings().getMapMultisampling(); + if (multisamples > 0) + { + QSurfaceFormat format; + format.setSamples(multisamples); + ui->map->setFormat(format); + } + + m_osmPort = 0; // Pick a free port m_templateServer = new ADSBOSMTemplateServer("q2RVNAe3eFKCH4XsrE3r", m_osmPort); ui->map->setAttribute(Qt::WA_AcceptTouchEvents, true); @@ -4767,7 +4640,6 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map.qml"))); connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); - connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &ADSBDemodGUI::downloadFinished); m_adsbDemod = reinterpret_cast(rxChannel); //new ADSBDemod(m_deviceUISet->m_deviceSourceAPI); m_adsbDemod->setMessageQueueToGUI(getInputMessageQueue()); @@ -4836,28 +4708,28 @@ ADSBDemodGUI::ADSBDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb ui->flightDetails->setVisible(false); ui->aircraftDetails->setVisible(false); - AircraftInformation::init(); - // Read aircraft information database, if it has previously been downloaded - if (!readFastDB(AircraftInformation::getFastDBFilename())) - { - if (readOSNDB(AircraftInformation::getOSNDBFilename())) - AircraftInformation::writeFastDB(AircraftInformation::getFastDBFilename(), m_aircraftInfo); - } - // Read airport information database, if it has previously been downloaded - m_airportInfo = AirportInformation::readAirportsDB(getAirportDBFilename()); - if (m_airportInfo != nullptr) - AirportInformation::readFrequenciesDB(getAirportFrequenciesDBFilename(), m_airportInfo); + AircraftInformation::init(); + connect(&m_osnDB, &OsnDB::downloadingURL, this, &ADSBDemodGUI::downloadingURL); + connect(&m_osnDB, &OsnDB::downloadError, this, &ADSBDemodGUI::downloadError); + connect(&m_osnDB, &OsnDB::downloadProgress, this, &ADSBDemodGUI::downloadProgress); + connect(&m_osnDB, &OsnDB::downloadAircraftInformationFinished, this, &ADSBDemodGUI::downloadAircraftInformationFinished); + m_aircraftInfo = OsnDB::getAircraftInformation(); + // Read airport information database, if it has previously been downloaded + connect(&m_ourAirportsDB, &OurAirportsDB::downloadingURL, this, &ADSBDemodGUI::downloadingURL); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadError, this, &ADSBDemodGUI::downloadError); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadProgress, this, &ADSBDemodGUI::downloadProgress); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadAirportInformationFinished, this, &ADSBDemodGUI::downloadAirportInformationFinished); + m_airportInfo = OurAirportsDB::getAirportsById(); + + // Read airspaces and NAVAIDs connect(&m_openAIP, &OpenAIP::downloadingURL, this, &ADSBDemodGUI::downloadingURL); connect(&m_openAIP, &OpenAIP::downloadError, this, &ADSBDemodGUI::downloadError); connect(&m_openAIP, &OpenAIP::downloadAirspaceFinished, this, &ADSBDemodGUI::downloadAirspaceFinished); connect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &ADSBDemodGUI::downloadNavAidsFinished); - - // Read airspaces - m_airspaces = OpenAIP::readAirspaces(); - // Read NavAids - m_navAids = OpenAIP::readNavAids(); + m_airspaces = OpenAIP::getAirspaces(); + m_navAids = OpenAIP::getNavAids(); // Get station position Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); @@ -4929,26 +4801,24 @@ ADSBDemodGUI::~ADSBDemodGUI() disconnect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &ADSBDemodGUI::downloadNavAidsFinished); disconnect(&m_planeSpotters, &PlaneSpotters::aircraftPhoto, this, &ADSBDemodGUI::aircraftPhoto); disconnect(&m_redrawMapTimer, &QTimer::timeout, this, &ADSBDemodGUI::redrawMap); + disconnect(&MainCore::instance()->getMasterTimer(), &QTimer::timeout, this, &ADSBDemodGUI::tick); m_redrawMapTimer.stop(); + // Remove aircraft from Map feature + QHash::iterator i = m_aircraft.begin(); + while (i != m_aircraft.end()) + { + Aircraft *aircraft = i.value(); + clearFromMap(QString("%1").arg(aircraft->m_icao, 0, 16)); + ++i; + } delete ui; qDeleteAll(m_aircraft); - if (m_airportInfo) { - qDeleteAll(*m_airportInfo); - } - if (m_aircraftInfo) { - qDeleteAll(*m_aircraftInfo); - } if (m_flightInformation) { disconnect(m_flightInformation, &FlightInformation::flightUpdated, this, &ADSBDemodGUI::flightInformationUpdated); delete m_flightInformation; } - if (m_aviationWeather) - { - delete m_aviationWeather; - } - qDeleteAll(m_airspaces); - qDeleteAll(m_navAids); + delete m_aviationWeather; qDeleteAll(m_3DModelMatch); delete m_networkManager; } @@ -5116,7 +4986,7 @@ void ADSBDemodGUI::tick() // Tick is called 20x a second - lets check this every 10 seconds if (m_tickCount % (20*10) == 0) { - // Remove aircraft that haven't been heard of for a minute as probably out of range + // Remove aircraft that haven't been heard of for a user-defined time, as probably out of range QDateTime now = QDateTime::currentDateTime(); qint64 nowSecs = now.toSecsSinceEpoch(); QHash::iterator i = m_aircraft.begin(); @@ -5142,18 +5012,7 @@ void ADSBDemodGUI::tick() // Remove aircraft from hash i = m_aircraft.erase(i); // Remove from map feature - QList mapPipes; - MainCore::instance()->getMessagePipes().getMessagePipes(this, "mapitems", mapPipes); - - for (const auto& pipe : mapPipes) - { - MessageQueue *messageQueue = qobject_cast(pipe->m_element); - SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); - swgMapItem->setName(new QString(QString("%1").arg(aircraft->m_icao, 0, 16))); - swgMapItem->setImage(new QString("")); - MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_adsbDemod, swgMapItem); - messageQueue->push(msg); - } + clearFromMap(QString("%1").arg(aircraft->m_icao, 0, 16)); // And finally free its memory delete aircraft; @@ -5376,6 +5235,9 @@ void ADSBDemodGUI::on_logOpen_clicked() QFile file(fileNames[0]); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QDateTime startTime = QDateTime::currentDateTime(); + m_loadingData = true; + ui->adsbData->blockSignals(true); QTextStream in(&file); QString error; QHash colIndexes = CSV::readHeader(in, {"Data", "Correlation"}, error); @@ -5414,7 +5276,7 @@ void ADSBDemodGUI::on_logOpen_clicked() } //qDebug() << "bytes.szie " << bytes.size() << " crc " << Qt::hex << crcCalc; handleADSB(bytes, dateTime, correlation, correlation, crcCalc, false); - if ((count > 0) && (count % 10000 == 0)) + if ((count > 0) && (count % 100000 == 0)) { dialog.setText(QString("Reading ADS-B data\n%1 (Skipped %2)").arg(count).arg(countOtherDF)); QApplication::processEvents(); @@ -5437,6 +5299,13 @@ void ADSBDemodGUI::on_logOpen_clicked() { QMessageBox::critical(this, "ADS-B", error); } + ui->adsbData->blockSignals(false); + m_loadingData = false; + if (m_settings.m_autoResizeTableColumns) + ui->adsbData->resizeColumnsToContents(); + ui->adsbData->setSortingEnabled(true); + QDateTime finishTime = QDateTime::currentDateTime(); + qDebug() << "Read CSV in " << startTime.secsTo(finishTime); } else { @@ -5455,6 +5324,15 @@ void ADSBDemodGUI::downloadingURL(const QString& url) } } +void ADSBDemodGUI::downloadProgress(qint64 bytesRead, qint64 totalBytes) +{ + if (m_progressDialog) + { + m_progressDialog->setMaximum(totalBytes); + m_progressDialog->setValue(bytesRead); + } +} + void ADSBDemodGUI::downloadError(const QString& error) { QMessageBox::critical(this, "ADS-B", error); @@ -5471,7 +5349,7 @@ void ADSBDemodGUI::downloadAirspaceFinished() if (m_progressDialog) { m_progressDialog->setLabelText("Reading airspaces."); } - m_airspaces = OpenAIP::readAirspaces(); + m_airspaces = OpenAIP::getAirspaces(); updateAirspaces(); m_openAIP.downloadNavAids(); } @@ -5481,7 +5359,7 @@ void ADSBDemodGUI::downloadNavAidsFinished() if (m_progressDialog) { m_progressDialog->setLabelText("Reading NAVAIDs."); } - m_navAids = OpenAIP::readNavAids(); + m_navAids = OpenAIP::getNavAids(); updateNavAids(); if (m_progressDialog) { @@ -5491,6 +5369,50 @@ void ADSBDemodGUI::downloadNavAidsFinished() } } +void ADSBDemodGUI::downloadAircraftInformationFinished() +{ + if (m_progressDialog) + { + delete m_progressDialog; + m_progressDialog = new QProgressDialog("Reading Aircraft Information.", "", 0, 1, this); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); + m_progressDialog->setWindowModality(Qt::WindowModal); + m_progressDialog->show(); + QApplication::processEvents(); + } + m_aircraftInfo = OsnDB::getAircraftInformation(); + m_aircraftModel.updateAircraftInformation(m_aircraftInfo); + if (m_progressDialog) + { + m_progressDialog->close(); + delete m_progressDialog; + m_progressDialog = nullptr; + } +} + +void ADSBDemodGUI::downloadAirportInformationFinished() +{ + if (m_progressDialog) + { + delete m_progressDialog; + m_progressDialog = new QProgressDialog("Reading Airport Information.", "", 0, 1, this); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); + m_progressDialog->setWindowModality(Qt::WindowModal); + m_progressDialog->show(); + QApplication::processEvents(); + } + m_airportInfo = OurAirportsDB::getAirportsById(); + updateAirports(); + if (m_progressDialog) + { + m_progressDialog->close(); + delete m_progressDialog; + m_progressDialog = nullptr; + } +} + int ADSBDemodGUI::squawkDecode(int modeA) const { int a, b, c, d; @@ -5771,16 +5693,21 @@ void ADSBDemodGUI::preferenceChanged(int elementType) Real stationLongitude = MainCore::instance()->getSettings().getLongitude(); Real stationAltitude = MainCore::instance()->getSettings().getAltitude(); - if ( (stationLatitude != m_azEl.getLocationSpherical().m_latitude) - || (stationLongitude != m_azEl.getLocationSpherical().m_longitude) - || (stationAltitude != m_azEl.getLocationSpherical().m_altitude)) + QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude); + QGeoCoordinate previousPosition(m_azEl.getLocationSpherical().m_latitude, m_azEl.getLocationSpherical().m_longitude, m_azEl.getLocationSpherical().m_altitude); + + if (stationPosition != previousPosition) { m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); - // Update distances and what is visible - updateAirports(); - updateAirspaces(); - updateNavAids(); + // Update distances and what is visible, but only do it if position has changed significantly + if (!m_lastFullUpdatePosition.isValid() || (stationPosition.distanceTo(m_lastFullUpdatePosition) >= 1000)) + { + updateAirports(); + updateAirspaces(); + updateNavAids(); + m_lastFullUpdatePosition = stationPosition; + } // Update icon position on Map QQuickItem *item = ui->map->rootObject(); @@ -5875,3 +5802,4 @@ void ADSBDemodGUI::updateAbsoluteCenterFrequency() { setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); } + diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.h b/plugins/channelrx/demodadsb/adsbdemodgui.h index 2d0d414a1..c54f66ecd 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.h +++ b/plugins/channelrx/demodadsb/adsbdemodgui.h @@ -37,7 +37,6 @@ #include "util/messagequeue.h" #include "util/azel.h" #include "util/movingaverage.h" -#include "util/httpdownloadmanager.h" #include "util/flightinformation.h" #include "util/openaip.h" #include "util/planespotters.h" @@ -47,7 +46,7 @@ #include "SWGMapItem.h" #include "adsbdemodsettings.h" -#include "ourairportsdb.h" +#include "util/ourairportsdb.h" #include "util/osndb.h" class PluginAPI; @@ -464,7 +463,19 @@ public: allAircraftUpdated(); } - Q_INVOKABLE void findOnMap(int index); + Q_INVOKABLE void findOnMap(int index); + + void updateAircraftInformation(QSharedPointer> aircraftInfo) + { + for (auto aircraft : m_aircrafts) + { + if (aircraftInfo->contains(aircraft->m_icao)) { + aircraft->m_aircraftInfo = aircraftInfo->value(aircraft->m_icao); + } else { + aircraft->m_aircraftInfo = nullptr; + } + } + } private: QList m_aircrafts; @@ -493,7 +504,7 @@ public: { } - Q_INVOKABLE void addAirport(AirportInformation *airport, float az, float el, float distance) { + Q_INVOKABLE void addAirport(const AirportInformation *airport, float az, float el, float distance) { QString text; int rows; @@ -558,7 +569,7 @@ public: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } - void airportFreq(AirportInformation *airport, float az, float el, float distance, QString& text, int& rows) { + void airportFreq(const AirportInformation *airport, float az, float el, float distance, QString& text, int& rows) { // Create the text to go in the bubble next to the airport // Display name and frequencies QStringList list; @@ -567,7 +578,7 @@ public: rows = 1; for (int i = 0; i < airport->m_frequencies.size(); i++) { - AirportInformation::FrequencyInformation *frequencyInfo = airport->m_frequencies[i]; + const AirportInformation::FrequencyInformation *frequencyInfo = airport->m_frequencies[i]; list.append(QString("%1: %2 MHz").arg(frequencyInfo->m_type).arg(frequencyInfo->m_frequency)); rows++; } @@ -577,7 +588,7 @@ public: text = list.join("\n"); } - void airportUpdated(AirportInformation *airport) { + void airportUpdated(const AirportInformation *airport) { int row = m_airports.indexOf(airport); if (row >= 0) { @@ -614,7 +625,7 @@ public: private: ADSBDemodGUI *m_gui; - QList m_airports; + QList m_airports; QList m_airportDataFreq; QList m_airportDataFreqRows; QList m_showFreq; @@ -894,14 +905,15 @@ private: MessageQueue m_inputMessageQueue; QHash m_aircraft; // Hashed on ICAO - QHash *m_aircraftInfo; - QHash *m_airportInfo; // Hashed on id + QSharedPointer> m_aircraftInfo; + QSharedPointer> m_airportInfo; // Hashed on id AircraftModel m_aircraftModel; AirportModel m_airportModel; AirspaceModel m_airspaceModel; NavAidModel m_navAidModel; - QList m_airspaces; - QList m_navAids; + QSharedPointer> m_airspaces; + QSharedPointer> m_navAids; + QGeoCoordinate m_lastFullUpdatePosition; AzEl m_azEl; // Position of station Aircraft *m_trackAircraft; // Aircraft we want to track in Channel Report @@ -920,10 +932,11 @@ private: AviationWeather *m_aviationWeather; QString m_photoLink; WebAPIAdapterInterface *m_webAPIAdapterInterface; - HttpDownloadManager m_dlm; QProgressDialog *m_progressDialog; quint16 m_osmPort; OpenAIP m_openAIP; + OsnDB m_osnDB; + OurAirportsDB m_ourAirportsDB; ADSBOSMTemplateServer *m_templateServer; QRandomGenerator m_random; QHash m_3DModels; // Hashed aircraft_icao or just aircraft @@ -935,6 +948,7 @@ private: QTimer m_importTimer; QTimer m_redrawMapTimer; QNetworkAccessManager *m_networkManager; + bool m_loadingData; static const char m_idMap[]; static const QString m_categorySetA[]; @@ -957,6 +971,7 @@ private: void updatePosition(Aircraft *aircraft); bool updateLocalPosition(Aircraft *aircraft, double latitude, double longitude, bool surfacePosition); + void clearFromMap(const QString& name); void sendToMap(Aircraft *aircraft, QList *animations); Aircraft *getAircraft(int icao, bool &newAircraft); void callsignToFlight(Aircraft *aircraft); @@ -984,17 +999,8 @@ private: QString subAircraftString(Aircraft *aircraft, const QString &string); void resizeTable(); QString getDataDir(); - QString getAirportDBFilename(); - QString getAirportFrequenciesDBFilename(); - QString getOSNDBZipFilename(); - QString getOSNDBFilename(); - QString getFastDBFilename(); - qint64 fileAgeInDays(QString filename); - bool confirmDownload(QString filename); void readAirportDB(const QString& filename); void readAirportFrequenciesDB(const QString& filename); - bool readOSNDB(const QString& filename); - bool readFastDB(const QString& filename); void update3DModels(); void updateAirports(); void updateAirspaces(); @@ -1047,8 +1053,6 @@ private slots: void onMenuDialogCalled(const QPoint& p); void handleInputMessages(); void tick(); - void updateDownloadProgress(qint64 bytesRead, qint64 totalBytes); - void downloadFinished(const QString& filename, bool success); void on_device_currentIndexChanged(int index); void feedSelect(const QPoint& p); void on_displaySettings_clicked(); @@ -1057,8 +1061,11 @@ private slots: void on_logOpen_clicked(); void downloadingURL(const QString& url); void downloadError(const QString& error); + void downloadProgress(qint64 bytesRead, qint64 totalBytes); void downloadAirspaceFinished(); void downloadNavAidsFinished(); + void downloadAircraftInformationFinished(); + void downloadAirportInformationFinished(); void photoClicked(); virtual void showEvent(QShowEvent *event) override; virtual bool eventFilter(QObject *obj, QEvent *event) override; @@ -1073,3 +1080,4 @@ signals: }; #endif // INCLUDE_ADSBDEMODGUI_H + diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.ui b/plugins/channelrx/demodadsb/adsbdemodgui.ui index aa682d8e4..85846e61a 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.ui +++ b/plugins/channelrx/demodadsb/adsbdemodgui.ui @@ -844,6 +844,9 @@ QAbstractItemView::SingleSelection + + QAbstractItemView::SelectRows + ICAO ID diff --git a/plugins/channelrx/demodadsb/map/map.qml b/plugins/channelrx/demodadsb/map/map.qml index 7c3b2e0ab..78cdbe368 100644 --- a/plugins/channelrx/demodadsb/map/map.qml +++ b/plugins/channelrx/demodadsb/map/map.qml @@ -140,8 +140,8 @@ Item { Grid { horizontalItemAlignment: Grid.AlignHCenter columnSpacing: 5 - layer.enabled: true - layer.smooth: true + //layer.enabled: true + //layer.smooth: true Image { id: image source: navAidImage @@ -206,8 +206,8 @@ Item { sourceItem: Grid { columns: 1 Grid { - layer.enabled: true - layer.smooth: true + //layer.enabled: true + //layer.smooth: true horizontalItemAlignment: Grid.AlignHCenter Text { id: airspaceText @@ -239,8 +239,8 @@ Item { sourceItem: Grid { columns: 1 Grid { - layer.enabled: true - layer.smooth: true + //layer.enabled: true + //layer.smooth: true horizontalItemAlignment: Grid.AlignHCenter Image { id: image @@ -334,8 +334,8 @@ Item { columns: 1 Grid { horizontalItemAlignment: Grid.AlignHCenter - layer.enabled: true - layer.smooth: true + //layer.enabled: true + //layer.smooth: true Image { id: image source: airportImage diff --git a/plugins/channelrx/demodadsb/ourairportsdb.h b/plugins/channelrx/demodadsb/ourairportsdb.h deleted file mode 100644 index 94ddec3df..000000000 --- a/plugins/channelrx/demodadsb/ourairportsdb.h +++ /dev/null @@ -1,303 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2020 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 . // -/////////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDE_OURAIRPORTSDB_H -#define INCLUDE_OURAIRPORTSDB_H - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "util/csv.h" -#include "adsbdemodsettings.h" - -#define AIRPORTS_URL "https://davidmegginson.github.io/ourairports-data/airports.csv" -#define AIRPORT_FREQUENCIES_URL "https://davidmegginson.github.io/ourairports-data/airport-frequencies.csv" - -struct AirportInformation { - - struct FrequencyInformation { - QString m_type; - QString m_description; - float m_frequency; // In MHz - }; - - int m_id; - QString m_ident; - ADSBDemodSettings::AirportType m_type; - QString m_name; - float m_latitude; - float m_longitude; - float m_elevation; - QVector m_frequencies; - - ~AirportInformation() - { - qDeleteAll(m_frequencies); - } - - static QString trimQuotes(const QString s) - { - if (s.startsWith('\"') && s.endsWith('\"')) - return s.mid(1, s.size() - 2); - else - return s; - } - - // Read OurAirport's airport CSV file - // See comments for readOSNDB - static QHash *readAirportsDB(const QString &filename) - { - int cnt = 0; - QHash *airportInfo = nullptr; - - // Column numbers used for the data as of 2020/10/28 - int idCol = 0; - int identCol = 1; - int typeCol = 2; - int nameCol = 3; - int latitudeCol = 4; - int longitudeCol = 5; - int elevationCol = 6; - - qDebug() << "AirportInformation::readAirportsDB: " << filename; - - FILE *file; - QByteArray utfFilename = filename.toUtf8(); - QLocale cLocale(QLocale::C); - if ((file = fopen(utfFilename.constData(), "r")) != NULL) - { - char row[2048]; - - if (fgets(row, sizeof(row), file)) - { - airportInfo = new QHash(); - airportInfo->reserve(70000); - - // Read header - int idx = 0; - char *p = strtok(row, ","); - while (p != NULL) - { - if (!strcmp(p, "id")) - idCol = idx; - else if (!strcmp(p, "ident")) - identCol = idx; - else if (!strcmp(p, "type")) - typeCol = idx; - else if (!strcmp(p, "name")) - nameCol = idx; - else if (!strcmp(p, "latitude_deg")) - latitudeCol = idx; - else if (!strcmp(p, "longitude_deg")) - longitudeCol = idx; - else if (!strcmp(p, "elevation_ft")) - elevationCol = idx; - p = strtok(NULL, ","); - idx++; - } - // Read data - while (fgets(row, sizeof(row), file)) - { - int id = 0; - char *idString = NULL; - char *ident = NULL; - size_t identLen = 0; - char *type = NULL; - size_t typeLen = 0; - char *name = NULL; - size_t nameLen = 0; - float latitude = 0.0f; - char *latitudeString = NULL; - size_t latitudeLen = 0; - float longitude = 0.0f; - char *longitudeString = NULL; - size_t longitudeLen = 0; - float elevation = 0.0f; - char *elevationString = NULL; - size_t elevationLen = 0; - - p = strtok(row, ","); - idx = 0; - while (p != NULL) - { - // Read strings, stripping quotes - if (idx == idCol) - { - idString = p; - idString[strlen(idString)] = '\0'; - id = strtol(idString, NULL, 10); - } - else if (idx == identCol) - { - ident = p+1; - identLen = strlen(ident)-1; - ident[identLen] = '\0'; - } - else if (idx == typeCol) - { - type = p+1; - typeLen = strlen(type)-1; - type[typeLen] = '\0'; - } - else if (idx == nameCol) - { - name = p+1; - nameLen = strlen(name)-1; - name[nameLen] = '\0'; - } - else if (idx == latitudeCol) - { - latitudeString = p; - latitudeLen = strlen(latitudeString)-1; - latitudeString[latitudeLen] = '\0'; - latitude = cLocale.toFloat(latitudeString); - } - else if (idx == longitudeCol) - { - longitudeString = p; - longitudeLen = strlen(longitudeString)-1; - longitudeString[longitudeLen] = '\0'; - longitude = cLocale.toFloat(longitudeString); - } - else if (idx == elevationCol) - { - elevationString = p; - elevationLen = strlen(elevationString)-1; - elevationString[elevationLen] = '\0'; - elevation = cLocale.toFloat(elevationString); - } - p = strtok(NULL, ","); - idx++; - } - - // Only create the entry if we have some interesting data - if (((latitude != 0.0f) || (longitude != 0.0f)) && (type && strcmp(type, "closed"))) - { - AirportInformation *airport = new AirportInformation(); - airport->m_id = id; - airport->m_ident = QString(ident); - if (!strcmp(type, "small_airport")) - airport->m_type = ADSBDemodSettings::AirportType::Small; - else if (!strcmp(type, "medium_airport")) - airport->m_type = ADSBDemodSettings::AirportType::Medium; - else if (!strcmp(type, "large_airport")) - airport->m_type = ADSBDemodSettings::AirportType::Large; - else if (!strcmp(type, "heliport")) - airport->m_type = ADSBDemodSettings::AirportType::Heliport; - airport->m_name = QString(name); - airport->m_latitude = latitude; - airport->m_longitude = longitude; - airport->m_elevation = elevation; - airportInfo->insert(id, airport); - cnt++; - } - } - } - fclose(file); - } - else - qDebug() << "AirportInformation::readAirportsDB: Failed to open " << filename; - - qDebug() << "AirportInformation::readAirportsDB: Read " << cnt << " airports"; - - return airportInfo; - } - - // Read OurAirport's airport frequencies CSV file - static bool readFrequenciesDB(const QString &filename, QHash *airportInfo) - { - int cnt = 0; - - // Column numbers used for the data as of 2020/10/28 - int airportRefCol = 1; - int typeCol = 3; - int descriptionCol = 4; - int frequencyCol = 5; - - qDebug() << "AirportInformation::readFrequenciesDB: " << filename; - - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) - { - QList colNames; - int idx; - - // Read header - if (!file.atEnd()) - { - QByteArray row = file.readLine().trimmed(); - colNames = row.split(','); - // Work out which columns the data is in, based on the headers - idx = colNames.indexOf("airport_ref"); - if (idx >= 0) - airportRefCol = idx; - idx = colNames.indexOf("type"); - if (idx >= 0) - typeCol = idx; - idx = colNames.indexOf("descrption"); - if (idx >= 0) - descriptionCol = idx; - idx = colNames.indexOf("frequency_mhz"); - if (idx >= 0) - frequencyCol = idx; - } - // Read data - while (!file.atEnd()) - { - QByteArray row = file.readLine(); - QList cols = row.split(','); - - bool ok = false; - int airportRef = cols[airportRefCol].toInt(&ok, 10); - if (ok) - { - if (airportInfo->contains(airportRef)) - { - QString type = trimQuotes(cols[typeCol]); - QString description = trimQuotes(cols[descriptionCol]); - float frequency = cols[frequencyCol].toFloat(); - - FrequencyInformation *frequencyInfo = new FrequencyInformation(); - frequencyInfo->m_type = type; - frequencyInfo->m_description = description; - frequencyInfo->m_frequency = frequency; - airportInfo->value(airportRef)->m_frequencies.append(frequencyInfo); - cnt++; - } - } - } - file.close(); - } - else - qDebug() << "Failed to open " << filename << " " << file.errorString(); - - qDebug() << "AirportInformation::readFrequenciesDB: - read " << cnt << " airports"; - - return airportInfo; - } - -}; - -#endif diff --git a/plugins/feature/map/CMakeLists.txt b/plugins/feature/map/CMakeLists.txt index 2b8d003ef..6e610c9e5 100644 --- a/plugins/feature/map/CMakeLists.txt +++ b/plugins/feature/map/CMakeLists.txt @@ -54,12 +54,14 @@ if(NOT SERVER_MODE) mapradiotimedialog.ui mapcolordialog.cpp mapmodel.cpp + mapitem.cpp mapwebsocketserver.cpp cesiuminterface.cpp czml.cpp map.qrc icons.qrc cesium.qrc + data.qrc ) set(map_HEADERS ${map_HEADERS} @@ -72,6 +74,7 @@ if(NOT SERVER_MODE) mapradiotimedialog.h mapcolordialog.h mapmodel.h + mapitem.h mapwebsocketserver.h cesiuminterface.h czml.h diff --git a/plugins/feature/map/cesiuminterface.cpp b/plugins/feature/map/cesiuminterface.cpp index 7be9cf9d0..a0cd030d9 100644 --- a/plugins/feature/map/cesiuminterface.cpp +++ b/plugins/feature/map/cesiuminterface.cpp @@ -231,8 +231,25 @@ void CesiumInterface::czml(QJsonObject &obj) send(obj); } -void CesiumInterface::update(MapItem *mapItem, bool isTarget, bool isSelected) +void CesiumInterface::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected) { QJsonObject obj = m_czml.update(mapItem, isTarget, isSelected); czml(obj); } + +void CesiumInterface::update(PolygonMapItem *mapItem) +{ + QJsonObject obj = m_czml.update(mapItem); + czml(obj); +} + +void CesiumInterface::update(PolylineMapItem *mapItem) +{ + QJsonObject obj = m_czml.update(mapItem); + czml(obj); +} + +void CesiumInterface::setPosition(const QGeoCoordinate& position) +{ + m_czml.setPosition(position); +} diff --git a/plugins/feature/map/cesiuminterface.h b/plugins/feature/map/cesiuminterface.h index 46a4a4bdb..852f676b9 100644 --- a/plugins/feature/map/cesiuminterface.h +++ b/plugins/feature/map/cesiuminterface.h @@ -22,7 +22,9 @@ #include "czml.h" #include "SWGMapAnimation.h" -class MapItem; +class ObjectMapItem; +class PolygonMapItem; +class PolylineMapItem; class CesiumInterface : public MapWebSocketServer { @@ -72,7 +74,10 @@ public: void removeAllCZMLEntities(); void initCZML(); void czml(QJsonObject &obj); - void update(MapItem *mapItem, bool isTarget, bool isSelected); + void update(ObjectMapItem *mapItem, bool isTarget, bool isSelected); + void update(PolygonMapItem *mapItem); + void update(PolylineMapItem *mapItem); + void setPosition(const QGeoCoordinate& position); protected: diff --git a/plugins/feature/map/czml.cpp b/plugins/feature/map/czml.cpp index 8e08db6dd..b807a9081 100644 --- a/plugins/feature/map/czml.cpp +++ b/plugins/feature/map/czml.cpp @@ -28,6 +28,22 @@ CZML::CZML(const MapSettings *settings) : { } +// Set position from which distance filter is calculated +void CZML::setPosition(const QGeoCoordinate& position) +{ + m_position = position; +} + +bool CZML::filter(const MapItem *mapItem) const +{ + return ( !mapItem->m_itemSettings->m_filterName.isEmpty() + && !mapItem->m_itemSettings->m_filterNameRE.match(mapItem->m_name).hasMatch() + ) + || ( (mapItem->m_itemSettings->m_filterDistance > 0) + && (m_position.distanceTo(QGeoCoordinate(mapItem->m_latitude, mapItem->m_longitude, mapItem->m_altitude)) > mapItem->m_itemSettings->m_filterDistance) + ); +} + QJsonObject CZML::init() { QString start = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); @@ -48,11 +64,150 @@ QJsonObject CZML::init() return doc; } +QJsonObject CZML::update(PolygonMapItem *mapItem) +{ + QString id = mapItem->m_name; -QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) + QJsonObject obj { + {"id", id} // id must be unique + }; + + if ( !mapItem->m_itemSettings->m_enabled + || !mapItem->m_itemSettings->m_display3DTrack + || filter(mapItem) + ) + { + // Delete obj completely (including any history) + obj.insert("delete", true); + return obj; + } + + QJsonArray positions; + for (const auto c : mapItem->m_points) + { + positions.append(c->longitude()); + positions.append(c->latitude()); + positions.append(c->altitude()); + } + + QJsonObject positionList { + {"cartographicDegrees", positions}, + }; + + QColor color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + QJsonArray colorRGBA { + color.red(), color.green(), color.blue(), color.alpha() + }; + QJsonObject colorObj { + {"rgba", colorRGBA} + }; + + QJsonObject solidColor { + {"color", colorObj}, + }; + + QJsonObject material { + {"solidColor", solidColor} + }; + + QJsonArray outlineColorRGBA { + 0, 0, 0, 255 + }; + QJsonObject outlineColor { + {"rgba", outlineColorRGBA} + }; + + QJsonObject polygon { + {"positions", positionList}, + {"height", mapItem->m_altitude}, + {"extrudedHeight", mapItem->m_extrudedHeight}, + {"material", material}, + {"outline", true}, + {"outlineColor", outlineColor} + }; + + obj.insert("polygon", polygon); + obj.insert("description", mapItem->m_label); + + //qDebug() << "Polygon " << obj; + return obj; +} + +QJsonObject CZML::update(PolylineMapItem *mapItem) +{ + QString id = mapItem->m_name; + + QJsonObject obj { + {"id", id} // id must be unique + }; + + if ( !mapItem->m_itemSettings->m_enabled + || !mapItem->m_itemSettings->m_display3DTrack + || filter(mapItem) + ) + { + // Delete obj completely (including any history) + obj.insert("delete", true); + return obj; + } + + QJsonArray positions; + for (const auto c : mapItem->m_points) + { + positions.append(c->longitude()); + positions.append(c->latitude()); + positions.append(c->altitude()); + } + + QJsonObject positionList { + {"cartographicDegrees", positions}, + }; + + QColor color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + QJsonArray colorRGBA { + color.red(), color.green(), color.blue(), color.alpha() + }; + QJsonObject colorObj { + {"rgba", colorRGBA} + }; + + QJsonObject solidColor { + {"color", colorObj}, + }; + + QJsonObject material { + {"solidColor", solidColor} + }; + + QJsonObject polyline { + {"positions", positionList}, + {"material", material} + }; + + obj.insert("polyline", polyline); + obj.insert("description", mapItem->m_label); + + //qDebug() << "Polyline " << obj; + return obj; +} + +QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected) { (void) isTarget; + QString id = mapItem->m_name; + + QJsonObject obj { + {"id", id} // id must be unique + }; + + if (!mapItem->m_itemSettings->m_enabled || filter(mapItem)) + { + // Delete obj completely (including any history) + obj.insert("delete", true); + return obj; + } + // Don't currently use CLIP_TO_GROUND in Cesium due to Jitter bug // https://github.com/CesiumGS/cesium/issues/4049 // Instead we implement our own clipping code in map3d.html @@ -65,8 +220,6 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) dt = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs); } - QString id = mapItem->m_name; - // Keep a hash of the time we first saw each item bool existingId = m_ids.contains(id); if (!existingId) { @@ -77,7 +230,7 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) bool fixedPosition = mapItem->m_fixedPosition; if (mapItem->m_image == "") { - // Need to remove this from the map + // Need to remove this from the map (but history is retained) removeObj = true; } @@ -143,10 +296,10 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) hasMoved = true; m_hasMoved.insert(id, true); } - if (hasMoved) + if (hasMoved && (mapItem->m_itemSettings->m_extrapolate > 0)) { position.insert("forwardExtrapolationType", "EXTRAPOLATE"); - position.insert("forwardExtrapolationDuration", 60); + position.insert("forwardExtrapolationDuration", mapItem->m_itemSettings->m_extrapolate); // Use linear interpolation for now - other two can go crazy with aircraft on the ground //position.insert("interpolationAlgorithm", "HERMITE"); //position.insert("interpolationDegree", "2"); @@ -182,7 +335,7 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) QJsonObject orientation { {"unitQuaternion", quaternion}, {"forwardExtrapolationType", "HOLD"}, // If we extrapolate, aircraft tend to spin around - {"forwardExtrapolationDuration", 60}, + {"forwardExtrapolationDuration", mapItem->m_itemSettings->m_extrapolate}, // {"interpolationAlgorithm", "LAGRANGE"} }; QJsonObject orientationPosition { @@ -269,7 +422,10 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) // Prevent labels from being too cluttered when zoomed out // FIXME: These values should come from mapItem or mapItemSettings float displayDistanceMax = std::numeric_limits::max(); - if ((mapItem->m_group == "Beacons") || (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")) { + if ((mapItem->m_group == "Beacons") + || (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB") + || (mapItem->m_group == "NavAid") + ) { displayDistanceMax = 1000000; } else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) { displayDistanceMax = 10000000; @@ -330,10 +486,6 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) {"verticalOrigin", "BOTTOM"} // To stop it being cut in half when zoomed out }; - QJsonObject obj { - {"id", id} // id must be unique - }; - if (!removeObj) { obj.insert("position", position); @@ -368,9 +520,11 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) } else { - QString oneMin = QDateTime::currentDateTimeUtc().addSecs(60).toString(Qt::ISODateWithMs); - QString createdToNow = QString("%1/%2").arg(m_ids[id]).arg(oneMin); // From when object was created to now - obj.insert("availability", createdToNow); + if (mapItem->m_availableUntil.isValid()) + { + QString period = QString("%1/%2").arg(m_ids[id]).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs)); + obj.insert("availability", period); + } } } m_lastPosition.insert(id, coords); @@ -391,3 +545,4 @@ QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) return obj; } + diff --git a/plugins/feature/map/czml.h b/plugins/feature/map/czml.h index f97be9095..ad737a1b0 100644 --- a/plugins/feature/map/czml.h +++ b/plugins/feature/map/czml.h @@ -21,9 +21,13 @@ #include #include #include +#include struct MapSettings; class MapItem; +class ObjectMapItem; +class PolygonMapItem; +class PolylineMapItem; class CZML { @@ -32,11 +36,16 @@ private: QHash m_ids; QHash m_lastPosition; QHash m_hasMoved; + QGeoCoordinate m_position; public: CZML(const MapSettings *settings); QJsonObject init(); - QJsonObject update(MapItem *mapItem, bool isTarget, bool isSelected); + QJsonObject update(ObjectMapItem *mapItem, bool isTarget, bool isSelected); + QJsonObject update(PolygonMapItem *mapItem); + QJsonObject update(PolylineMapItem *mapItem); + bool filter(const MapItem *mapItem) const; + void setPosition(const QGeoCoordinate& position); signals: void connected(); diff --git a/plugins/feature/map/data.qrc b/plugins/feature/map/data.qrc new file mode 100644 index 000000000..b864c3579 --- /dev/null +++ b/plugins/feature/map/data.qrc @@ -0,0 +1,5 @@ + + + data/transmitters.csv + + diff --git a/plugins/feature/map/data/transmitters.csv b/plugins/feature/map/data/transmitters.csv new file mode 100644 index 000000000..1640a2a93 --- /dev/null +++ b/plugins/feature/map/data/transmitters.csv @@ -0,0 +1,3478 @@ +Type,Id,Name,Frequency (Hz),Latitude,Longitude,Altitude (m),Power,TII Main,TII Sub +AM,Acacia Radio Annesley Woodhouse Annesley - Acacia Centre,Acacia Radio,1287000,53.079047,-1.250137,173,0.001,, +AM,Akash Radio Leeds Leeds Leeds - Potternewton Lane,Akash Radio Leeds,1323000,53.825157,-1.547404,113,0.03,, +AM,Anker Radio George Eliot Hospital,Anker Radio,1386000,52.511208,-1.473932,102,0.001,, +AM,Asian Sound Radio East Lancashire Ashton Moss C,Asian Sound Radio,1377000,53.491685,-2.115866,101,0.08,, +AM,Asian Sound Radio East Lancashire HASLINGDEN,Asian Sound Radio,963000,53.708491,-2.311438,336,0.2,, +AM,BBC Asian Network Midlands Freemens Common,BBC Asian Network,837000,52.61896,-1.13158,79,0.7,, +AM,BBC Asian Network Midlands Gunthorpe,BBC Asian Network,1449000,52.617188,-0.247858,3,0.15,, +AM,BBC Asian Network Midlands Langley Mill,BBC Asian Network,1458000,52.56878,-1.765371,91,5,, +AM,BBC Asian Network Midlands Sedgley,BBC Asian Network,828000,52.542856,-2.14151,160,0.2,, +AM,BBC Guernsey (AM) Guernsey Rohais,BBC Guernsey (AM),1116000,49.46174,-2.562965,28,0.5,, +AM,BBC Radio 4 (AM) Cumbria Brisco,BBC Radio 4 (AM),1485000,54.864686,-2.917634,38,0.79,, +AM,BBC Radio 4 (AM) London Crystal Palace,BBC Radio 4 (AM),720000,51.424161,-0.074944,110,0.24,, +AM,BBC Radio 4 (AM) A Enniskillen AM,BBC Radio 4 (AM),774000,54.359626,-7.642841,70,0.5,, +AM,BBC Radio 4 (AM) Northern Ireland Lisnagarvey,BBC Radio 4 (AM),720000,54.489676,-6.060423,44,10,, +AM,BBC Radio 4 (AM) Londonderry Londonderry MF,BBC Radio 4 (AM),720000,55.004653,-7.367867,175,0.25,, +AM,BBC Radio 4 (AM) Devon Plumer Barracks,BBC Radio 4 (AM),774000,50.406749,-4.125485,101,0.2,, +AM,BBC Radio 4 (AM) North East Scotland Redmoss MF,BBC Radio 4 (AM),1449000,57.114016,-2.095089,90,2,, +AM,BBC Radio 4 (AM) Cornwall Redruth MF,BBC Radio 4 (AM),756000,50.217887,-5.2124,182,2,, +AM,BBC Radio 4 (AM) Newcastle Wrekenton,BBC Radio 4 (AM),603000,54.932736,-1.572984,152,0.5,, +AM,BBC Radio 5 live Cumbria Barrow,BBC Radio 5 live,693000,54.126165,-3.198099,23,0.6,, +AM,BBC Radio 5 live Sussex Bexhill MF,BBC Radio 5 live,693000,50.837744,0.447085,15,0.4,, +AM,BBC Radio 5 live London and the South East Brookmans Park,BBC Radio 5 live,909000,51.727838,-0.176464,127,200,, +AM,BBC Radio 5 live North Scotland Burghead,BBC Radio 5 live,693000,57.69801,-3.46983,40,25,, +AM,BBC Radio 5 live A Clevedon,BBC Radio 5 live,909000,51.423652,-2.863893,5,75,, +AM,BBC Radio 5 live Dartford Tunnel,BBC Radio 5 live,909000,51.469253,0.260425,3,0.004,, +AM,BBC Radio 5 live Midlands Droitwich,BBC Radio 5 live,693000,52.298545,-2.105388,48,150,, +AM,BBC Radio 5 live A Enniskillen AM,BBC Radio 5 live,693000,54.359626,-7.642841,70,0.4,, +AM,BBC Radio 5 live Newcastle Fareham,BBC Radio 5 live,909000,50.849331,-1.226918,30,0.8,, +AM,BBC Radio 5 live South Hampshire & Isle of Wight Fern Barrow,BBC Radio 5 live,909000,50.732788,-1.902461,44,0.5,, +AM,BBC Radio 5 live Kent Folkestone MF,BBC Radio 5 live,693000,51.105014,1.219804,145,0.4,, +AM,BBC Radio 5 live Northern Ireland Lisnagarvey,BBC Radio 5 live,909000,54.489676,-6.060423,44,5,, +AM,BBC Radio 5 live Londonderry Londonderry MF,BBC Radio 5 live,909000,55.004653,-7.367867,175,0.28,, +AM,BBC Radio 5 live Northern England Moorside Edge,BBC Radio 5 live,909000,53.635238,-1.894553,340,400,, +AM,BBC Radio 5 live Exeter Pearce's Hill,BBC Radio 5 live,909000,50.683057,-3.514661,82,0.2,, +AM,BBC Radio 5 live Norfolk Postwick,BBC Radio 5 live,693000,52.626852,1.402572,26,15,, +AM,BBC Radio 5 live North East Scotland Redmoss MF,BBC Radio 5 live,693000,57.114016,-2.095089,90,0.8,, +AM,BBC Radio 5 live Cornwall Redruth MF,BBC Radio 5 live,909000,50.217887,-5.2124,182,0.4,, +AM,BBC Radio 5 live Sussex Southwick,BBC Radio 5 live,693000,50.832284,-0.24922,5,0.6,, +AM,BBC Radio 5 live Northumberland Stagshaw,BBC Radio 5 live,693000,55.0326,-2.023437,213,50,, +AM,BBC Radio 5 live South West England Start Point,BBC Radio 5 live,693000,50.228364,-3.663965,122,80,, +AM,BBC Radio 5 live Wales Tywyn,BBC Radio 5 live,990000,52.582906,-4.096237,6,0.4,, +AM,BBC Radio 5 live Central Scotland Westerglen,BBC Radio 5 live,909000,55.974427,-3.818043,155,70,, +AM,BBC Radio 5 live Cumbria Whitehaven MF,BBC Radio 5 live,909000,54.539612,-3.58734,8,0.4,, +AM,BBC Radio Cumbria (AM) Cumbria Barrow,BBC Radio Cumbria (AM),837000,54.126165,-3.198099,23,1,, +AM,BBC Radio Cumbria (AM) Cumbria Brisco,BBC Radio Cumbria (AM),756000,54.864686,-2.917634,38,1,, +AM,BBC Radio Derby (AM) Derbyshire Burnaston,BBC Radio Derby (AM),1116000,52.879237,-1.560132,60,1,, +AM,BBC Radio Gloucestershire (AM) Gloucestershire Berkeley Heath,BBC Radio Gloucestershire (AM),1413000,51.691681,-2.422387,17,0.2,, +AM,BBC Radio Jersey (AM) Jersey Trinity,BBC Radio Jersey (AM),1026000,49.2247,-2.107062,105,1,, +AM,BBC Radio Norfolk (AM) Norfolk West Lynn,BBC Radio Norfolk (AM),873000,52.742777,0.386521,3,0.25,, +AM,BBC Radio Scotland (AM) Scotland Burghead,BBC Radio Scotland (AM),810000,57.69801,-3.46983,40,100,, +AM,BBC Radio Scotland (AM) Central Scotland Westerglen,BBC Radio Scotland (AM),810000,55.974427,-3.818043,155,100,, +AM,BBC Radio Wales (AM) Wales Washford,BBC Radio Wales (AM),882000,51.161365,-3.347935,41,10,, +AM,BBC Somerset (AM) Somerset Taunton MF,BBC Somerset (AM),1566000,51.02226,-3.088245,21,1,, +AM,BFBS Bovington Bovington Camp,BFBS,1287000,50.701288,-2.239253,50,0.001,, +AM,BFBS Gurkha Radio Aldershot Aldershot Garrison - St Omer Barracks,BFBS Gurkha Radio,1278000,51.260401,-0.743088,83,0.001,, +AM,BFBS Gurkha Radio Stafford Beaconside Stafford,BFBS Gurkha Radio,1278000,52.815291,-2.091948,78,0.004,, +AM,BFBS Gurkha Radio Blandford Forum Blandford Camp,BFBS Gurkha Radio,1287000,50.875033,-2.115071,80,0.001,, +AM,BFBS Gurkha Radio Nuneaton Bramcote Barracks,BFBS Gurkha Radio,1134000,52.492879,-1.400508,111,0.001,, +AM,BFBS Gurkha Radio Brecon Brecon Barracks,BFBS Gurkha Radio,1287000,51.946758,-3.390856,133,0.001,, +AM,BFBS Gurkha Radio Gillingham Brompton Barracks,BFBS Gurkha Radio,1287000,51.39174,0.5326,38,0.001,, +AM,BFBS Gurkha Radio Hullavington Buckley Barracks,BFBS Gurkha Radio,1287000,51.530529,-2.128252,91,0.001,, +AM,BFBS Gurkha Radio Salisbury Bulford Camp,BFBS Gurkha Radio,1134000,51.18517,-1.722235,184,0.001,, +AM,BFBS Gurkha Radio Abingdon Dalton Barracks,BFBS Gurkha Radio,1134000,51.688584,-1.304137,67,0.001,, +AM,BFBS Gurkha Radio Innsworth Imjin Barracks,BFBS Gurkha Radio,1287000,51.890072,-2.200475,10,0.001,, +AM,BFBS Gurkha Radio Camberley Sandhurst,BFBS Gurkha Radio,1134000,51.346493,-0.806784,59,0.001,, +AM,BFBS Gurkha Radio Folkestone Sir John Moore Bks,BFBS Gurkha Radio,1287000,51.076852,1.130745,80,0.001,, +AM,BFBS Gurkha Radio Perham Down Swinton Barracks,BFBS Gurkha Radio,1278000,51.238702,-1.634678,132,0.001,, +AM,BFBS Gurkha Radio Catterick Vimy Barracks,BFBS Gurkha Radio,1134000,54.362774,-1.718342,186,0.001,, +AM,BFBS Gurkha Radio Warminster Warminster - BFBS Community Centre,BFBS Gurkha Radio,1287000,51.208761,-2.165146,132,0.001,, +AM,Bradford Asian Radio Bradford Bradford - St Barnabas Church,Bradford Asian Radio,1413000,53.816917,-1.783525,186,0.01,, +AM,Canterbury Hospital Radio Canterbury Kent and Canterbury Hospital,Canterbury Hospital Radio,945000,51.266026,1.086243,42,0.001,, +AM,Carillon Radio Coalville Coalville Community Hospital,Carillon Radio,1431000,52.722761,-1.351463,160,0.001,, +AM,Carillon Wellbeing Radio Coalville Coalville Town FC,Carillon Wellbeing Radio,1476000,52.722298,-1.38271,152,0.25,, +AM,CHR 1431 AM Chichester St Richard's Hospital,CHR 1431 AM,1431000,50.846129,-0.769877,20,0.001,, +AM,Clyde 2 Glasgow Dechmont Hill,Clyde 2,1152000,55.794472,-4.159643,32,3.6,, +AM,Dales Radio Yorkshire Dales Hawes MF,Dales Radio,936000,54.307766,-2.193151,230,0.15,, +AM,Desi Radio Southall Glade Lane,Desi Radio,1602000,51.50439,-0.359891,27,0.16,, +AM,Downtown Radio Belfast Knock-Breckan,Downtown Radio,1026000,54.537281,-5.882789,137,1.7,, +AM,Flame CCR Wirral Willaston,Flame CCR,1521000,53.285675,-3.009447,49,0.07,, +AM,Forth 2 Edinburgh Colinswell,Forth 2,1548000,56.061352,-3.252606,7,2.2,, +AM,Gold Manchester Ashton Moss (NGW),Gold,1458000,53.482863,-2.128802,101,5,, +AM,Gold Peterborough Gunthorpe,Gold,1332000,52.617188,-0.247858,3,0.6,, +AM,Gold Northampton Kings Heath,Gold,1557000,52.263422,-0.917137,63,0.760001,, +AM,Gold Derby Quarndon,Gold,945000,52.973416,-1.507057,125,0.2,, +AM,Gold London Saffron Green,Gold,1548000,51.66492,-0.243194,98,97.5001,, +AM,Gold Nottingham Trowell,Gold,999000,52.953184,-1.248147,69,0.25,, +AM,Greatest Hits Radio Barnsley Ardsley,Greatest Hits Radio,1305000,53.550702,-1.412685,94,0.15,, +AM,Greatest Hits Radio Doncaster Crimpsall,Greatest Hits Radio,990000,53.522412,-1.147759,12,0.25,, +AM,Greatest Hits Radio Tyne and Wear Greenside,Greatest Hits Radio,1152000,54.95869,-1.765727,122,1.8,, +AM,Greatest Hits Radio Stoke-on-Trent Sideway,Greatest Hits Radio,1170000,52.987686,-2.185565,101,0.2,, +AM,Greatest Hits Radio Sheffield and Rotherham Skew Hill,Greatest Hits Radio,1548000,53.435456,-1.509255,229,0.74,, +AM,Greatest Hits Radio Teesside Stockton,Greatest Hits Radio,1170000,54.588636,-1.349732,38,0.32,, +AM,Greatest Hits Radio Bradford Tyersal Lane,Greatest Hits Radio,1278000,53.785735,-1.702486,186,0.43,, +AM,Greatest Hits Radio Halifax and Huddersfield Vicars Lot,Greatest Hits Radio,1530000,53.651685,-1.892848,340,0.74,, +AM,Hub Radio North Bristol UWE Frenchay Campus,Hub Radio,1449000,51.499642,-2.548824,77,0.001,, +AM,Jeo Radio London Sewardstone,Jeo Radio,1584000,51.659623,-0.007147,18,0.5,, +AM,L and D Radio Luton & Dunstable Luton and Dunstable Hospital,L and D Radio,1134000,51.893859,-0.47553,136,0.001,, +AM,LBC News London Saffron Green,LBC News,1152000,51.66492,-0.243194,98,23.5,, +AM,Leicester Community Radio Leicester St. Peter's Highfield Church,Leicester Community Radio,1449000,52.629934,-1.116632,80,0.001,, +AM,Livewire Radio Norwich Colney Training Centre,Livewire Radio,1350000,52.613702,1.206812,32,0.001,, +AM,Lyca Dil Se 1035 London Glade Lane,Lyca Dil Se 1035,1035000,51.50439,-0.359891,27,2.5,, +AM,Lyca Radio 1458 London Brookmans Park,Lyca Radio 1458,1458000,51.727838,-0.176464,127,125,, +AM,Manx Radio (AM) Isle of Man FOXDALE,Manx Radio (AM),1368000,54.167479,-4.61788,164,20,, +AM,MFR 2 Inverness Tarbat Ness,MFR 2,1107000,57.831682,-3.804014,12,1.5,, +AM,Mid-Downs Radio Haywards Heath Princess Royal Hospital,Mid-Downs Radio,1350000,50.989879,-0.090688,79,0.001,, +AM,Panjab Radio Greater London Crystal Palace,Panjab Radio,558000,51.424161,-0.074944,110,2.5,, +AM,Premier Christian Radio London Bow,Premier Christian Radio,1332000,51.532918,-0.017732,1,1,, +AM,Premier Christian Radio London Chingford,Premier Christian Radio,1305000,51.650405,-0.00972,15,0.5,, +AM,Premier Christian Radio London Dartford Marshes,Premier Christian Radio,1413000,51.470819,0.235016,3,0.5,, +AM,Premier Christian Radio London Heathrow,Premier Christian Radio,1413000,51.483903,-0.446896,25,0.5,, +AM,Premier Christian Radio London North Looe,Premier Christian Radio,1305000,51.334992,-0.237316,83,0.5,, +AM,Premier Christian Radio Guildford Peasmarsh,Premier Christian Radio,1566000,51.203838,-0.592895,39,0.8,, +AM,Radio BGWS Farnborough,Radio BGWS,1179000,51.308305,-0.766169,72,0.001,, +AM,Radio Caroline Suffolk Orfordness-A,Radio Caroline,648000,52.102899,1.574156,5,4,, +AM,Radio Cavell Oldham Royal Oldham Hospital,Radio Cavell,1350000,53.55235,-2.122221,176,0.001,, +AM,Radio City Swansea Swansea Mynydd-bach-y-glo,Radio City Swansea,1386000,51.642985,-4.003012,49,0.002,, +AM,Radio Clatterbridge Clatterbridge Clatterbridge Hospital,Radio Clatterbridge,1386000,53.332267,-3.027065,27,0.001,, +AM,Radio Panj Coventry Ryton on Dunsmore,Radio Panj,1521000,52.362389,-1.428267,80,0.1,, +AM,Radio Ramadan 365 Glasgow Glasgow - Shieldhall,Radio Ramadan 365,1530000,55.86858,-4.338619,2,0.04,, +AM,Radio Redhill Redhill East Surrey Hospital,Radio Redhill,1431000,51.218538,-0.161434,66,0.001,, +AM,Radio Seerah Leicester Leicester - SIR Fabrics,Radio Seerah,1575000,52.627046,-1.106748,73,0.004,, +AM,Radio Warrington Warrington,Radio Warrington,1332000,53.376934,-2.626032,1,0.1,, +AM,Radio XL Birmingham Langley Mill B,Radio XL,1296000,52.566973,-1.760955,91,10,, +AM,RaW 1251 AM Coventry University of Warwick,RaW 1251 AM,1251000,52.379413,-1.562174,90,0.001,, +AM,Reindeer Lodge Mold Black Meadows - Mold,Reindeer Lodge,1287000,53.128628,-3.098319,149,0.001,, +AM,Sabras Radio Leicester Freemens Common,Sabras Radio,1260000,52.61896,-1.13158,79,0.29,, +AM,Salaam BCR BURY Bury - Jackson Fold Farm,Salaam BCR,1566000,53.579265,-2.264119,115,0.06,, +AM,Smooth Radio Chelmsford Bakers Wood,Smooth Radio,1359000,51.705132,0.401129,90,0.28,, +AM,Smooth Radio Eastbourne Bexhill MF,Smooth Radio,945000,50.837744,0.447085,15,0.7,, +AM,Smooth Radio Swindon Blunsdon MF,Smooth Radio,1161000,51.609004,-1.794462,147,0.16,, +AM,Smooth Radio Great Yarmouth and Norwich Brundall,Smooth Radio,1152000,52.618097,1.403665,3,0.83,, +AM,Smooth Radio Portsmouth Farlington Marshes,Smooth Radio,1170000,50.842237,-1.024216,3,0.12,, +AM,Smooth Radio Wrexham and Deeside Farndon,Smooth Radio,1260000,53.092461,-2.889935,7,0.64,, +AM,Smooth Radio Bournemouth Fern Barrow,Smooth Radio,828000,50.732788,-1.902461,44,0.27,, +AM,Smooth Radio Ipswich Foxhall Heath,Smooth Radio,1170000,52.055203,1.226159,31,0.28,, +AM,Smooth Radio Bury St Edmunds Great Barton,Smooth Radio,1251000,52.283402,0.764679,61,0.760001,, +AM,Smooth Radio Maidstone and Medway Hoo St Werburgh MF,Smooth Radio,1242000,51.427787,0.57056,20,0.32,, +AM,Smooth Radio Gloucester and Cheltenham Little Shurdington,Smooth Radio,774000,51.856009,-2.127435,69,0.14,, +AM,Smooth Radio East Kent Littlebourne,Smooth Radio,603000,51.287498,1.157969,32,0.4,, +AM,Smooth Radio West Wiltshire Naish Hill,Smooth Radio,936000,51.416202,-2.076595,175,0.18,, +AM,Smooth Radio Plymouth Plumer Barracks,Smooth Radio,1152000,50.406749,-4.125485,101,0.32,, +AM,Smooth Radio Southend Rayleigh MF,Smooth Radio,1431000,51.582733,0.639928,33,0.35,, +AM,Smooth Radio Brighton Southwick,Smooth Radio,1323000,50.832284,-0.24922,5,0.5,, +AM,Smooth Radio Southampton Veals Farm,Smooth Radio,1557000,50.883626,-1.437023,4,0.5,, +AM,Stag Radio Guildford University of Surrey,Stag Radio,1350000,51.242457,-0.588852,70,0.001,, +AM,Sunrise Radio West London Glade Lane,Sunrise Radio,972000,51.50439,-0.359891,27,1.6,, +AM,Sunrise Radio London Lea Bridge Road,Sunrise Radio,963000,51.561867,-0.038692,10,0.950001,, +AM,Sunshine Radio (AM) Ludlow Villa Farm Greete,Sunshine Radio (AM),855000,52.334295,-2.63292,92,0.15,, +AM,Talk Sport Lincs Boston MF,Talk Sport,1107000,52.985991,-0.124813,0,1,, +AM,Talk Sport London Brookmans Park,Talk Sport,1089000,51.727838,-0.176464,127,400,, +AM,Talk Sport Nottingham Clipstone,Talk Sport,1071000,53.171618,-1.128693,91,1,, +AM,Talk Sport Dartford Tunnel,Talk Sport,1089000,51.469253,0.260425,3,0.004,, +AM,Talk Sport Worcestershire Droitwich,Talk Sport,1053000,52.298545,-2.105388,48,500,, +AM,Talk Sport Dumfries Dumfries MF,Talk Sport,1053000,55.027822,-3.480171,12,10,, +AM,Talk Sport West Sussex Duxhurst,Talk Sport,1107000,51.193421,-0.200217,65,1,, +AM,Talk Sport Hampshire Fareham,Talk Sport,1107000,50.849331,-1.226918,30,1,, +AM,Talk Sport Bournemouth Fern Barrow,Talk Sport,1053000,50.732788,-1.902461,44,1,, +AM,Talk Sport Dundee Greenside Scalp,Talk Sport,1053000,56.449964,-2.924023,15,1,, +AM,Talk Sport Humberside Hull MF,Talk Sport,1053000,53.715959,-0.229947,8,1,, +AM,Talk Sport Antrim Lisnagarvey,Talk Sport,1089000,54.489676,-6.060423,44,12.5,, +AM,Talk Sport East Kent Lydd,Talk Sport,1107000,50.950918,0.91565,2,2,, +AM,Talk Sport West Yorkshire Moorside Edge,Talk Sport,1089000,53.635238,-1.894553,340,400,, +AM,Talk Sport Norfolk Postwick,Talk Sport,1053000,52.626852,1.402572,26,18,, +AM,Talk Sport Sussex Southwick,Talk Sport,1053000,50.832284,-0.24922,5,2.2,, +AM,Talk Sport Cleveland Stockton,Talk Sport,1053000,54.588636,-1.349732,38,1,, +AM,Talk Sport West Kent Tonbridge,Talk Sport,1053000,51.139442,0.224404,104,4,, +AM,Talk Sport Merseyside Wallasey,Talk Sport,1107000,53.42567,-3.046924,40,0.5,, +AM,Talk Sport Somerset Washford,Talk Sport,1089000,51.161365,-3.347935,41,80,, +AM,Talk Sport Central Scotland Westerglen,Talk Sport,1089000,55.974427,-3.818043,155,125,, +AM,Talk Sport Newcastle Wrekenton,Talk Sport,1071000,54.932736,-1.572984,152,1,, +AM,Tay 2 Perth Friarton Road,Tay 2,1584000,56.376097,-3.427597,20,0.21,, +AM,Tay 2 Dundee Greenside Scalp,Tay 2,1161000,56.449964,-2.924023,15,1.4,, +AM,University Radio York York University of York,University Radio York,1350000,53.947755,-1.053788,15,0.001,, +AM,WestSound Ayr Symington,WestSound,1035000,55.567692,-4.574968,50,0.32,, +FM,10 Radio Wiveliscombe Milverton,10 Radio,105300000,51.027561,-3.275316,148,0.025,, +FM,1055 The Point Milton Keynes Milton Keynes - Saxon Gate,1055 The Point,105500000,52.043319,-0.761199,111,0.025,, +FM,107FM East Hull Hull - Muswell Court,107FM,107400000,53.775775,-0.281912,4,0.025,, +FM,1BTN Brighton and Hove Brighton Race Course,1BTN,101400000,50.829169,-0.112873,124,0.025,, +FM,3FM Isle of Man Beary Peark,3FM,106200000,54.216063,-4.616199,289,0.26,, +FM,3FM Cronk ny Arrey,3FM,104200000,54.067672,-4.76307,159,0.05,, +FM,3FM Isle of Man DOUGLAS,3FM,105000000,54.140551,-4.493075,143,1.8,, +FM,3FM Ramsey Ramsey Town C,3FM,104200000,54.324756,-4.386402,2,0.05,, +FM,3FM Isle of Man Snaefell,3FM,106600000,54.262536,-4.462965,610,0.201,, +FM,3TFM Community Radio for Health Saltcoats,3TFM Community Radio for Health,103100000,55.646655,-4.774865,47,0.025,, +FM,60 North FM Shetland Isles Brae Marina (LRSL),60 North FM,87700000,60.392566,-1.364989,0,0.001,, +FM,60 North FM Shetland Isles Bressay and Noss Nature Reserve (LRSL),60 North FM,87700000,60.120015,-1.121929,1,0.001,, +FM,60 North FM Shetland Isles Eshares (LRSL),60 North FM,87700000,60.489627,-1.62696,58,0.001,, +FM,60 North FM Shetland Isles Haroldswick (LRSL),60 North FM,87700000,60.798307,-0.82047,33,0.001,, +FM,60 North FM Shetland Isles Sumburgh Head Nature Reserve (LRSL),60 North FM,87700000,59.854256,-1.275309,65,0.001,, +FM,60 North FM Shetland Isles Walls Marina (LRSL),60 North FM,87700000,60.22636,-1.563163,1,0.001,, +FM,Abbey 104 Sherborne,Abbey 104,104700000,50.959202,-2.514807,116,0.025,, +FM,Academy FM 105.9 Folkestone Folkestone Academy,Academy FM 105.9,105900000,51.091482,1.1653,42,0.025,, +FM,Academy FM 107.8 Thanet Ramsgate - Royal Harbour Academy,Academy FM 107.8,107800000,51.350467,1.392417,49,0.05,, +FM,AIR Weymouth,AIR,107200000,50.59912,-2.476532,64,0.1,, +FM,Aldergrove and Antrim FM Aldergrove and Antrim RAF Aldergrove,Aldergrove and Antrim FM,106500000,54.65275,-6.21226,86,0.025,, +FM,Alfred Shaftesbury Shaftesbury - Trinity Centre,Alfred,107300000,51.00592,-2.199129,218,0.025,, +FM,Alive Radio Dumfries,Alive Radio,107300000,55.065505,-3.619211,40,0.08,, +FM,ALL FM Manchester Manchester - Bickerdike Court,ALL FM,96900000,53.4542,-2.1888,56,0.05,, +FM,Amber Sound FM Belper Belper - King Street,Amber Sound FM,98400000,53.02319,-1.479655,81,0.025,, +FM,Amber Sound FM Ripley,Amber Sound FM,107200000,53.048812,-1.403566,152,0.025,, +FM,Ambur Radio Walsall Walsall - Townend House,Ambur Radio,99400000,52.586091,-1.984607,125,0.05,, +FM,Andover Radio Andover Andover - Michedever Road,Andover Radio,95900000,51.204151,-1.465333,114,0.025,, +FM,Angel Radio Portsmouth and Havant Fort Widley,Angel Radio,98600000,50.853922,-1.067587,102,0.1,, +FM,Angel Radio Isle of Wight Isle Of Wight Newport - Garretts Farm,Angel Radio Isle of Wight,91500000,50.683007,-1.275373,97,0.025,, +FM,Anker Radio Nuneaton George Eliot Hospital - maternity,Anker Radio,88900000,52.512434,-1.476864,100,0.001,, +FM,Apple FM Taunton Taunton - Musgrove Park Hospital,Apple FM,97300000,51.011891,-3.119648,20,0.025,, +FM,Argyll FM Kintyre Islay and Jura Ballygroggan,Argyll FM,107100000,55.405447,-5.746276,182,0.625,, +FM,Argyll FM Kintyre Islay and Jura Campbeltown - Baraskomill,Argyll FM,106500000,55.433038,-5.569557,92,0.5,, +FM,Argyll FM Kintyre Islay and Jura South Knapdale,Argyll FM,107700000,55.91751,-5.462223,481,0.5,, +FM,Ashdown Radio Crowborough,Ashdown Radio,94700000,51.05856,0.158875,221,0.025,, +FM,Ashdown Radio Uckfield,Ashdown Radio,105000000,50.967238,0.111767,47,0.125,, +FM,Asian Star 101.6 FM Slough Slough - Queensmere,Asian Star 101.6 FM,101600000,51.508658,-0.591451,30,0.025,, +FM,Awaaz FM Southampton Midanbury - The Castle,Awaaz FM,99800000,50.92624,-1.364941,62,0.0216,, +FM,Awaz FM Glasgow Glasgow - Dundasvale Court - Block 2,Awaz FM,107200000,55.869339,-4.259542,30,0.025,, +FM,B Radio Farnborough Farnborough - Westmead House,B Radio,106500000,51.289155,-0.757935,68,0.1,, +FM,B Radio Reading Reading - Fountain House,B Radio,95600000,51.454569,-0.977254,43,0.1,, +FM,Bailrigg FM Lancaster Lancaster University,Bailrigg FM,95300000,54.008228,-2.785769,57,0.002,, +FM,Bangor FM Bangor Bangor - SERC,Bangor FM,107900000,54.65614,-5.662282,22,0.025,, +FM,Basildon Hospital Radio (BHR) Basildon Basildon - University Hospital,Basildon Hospital Radio (BHR),87700000,51.558561,0.452552,50,5e-05,, +FM,BBC Coventry and Warwickshire Nuneaton Hartshill Quarry,BBC Coventry and Warwickshire,104000000,52.540463,-1.503661,140,0.05,, +FM,BBC Coventry and Warwickshire Coventry and Warwickshire Lark Stoke,BBC Coventry and Warwickshire,103700000,52.081613,-1.728531,260,0.7,, +FM,BBC Coventry and Warwickshire Coventry Meriden VHF,BBC Coventry and Warwickshire,94800000,52.457753,-1.624974,178,1.1,, +FM,BBC Essex (FM) Essex Benfleet,BBC Essex (FM),95300000,51.550856,0.5811,80,1.0064,, +FM,BBC Essex (FM) Dartford Tunnel,BBC Essex (FM),95300000,51.469253,0.260425,3,0,, +FM,BBC Essex (FM) Essex Great Braxted,BBC Essex (FM),103500000,51.801904,0.710885,81,6,, +FM,BBC Guernsey (FM) Alderney,BBC Guernsey (FM),99000000,49.717948,-2.189296,69,0.025,, +FM,BBC Guernsey (FM) Guernsey Les Touillets,BBC Guernsey (FM),93200000,49.457792,-2.579202,76,0.5,, +FM,BBC Hereford and Worcester (FM) Redditch Headless Cross GSC,BBC Hereford and Worcester (FM),104400000,52.294535,-1.946877,162,0.1,, +FM,BBC Hereford and Worcester (FM) Kidderminster KIDDERMINSTER,BBC Hereford and Worcester (FM),104600000,52.363252,-2.282794,82,0.25,, +FM,BBC Hereford and Worcester (FM) Worcestershire MALVERN,BBC Hereford and Worcester (FM),104000000,52.115715,-2.330146,161,1,, +FM,BBC Hereford and Worcester (FM) Herefordshire RIDGE HILL,BBC Hereford and Worcester (FM),94700000,51.997433,-2.53989,204,1,, +FM,BBC Lincolnshire (FM) Lincolnshire Belmont,BBC Lincolnshire (FM),94900000,53.335814,-0.171983,125,3,, +FM,BBC Lincolnshire (FM) Grantham,BBC Lincolnshire (FM),104700000,52.892406,-0.655971,115,0.05,, +FM,BBC London 94.9 London Crystal Palace,BBC London 94.9,94900000,51.424161,-0.074944,110,2,, +FM,BBC London 94.9 Dartford Tunnel,BBC London 94.9,94900000,51.469253,0.260425,3,0,, +FM,BBC Newcastle (FM) Northumberland CHATTON,BBC Newcastle (FM),96000000,55.531826,-1.834261,192,2.8,, +FM,BBC Newcastle (FM) Newcastle Fenham,BBC Newcastle (FM),104400000,54.978039,-1.662966,120,0.042,, +FM,BBC Newcastle (FM) Newcastle NEWTON,BBC Newcastle (FM),103700000,54.982447,-1.945593,165,0.1,, +FM,BBC Newcastle (FM) Tyneside PONTOP PIKE,BBC Newcastle (FM),95400000,54.868838,-1.771233,302,5,, +FM,BBC Oxford Oxfordshire Oxford,BBC Oxford,95200000,51.790638,-1.179183,130,2.54081,, +FM,BBC Radio 1 Aberdare,BBC Radio 1,98800000,51.702007,-3.398531,282,0.021,, +FM,BBC Radio 1 Abergavenny,BBC Radio 1,98300000,51.807795,-3.097781,433,0.02,, +FM,BBC Radio 1 Pontypool Abertillery,BBC Radio 1,98600000,51.714666,-3.12404,400,0.01,, +FM,BBC Radio 1 Argyll & Bute Ardgour,BBC Radio 1,98300000,56.712175,-5.290096,12,0.005,, +FM,BBC Radio 1 Scottish Borders Ashkirk,BBC Radio 1,98700000,55.510631,-2.840766,303,20.3208,, +FM,BBC Radio 1 Axe Valley,BBC Radio 1,99100000,50.74593,-3.016766,169,0.08,, +FM,BBC Radio 1 Argyll & Bute Ballachulish,BBC Radio 1,97700000,56.684359,-5.170394,14,0.015,, +FM,BBC Radio 1 North Antrim Ballycastle,BBC Radio 1,98400000,55.215995,-6.309339,150,0.05,, +FM,BBC Radio 1 Skipton Barnoldswick,BBC Radio 1,99300000,53.928368,-2.15728,191,0.02,, +FM,BBC Radio 1 North Devon Barnstaple,BBC Radio 1,98100000,51.040763,-4.13826,116,0.5,, +FM,BBC Radio 1 Basingstoke,BBC Radio 1,99700000,51.226848,-1.080409,176,0.02,, +FM,BBC Radio 1 Bath,BBC Radio 1,98600000,51.38779,-2.332925,175,0.041,, +FM,BBC Radio 1 Torbay Beacon Hill North,BBC Radio 1,98400000,50.447129,-3.610498,196,0.794328,, +FM,BBC Radio 1 Leeds Beecroft Hill,BBC Radio 1,99400000,53.811543,-1.641253,149,0.2,, +FM,BBC Radio 1 Lincolnshire Belmont,BBC Radio 1,98300000,53.335814,-0.171983,125,8.3,, +FM,BBC Radio 1 Stirling Ben Gullipen,BBC Radio 1,98300000,56.212308,-4.263559,415,1,, +FM,BBC Radio 1 Borders Berwick-upon-Tweed,BBC Radio 1,98200000,55.785854,-2.032822,129,0.05,, +FM,BBC Radio 1 Snowdonia Betws Y Coed,BBC Radio 1,97800000,53.108346,-3.75717,303,0.025,, +FM,BBC Radio 1 Bexhill,BBC Radio 1,99200000,50.84568,0.475913,45,0.0117,, +FM,BBC Radio 1 North Yorkshire Bilsdale - Station Tower,BBC Radio 1,98600000,54.358848,-1.149317,381,2,, +FM,BBC Radio 1 Central Scotland Black Hill,BBC Radio 1,99500000,55.860005,-3.873572,275,122.155,, +FM,BBC Radio 1 Blaenavon,BBC Radio 1,98100000,51.750968,-3.048742,373,0.01,, +FM,BBC Radio 1 Ceredigion Blaenplwyf,BBC Radio 1,98300000,52.360613,-4.102585,174,125,, +FM,BBC Radio 1 Swindon Blunsdon,BBC Radio 1,98600000,51.609363,-1.794027,148,0.2,, +FM,BBC Radio 1 Milton Keynes Bow Brickhill,BBC Radio 1,98200000,51.998806,-0.669784,169,5,, +FM,BBC Radio 1 Islay Bowmore,BBC Radio 1,97700000,55.750128,-6.274228,46,0.061,, +FM,BBC Radio 1 Mid Wales Brecon VHF,BBC Radio 1,98500000,51.959448,-3.412497,248,0.01,, +FM,BBC Radio 1 Shetland Bressay,BBC Radio 1,97900000,60.130079,-1.096281,224,25,, +FM,BBC Radio 1 Dorchester and Weymouth Bridport,BBC Radio 1,98700000,50.720805,-2.775678,64,0.2,, +FM,BBC Radio 1 Omagh and Enniskillen Brougher Mountain,BBC Radio 1,99000000,54.423191,-7.462812,305,5,, +FM,BBC Radio 1 North Derbyshire Buxton,BBC Radio 1,99600000,53.275085,-1.911935,432,0.05,, +FM,BBC Radio 1 Calne CALNE,BBC Radio 1,97800000,51.428815,-2.005417,104,0.032,, +FM,BBC Radio 1 Kirkcudbright CAMBRET HILL,BBC Radio 1,98300000,54.893698,-4.302038,343,0.065,, +FM,BBC Radio 1 South Newry Camlough,BBC Radio 1,98300000,54.160861,-6.385716,340,2,, +FM,BBC Radio 1 Kintyre Islay and Jura CAMPBELTOWN,BBC Radio 1,98000000,55.413714,-5.624241,83,0.2,, +FM,BBC Radio 1 Carmarthen,BBC Radio 1,98500000,51.867126,-4.309135,137,0.01,, +FM,BBC Radio 1 Carmarthenshire Carmel,BBC Radio 1,98000000,51.818501,-4.066216,255,1.7895,, +FM,BBC Radio 1 Newtownabbey Carnmoney Hill,BBC Radio 1,98400000,54.676355,-5.928737,207,0.02,, +FM,BBC Radio 1 Caterham Valley CATERHAM,BBC Radio 1,99300000,51.284605,-0.075623,173,0.015,, +FM,BBC Radio 1 Gloucestershire CHALFORD,BBC Radio 1,98800000,51.714525,-2.170893,174,0.1,, +FM,BBC Radio 1 Somerset Chard,BBC Radio 1,98500000,50.874213,-2.913244,173,0.05,, +FM,BBC Radio 1 Northumberland CHATTON,BBC Radio 1,99700000,55.531826,-1.834261,192,2.8,, +FM,BBC Radio 1 South Yorkshire Chesterfield,BBC Radio 1,98600000,53.283991,-1.427446,179,0.4,, +FM,BBC Radio 1 Chippenham,BBC Radio 1,98400000,51.474065,-2.12579,103,0.009,, +FM,BBC Radio 1 Gloucestershire Churchdown Hill,BBC Radio 1,98600000,51.868264,-2.175397,147,0.036,, +FM,BBC Radio 1 Cirencester,BBC Radio 1,97700000,51.717612,-1.968107,112,0.01,, +FM,BBC Radio 1 North Uist Clettraval,BBC Radio 1,97700000,57.617267,-7.44252,123,2,, +FM,BBC Radio 1 Wye Valley CLYRO,BBC Radio 1,99800000,52.08185,-3.162868,215,0.01,, +FM,BBC Radio 1 Ilfracombe Combe Martin,BBC Radio 1,98700000,51.197266,-4.031278,144,0.004,, +FM,BBC Radio 1 Conway CONWY,BBC Radio 1,98700000,53.271606,-3.828796,114,0.05,, +FM,BBC Radio 1 Calder Valley CORNHOLME,BBC Radio 1,99300000,53.734447,-2.125781,312,0.02,, +FM,BBC Radio 1 Folkestone Creteway Down,BBC Radio 1,98300000,51.099738,1.182579,166,0.1,, +FM,BBC Radio 1 Crieff CRIEFF,BBC Radio 1,98900000,56.357222,-3.920232,219,0.125,, +FM,BBC Radio 1 Croeserw,BBC Radio 1,98600000,51.644491,-3.652146,343,0.01,, +FM,BBC Radio 1 London Crystal Palace,BBC Radio 1,98500000,51.424161,-0.074944,110,4,, +FM,BBC Radio 1 Cwmafan,BBC Radio 1,98100000,51.628417,-3.737817,210,0.01,, +FM,BBC Radio 1 South Uist Daliburgh,BBC Radio 1,98900000,57.169643,-7.402558,6,1,, +FM,BBC Radio 1 Dartford Tunnel,BBC Radio 1,98800000,51.469253,0.260425,3,0,, +FM,BBC Radio 1 Ayrshire DARVEL,BBC Radio 1,99100000,55.579376,-4.289814,286,5,, +FM,BBC Radio 1 Darwen DARWEN,BBC Radio 1,99100000,53.696571,-2.443266,261,0.01,, +FM,BBC Radio 1 Gwynedd DEINIOLEN,BBC Radio 1,97700000,53.137063,-4.129256,322,0.05,, +FM,BBC Radio 1 Northern Ireland Divis A,BBC Radio 1,99700000,54.607538,-6.009471,368,61.0773,, +FM,BBC Radio 1 Snowdonia DOLGELLAU,BBC Radio 1,99700000,52.749344,-3.886853,103,0.025,, +FM,BBC Radio 1 Isle of Man DOUGLAS,BBC Radio 1,98000000,54.140551,-4.493075,143,5.7,, +FM,BBC Radio 1 Aberdeen Durris,BBC Radio 1,99000000,56.999987,-2.389944,325,1.05,, +FM,BBC Radio 1 South Wales EBBW VALE,BBC Radio 1,98000000,51.771714,-3.218938,442,0.015,, +FM,BBC Radio 1 Frome Egford Hill,BBC Radio 1,98700000,51.231585,-2.3391,132,0.06,, +FM,BBC Radio 1 Isle of Lewis Eitshal,BBC Radio 1,99400000,58.179148,-6.584974,206,2,, +FM,BBC Radio 1 Exeter Exeter (St Thomas),BBC Radio 1,98600000,50.719122,-3.562148,102,0.0275,, +FM,BBC Radio 1 Borders EYEMOUTH,BBC Radio 1,99300000,55.83228,-2.085863,197,0.1,, +FM,BBC Radio 1 Newcastle Fenham,BBC Radio 1,99400000,54.978039,-1.662966,120,0.042,, +FM,BBC Radio 1 South Wales Ferndale,BBC Radio 1,99200000,51.662873,-3.437376,386,0.01,, +FM,BBC Radio 1 Snowdonia FFESTINIOG,BBC Radio 1,97700000,52.934172,-3.921818,308,0.082081,, +FM,BBC Radio 1 Angus Forfar,BBC Radio 1,97900000,56.55718,-2.843663,206,10,, +FM,BBC Radio 1 Great Glen Fort Augustus,BBC Radio 1,98200000,57.105987,-4.70869,307,1,, +FM,BBC Radio 1 Fort William,BBC Radio 1,98900000,56.848302,-5.088621,16,2,, +FM,BBC Radio 1 Ayr GIRVAN,BBC Radio 1,98500000,55.244736,-4.814596,195,0.1,, +FM,BBC Radio 1 Isle of Mull Glengorm,BBC Radio 1,99100000,56.632166,-6.133249,253,2.5,, +FM,BBC Radio 1 Tiverton Gogwell,BBC Radio 1,99200000,50.889583,-3.469029,233,0.3,, +FM,BBC Radio 1 Grantham,BBC Radio 1,97700000,52.892406,-0.655971,115,0.05,, +FM,BBC Radio 1 Cairngorms Grantown,BBC Radio 1,99400000,57.320173,-3.656313,389,0.36,, +FM,BBC Radio 1 Guildford GUILDFORD,BBC Radio 1,97700000,51.228358,-0.605467,142,2,, +FM,BBC Radio 1 Rossendale HASLINGDEN,BBC Radio 1,99500000,53.708491,-2.311438,336,0.1,, +FM,BBC Radio 1 Hastings,BBC Radio 1,97700000,50.861622,0.566431,76,0.25,, +FM,BBC Radio 1 Pembrokeshire Haverfordwest,BBC Radio 1,98900000,51.899195,-4.86653,186,9.54993,, +FM,BBC Radio 1 Hebden Bridge HEBDEN BRIDGE,BBC Radio 1,98000000,53.737298,-2.018458,220,0.025,, +FM,BBC Radio 1 Caversham Hemdean,BBC Radio 1,99400000,51.481196,-0.978531,69,1,, +FM,BBC Radio 1 High Wycombe,BBC Radio 1,99600000,51.640531,-0.763678,156,0.025,, +FM,BBC Radio 1 Teignmouth Holcombe Down,BBC Radio 1,99000000,50.566699,-3.506408,205,0.1,, +FM,BBC Radio 1 Yorkshire and Lancashire Holme Moss,BBC Radio 1,98900000,53.533184,-1.858296,524,124.983,, +FM,BBC Radio 1 Weston-super-Mare Hutton,BBC Radio 1,99000000,51.325227,-2.918301,73,0.04,, +FM,BBC Radio 1 Bradford Idle,BBC Radio 1,98100000,53.832945,-1.752142,215,0.01,, +FM,BBC Radio 1 Bristol Ilchester Crescent,BBC Radio 1,98900000,51.427947,-2.609813,48,0.649999,, +FM,BBC Radio 1 Borders INNERLEITHEN,BBC Radio 1,99100000,55.620243,-3.07272,244,0.011,, +FM,BBC Radio 1 Isles of Scilly,BBC Radio 1,98400000,49.932504,-6.305394,32,0.03,, +FM,BBC Radio 1 Devon Ivybridge,BBC Radio 1,99500000,50.368664,-3.926486,142,0.05,, +FM,BBC Radio 1 Orkney Keelylang Hill B,BBC Radio 1,98900000,58.975283,-3.083943,220,16,, +FM,BBC Radio 1 West Yorkshire KEIGHLEY,BBC Radio 1,98500000,53.895532,-1.896639,305,1,, +FM,BBC Radio 1 South Lakes Kendal,BBC Radio 1,98600000,54.314559,-2.708127,172,0.05,, +FM,BBC Radio 1 Kenley KENLEY,BBC Radio 1,98000000,51.317089,-0.09335,152,0.025,, +FM,BBC Radio 1 North Lakes Keswick Forest,BBC Radio 1,99200000,54.608739,-3.204646,215,0,, +FM,BBC Radio 1 Kilkeel,BBC Radio 1,99000000,54.095375,-6.041723,267,0.1,, +FM,BBC Radio 1 Swansea Kilvey Hill,BBC Radio 1,99100000,51.62891,-3.919745,193,0.5,, +FM,BBC Radio 1 DartmouthDartmouth Kingswear,BBC Radio 1,99400000,50.349538,-3.565025,107,0.005,, +FM,BBC Radio 1 Cairngorms KINGUSSIE,BBC Radio 1,98700000,57.061393,-4.030507,351,0.05,, +FM,BBC Radio 1 Kinlochleven,BBC Radio 1,99300000,56.722796,-4.978914,273,0.005,, +FM,BBC Radio 1 Borders Kirkconnel,BBC Radio 1,98300000,55.412875,-3.983785,480,0.02,, +FM,BBC Radio 1 Perth Kirkton Mailer,BBC Radio 1,98600000,56.372293,-3.453361,178,0.5,, +FM,BBC Radio 1 Moray Knockmore,BBC Radio 1,97800000,57.532303,-3.135126,353,1,, +FM,BBC Radio 1 Larne,BBC Radio 1,98700000,54.862024,-5.828421,119,0.05,, +FM,BBC Radio 1 Jersey Les Platons,BBC Radio 1,97100000,49.246023,-2.1012,134,8,, +FM,BBC Radio 1 East Ayrshire LETHANHILL,BBC Radio 1,97900000,55.36421,-4.464995,300,0.11,, +FM,BBC Radio 1 Limavady,BBC Radio 1,99200000,55.109172,-6.886121,341,1.7,, +FM,BBC Radio 1 Anglesey LLANDDONA,BBC Radio 1,99400000,53.307002,-4.12825,146,10.5,, +FM,BBC Radio 1 Snowdonia LLANDECWYN,BBC Radio 1,99000000,52.914419,-4.017769,268,0.1,, +FM,BBC Radio 1 Mid Wales LLANDINAM,BBC Radio 1,99700000,52.479547,-3.399945,454,0.022,, +FM,BBC Radio 1 Llandrindod Wells,BBC Radio 1,98700000,52.261047,-3.439354,432,1.39,, +FM,BBC Radio 1 Ceredigion LLANDYFRIOG,BBC Radio 1,99700000,52.045314,-4.410045,116,0.05,, +FM,BBC Radio 1 Llanfyllin LLANFYLLIN,BBC Radio 1,98700000,52.753349,-3.259797,264,0.003972,, +FM,BBC Radio 1 Bridgend Llangeinor,BBC Radio 1,98200000,51.586117,-3.581462,306,0.01,, +FM,BBC Radio 1 Llangollen and Cheshire Llangollen VHF,BBC Radio 1,98500000,53.031048,-3.180791,556,7.80001,, +FM,BBC Radio 1 Mid Wales LLANIDLOES,BBC Radio 1,97700000,52.446933,-3.549585,244,0.025,, +FM,BBC Radio 1 Llanrhaeadr ym Mochnant LLANRHAEADR YM MOCHN,BBC Radio 1,99400000,52.825889,-3.226423,333,0.025,, +FM,BBC Radio 1 Snowdonia LLWYN ONN,BBC Radio 1,97900000,52.737496,-4.03773,277,0.25,, +FM,BBC Radio 1 Talgarth LLYSWEN,BBC Radio 1,99200000,52.016761,-3.258522,224,0.01,, +FM,BBC Radio 1 Londonderry,BBC Radio 1,98300000,55.00421,-7.368968,175,15.6,, +FM,BBC Radio 1 Welshpool LONG MOUNTAIN,BBC Radio 1,99200000,52.644306,-3.086542,400,0.024,, +FM,BBC Radio 1 Calderdale LUDDENDEN,BBC Radio 1,98300000,53.720111,-1.92874,256,0.084,, +FM,BBC Radio 1 Ludlow LUDLOW,BBC Radio 1,99200000,52.362722,-2.738327,216,0.005,, +FM,BBC Radio 1 Lyme Regis,BBC Radio 1,99300000,50.737572,-2.929856,162,0.05,, +FM,BBC Radio 1 Snowdonia MACHYNLLETH,BBC Radio 1,99000000,52.586449,-3.885603,100,0.06,, +FM,BBC Radio 1 Cambridgeshire Madingley,BBC Radio 1,98500000,52.215394,0.036667,62,0.13,, +FM,BBC Radio 1 Mallaig,BBC Radio 1,97700000,57.001499,-5.828475,71,0.02,, +FM,BBC Radio 1 Colchester and Ipswich Manningtree,BBC Radio 1,97700000,51.92387,1.086097,35,2.5,, +FM,BBC Radio 1 Marlborough MARLBOROUGH,BBC Radio 1,99700000,51.417912,-1.699977,192,0.1,, +FM,BBC Radio 1 Aberdeenshire Meldrum,BBC Radio 1,98300000,57.386674,-2.400197,245,75,, +FM,BBC Radio 1 Wester Ross Melvaig,BBC Radio 1,98700000,57.842382,-5.782182,271,17.5903,, +FM,BBC Radio 1 Membury,BBC Radio 1,98400000,51.484903,-1.558836,211,0.125,, +FM,BBC Radio 1 Mickleham MICKLEHAM,BBC Radio 1,99300000,51.272007,-0.333055,103,0.025,, +FM,BBC Radio 1 Dumbarton Millburn Muir,BBC Radio 1,97900000,55.982322,-4.600708,152,0.025,, +FM,BBC Radio 1 West Somerset Minehead,BBC Radio 1,98600000,51.19744,-3.48052,79,0.01,, +FM,BBC Radio 1 Morecambe Bay,BBC Radio 1,99600000,54.202505,-3.167346,259,5,, +FM,BBC Radio 1 Cardigan Bay Mynydd Pencarreg,BBC Radio 1,99300000,52.067402,-4.076523,411,0.2,, +FM,BBC Radio 1 Gloucestershire NAILSWORTH,BBC Radio 1,97800000,51.690257,-2.219843,114,0.1,, +FM,BBC Radio 1 Isle of Lewis Ness of Lewis,BBC Radio 1,97900000,58.461531,-6.230808,100,0.162,, +FM,BBC Radio 1 Newbury,BBC Radio 1,97800000,51.414621,-1.290472,130,0.1,, +FM,BBC Radio 1 Newhaven NEWHAVEN,BBC Radio 1,99300000,50.787726,0.035848,81,0.05,, +FM,BBC Radio 1 Newcastle NEWTON,BBC Radio 1,99000000,54.982447,-1.945593,165,0.1,, +FM,BBC Radio 1 Devon North Hessary Tor,BBC Radio 1,97700000,50.550217,-4.008413,508,80,, +FM,BBC Radio 1 Northamptonshire Northampton,BBC Radio 1,98500000,52.275355,-0.885481,127,0.0615,, +FM,BBC Radio 1 Oban and Mull Oban,BBC Radio 1,98500000,56.40396,-5.485882,122,2.5,, +FM,BBC Radio 1 Ogmore Vale,BBC Radio 1,98800000,51.593504,-3.546781,298,0.01,, +FM,BBC Radio 1 Okehampton,BBC Radio 1,98300000,50.753618,-4.005483,208,0.025,, +FM,BBC Radio 1 Scarborough OLIVERS MOUNT,BBC Radio 1,99500000,54.267351,-0.404494,151,0.125,, +FM,BBC Radio 1 Oxfordshire Oxford,BBC Radio 1,99100000,51.790638,-1.179183,130,17.5903,, +FM,BBC Radio 1 Borders PEEBLES,BBC Radio 1,98000000,55.661832,-3.227732,352,0.02,, +FM,BBC Radio 1 Bodmin Penaligon Downs,BBC Radio 1,98400000,50.481389,-4.782638,116,0.2,, +FM,BBC Radio 1 Burnley Pendle Forest,BBC Radio 1,97800000,53.841893,-2.266074,276,0.5,, +FM,BBC Radio 1 Penicuik PENICUIK,BBC Radio 1,98200000,55.819142,-3.194377,248,0.132,, +FM,BBC Radio 1 Portree Penifiler,BBC Radio 1,97700000,57.395531,-6.180316,52,0.005,, +FM,BBC Radio 1 Colwyn Bay Penmaen Rhos,BBC Radio 1,98000000,53.286941,-3.684982,125,0.1,, +FM,BBC Radio 1 Cwmbran Pennar,BBC Radio 1,99100000,51.656112,-3.144564,228,0.005,, +FM,BBC Radio 1 Huntingdonshire Peterborough,BBC Radio 1,99700000,52.507615,-0.343282,56,18.7,, +FM,BBC Radio 1 Pitlochry PITLOCHRY,BBC Radio 1,98800000,56.687307,-3.759852,401,0,, +FM,BBC Radio 1 Plymouth Plympton,BBC Radio 1,98100000,50.381346,-4.067802,113,0.04,, +FM,BBC Radio 1 Tyneside PONTOP PIKE,BBC Radio 1,98100000,54.868838,-1.771233,302,67.5,, +FM,BBC Radio 1 Pontypool,BBC Radio 1,98800000,51.685712,-3.036095,250,0.025,, +FM,BBC Radio 1 Islay PORT ELLEN,BBC Radio 1,98600000,55.627446,-6.227617,93,0.065,, +FM,BBC Radio 1 Pontypridd Porth,BBC Radio 1,98000000,51.617787,-3.403306,247,0.01,, +FM,BBC Radio 1 West Cornwall Redruth,BBC Radio 1,99300000,50.209786,-5.238489,237,8.80001,, +FM,BBC Radio 1 Neath Valley RHEOLA,BBC Radio 1,97800000,51.741661,-3.679878,409,0.01,, +FM,BBC Radio 1 Rhymney RHYMNEY,BBC Radio 1,98900000,51.730587,-3.264891,419,0.02,, +FM,BBC Radio 1 Herefordshire RIDGE HILL,BBC Radio 1,98200000,51.997433,-2.53989,204,5,, +FM,BBC Radio 1 Highland Rosemarkie,BBC Radio 1,99200000,57.633195,-4.075231,206,9.77237,, +FM,BBC Radio 1 Coupar Rosemount,BBC Radio 1,99200000,56.576982,-3.298257,81,0.032,, +FM,BBC Radio 1 Helensburgh Rosneath,BBC Radio 1,98800000,55.991937,-4.793795,107,0.025,, +FM,BBC Radio 1 South Newry Rostrevor Forest,BBC Radio 1,97900000,54.092725,-6.182015,215,0.02,, +FM,BBC Radio 1 Bute ROTHESAY,BBC Radio 1,98100000,55.878164,-4.998928,166,0.285,, +FM,BBC Radio 1 Hampshire and Isle of Wight ROWRIDGE,BBC Radio 1,98200000,50.676724,-1.370435,143,125,, +FM,BBC Radio 1 Caithness Rumster Forest,BBC Radio 1,99700000,58.328006,-3.371413,221,5,, +FM,BBC Radio 1 Greater Manchester SADDLEWORTH,BBC Radio 1,99300000,53.541535,-2.020635,347,0.1,, +FM,BBC Radio 1 Salcombe,BBC Radio 1,99100000,50.244921,-3.749115,61,0.198582,, +FM,BBC Radio 1 Salisbury SALISBURY,BBC Radio 1,99400000,51.055855,-1.806762,107,0.012619,, +FM,BBC Radio 1 Cumbria & South West Scotland Sandale,BBC Radio 1,97700000,54.749109,-3.14076,363,125,, +FM,BBC Radio 1 South Yorkshire Sheffield,BBC Radio 1,99500000,53.379385,-1.513809,247,0.16,, +FM,BBC Radio 1 Skye & Lochalsh Skriaig,BBC Radio 1,98100000,57.38642,-6.24289,390,9.68481,, +FM,BBC Radio 1 Kintyre Islay and Jura South Knapdale,BBC Radio 1,98900000,55.91751,-5.462223,481,1.1,, +FM,BBC Radio 1 Derbyshire Stanton Moor,BBC Radio 1,99400000,53.170195,-1.633756,305,0.6,, +FM,BBC Radio 1 Inveraray Strachur,BBC Radio 1,98400000,56.179137,-5.071531,175,0.01,, +FM,BBC Radio 1 Galloway STRANRAER,BBC Radio 1,99300000,54.928074,-4.949004,190,0.031,, +FM,BBC Radio 1 West Midlands SUTTON COLDFIELD,BBC Radio 1,97900000,52.600539,-1.833856,169,62.5,, +FM,BBC Radio 1 Dover Swingate,BBC Radio 1,99500000,51.137832,1.335758,122,5.5,, +FM,BBC Radio 1 Norfolk TACOLNESTON,BBC Radio 1,99300000,52.518759,1.138705,64,119.374,, +FM,BBC Radio 1 Calder Valley TODMORDEN,BBC Radio 1,98500000,53.713283,-2.065416,221,0.1,, +FM,BBC Radio 1 Rhondda Valley Ton Pentre,BBC Radio 1,98400000,51.64954,-3.504023,323,0.003,, +FM,BBC Radio 1 Tullich TULLICH,BBC Radio 1,99700000,57.07283,-3.027203,463,0.025,, +FM,BBC Radio 1 Ullapool,BBC Radio 1,97900000,57.892557,-5.135709,143,0.05,, +FM,BBC Radio 1 Neath Valley Varteg Hill,BBC Radio 1,98500000,51.754672,-3.773522,351,0.024,, +FM,BBC Radio 1 Ventnor,BBC Radio 1,99000000,50.601933,-1.199325,227,0.025,, +FM,BBC Radio 1 Calder Valley WALSDEN SOUTH,BBC Radio 1,98000000,53.690612,-2.096576,227,0.01,, +FM,BBC Radio 1 County Durham WEARDALE,BBC Radio 1,99300000,54.74117,-1.961914,404,0.09,, +FM,BBC Radio 1 Wensleydale,BBC Radio 1,97900000,54.312324,-2.013338,402,0.05,, +FM,BBC Radio 1 South Glamorgan Wenvoe,BBC Radio 1,99500000,51.459476,-3.282282,129,125,, +FM,BBC Radio 1 North Ayrshire WEST KILBRIDE,BBC Radio 1,98700000,55.695641,-4.84184,170,0.05,, +FM,BBC Radio 1 Trowbridge WESTWOOD,BBC Radio 1,97900000,51.336803,-2.264225,87,0.05,, +FM,BBC Radio 1 Weymouth,BBC Radio 1,99600000,50.59912,-2.476532,64,0.1,, +FM,BBC Radio 1 Whalley WHALLEY,BBC Radio 1,99200000,53.812989,-2.411689,172,0.01,, +FM,BBC Radio 1 West Yorkshire WHARFEDALE,BBC Radio 1,98000000,53.932231,-1.699923,212,0.02,, +FM,BBC Radio 1 Whitby WHITBY BUSINESS PARK,BBC Radio 1,99200000,54.473215,-0.595572,60,0.054,, +FM,BBC Radio 1 West Cumbria Whitehaven - VHF,BBC Radio 1,99300000,54.499453,-3.558449,125,0.13,, +FM,BBC Radio 1 Brighton WHITEHAWK HILL,BBC Radio 1,99700000,50.824954,-0.113611,120,0.25,, +FM,BBC Radio 1 Windermere WINDERMERE,BBC Radio 1,97900000,54.37436,-2.951306,216,0.032,, +FM,BBC Radio 1 Lancashire WINTER HILL,BBC Radio 1,98200000,53.625547,-2.514794,439,2,, +FM,BBC Radio 1 North Yorkshire Woolmoor,BBC Radio 1,99600000,54.287102,-1.299034,257,5,, +FM,BBC Radio 1 London and the South East Wrotham,BBC Radio 1,98800000,51.320696,0.287702,219,62.5,, +FM,BBC Radio 2 Aberdare,BBC Radio 2,89200000,51.702007,-3.398531,282,0.021,, +FM,BBC Radio 2 Abergavenny,BBC Radio 2,88700000,51.807795,-3.097781,433,0.02,, +FM,BBC Radio 2 Pontypool Abertillery,BBC Radio 2,89000000,51.714666,-3.12404,400,0.01,, +FM,BBC Radio 2 Argyll & Bute Ardgour,BBC Radio 2,88700000,56.712175,-5.290096,12,0.005,, +FM,BBC Radio 2 Scottish Borders Ashkirk,BBC Radio 2,89100000,55.510631,-2.840766,303,20.3208,, +FM,BBC Radio 2 Axe Valley,BBC Radio 2,89500000,50.74593,-3.016766,169,0.08,, +FM,BBC Radio 2 Argyll & Bute Ballachulish,BBC Radio 2,88100000,56.684359,-5.170394,14,0.015,, +FM,BBC Radio 2 North Antrim Ballycastle,BBC Radio 2,88800000,55.215995,-6.309339,150,0.05,, +FM,BBC Radio 2 Skipton Barnoldswick,BBC Radio 2,89700000,53.928368,-2.15728,191,0.02,, +FM,BBC Radio 2 North Devon Barnstaple,BBC Radio 2,88500000,51.040763,-4.13826,116,0.5,, +FM,BBC Radio 2 Basingstoke,BBC Radio 2,90100000,51.226848,-1.080409,176,0.02,, +FM,BBC Radio 2 Bath,BBC Radio 2,89000000,51.38779,-2.332925,175,0.041,, +FM,BBC Radio 2 Torbay Beacon Hill North,BBC Radio 2,88700000,50.447129,-3.610498,196,0.794328,, +FM,BBC Radio 2 Leeds Beecroft Hill,BBC Radio 2,89800000,53.811543,-1.641253,149,0.2,, +FM,BBC Radio 2 Lincolnshire Belmont,BBC Radio 2,88800000,53.335814,-0.171983,125,8.3,, +FM,BBC Radio 2 Stirling Ben Gullipen,BBC Radio 2,88700000,56.212308,-4.263559,415,1,, +FM,BBC Radio 2 Borders Berwick-upon-Tweed,BBC Radio 2,88600000,55.785854,-2.032822,129,0.05,, +FM,BBC Radio 2 Snowdonia Betws Y Coed,BBC Radio 2,88200000,53.108346,-3.75717,303,0.025,, +FM,BBC Radio 2 Bexhill,BBC Radio 2,88200000,50.84568,0.475913,45,0.0117,, +FM,BBC Radio 2 North Yorkshire Bilsdale - Station Tower,BBC Radio 2,89000000,54.358848,-1.149317,381,2,, +FM,BBC Radio 2 Central Scotland Black Hill,BBC Radio 2,89900000,55.860005,-3.873572,275,122.155,, +FM,BBC Radio 2 Blaenavon,BBC Radio 2,88500000,51.750968,-3.048742,373,0.01,, +FM,BBC Radio 2 Ceredigion Blaenplwyf,BBC Radio 2,88700000,52.360613,-4.102585,174,125,, +FM,BBC Radio 2 Swindon Blunsdon,BBC Radio 2,89000000,51.609363,-1.794027,148,0.2,, +FM,BBC Radio 2 Milton Keynes Bow Brickhill,BBC Radio 2,88600000,51.998806,-0.669784,169,5,, +FM,BBC Radio 2 Islay Bowmore,BBC Radio 2,88100000,55.750128,-6.274228,46,0.061,, +FM,BBC Radio 2 Mid Wales Brecon VHF,BBC Radio 2,88900000,51.959448,-3.412497,248,0.01,, +FM,BBC Radio 2 Shetland Bressay,BBC Radio 2,88300000,60.130079,-1.096281,224,25,, +FM,BBC Radio 2 Dorchester and Weymouth Bridport,BBC Radio 2,89100000,50.720805,-2.775678,64,0.2,, +FM,BBC Radio 2 Omagh and Enniskillen Brougher Mountain,BBC Radio 2,89400000,54.423191,-7.462812,305,5,, +FM,BBC Radio 2 North Derbyshire Buxton,BBC Radio 2,90000000,53.275085,-1.911935,432,0.05,, +FM,BBC Radio 2 Calne CALNE,BBC Radio 2,88200000,51.428815,-2.005417,104,0.032,, +FM,BBC Radio 2 Kirkcudbright CAMBRET HILL,BBC Radio 2,88700000,54.893698,-4.302038,343,0.0625,, +FM,BBC Radio 2 South Newry Camlough,BBC Radio 2,88700000,54.160861,-6.385716,340,2,, +FM,BBC Radio 2 Kintyre Islay and Jura CAMPBELTOWN,BBC Radio 2,88400000,55.413714,-5.624241,83,0.2,, +FM,BBC Radio 2 Carmarthen,BBC Radio 2,88900000,51.867126,-4.309135,137,0.01,, +FM,BBC Radio 2 Carmarthenshire Carmel,BBC Radio 2,88400000,51.818501,-4.066216,255,1.6,, +FM,BBC Radio 2 Newtownabbey Carnmoney Hill,BBC Radio 2,88800000,54.676355,-5.928737,207,0.02,, +FM,BBC Radio 2 Caterham Valley CATERHAM,BBC Radio 2,89700000,51.284605,-0.075623,173,0.015,, +FM,BBC Radio 2 Gloucestershire CHALFORD,BBC Radio 2,89200000,51.714525,-2.170893,174,0.1,, +FM,BBC Radio 2 Somerset Chard,BBC Radio 2,88900000,50.874213,-2.913244,173,0.05,, +FM,BBC Radio 2 Northumberland CHATTON,BBC Radio 2,90100000,55.531826,-1.834261,192,2.8,, +FM,BBC Radio 2 South Yorkshire Chesterfield,BBC Radio 2,89000000,53.283991,-1.427446,179,0.4,, +FM,BBC Radio 2 Chippenham,BBC Radio 2,88800000,51.474065,-2.12579,103,0.009,, +FM,BBC Radio 2 Gloucestershire Churchdown Hill,BBC Radio 2,89000000,51.868264,-2.175397,147,0.036,, +FM,BBC Radio 2 Cirencester,BBC Radio 2,88100000,51.717612,-1.968107,112,0.01,, +FM,BBC Radio 2 North Uist Clettraval,BBC Radio 2,88100000,57.617267,-7.44252,123,2,, +FM,BBC Radio 2 Wye Valley CLYRO,BBC Radio 2,88100000,52.08185,-3.162868,215,0.01,, +FM,BBC Radio 2 Ilfracombe Combe Martin,BBC Radio 2,89100000,51.197266,-4.031278,144,0.004,, +FM,BBC Radio 2 Conway CONWY,BBC Radio 2,89100000,53.271606,-3.828796,114,0.05,, +FM,BBC Radio 2 Calder Valley CORNHOLME,BBC Radio 2,89700000,53.734447,-2.125781,312,0.02,, +FM,BBC Radio 2 Folkestone Creteway Down,BBC Radio 2,88400000,51.099738,1.182579,166,0.1,, +FM,BBC Radio 2 Crieff CRIEFF,BBC Radio 2,89300000,56.357222,-3.920232,219,0.125,, +FM,BBC Radio 2 Croeserw,BBC Radio 2,89000000,51.644491,-3.652146,343,0.01,, +FM,BBC Radio 2 London Crystal Palace,BBC Radio 2,88800000,51.424161,-0.074944,110,4,, +FM,BBC Radio 2 Cwmafan,BBC Radio 2,88500000,51.628417,-3.737817,210,0.01,, +FM,BBC Radio 2 South Uist Daliburgh,BBC Radio 2,89300000,57.169643,-7.402558,6,1,, +FM,BBC Radio 2 Dartford Tunnel,BBC Radio 2,89100000,51.469253,0.260425,3,0,, +FM,BBC Radio 2 Ayrshire DARVEL,BBC Radio 2,89500000,55.579376,-4.289814,286,5,, +FM,BBC Radio 2 Darwen DARWEN,BBC Radio 2,89500000,53.696571,-2.443266,261,0.01,, +FM,BBC Radio 2 Gwynedd DEINIOLEN,BBC Radio 2,88100000,53.137063,-4.129256,322,0.05,, +FM,BBC Radio 2 Northern Ireland Divis A,BBC Radio 2,90100000,54.607538,-6.009471,368,61.0773,, +FM,BBC Radio 2 Snowdonia DOLGELLAU,BBC Radio 2,90100000,52.749344,-3.886853,103,0.025,, +FM,BBC Radio 2 Isle of Man DOUGLAS,BBC Radio 2,88400000,54.140551,-4.493075,143,5.7,, +FM,BBC Radio 2 Aberdeen Durris,BBC Radio 2,89400000,56.999987,-2.389944,325,1.05,, +FM,BBC Radio 2 South Wales EBBW VALE,BBC Radio 2,88400000,51.771714,-3.218938,442,0.015,, +FM,BBC Radio 2 Frome Egford Hill,BBC Radio 2,89100000,51.231585,-2.3391,132,0.06,, +FM,BBC Radio 2 Isle of Lewis Eitshal,BBC Radio 2,89800000,58.179148,-6.584974,206,2,, +FM,BBC Radio 2 Exeter Exeter (St Thomas),BBC Radio 2,89000000,50.719122,-3.562148,102,0.0275,, +FM,BBC Radio 2 Borders EYEMOUTH,BBC Radio 2,89700000,55.83228,-2.085863,197,0.1,, +FM,BBC Radio 2 Newcastle Fenham,BBC Radio 2,89800000,54.978039,-1.662966,120,0.042,, +FM,BBC Radio 2 South Wales Ferndale,BBC Radio 2,89600000,51.662873,-3.437376,386,0.01,, +FM,BBC Radio 2 Snowdonia FFESTINIOG,BBC Radio 2,88100000,52.934172,-3.921818,308,0.082081,, +FM,BBC Radio 2 Angus Forfar,BBC Radio 2,88300000,56.55718,-2.843663,206,10,, +FM,BBC Radio 2 Great Glen Fort Augustus,BBC Radio 2,88600000,57.105987,-4.70869,307,1,, +FM,BBC Radio 2 Fort William,BBC Radio 2,89300000,56.848302,-5.088621,16,2,, +FM,BBC Radio 2 Ayr GIRVAN,BBC Radio 2,88900000,55.244736,-4.814596,195,0.1,, +FM,BBC Radio 2 Isle of Mull Glengorm,BBC Radio 2,89500000,56.632166,-6.133249,253,2.5,, +FM,BBC Radio 2 Tiverton Gogwell,BBC Radio 2,89600000,50.889583,-3.469029,233,0.3,, +FM,BBC Radio 2 Grantham,BBC Radio 2,88100000,52.892406,-0.655971,115,0.05,, +FM,BBC Radio 2 Cairngorms Grantown,BBC Radio 2,89800000,57.320173,-3.656313,389,0.36,, +FM,BBC Radio 2 Guildford GUILDFORD,BBC Radio 2,88100000,51.228358,-0.605467,142,2,, +FM,BBC Radio 2 Rossendale HASLINGDEN,BBC Radio 2,89900000,53.708491,-2.311438,336,0.1,, +FM,BBC Radio 2 Hastings,BBC Radio 2,89600000,50.861622,0.566431,76,0.25,, +FM,BBC Radio 2 Pembrokeshire Haverfordwest,BBC Radio 2,89300000,51.899195,-4.86653,186,9.54993,, +FM,BBC Radio 2 Hebden Bridge HEBDEN BRIDGE,BBC Radio 2,88400000,53.737298,-2.018458,220,0.025,, +FM,BBC Radio 2 Caversham Hemdean,BBC Radio 2,89800000,51.481196,-0.978531,69,1,, +FM,BBC Radio 2 High Wycombe,BBC Radio 2,90000000,51.640531,-0.763678,156,0.025,, +FM,BBC Radio 2 Teignmouth Holcombe Down,BBC Radio 2,89400000,50.566699,-3.506408,205,0.1,, +FM,BBC Radio 2 Yorkshire and Lancashire Holme Moss,BBC Radio 2,89300000,53.533184,-1.858296,524,125,, +FM,BBC Radio 2 Weston-super-Mare Hutton,BBC Radio 2,89400000,51.325227,-2.918301,73,0.04,, +FM,BBC Radio 2 Bradford Idle,BBC Radio 2,88500000,53.832945,-1.752142,215,0.01,, +FM,BBC Radio 2 Bristol Ilchester Crescent,BBC Radio 2,89300000,51.427947,-2.609813,48,0.649999,, +FM,BBC Radio 2 Borders INNERLEITHEN,BBC Radio 2,89500000,55.620243,-3.07272,244,0.011,, +FM,BBC Radio 2 Isles of Scilly,BBC Radio 2,88800000,49.932504,-6.305394,32,0.03,, +FM,BBC Radio 2 Devon Ivybridge,BBC Radio 2,89900000,50.368664,-3.926486,142,0.05,, +FM,BBC Radio 2 Orkney Keelylang Hill B,BBC Radio 2,89300000,58.975283,-3.083943,220,16,, +FM,BBC Radio 2 West Yorkshire KEIGHLEY,BBC Radio 2,88900000,53.895532,-1.896639,305,1,, +FM,BBC Radio 2 South Lakes Kendal,BBC Radio 2,89000000,54.314559,-2.708127,172,0.05,, +FM,BBC Radio 2 Kenley KENLEY,BBC Radio 2,88400000,51.317089,-0.09335,152,0.025,, +FM,BBC Radio 2 North Lakes Keswick Forest,BBC Radio 2,89600000,54.608739,-3.204646,215,0,, +FM,BBC Radio 2 Kilkeel,BBC Radio 2,89400000,54.095375,-6.041723,267,0.1,, +FM,BBC Radio 2 Swansea Kilvey Hill,BBC Radio 2,89500000,51.62891,-3.919745,193,0.5,, +FM,BBC Radio 2 Dartmouth Kingswear,BBC Radio 2,89800000,50.349538,-3.565025,107,0.005,, +FM,BBC Radio 2 Cairngorms KINGUSSIE,BBC Radio 2,89100000,57.061393,-4.030507,351,0.05,, +FM,BBC Radio 2 Kinlochleven,BBC Radio 2,89700000,56.722796,-4.978914,273,0.005,, +FM,BBC Radio 2 Borders Kirkconnel,BBC Radio 2,88700000,55.412875,-3.983785,480,0.02,, +FM,BBC Radio 2 Perth Kirkton Mailer,BBC Radio 2,89000000,56.372293,-3.453361,178,0.5,, +FM,BBC Radio 2 Moray Knockmore,BBC Radio 2,88200000,57.532303,-3.135126,353,1,, +FM,BBC Radio 2 Larne,BBC Radio 2,89100000,54.862024,-5.828421,119,0.05,, +FM,BBC Radio 2 Jersey Les Platons,BBC Radio 2,89600000,49.246023,-2.1012,134,8,, +FM,BBC Radio 2 East Ayrshire LETHANHILL,BBC Radio 2,88300000,55.36421,-4.464995,300,0.11,, +FM,BBC Radio 2 Limavady,BBC Radio 2,89600000,55.109172,-6.886121,341,1.7,, +FM,BBC Radio 2 Anglesey LLANDDONA,BBC Radio 2,89800000,53.307002,-4.12825,146,10.5,, +FM,BBC Radio 2 Snowdonia LLANDECWYN,BBC Radio 2,89400000,52.914419,-4.017769,268,0.1,, +FM,BBC Radio 2 Mid Wales LLANDINAM,BBC Radio 2,90100000,52.479547,-3.399945,454,0.022,, +FM,BBC Radio 2 Llandrindod Wells,BBC Radio 2,89100000,52.261047,-3.439354,432,1.39,, +FM,BBC Radio 2 Ceredigion LLANDYFRIOG,BBC Radio 2,90100000,52.045314,-4.410045,116,0.05,, +FM,BBC Radio 2 Llanfyllin LLANFYLLIN,BBC Radio 2,89100000,52.753349,-3.259797,264,0.003972,, +FM,BBC Radio 2 Bridgend Llangeinor,BBC Radio 2,88600000,51.586117,-3.581462,306,0.01,, +FM,BBC Radio 2 Llangollen and Cheshire Llangollen VHF,BBC Radio 2,88900000,53.031048,-3.180791,556,7.80001,, +FM,BBC Radio 2 Mid Wales LLANIDLOES,BBC Radio 2,88100000,52.446933,-3.549585,244,0.025,, +FM,BBC Radio 2 Llanrhaeadr ym Mochnant LLANRHAEADR YM MOCHN,BBC Radio 2,89800000,52.825889,-3.226423,333,0.025,, +FM,BBC Radio 2 Snowdonia LLWYN ONN,BBC Radio 2,88300000,52.737496,-4.03773,277,0.25,, +FM,BBC Radio 2 Talgarth LLYSWEN,BBC Radio 2,89600000,52.016761,-3.258522,224,0.01,, +FM,BBC Radio 2 Londonderry,BBC Radio 2,88700000,55.00421,-7.368968,175,15.6,, +FM,BBC Radio 2 Welshpool LONG MOUNTAIN,BBC Radio 2,89600000,52.644306,-3.086542,400,0.024,, +FM,BBC Radio 2 Calderdale LUDDENDEN,BBC Radio 2,88700000,53.720111,-1.92874,256,0.084,, +FM,BBC Radio 2 Ludlow LUDLOW,BBC Radio 2,89600000,52.362722,-2.738327,216,0.005,, +FM,BBC Radio 2 Lyme Regis,BBC Radio 2,89700000,50.737572,-2.929856,162,0.05,, +FM,BBC Radio 2 Snowdonia MACHYNLLETH,BBC Radio 2,89400000,52.586449,-3.885603,100,0.06,, +FM,BBC Radio 2 Cambridgeshire Madingley,BBC Radio 2,88900000,52.215394,0.036667,62,0.13,, +FM,BBC Radio 2 Mallaig,BBC Radio 2,88100000,57.001499,-5.828475,71,0.02,, +FM,BBC Radio 2 Colchester and Ipswich Manningtree,BBC Radio 2,88100000,51.92387,1.086097,35,2.5,, +FM,BBC Radio 2 Marlborough MARLBOROUGH,BBC Radio 2,90100000,51.417912,-1.699977,192,0.1,, +FM,BBC Radio 2 Aberdeenshire Meldrum,BBC Radio 2,88700000,57.386674,-2.400197,245,75,, +FM,BBC Radio 2 Wester Ross Melvaig,BBC Radio 2,89100000,57.842382,-5.782182,271,17.5903,, +FM,BBC Radio 2 Membury,BBC Radio 2,88900000,51.484903,-1.558836,211,0.125,, +FM,BBC Radio 2 Mickleham MICKLEHAM,BBC Radio 2,89700000,51.272007,-0.333055,103,0.025,, +FM,BBC Radio 2 Dumbarton Millburn Muir,BBC Radio 2,88300000,55.982322,-4.600708,152,0.025,, +FM,BBC Radio 2 West Somerset Minehead,BBC Radio 2,89000000,51.19744,-3.48052,79,0.01,, +FM,BBC Radio 2 Morecambe Bay,BBC Radio 2,90000000,54.202505,-3.167346,259,5,, +FM,BBC Radio 2 Cardigan Bay Mynydd Pencarreg,BBC Radio 2,89700000,52.067402,-4.076523,411,0.2,, +FM,BBC Radio 2 Gloucestershire NAILSWORTH,BBC Radio 2,88200000,51.690257,-2.219843,114,0.1,, +FM,BBC Radio 2 Isle of Lewis Ness of Lewis,BBC Radio 2,88300000,58.461531,-6.230808,100,0.162,, +FM,BBC Radio 2 Newbury,BBC Radio 2,88200000,51.414621,-1.290472,130,0.1,, +FM,BBC Radio 2 Newhaven NEWHAVEN,BBC Radio 2,89700000,50.787726,0.035848,81,0.05,, +FM,BBC Radio 2 Newcastle NEWTON,BBC Radio 2,89400000,54.982447,-1.945593,165,0.1,, +FM,BBC Radio 2 Devon North Hessary Tor,BBC Radio 2,88100000,50.550217,-4.008413,508,80,, +FM,BBC Radio 2 Northamptonshire Northampton,BBC Radio 2,88900000,52.275355,-0.885481,127,0.0615,, +FM,BBC Radio 2 Oban and Mull Oban,BBC Radio 2,88900000,56.40396,-5.485882,122,2.5,, +FM,BBC Radio 2 Ogmore Vale,BBC Radio 2,89200000,51.593504,-3.546781,298,0.01,, +FM,BBC Radio 2 Okehampton,BBC Radio 2,88700000,50.753618,-4.005483,208,0.025,, +FM,BBC Radio 2 Scarborough OLIVERS MOUNT,BBC Radio 2,89900000,54.267351,-0.404494,151,0.125,, +FM,BBC Radio 2 Oxfordshire Oxford,BBC Radio 2,89500000,51.790638,-1.179183,130,17.5903,, +FM,BBC Radio 2 Borders PEEBLES,BBC Radio 2,88400000,55.661832,-3.227732,352,0.02,, +FM,BBC Radio 2 Bodmin Penaligon Downs,BBC Radio 2,88800000,50.481389,-4.782638,116,0.2,, +FM,BBC Radio 2 Burnley Pendle Forest,BBC Radio 2,90200000,53.841893,-2.266074,276,0.5,, +FM,BBC Radio 2 Penicuik PENICUIK,BBC Radio 2,88600000,55.819142,-3.194377,248,0.132,, +FM,BBC Radio 2 Portree Penifiler,BBC Radio 2,88100000,57.395531,-6.180316,52,0.005,, +FM,BBC Radio 2 Colwyn Bay Penmaen Rhos,BBC Radio 2,88400000,53.286941,-3.684982,125,0.1,, +FM,BBC Radio 2 Cwmbran Pennar,BBC Radio 2,89500000,51.656112,-3.144564,228,0.005,, +FM,BBC Radio 2 Huntingdonshire Peterborough,BBC Radio 2,90100000,52.507615,-0.343282,56,18.7,, +FM,BBC Radio 2 Pitlochry PITLOCHRY,BBC Radio 2,89200000,56.687307,-3.759852,401,0,, +FM,BBC Radio 2 Plymouth Plympton,BBC Radio 2,88500000,50.381346,-4.067802,113,0.04,, +FM,BBC Radio 2 Tyneside PONTOP PIKE,BBC Radio 2,88500000,54.868838,-1.771233,302,67.5,, +FM,BBC Radio 2 Pontypool,BBC Radio 2,89200000,51.685712,-3.036095,250,0.025,, +FM,BBC Radio 2 Islay PORT ELLEN,BBC Radio 2,89000000,55.627446,-6.227617,93,0.065,, +FM,BBC Radio 2 Pontypridd Porth,BBC Radio 2,88400000,51.617787,-3.403306,247,0.01,, +FM,BBC Radio 2 West Cornwall Redruth,BBC Radio 2,89700000,50.209786,-5.238489,237,8.80001,, +FM,BBC Radio 2 Neath Valley RHEOLA,BBC Radio 2,88200000,51.741661,-3.679878,409,0.01,, +FM,BBC Radio 2 Rhymney RHYMNEY,BBC Radio 2,89300000,51.730587,-3.264891,419,0.02,, +FM,BBC Radio 2 Herefordshire RIDGE HILL,BBC Radio 2,88600000,51.997433,-2.53989,204,5,, +FM,BBC Radio 2 Highland Rosemarkie,BBC Radio 2,89600000,57.633195,-4.075231,206,9.77237,, +FM,BBC Radio 2 Coupar Rosemount,BBC Radio 2,89600000,56.576982,-3.298257,81,0.032,, +FM,BBC Radio 2 Helensburgh Rosneath,BBC Radio 2,89200000,55.991937,-4.793795,107,0.025,, +FM,BBC Radio 2 South Newry Rostrevor Forest,BBC Radio 2,88300000,54.092725,-6.182015,215,0.02,, +FM,BBC Radio 2 Bute ROTHESAY,BBC Radio 2,88500000,55.878164,-4.998928,166,0.285,, +FM,BBC Radio 2 Hampshire and Isle of Wight ROWRIDGE,BBC Radio 2,88500000,50.676724,-1.370435,143,125,, +FM,BBC Radio 2 Caithness Rumster Forest,BBC Radio 2,90100000,58.328006,-3.371413,221,5,, +FM,BBC Radio 2 Greater Manchester SADDLEWORTH,BBC Radio 2,89800000,53.541535,-2.020635,347,0.1,, +FM,BBC Radio 2 Salcombe,BBC Radio 2,89500000,50.244921,-3.749115,61,0.198582,, +FM,BBC Radio 2 Salisbury SALISBURY,BBC Radio 2,89800000,51.055855,-1.806762,107,0.012619,, +FM,BBC Radio 2 Cumbria & South West Scotland Sandale,BBC Radio 2,88100000,54.749109,-3.14076,363,125,, +FM,BBC Radio 2 South Yorkshire Sheffield,BBC Radio 2,89900000,53.379385,-1.513809,247,0.16,, +FM,BBC Radio 2 Skye & Lochalsh Skriaig,BBC Radio 2,88500000,57.38642,-6.24289,390,9.68481,, +FM,BBC Radio 2 Kintyre Islay and Jura South Knapdale,BBC Radio 2,89300000,55.91751,-5.462223,481,1.1,, +FM,BBC Radio 2 Derbyshire Stanton Moor,BBC Radio 2,89800000,53.170195,-1.633756,305,0.6,, +FM,BBC Radio 2 Inveraray Strachur,BBC Radio 2,88800000,56.179137,-5.071531,175,0.01,, +FM,BBC Radio 2 Galloway STRANRAER,BBC Radio 2,89700000,54.928074,-4.949004,190,0.031,, +FM,BBC Radio 2 West Midlands SUTTON COLDFIELD,BBC Radio 2,88300000,52.600539,-1.833856,169,62.5,, +FM,BBC Radio 2 Dover Swingate,BBC Radio 2,90000000,51.137832,1.335758,122,5.5,, +FM,BBC Radio 2 Norfolk TACOLNESTON,BBC Radio 2,89700000,52.518759,1.138705,64,119.374,, +FM,BBC Radio 2 Calder Valley TODMORDEN,BBC Radio 2,88900000,53.713283,-2.065416,221,0.1,, +FM,BBC Radio 2 Rhondda Valley Ton Pentre,BBC Radio 2,88800000,51.64954,-3.504023,323,0.003,, +FM,BBC Radio 2 Tullich TULLICH,BBC Radio 2,90100000,57.07283,-3.027203,463,0.025,, +FM,BBC Radio 2 Ullapool,BBC Radio 2,88300000,57.892557,-5.135709,143,0.05,, +FM,BBC Radio 2 Neath Valley Varteg Hill,BBC Radio 2,88900000,51.754672,-3.773522,351,0.024,, +FM,BBC Radio 2 Ventnor,BBC Radio 2,89400000,50.601933,-1.199325,227,0.025,, +FM,BBC Radio 2 Calder Valley WALSDEN SOUTH,BBC Radio 2,88400000,53.690612,-2.096576,227,0.01,, +FM,BBC Radio 2 County Durham WEARDALE,BBC Radio 2,89700000,54.74117,-1.961914,404,0.09,, +FM,BBC Radio 2 Wensleydale,BBC Radio 2,88300000,54.312324,-2.013338,402,0.05,, +FM,BBC Radio 2 South Glamorgan Wenvoe,BBC Radio 2,89900000,51.459476,-3.282282,129,125,, +FM,BBC Radio 2 North Ayrshire WEST KILBRIDE,BBC Radio 2,89100000,55.695641,-4.84184,170,0.05,, +FM,BBC Radio 2 Trowbridge WESTWOOD,BBC Radio 2,88300000,51.336803,-2.264225,87,0.05,, +FM,BBC Radio 2 Weymouth,BBC Radio 2,90000000,50.59912,-2.476532,64,0.1,, +FM,BBC Radio 2 Whalley WHALLEY,BBC Radio 2,89600000,53.812989,-2.411689,172,0.01,, +FM,BBC Radio 2 West Yorkshire WHARFEDALE,BBC Radio 2,88400000,53.932231,-1.699923,212,0.02,, +FM,BBC Radio 2 Whitby WHITBY BUSINESS PARK,BBC Radio 2,89600000,54.473215,-0.595572,60,0.063,, +FM,BBC Radio 2 West Cumbria Whitehaven - VHF,BBC Radio 2,89700000,54.499453,-3.558449,125,0.13,, +FM,BBC Radio 2 Brighton WHITEHAWK HILL,BBC Radio 2,90100000,50.824954,-0.113611,120,0.25,, +FM,BBC Radio 2 Windermere WINDERMERE,BBC Radio 2,88300000,54.37436,-2.951306,216,0.032,, +FM,BBC Radio 2 Lancashire WINTER HILL,BBC Radio 2,88600000,53.625547,-2.514794,439,2,, +FM,BBC Radio 2 North Yorkshire Woolmoor,BBC Radio 2,90200000,54.287102,-1.299034,257,5,, +FM,BBC Radio 2 London and the South East Wrotham,BBC Radio 2,89100000,51.320696,0.287702,219,125,, +FM,BBC Radio 3 Aberdare,BBC Radio 3,91400000,51.702007,-3.398531,282,0.021,, +FM,BBC Radio 3 Abergavenny,BBC Radio 3,90900000,51.807795,-3.097781,433,0.02,, +FM,BBC Radio 3 Argyll & Bute Ardgour,BBC Radio 3,90900000,56.712175,-5.290096,12,0.005,, +FM,BBC Radio 3 Scottish Borders Ashkirk,BBC Radio 3,91300000,55.510631,-2.840766,303,20.3208,, +FM,BBC Radio 3 Axe Valley,BBC Radio 3,91700000,50.74593,-3.016766,169,0.08,, +FM,BBC Radio 3 Argyll & Bute Ballachulish,BBC Radio 3,90300000,56.684359,-5.170394,14,0.015,, +FM,BBC Radio 3 North Antrim Ballycastle,BBC Radio 3,91000000,55.215995,-6.309339,150,0.05,, +FM,BBC Radio 3 Skipton Barnoldswick,BBC Radio 3,91900000,53.928368,-2.15728,191,0.02,, +FM,BBC Radio 3 North Devon Barnstaple,BBC Radio 3,90700000,51.040763,-4.13826,116,0.5,, +FM,BBC Radio 3 Basingstoke,BBC Radio 3,92300000,51.226848,-1.080409,176,0.02,, +FM,BBC Radio 3 Bath,BBC Radio 3,91200000,51.38779,-2.332925,175,0.041,, +FM,BBC Radio 3 Torbay Beacon Hill North,BBC Radio 3,90900000,50.447129,-3.610498,196,0.794328,, +FM,BBC Radio 3 Leeds Beecroft Hill,BBC Radio 3,92000000,53.811543,-1.641253,149,0.2,, +FM,BBC Radio 3 Lincolnshire Belmont,BBC Radio 3,90900000,53.335814,-0.171983,125,8.3,, +FM,BBC Radio 3 Stirling Ben Gullipen,BBC Radio 3,90900000,56.212308,-4.263559,415,1,, +FM,BBC Radio 3 Borders Berwick-upon-Tweed,BBC Radio 3,90800000,55.785854,-2.032822,129,0.05,, +FM,BBC Radio 3 Bexhill,BBC Radio 3,92200000,50.84568,0.475913,45,0.0117,, +FM,BBC Radio 3 North Yorkshire Bilsdale - Station Tower,BBC Radio 3,91200000,54.358848,-1.149317,381,2,, +FM,BBC Radio 3 Central Scotland Black Hill,BBC Radio 3,92100000,55.860005,-3.873572,275,122.155,, +FM,BBC Radio 3 Blaenavon,BBC Radio 3,90700000,51.750968,-3.048742,373,0.01,, +FM,BBC Radio 3 Ceredigion Blaenplwyf,BBC Radio 3,90900000,52.360613,-4.102585,174,125,, +FM,BBC Radio 3 Swindon Blunsdon,BBC Radio 3,91200000,51.609363,-1.794027,148,0.2,, +FM,BBC Radio 3 Milton Keynes Bow Brickhill,BBC Radio 3,90800000,51.998806,-0.669784,169,5,, +FM,BBC Radio 3 Islay Bowmore,BBC Radio 3,90300000,55.750128,-6.274228,46,0.061,, +FM,BBC Radio 3 Shetland Bressay,BBC Radio 3,90500000,60.130079,-1.096281,224,25,, +FM,BBC Radio 3 Dorchester and Weymouth Bridport,BBC Radio 3,91300000,50.720805,-2.775678,64,0.2,, +FM,BBC Radio 3 Omagh and Enniskillen Brougher Mountain,BBC Radio 3,91600000,54.423191,-7.462812,305,5,, +FM,BBC Radio 3 North Derbyshire Buxton,BBC Radio 3,92200000,53.275085,-1.911935,432,0.05,, +FM,BBC Radio 3 Calne CALNE,BBC Radio 3,90400000,51.428815,-2.005417,104,0.032,, +FM,BBC Radio 3 Kirkcudbright CAMBRET HILL,BBC Radio 3,90900000,54.893698,-4.302038,343,0.065,, +FM,BBC Radio 3 South Newry Camlough,BBC Radio 3,90900000,54.160861,-6.385716,340,2,, +FM,BBC Radio 3 Kintyre Islay and Jura CAMPBELTOWN,BBC Radio 3,90600000,55.413714,-5.624241,83,0.2,, +FM,BBC Radio 3 Carmarthenshire Carmel,BBC Radio 3,90600000,51.818501,-4.066216,255,1.6,, +FM,BBC Radio 3 Newtownabbey Carnmoney Hill,BBC Radio 3,91000000,54.676355,-5.928737,207,0.02,, +FM,BBC Radio 3 Caterham Valley CATERHAM,BBC Radio 3,91900000,51.284605,-0.075623,173,0.015,, +FM,BBC Radio 3 Gloucestershire CHALFORD,BBC Radio 3,91400000,51.714525,-2.170893,174,0.1,, +FM,BBC Radio 3 Somerset Chard,BBC Radio 3,91100000,50.874213,-2.913244,173,0.05,, +FM,BBC Radio 3 Northumberland CHATTON,BBC Radio 3,92300000,55.531826,-1.834261,192,2.8,, +FM,BBC Radio 3 South Yorkshire Chesterfield,BBC Radio 3,91200000,53.283991,-1.427446,179,0.4,, +FM,BBC Radio 3 Chippenham,BBC Radio 3,91000000,51.474065,-2.12579,103,0.009,, +FM,BBC Radio 3 Gloucestershire Churchdown Hill,BBC Radio 3,91200000,51.868264,-2.175397,147,0.036,, +FM,BBC Radio 3 Cirencester,BBC Radio 3,90300000,51.717612,-1.968107,112,0.01,, +FM,BBC Radio 3 North Uist Clettraval,BBC Radio 3,90300000,57.617267,-7.44252,123,2,, +FM,BBC Radio 3 Ilfracombe Combe Martin,BBC Radio 3,91300000,51.197266,-4.031278,144,0.004,, +FM,BBC Radio 3 Calder Valley CORNHOLME,BBC Radio 3,91900000,53.734447,-2.125781,312,0.02,, +FM,BBC Radio 3 Folkestone Creteway Down,BBC Radio 3,90600000,51.099738,1.182579,166,0.1,, +FM,BBC Radio 3 Crieff CRIEFF,BBC Radio 3,91500000,56.357222,-3.920232,219,0.125,, +FM,BBC Radio 3 London Crystal Palace,BBC Radio 3,91000000,51.424161,-0.074944,110,4,, +FM,BBC Radio 3 South Uist Daliburgh,BBC Radio 3,91500000,57.169643,-7.402558,6,1,, +FM,BBC Radio 3 Dartford Tunnel,BBC Radio 3,91300000,51.469253,0.260425,3,0,, +FM,BBC Radio 3 Ayrshire DARVEL,BBC Radio 3,91700000,55.579376,-4.289814,286,5,, +FM,BBC Radio 3 Darwen DARWEN,BBC Radio 3,91700000,53.696571,-2.443266,261,0.01,, +FM,BBC Radio 3 Northern Ireland Divis A,BBC Radio 3,92300000,54.607538,-6.009471,368,61.0773,, +FM,BBC Radio 3 Isle of Man DOUGLAS,BBC Radio 3,90600000,54.140551,-4.493075,143,5.7,, +FM,BBC Radio 3 Aberdeen Durris,BBC Radio 3,91600000,56.999987,-2.389944,325,1.05,, +FM,BBC Radio 3 South Wales EBBW VALE,BBC Radio 3,90600000,51.771714,-3.218938,442,0.015,, +FM,BBC Radio 3 Frome Egford Hill,BBC Radio 3,91300000,51.231585,-2.3391,132,0.06,, +FM,BBC Radio 3 Isle of Lewis Eitshal,BBC Radio 3,92000000,58.179148,-6.584974,206,2,, +FM,BBC Radio 3 Exeter Exeter (St Thomas),BBC Radio 3,91200000,50.719122,-3.562148,102,0.0275,, +FM,BBC Radio 3 Borders EYEMOUTH,BBC Radio 3,91900000,55.83228,-2.085863,197,0.1,, +FM,BBC Radio 3 Newcastle Fenham,BBC Radio 3,92000000,54.978039,-1.662966,120,0.042,, +FM,BBC Radio 3 Angus Forfar,BBC Radio 3,90500000,56.55718,-2.843663,206,10,, +FM,BBC Radio 3 Great Glen Fort Augustus,BBC Radio 3,90800000,57.105987,-4.70869,307,1,, +FM,BBC Radio 3 Fort William,BBC Radio 3,91500000,56.848302,-5.088621,16,2,, +FM,BBC Radio 3 Ayr GIRVAN,BBC Radio 3,91100000,55.244736,-4.814596,195,0.1,, +FM,BBC Radio 3 Isle of Mull Glengorm,BBC Radio 3,91700000,56.632166,-6.133249,253,2.5,, +FM,BBC Radio 3 Tiverton Gogwell,BBC Radio 3,91800000,50.889583,-3.469029,233,0.3,, +FM,BBC Radio 3 Grantham,BBC Radio 3,90300000,52.892406,-0.655971,115,0.05,, +FM,BBC Radio 3 Cairngorms Grantown,BBC Radio 3,92000000,57.320173,-3.656313,389,0.36,, +FM,BBC Radio 3 Guildford GUILDFORD,BBC Radio 3,90300000,51.228358,-0.605467,142,2,, +FM,BBC Radio 3 Rossendale HASLINGDEN,BBC Radio 3,92100000,53.708491,-2.311438,336,0.1,, +FM,BBC Radio 3 Hastings,BBC Radio 3,91800000,50.861622,0.566431,76,0.25,, +FM,BBC Radio 3 Pembrokeshire Haverfordwest,BBC Radio 3,91500000,51.899195,-4.86653,186,9.54993,, +FM,BBC Radio 3 Hebden Bridge HEBDEN BRIDGE,BBC Radio 3,90600000,53.737298,-2.018458,220,0.025,, +FM,BBC Radio 3 Caversham Hemdean,BBC Radio 3,92000000,51.481196,-0.978531,69,1,, +FM,BBC Radio 3 High Wycombe,BBC Radio 3,92200000,51.640531,-0.763678,156,0.025,, +FM,BBC Radio 3 Teignmouth Holcombe Down,BBC Radio 3,91600000,50.566699,-3.506408,205,0.1,, +FM,BBC Radio 3 Yorkshire and Lancashire Holme Moss,BBC Radio 3,91500000,53.533184,-1.858296,524,125,, +FM,BBC Radio 3 Weston-super-Mare Hutton,BBC Radio 3,91600000,51.325227,-2.918301,73,0.04,, +FM,BBC Radio 3 Bradford Idle,BBC Radio 3,90700000,53.832945,-1.752142,215,0.01,, +FM,BBC Radio 3 Bristol Ilchester Crescent,BBC Radio 3,91500000,51.427947,-2.609813,48,0.649999,, +FM,BBC Radio 3 Borders INNERLEITHEN,BBC Radio 3,91700000,55.620243,-3.07272,244,0.011,, +FM,BBC Radio 3 Isles of Scilly,BBC Radio 3,91000000,49.932504,-6.305394,32,0.03,, +FM,BBC Radio 3 Devon Ivybridge,BBC Radio 3,92100000,50.368664,-3.926486,142,0.05,, +FM,BBC Radio 3 Orkney Keelylang Hill B,BBC Radio 3,91500000,58.975283,-3.083943,220,16,, +FM,BBC Radio 3 West Yorkshire KEIGHLEY,BBC Radio 3,91100000,53.895532,-1.896639,305,1,, +FM,BBC Radio 3 South Lakes Kendal,BBC Radio 3,91200000,54.314559,-2.708127,172,0.05,, +FM,BBC Radio 3 Kenley KENLEY,BBC Radio 3,90600000,51.317089,-0.09335,152,0.025,, +FM,BBC Radio 3 North Lakes Keswick Forest,BBC Radio 3,91800000,54.608739,-3.204646,215,0,, +FM,BBC Radio 3 Kilkeel,BBC Radio 3,91600000,54.095375,-6.041723,267,0.1,, +FM,BBC Radio 3 Swansea Kilvey Hill,BBC Radio 3,91700000,51.62891,-3.919745,193,0.5,, +FM,BBC Radio 3 Dartmouth Kingswear,BBC Radio 3,92000000,50.349538,-3.565025,107,0.005,, +FM,BBC Radio 3 Cairngorms KINGUSSIE,BBC Radio 3,91300000,57.061393,-4.030507,351,0.05,, +FM,BBC Radio 3 Kinlochleven,BBC Radio 3,91900000,56.722796,-4.978914,273,0.005,, +FM,BBC Radio 3 Borders Kirkconnel,BBC Radio 3,90900000,55.412875,-3.983785,480,0.02,, +FM,BBC Radio 3 Perth Kirkton Mailer,BBC Radio 3,91200000,56.372293,-3.453361,178,0.5,, +FM,BBC Radio 3 Moray Knockmore,BBC Radio 3,90400000,57.532303,-3.135126,353,1,, +FM,BBC Radio 3 Larne,BBC Radio 3,91300000,54.862024,-5.828421,119,0.05,, +FM,BBC Radio 3 Jersey Les Platons,BBC Radio 3,91100000,49.246023,-2.1012,134,8,, +FM,BBC Radio 3 East Ayrshire LETHANHILL,BBC Radio 3,90500000,55.36421,-4.464995,300,0.11,, +FM,BBC Radio 3 Limavady,BBC Radio 3,91800000,55.109172,-6.886121,341,1.7,, +FM,BBC Radio 3 Anglesey LLANDDONA,BBC Radio 3,92000000,53.307002,-4.12825,146,10.5,, +FM,BBC Radio 3 Londonderry,BBC Radio 3,90900000,55.00421,-7.368968,175,15.6,, +FM,BBC Radio 3 Calderdale LUDDENDEN,BBC Radio 3,90900000,53.720111,-1.92874,256,0.084,, +FM,BBC Radio 3 Ludlow LUDLOW,BBC Radio 3,91800000,52.362722,-2.738327,216,0.005,, +FM,BBC Radio 3 Lyme Regis,BBC Radio 3,91900000,50.737572,-2.929856,162,0.05,, +FM,BBC Radio 3 Cambridgeshire Madingley,BBC Radio 3,91100000,52.215394,0.036667,62,0.13,, +FM,BBC Radio 3 Mallaig,BBC Radio 3,90300000,57.001499,-5.828475,71,0.02,, +FM,BBC Radio 3 Colchester and Ipswich Manningtree,BBC Radio 3,90300000,51.92387,1.086097,35,2.5,, +FM,BBC Radio 3 Marlborough MARLBOROUGH,BBC Radio 3,92300000,51.417912,-1.699977,192,0.1,, +FM,BBC Radio 3 Aberdeenshire Meldrum,BBC Radio 3,90900000,57.386674,-2.400197,245,75,, +FM,BBC Radio 3 Wester Ross Melvaig,BBC Radio 3,91300000,57.842382,-5.782182,271,17.5903,, +FM,BBC Radio 3 Membury,BBC Radio 3,91100000,51.484903,-1.558836,211,0.125,, +FM,BBC Radio 3 Mickleham MICKLEHAM,BBC Radio 3,91900000,51.272007,-0.333055,103,0.025,, +FM,BBC Radio 3 Dumbarton Millburn Muir,BBC Radio 3,90500000,55.982322,-4.600708,152,0.025,, +FM,BBC Radio 3 West Somerset Minehead,BBC Radio 3,91200000,51.19744,-3.48052,79,0.01,, +FM,BBC Radio 3 Morecambe Bay,BBC Radio 3,92200000,54.202505,-3.167346,259,5,, +FM,BBC Radio 3 Gloucestershire NAILSWORTH,BBC Radio 3,90400000,51.690257,-2.219843,114,0.1,, +FM,BBC Radio 3 Isle of Lewis Ness of Lewis,BBC Radio 3,90500000,58.461531,-6.230808,100,0.162,, +FM,BBC Radio 3 Newbury,BBC Radio 3,90400000,51.414621,-1.290472,130,0.1,, +FM,BBC Radio 3 Newhaven NEWHAVEN,BBC Radio 3,91900000,50.787726,0.035848,81,0.05,, +FM,BBC Radio 3 Newcastle NEWTON,BBC Radio 3,91600000,54.982447,-1.945593,165,0.1,, +FM,BBC Radio 3 Devon North Hessary Tor,BBC Radio 3,90300000,50.550217,-4.008413,508,80,, +FM,BBC Radio 3 Northamptonshire Northampton,BBC Radio 3,91100000,52.275355,-0.885481,127,0.0615,, +FM,BBC Radio 3 Oban and Mull Oban,BBC Radio 3,91100000,56.40396,-5.485882,122,2.5,, +FM,BBC Radio 3 Okehampton,BBC Radio 3,90900000,50.753618,-4.005483,208,0.025,, +FM,BBC Radio 3 Scarborough OLIVERS MOUNT,BBC Radio 3,92100000,54.267351,-0.404494,151,0.125,, +FM,BBC Radio 3 Oxfordshire Oxford,BBC Radio 3,91700000,51.790638,-1.179183,130,17.5903,, +FM,BBC Radio 3 Borders PEEBLES,BBC Radio 3,90600000,55.661832,-3.227732,352,0.02,, +FM,BBC Radio 3 Bodmin Penaligon Downs,BBC Radio 3,91000000,50.481389,-4.782638,116,0.2,, +FM,BBC Radio 3 Burnley Pendle Forest,BBC Radio 3,92600000,53.841893,-2.266074,276,0.5,, +FM,BBC Radio 3 Penicuik PENICUIK,BBC Radio 3,90800000,55.819142,-3.194377,248,0.132,, +FM,BBC Radio 3 Portree Penifiler,BBC Radio 3,90300000,57.395531,-6.180316,52,0.005,, +FM,BBC Radio 3 Huntingdonshire Peterborough,BBC Radio 3,92300000,52.507615,-0.343282,56,18.7,, +FM,BBC Radio 3 Pitlochry PITLOCHRY,BBC Radio 3,91400000,56.687307,-3.759852,401,0,, +FM,BBC Radio 3 Plymouth Plympton,BBC Radio 3,90700000,50.381346,-4.067802,113,0.04,, +FM,BBC Radio 3 Tyneside PONTOP PIKE,BBC Radio 3,90700000,54.868838,-1.771233,302,67.5,, +FM,BBC Radio 3 Islay PORT ELLEN,BBC Radio 3,91200000,55.627446,-6.227617,93,0.065,, +FM,BBC Radio 3 Pontypridd Porth,BBC Radio 3,90600000,51.617787,-3.403306,247,0.01,, +FM,BBC Radio 3 West Cornwall Redruth,BBC Radio 3,91900000,50.209786,-5.238489,237,8.80001,, +FM,BBC Radio 3 Herefordshire RIDGE HILL,BBC Radio 3,90800000,51.997433,-2.53989,204,5,, +FM,BBC Radio 3 Highland Rosemarkie,BBC Radio 3,91800000,57.633195,-4.075231,206,9.77237,, +FM,BBC Radio 3 Coupar Rosemount,BBC Radio 3,91800000,56.576982,-3.298257,81,0.032,, +FM,BBC Radio 3 Helensburgh Rosneath,BBC Radio 3,91400000,55.991937,-4.793795,107,0.025,, +FM,BBC Radio 3 South Newry Rostrevor Forest,BBC Radio 3,90500000,54.092725,-6.182015,215,0.02,, +FM,BBC Radio 3 Bute ROTHESAY,BBC Radio 3,90700000,55.878164,-4.998928,166,0.285,, +FM,BBC Radio 3 Hampshire and Isle of Wight ROWRIDGE,BBC Radio 3,90700000,50.676724,-1.370435,143,125,, +FM,BBC Radio 3 Caithness Rumster Forest,BBC Radio 3,92300000,58.328006,-3.371413,221,5,, +FM,BBC Radio 3 Greater Manchester SADDLEWORTH,BBC Radio 3,91900000,53.541535,-2.020635,347,0.1,, +FM,BBC Radio 3 Salcombe,BBC Radio 3,91700000,50.244921,-3.749115,61,0.198582,, +FM,BBC Radio 3 Salisbury SALISBURY,BBC Radio 3,92000000,51.055855,-1.806762,107,0.012619,, +FM,BBC Radio 3 Cumbria & South West Scotland Sandale,BBC Radio 3,90300000,54.749109,-3.14076,363,125,, +FM,BBC Radio 3 South Yorkshire Sheffield,BBC Radio 3,92100000,53.379385,-1.513809,247,0.16,, +FM,BBC Radio 3 Skye & Lochalsh Skriaig,BBC Radio 3,90700000,57.38642,-6.24289,390,9.68481,, +FM,BBC Radio 3 Kintyre Islay and Jura South Knapdale,BBC Radio 3,91500000,55.91751,-5.462223,481,1.1,, +FM,BBC Radio 3 Derbyshire Stanton Moor,BBC Radio 3,92000000,53.170195,-1.633756,305,0.6,, +FM,BBC Radio 3 Inveraray Strachur,BBC Radio 3,91000000,56.179137,-5.071531,175,0.01,, +FM,BBC Radio 3 Galloway STRANRAER,BBC Radio 3,91900000,54.928074,-4.949004,190,0.031,, +FM,BBC Radio 3 West Midlands SUTTON COLDFIELD,BBC Radio 3,90500000,52.600539,-1.833856,169,62.5,, +FM,BBC Radio 3 Dover Swingate,BBC Radio 3,92400000,51.137832,1.335758,122,5.5,, +FM,BBC Radio 3 Norfolk TACOLNESTON,BBC Radio 3,91900000,52.518759,1.138705,64,119.374,, +FM,BBC Radio 3 Calder Valley TODMORDEN,BBC Radio 3,91100000,53.713283,-2.065416,221,0.1,, +FM,BBC Radio 3 Rhondda Valley Ton Pentre,BBC Radio 3,91000000,51.64954,-3.504023,323,0.003,, +FM,BBC Radio 3 Tullich TULLICH,BBC Radio 3,92300000,57.07283,-3.027203,463,0.025,, +FM,BBC Radio 3 Ullapool,BBC Radio 3,90500000,57.892557,-5.135709,143,0.05,, +FM,BBC Radio 3 Neath Valley Varteg Hill,BBC Radio 3,91100000,51.754672,-3.773522,351,0.024,, +FM,BBC Radio 3 Ventnor,BBC Radio 3,91700000,50.601933,-1.199325,227,0.025,, +FM,BBC Radio 3 Calder Valley WALSDEN SOUTH,BBC Radio 3,90600000,53.690612,-2.096576,227,0.01,, +FM,BBC Radio 3 County Durham WEARDALE,BBC Radio 3,91900000,54.74117,-1.961914,404,0.09,, +FM,BBC Radio 3 Wensleydale,BBC Radio 3,90500000,54.312324,-2.013338,402,0.05,, +FM,BBC Radio 3 South Glamorgan Wenvoe,BBC Radio 3,92100000,51.459476,-3.282282,129,125,, +FM,BBC Radio 3 North Ayrshire WEST KILBRIDE,BBC Radio 3,91300000,55.695641,-4.84184,170,0.05,, +FM,BBC Radio 3 Trowbridge WESTWOOD,BBC Radio 3,90500000,51.336803,-2.264225,87,0.05,, +FM,BBC Radio 3 Weymouth,BBC Radio 3,92200000,50.59912,-2.476532,64,0.1,, +FM,BBC Radio 3 Whalley WHALLEY,BBC Radio 3,91800000,53.812989,-2.411689,172,0.01,, +FM,BBC Radio 3 West Yorkshire WHARFEDALE,BBC Radio 3,90600000,53.932231,-1.699923,212,0.02,, +FM,BBC Radio 3 Whitby WHITBY BUSINESS PARK,BBC Radio 3,91800000,54.473215,-0.595572,60,0.062,, +FM,BBC Radio 3 West Cumbria Whitehaven - VHF,BBC Radio 3,91900000,54.499453,-3.558449,125,0.13,, +FM,BBC Radio 3 Brighton WHITEHAWK HILL,BBC Radio 3,92300000,50.824954,-0.113611,120,0.25,, +FM,BBC Radio 3 Windermere WINDERMERE,BBC Radio 3,90500000,54.37436,-2.951306,216,0.032,, +FM,BBC Radio 3 Lancashire WINTER HILL,BBC Radio 3,90800000,53.625547,-2.514794,439,2,, +FM,BBC Radio 3 North Yorkshire Woolmoor,BBC Radio 3,92200000,54.287102,-1.299034,257,5,, +FM,BBC Radio 3 London and the South East Wrotham,BBC Radio 3,91300000,51.320696,0.287702,219,125,, +FM,BBC Radio 4 (FM) Aberdare,BBC Radio 4 (FM),104700000,51.702007,-3.398531,282,0.021,, +FM,BBC Radio 4 (FM) Abergavenny,BBC Radio 4 (FM),93100000,51.807795,-3.097781,433,0.02,, +FM,BBC Radio 4 (FM) Pontypool Abertillery,BBC Radio 4 (FM),93400000,51.714666,-3.12404,400,0.01,, +FM,BBC Radio 4 (FM) Argyll & Bute Ardgour,BBC Radio 4 (FM),95100000,56.712175,-5.290096,12,0.005,, +FM,BBC Radio 4 (FM) Scottish Borders Ashkirk,BBC Radio 4 (FM),103900000,55.510631,-2.840766,303,20.3208,, +FM,BBC Radio 4 (FM) Axe Valley,BBC Radio 4 (FM),93900000,50.74593,-3.016766,169,0.08,, +FM,BBC Radio 4 (FM) Argyll & Bute Ballachulish,BBC Radio 4 (FM),94700000,56.684359,-5.170394,14,0.015,, +FM,BBC Radio 4 (FM) North Antrim Ballycastle,BBC Radio 4 (FM),93200000,55.215995,-6.309339,150,0.05,, +FM,BBC Radio 4 (FM) Skipton Barnoldswick,BBC Radio 4 (FM),94100000,53.928368,-2.15728,191,0.02,, +FM,BBC Radio 4 (FM) North Devon Barnstaple,BBC Radio 4 (FM),92900000,51.040763,-4.13826,116,0.5,, +FM,BBC Radio 4 (FM) Basingstoke,BBC Radio 4 (FM),94500000,51.226848,-1.080409,176,0.02,, +FM,BBC Radio 4 (FM) Bath,BBC Radio 4 (FM),93400000,51.38779,-2.332925,175,0.041,, +FM,BBC Radio 4 (FM) Torbay Beacon Hill North,BBC Radio 4 (FM),93100000,50.447129,-3.610498,196,0.794328,, +FM,BBC Radio 4 (FM) Leeds Beecroft Hill,BBC Radio 4 (FM),94200000,53.811543,-1.641253,149,0.2,, +FM,BBC Radio 4 (FM) Lincolnshire Belmont,BBC Radio 4 (FM),93100000,53.335814,-0.171983,125,8.3,, +FM,BBC Radio 4 (FM) Stirling Ben Gullipen,BBC Radio 4 (FM),104900000,56.212308,-4.263559,415,1,, +FM,BBC Radio 4 (FM) Borders Berwick-upon-Tweed,BBC Radio 4 (FM),93000000,55.785854,-2.032822,129,0.05,, +FM,BBC Radio 4 (FM) Snowdonia Betws Y Coed,BBC Radio 4 (FM),104900000,53.108346,-3.75717,303,0.025,, +FM,BBC Radio 4 (FM) Bexhill,BBC Radio 4 (FM),94600000,50.84568,0.475913,45,0.0117,, +FM,BBC Radio 4 (FM) North Yorkshire Bilsdale - Station Tower,BBC Radio 4 (FM),93400000,54.358848,-1.149317,381,2,, +FM,BBC Radio 4 (FM) Central Scotland Black Hill,BBC Radio 4 (FM),95800000,55.860005,-3.873572,275,97.7237,, +FM,BBC Radio 4 (FM) Blaenavon,BBC Radio 4 (FM),92900000,51.750968,-3.048742,373,0.01,, +FM,BBC Radio 4 (FM) Ceredigion Blaenplwyf,BBC Radio 4 (FM),104000000,52.360613,-4.102585,174,125,, +FM,BBC Radio 4 (FM) Swindon Blunsdon,BBC Radio 4 (FM),93400000,51.609363,-1.794027,148,0.2,, +FM,BBC Radio 4 (FM) Milton Keynes Bow Brickhill,BBC Radio 4 (FM),93000000,51.998806,-0.669784,169,5,, +FM,BBC Radio 4 (FM) Islay Bowmore,BBC Radio 4 (FM),95700000,55.750128,-6.274228,46,0.061,, +FM,BBC Radio 4 (FM) Mid Wales Brecon VHF,BBC Radio 4 (FM),104700000,51.959448,-3.412497,248,0.01,, +FM,BBC Radio 4 (FM) Shetland Bressay,BBC Radio 4 (FM),94900000,60.130079,-1.096281,224,25,, +FM,BBC Radio 4 (FM) Dorchester and Weymouth Bridport,BBC Radio 4 (FM),93500000,50.720805,-2.775678,64,0.2,, +FM,BBC Radio 4 (FM) Omagh and Enniskillen Brougher Mountain,BBC Radio 4 (FM),95600000,54.423191,-7.462812,305,5,, +FM,BBC Radio 4 (FM) North Derbyshire Buxton,BBC Radio 4 (FM),94400000,53.275085,-1.911935,432,0.05,, +FM,BBC Radio 4 (FM) Calne CALNE,BBC Radio 4 (FM),92600000,51.428815,-2.005417,104,0.032,, +FM,BBC Radio 4 (FM) Kirkcudbright CAMBRET HILL,BBC Radio 4 (FM),95300000,54.893698,-4.302038,343,0.065,, +FM,BBC Radio 4 (FM) South Newry Camlough,BBC Radio 4 (FM),104600000,54.160861,-6.385716,340,2,, +FM,BBC Radio 4 (FM) Kintyre Islay and Jura CAMPBELTOWN,BBC Radio 4 (FM),95200000,55.413714,-5.624241,83,0.2,, +FM,BBC Radio 4 (FM) Carmarthen,BBC Radio 4 (FM),95500000,51.867126,-4.309135,137,0.01,, +FM,BBC Radio 4 (FM) Carmarthenshire Carmel,BBC Radio 4 (FM),92800000,51.818501,-4.066216,255,1.6,, +FM,BBC Radio 4 (FM) Newtownabbey Carnmoney Hill,BBC Radio 4 (FM),93200000,54.676355,-5.928737,207,0.02,, +FM,BBC Radio 4 (FM) Caterham Valley CATERHAM,BBC Radio 4 (FM),94100000,51.284605,-0.075623,173,0.015,, +FM,BBC Radio 4 (FM) Gloucestershire CHALFORD,BBC Radio 4 (FM),93600000,51.714525,-2.170893,174,0.1,, +FM,BBC Radio 4 (FM) Somerset Chard,BBC Radio 4 (FM),93300000,50.874213,-2.913244,173,0.05,, +FM,BBC Radio 4 (FM) Northumberland CHATTON,BBC Radio 4 (FM),94500000,55.531826,-1.834261,192,2.8,, +FM,BBC Radio 4 (FM) South Yorkshire Chesterfield,BBC Radio 4 (FM),93400000,53.283991,-1.427446,179,0.4,, +FM,BBC Radio 4 (FM) Chippenham,BBC Radio 4 (FM),93200000,51.474065,-2.12579,103,0.009,, +FM,BBC Radio 4 (FM) Gloucestershire Churchdown Hill,BBC Radio 4 (FM),93400000,51.868264,-2.175397,147,0.036,, +FM,BBC Radio 4 (FM) Cirencester,BBC Radio 4 (FM),92500000,51.717612,-1.968107,112,0.01,, +FM,BBC Radio 4 (FM) North Uist Clettraval,BBC Radio 4 (FM),95100000,57.617267,-7.44252,123,2,, +FM,BBC Radio 4 (FM) Wye Valley CLYRO,BBC Radio 4 (FM),104900000,52.08185,-3.162868,215,0.01,, +FM,BBC Radio 4 (FM) Ilfracombe Combe Martin,BBC Radio 4 (FM),93500000,51.197266,-4.031278,144,0.004,, +FM,BBC Radio 4 (FM) Conway CONWY,BBC Radio 4 (FM),104400000,53.271606,-3.828796,114,0.05,, +FM,BBC Radio 4 (FM) Calder Valley CORNHOLME,BBC Radio 4 (FM),94100000,53.734447,-2.125781,312,0.02,, +FM,BBC Radio 4 (FM) Folkestone Creteway Down,BBC Radio 4 (FM),93100000,51.099738,1.182579,166,0.1,, +FM,BBC Radio 4 (FM) Crieff CRIEFF,BBC Radio 4 (FM),95300000,56.357222,-3.920232,219,0.125,, +FM,BBC Radio 4 (FM) Croeserw,BBC Radio 4 (FM),103600000,51.644491,-3.652146,343,0.01,, +FM,BBC Radio 4 (FM) London Crystal Palace,BBC Radio 4 (FM),93200000,51.424161,-0.074944,110,4,, +FM,BBC Radio 4 (FM) Cwmafan,BBC Radio 4 (FM),92900000,51.628417,-3.737817,210,0.01,, +FM,BBC Radio 4 (FM) South Uist Daliburgh,BBC Radio 4 (FM),95900000,57.169643,-7.402558,6,1,, +FM,BBC Radio 4 (FM) Dartford Tunnel,BBC Radio 4 (FM),93500000,51.469253,0.260425,3,0,, +FM,BBC Radio 4 (FM) Ayrshire DARVEL,BBC Radio 4 (FM),104300000,55.579376,-4.289814,286,5,, +FM,BBC Radio 4 (FM) Darwen DARWEN,BBC Radio 4 (FM),93900000,53.696571,-2.443266,261,0.01,, +FM,BBC Radio 4 (FM) Gwynedd DEINIOLEN,BBC Radio 4 (FM),104100000,53.137063,-4.129256,322,0.05,, +FM,BBC Radio 4 (FM) Northern Ireland Divis A,BBC Radio 4 (FM),96000000,54.607538,-6.009471,368,61.0773,, +FM,BBC Radio 4 (FM) Snowdonia DOLGELLAU,BBC Radio 4 (FM),103600000,52.749344,-3.886853,103,0.025,, +FM,BBC Radio 4 (FM) Isle of Man DOUGLAS,BBC Radio 4 (FM),92800000,54.140551,-4.493075,143,5.7,, +FM,BBC Radio 4 (FM) Aberdeen Durris,BBC Radio 4 (FM),95900000,56.999987,-2.389944,325,1.05,, +FM,BBC Radio 4 (FM) South Wales EBBW VALE,BBC Radio 4 (FM),92800000,51.771714,-3.218938,442,0.015,, +FM,BBC Radio 4 (FM) Frome Egford Hill,BBC Radio 4 (FM),93500000,51.231585,-2.3391,132,0.06,, +FM,BBC Radio 4 (FM) Isle of Lewis Eitshal,BBC Radio 4 (FM),95100000,58.179148,-6.584974,206,2,, +FM,BBC Radio 4 (FM) Exeter Exeter (St Thomas),BBC Radio 4 (FM),93400000,50.719122,-3.562148,102,0.0275,, +FM,BBC Radio 4 (FM) Borders EYEMOUTH,BBC Radio 4 (FM),104600000,55.83228,-2.085863,197,0.1,, +FM,BBC Radio 4 (FM) Newcastle Fenham,BBC Radio 4 (FM),94200000,54.978039,-1.662966,120,0.042,, +FM,BBC Radio 4 (FM) South Wales Ferndale,BBC Radio 4 (FM),94000000,51.662873,-3.437376,386,0.01,, +FM,BBC Radio 4 (FM) Snowdonia FFESTINIOG,BBC Radio 4 (FM),103500000,52.934172,-3.921818,308,0.082081,, +FM,BBC Radio 4 (FM) Angus Forfar,BBC Radio 4 (FM),94900000,56.55718,-2.843663,206,10,, +FM,BBC Radio 4 (FM) Great Glen Fort Augustus,BBC Radio 4 (FM),95200000,57.105987,-4.70869,307,1,, +FM,BBC Radio 4 (FM) Fort William,BBC Radio 4 (FM),95900000,56.848302,-5.088621,16,2,, +FM,BBC Radio 4 (FM) Ayr GIRVAN,BBC Radio 4 (FM),95300000,55.244736,-4.814596,195,0.1,, +FM,BBC Radio 4 (FM) Isle of Mull Glengorm,BBC Radio 4 (FM),96100000,56.632166,-6.133249,253,2.5,, +FM,BBC Radio 4 (FM) Tiverton Gogwell,BBC Radio 4 (FM),94000000,50.889583,-3.469029,233,0.3,, +FM,BBC Radio 4 (FM) Grantham,BBC Radio 4 (FM),92500000,52.892406,-0.655971,115,0.05,, +FM,BBC Radio 4 (FM) Cairngorms Grantown,BBC Radio 4 (FM),96100000,57.320173,-3.656313,389,0.36,, +FM,BBC Radio 4 (FM) Guildford GUILDFORD,BBC Radio 4 (FM),92500000,51.228358,-0.605467,142,2,, +FM,BBC Radio 4 (FM) Rossendale HASLINGDEN,BBC Radio 4 (FM),94300000,53.708491,-2.311438,336,0.1,, +FM,BBC Radio 4 (FM) Hastings,BBC Radio 4 (FM),94200000,50.861622,0.566431,76,0.25,, +FM,BBC Radio 4 (FM) Pembrokeshire Haverfordwest,BBC Radio 4 (FM),104900000,51.899195,-4.86653,186,9.54993,, +FM,BBC Radio 4 (FM) Hebden Bridge HEBDEN BRIDGE,BBC Radio 4 (FM),92800000,53.737298,-2.018458,220,0.025,, +FM,BBC Radio 4 (FM) Caversham Hemdean,BBC Radio 4 (FM),94200000,51.481196,-0.978531,69,1,, +FM,BBC Radio 4 (FM) High Wycombe,BBC Radio 4 (FM),94400000,51.640531,-0.763678,156,0.025,, +FM,BBC Radio 4 (FM) Teignmouth Holcombe Down,BBC Radio 4 (FM),93800000,50.566699,-3.506408,205,0.1,, +FM,BBC Radio 4 (FM) Yorkshire and Lancashire Holme Moss,BBC Radio 4 (FM),93700000,53.533184,-1.858296,524,125,, +FM,BBC Radio 4 (FM) Weston-super-Mare Hutton,BBC Radio 4 (FM),93800000,51.325227,-2.918301,73,0.04,, +FM,BBC Radio 4 (FM) Bradford Idle,BBC Radio 4 (FM),92900000,53.832945,-1.752142,215,0.01,, +FM,BBC Radio 4 (FM) Bristol Ilchester Crescent,BBC Radio 4 (FM),93700000,51.427947,-2.609813,48,0.649999,, +FM,BBC Radio 4 (FM) Borders INNERLEITHEN,BBC Radio 4 (FM),96100000,55.620243,-3.07272,244,0.011,, +FM,BBC Radio 4 (FM) Isles of Scilly,BBC Radio 4 (FM),93200000,49.932504,-6.305394,32,0.03,, +FM,BBC Radio 4 (FM) Devon Ivybridge,BBC Radio 4 (FM),94300000,50.368664,-3.926486,142,0.05,, +FM,BBC Radio 4 (FM) Orkney Keelylang Hill B,BBC Radio 4 (FM),96000000,58.975283,-3.083943,220,16,, +FM,BBC Radio 4 (FM) West Yorkshire KEIGHLEY,BBC Radio 4 (FM),93300000,53.895532,-1.896639,305,1,, +FM,BBC Radio 4 (FM) South Lakes Kendal,BBC Radio 4 (FM),93400000,54.314559,-2.708127,172,0.05,, +FM,BBC Radio 4 (FM) Kenley KENLEY,BBC Radio 4 (FM),92800000,51.317089,-0.09335,152,0.025,, +FM,BBC Radio 4 (FM) North Lakes Keswick Forest,BBC Radio 4 (FM),94000000,54.608739,-3.204646,215,0,, +FM,BBC Radio 4 (FM) Kilkeel,BBC Radio 4 (FM),103900000,54.095375,-6.041723,267,0.1,, +FM,BBC Radio 4 (FM) Swansea Kilvey Hill,BBC Radio 4 (FM),94600000,51.62891,-3.919745,193,0.5,, +FM,BBC Radio 4 (FM) Dartmouth Kingswear,BBC Radio 4 (FM),94200000,50.349538,-3.565025,107,0.005,, +FM,BBC Radio 4 (FM) Cairngorms KINGUSSIE,BBC Radio 4 (FM),95700000,57.061393,-4.030507,351,0.05,, +FM,BBC Radio 4 (FM) Kinlochleven,BBC Radio 4 (FM),95600000,56.722796,-4.978914,273,0.005,, +FM,BBC Radio 4 (FM) Borders Kirkconnel,BBC Radio 4 (FM),95300000,55.412875,-3.983785,480,0.02,, +FM,BBC Radio 4 (FM) Perth Kirkton Mailer,BBC Radio 4 (FM),94600000,56.372293,-3.453361,178,0.5,, +FM,BBC Radio 4 (FM) Moray Knockmore,BBC Radio 4 (FM),94800000,57.532303,-3.135126,353,1,, +FM,BBC Radio 4 (FM) Larne,BBC Radio 4 (FM),103500000,54.862024,-5.828421,119,0.05,, +FM,BBC Radio 4 (FM) Jersey Les Platons,BBC Radio 4 (FM),94800000,49.246023,-2.1012,134,8,, +FM,BBC Radio 4 (FM) East Ayrshire LETHANHILL,BBC Radio 4 (FM),94900000,55.36421,-4.464995,300,0.11,, +FM,BBC Radio 4 (FM) Limavady,BBC Radio 4 (FM),94000000,55.109172,-6.886121,341,1.7,, +FM,BBC Radio 4 (FM) Anglesey LLANDDONA,BBC Radio 4 (FM),103600000,53.307002,-4.12825,146,10.5,, +FM,BBC Radio 4 (FM) Snowdonia LLANDECWYN,BBC Radio 4 (FM),104900000,52.914419,-4.017769,268,0.1,, +FM,BBC Radio 4 (FM) Mid Wales LLANDINAM,BBC Radio 4 (FM),94500000,52.479547,-3.399945,454,0.022,, +FM,BBC Radio 4 (FM) Llandrindod Wells,BBC Radio 4 (FM),103800000,52.261047,-3.439354,432,1.35,, +FM,BBC Radio 4 (FM) Ceredigion LLANDYFRIOG,BBC Radio 4 (FM),104400000,52.045314,-4.410045,116,0.05,, +FM,BBC Radio 4 (FM) Llanfyllin LLANFYLLIN,BBC Radio 4 (FM),93500000,52.753349,-3.259797,264,0.003972,, +FM,BBC Radio 4 (FM) Bridgend Llangeinor,BBC Radio 4 (FM),93000000,51.586117,-3.581462,306,0.01,, +FM,BBC Radio 4 (FM) Llangollen and Cheshire Llangollen VHF,BBC Radio 4 (FM),93300000,53.031048,-3.180791,556,7.80001,, +FM,BBC Radio 4 (FM) Mid Wales LLANIDLOES,BBC Radio 4 (FM),104800000,52.446933,-3.549585,244,0.025,, +FM,BBC Radio 4 (FM) Llanrhaeadr ym Mochnant LLANRHAEADR YM MOCHN,BBC Radio 4 (FM),94200000,52.825889,-3.226423,333,0.025,, +FM,BBC Radio 4 (FM) Snowdonia LLWYN ONN,BBC Radio 4 (FM),104800000,52.737496,-4.03773,277,0.25,, +FM,BBC Radio 4 (FM) Talgarth LLYSWEN,BBC Radio 4 (FM),104400000,52.016761,-3.258522,224,0.01,, +FM,BBC Radio 4 (FM) Londonderry,BBC Radio 4 (FM),94900000,55.00421,-7.368968,175,5,, +FM,BBC Radio 4 (FM) Welshpool LONG MOUNTAIN,BBC Radio 4 (FM),94000000,52.644306,-3.086542,400,0.024,, +FM,BBC Radio 4 (FM) Calderdale LUDDENDEN,BBC Radio 4 (FM),93100000,53.720111,-1.92874,256,0.084,, +FM,BBC Radio 4 (FM) Ludlow LUDLOW,BBC Radio 4 (FM),94000000,52.362722,-2.738327,216,0.005,, +FM,BBC Radio 4 (FM) Lyme Regis,BBC Radio 4 (FM),94100000,50.737572,-2.929856,162,0.05,, +FM,BBC Radio 4 (FM) Snowdonia MACHYNLLETH,BBC Radio 4 (FM),103600000,52.586449,-3.885603,100,0.06,, +FM,BBC Radio 4 (FM) Cambridgeshire Madingley,BBC Radio 4 (FM),93300000,52.215394,0.036667,62,0.13,, +FM,BBC Radio 4 (FM) Mallaig,BBC Radio 4 (FM),94700000,57.001499,-5.828475,71,0.02,, +FM,BBC Radio 4 (FM) Colchester and Ipswich Manningtree,BBC Radio 4 (FM),92500000,51.92387,1.086097,35,2.5,, +FM,BBC Radio 4 (FM) Marlborough MARLBOROUGH,BBC Radio 4 (FM),94500000,51.417912,-1.699977,192,0.1,, +FM,BBC Radio 4 (FM) Aberdeenshire Meldrum,BBC Radio 4 (FM),95300000,57.386674,-2.400197,245,75,, +FM,BBC Radio 4 (FM) Wester Ross Melvaig,BBC Radio 4 (FM),95700000,57.842382,-5.782182,271,17.5903,, +FM,BBC Radio 4 (FM) Membury,BBC Radio 4 (FM),93300000,51.484903,-1.558836,211,0.125,, +FM,BBC Radio 4 (FM) Mickleham MICKLEHAM,BBC Radio 4 (FM),94100000,51.272007,-0.333055,103,0.025,, +FM,BBC Radio 4 (FM) Dumbarton Millburn Muir,BBC Radio 4 (FM),104100000,55.982322,-4.600708,152,0.025,, +FM,BBC Radio 4 (FM) West Somerset Minehead,BBC Radio 4 (FM),93400000,51.19744,-3.48052,79,0.01,, +FM,BBC Radio 4 (FM) Morecambe Bay,BBC Radio 4 (FM),94400000,54.202505,-3.167346,259,5,, +FM,BBC Radio 4 (FM) Cardigan Bay Mynydd Pencarreg,BBC Radio 4 (FM),103700000,52.067402,-4.076523,411,0.2,, +FM,BBC Radio 4 (FM) Gloucestershire NAILSWORTH,BBC Radio 4 (FM),92600000,51.690257,-2.219843,114,0.1,, +FM,BBC Radio 4 (FM) Isle of Lewis Ness of Lewis,BBC Radio 4 (FM),96100000,58.461531,-6.230808,100,0.162,, +FM,BBC Radio 4 (FM) Newbury,BBC Radio 4 (FM),92600000,51.414621,-1.290472,130,0.1,, +FM,BBC Radio 4 (FM) Newhaven NEWHAVEN,BBC Radio 4 (FM),94100000,50.787726,0.035848,81,0.05,, +FM,BBC Radio 4 (FM) Newcastle NEWTON,BBC Radio 4 (FM),93800000,54.982447,-1.945593,165,0.1,, +FM,BBC Radio 4 (FM) Devon North Hessary Tor,BBC Radio 4 (FM),92500000,50.550217,-4.008413,508,80,, +FM,BBC Radio 4 (FM) Northamptonshire Northampton,BBC Radio 4 (FM),93300000,52.275355,-0.885481,127,0.0615,, +FM,BBC Radio 4 (FM) Oban and Mull Oban,BBC Radio 4 (FM),95300000,56.40396,-5.485882,122,2.5,, +FM,BBC Radio 4 (FM) Ogmore Vale,BBC Radio 4 (FM),93600000,51.593504,-3.546781,298,0.01,, +FM,BBC Radio 4 (FM) Okehampton,BBC Radio 4 (FM),93100000,50.753618,-4.005483,208,0.025,, +FM,BBC Radio 4 (FM) Scarborough OLIVERS MOUNT,BBC Radio 4 (FM),94300000,54.267351,-0.404494,151,0.125,, +FM,BBC Radio 4 (FM) Oxfordshire Oxford,BBC Radio 4 (FM),93900000,51.790638,-1.179183,130,17.5903,, +FM,BBC Radio 4 (FM) Borders PEEBLES,BBC Radio 4 (FM),95000000,55.661832,-3.227732,352,0.02,, +FM,BBC Radio 4 (FM) Bodmin Penaligon Downs,BBC Radio 4 (FM),93200000,50.481389,-4.782638,116,0.2,, +FM,BBC Radio 4 (FM) Burnley Pendle Forest,BBC Radio 4 (FM),94600000,53.841893,-2.266074,276,0.5,, +FM,BBC Radio 4 (FM) Penicuik PENICUIK,BBC Radio 4 (FM),95500000,55.819142,-3.194377,248,0.132,, +FM,BBC Radio 4 (FM) Portree Penifiler,BBC Radio 4 (FM),96100000,57.395531,-6.180316,52,0.005,, +FM,BBC Radio 4 (FM) Colwyn Bay Penmaen Rhos,BBC Radio 4 (FM),104600000,53.286941,-3.684982,125,0.1,, +FM,BBC Radio 4 (FM) Cwmbran Pennar,BBC Radio 4 (FM),93900000,51.656112,-3.144564,228,0.005,, +FM,BBC Radio 4 (FM) Huntingdonshire Peterborough,BBC Radio 4 (FM),94500000,52.507615,-0.343282,56,18.7,, +FM,BBC Radio 4 (FM) Pitlochry PITLOCHRY,BBC Radio 4 (FM),103900000,56.687307,-3.759852,401,0,, +FM,BBC Radio 4 (FM) Plymouth Plympton,BBC Radio 4 (FM),92900000,50.381346,-4.067802,113,0.04,, +FM,BBC Radio 4 (FM) Tyneside PONTOP PIKE,BBC Radio 4 (FM),92900000,54.868838,-1.771233,302,67.5,, +FM,BBC Radio 4 (FM) Pontypool,BBC Radio 4 (FM),104800000,51.685712,-3.036095,250,0.025,, +FM,BBC Radio 4 (FM) Islay PORT ELLEN,BBC Radio 4 (FM),94700000,55.627446,-6.227617,93,0.065,, +FM,BBC Radio 4 (FM) Pontypridd Porth,BBC Radio 4 (FM),92800000,51.617787,-3.403306,247,0.01,, +FM,BBC Radio 4 (FM) West Cornwall Redruth,BBC Radio 4 (FM),94100000,50.209786,-5.238489,237,8.80001,, +FM,BBC Radio 4 (FM) Neath Valley RHEOLA,BBC Radio 4 (FM),104800000,51.741661,-3.679878,409,0.01,, +FM,BBC Radio 4 (FM) Rhymney RHYMNEY,BBC Radio 4 (FM),93700000,51.730587,-3.264891,419,0.02,, +FM,BBC Radio 4 (FM) Herefordshire RIDGE HILL,BBC Radio 4 (FM),93000000,51.997433,-2.53989,204,5,, +FM,BBC Radio 4 (FM) Highland Rosemarkie,BBC Radio 4 (FM),103600000,57.633195,-4.075231,206,9.77237,, +FM,BBC Radio 4 (FM) Coupar Rosemount,BBC Radio 4 (FM),95500000,56.576982,-3.298257,81,0.032,, +FM,BBC Radio 4 (FM) Helensburgh Rosneath,BBC Radio 4 (FM),103800000,55.991937,-4.793795,107,0.025,, +FM,BBC Radio 4 (FM) South Newry Rostrevor Forest,BBC Radio 4 (FM),103900000,54.092725,-6.182015,215,0.02,, +FM,BBC Radio 4 (FM) Bute ROTHESAY,BBC Radio 4 (FM),95100000,55.878164,-4.998928,166,0.285,, +FM,BBC Radio 4 (FM) Hampshire and Isle of Wight ROWRIDGE,BBC Radio 4 (FM),92900000,50.676724,-1.370435,143,125,, +FM,BBC Radio 4 (FM) Caithness Rumster Forest,BBC Radio 4 (FM),95600000,58.328006,-3.371413,221,5,, +FM,BBC Radio 4 (FM) Greater Manchester SADDLEWORTH,BBC Radio 4 (FM),94100000,53.541535,-2.020635,347,0.1,, +FM,BBC Radio 4 (FM) Salcombe,BBC Radio 4 (FM),93900000,50.244921,-3.749115,61,0.198582,, +FM,BBC Radio 4 (FM) Salisbury SALISBURY,BBC Radio 4 (FM),94200000,51.055855,-1.806762,107,0.012619,, +FM,BBC Radio 4 (FM) Cumbria & South West Scotland Sandale,BBC Radio 4 (FM),92500000,54.749109,-3.14076,363,125,, +FM,BBC Radio 4 (FM) South Yorkshire Sheffield,BBC Radio 4 (FM),94300000,53.379385,-1.513809,247,0.16,, +FM,BBC Radio 4 (FM) Skye & Lochalsh Skriaig,BBC Radio 4 (FM),94800000,57.38642,-6.24289,390,9.68481,, +FM,BBC Radio 4 (FM) Kintyre Islay and Jura South Knapdale,BBC Radio 4 (FM),95600000,55.91751,-5.462223,481,1.1,, +FM,BBC Radio 4 (FM) Derbyshire Stanton Moor,BBC Radio 4 (FM),94200000,53.170195,-1.633756,305,0.6,, +FM,BBC Radio 4 (FM) Inveraray Strachur,BBC Radio 4 (FM),95100000,56.179137,-5.071531,175,0.01,, +FM,BBC Radio 4 (FM) Galloway STRANRAER,BBC Radio 4 (FM),103600000,54.928074,-4.949004,190,0.031,, +FM,BBC Radio 4 (FM) West Midlands SUTTON COLDFIELD,BBC Radio 4 (FM),92700000,52.600539,-1.833856,169,62.5,, +FM,BBC Radio 4 (FM) Dover Swingate,BBC Radio 4 (FM),94400000,51.137832,1.335758,122,5.5,, +FM,BBC Radio 4 (FM) Norfolk TACOLNESTON,BBC Radio 4 (FM),94100000,52.518759,1.138705,64,119.374,, +FM,BBC Radio 4 (FM) Calder Valley TODMORDEN,BBC Radio 4 (FM),93300000,53.713283,-2.065416,221,0.1,, +FM,BBC Radio 4 (FM) Rhondda Valley Ton Pentre,BBC Radio 4 (FM),104300000,51.64954,-3.504023,323,0.003,, +FM,BBC Radio 4 (FM) Tullich TULLICH,BBC Radio 4 (FM),104500000,57.07283,-3.027203,463,0.025,, +FM,BBC Radio 4 (FM) Ullapool,BBC Radio 4 (FM),96100000,57.892557,-5.135709,143,0.05,, +FM,BBC Radio 4 (FM) Neath Valley Varteg Hill,BBC Radio 4 (FM),103500000,51.754672,-3.773522,351,0.024,, +FM,BBC Radio 4 (FM) Ventnor,BBC Radio 4 (FM),93800000,50.601933,-1.199325,227,0.025,, +FM,BBC Radio 4 (FM) Calder Valley WALSDEN SOUTH,BBC Radio 4 (FM),92800000,53.690612,-2.096576,227,0.01,, +FM,BBC Radio 4 (FM) County Durham WEARDALE,BBC Radio 4 (FM),94100000,54.74117,-1.961914,404,0.09,, +FM,BBC Radio 4 (FM) Wensleydale,BBC Radio 4 (FM),92700000,54.312324,-2.013338,402,0.05,, +FM,BBC Radio 4 (FM) South Glamorgan Wenvoe,BBC Radio 4 (FM),94300000,51.459476,-3.282282,129,125,, +FM,BBC Radio 4 (FM) North Ayrshire WEST KILBRIDE,BBC Radio 4 (FM),103500000,55.695641,-4.84184,170,0.05,, +FM,BBC Radio 4 (FM) Trowbridge WESTWOOD,BBC Radio 4 (FM),92700000,51.336803,-2.264225,87,0.05,, +FM,BBC Radio 4 (FM) Weymouth,BBC Radio 4 (FM),94400000,50.59912,-2.476532,64,0.1,, +FM,BBC Radio 4 (FM) Whalley WHALLEY,BBC Radio 4 (FM),94000000,53.812989,-2.411689,172,0.01,, +FM,BBC Radio 4 (FM) West Yorkshire WHARFEDALE,BBC Radio 4 (FM),92800000,53.932231,-1.699923,212,0.02,, +FM,BBC Radio 4 (FM) Whitby WHITBY BUSINESS PARK,BBC Radio 4 (FM),94000000,54.473215,-0.595572,60,0.056,, +FM,BBC Radio 4 (FM) West Cumbria Whitehaven - VHF,BBC Radio 4 (FM),94100000,54.499453,-3.558449,125,0.13,, +FM,BBC Radio 4 (FM) Brighton WHITEHAWK HILL,BBC Radio 4 (FM),94500000,50.824954,-0.113611,120,0.25,, +FM,BBC Radio 4 (FM) Windermere WINDERMERE,BBC Radio 4 (FM),92700000,54.37436,-2.951306,216,0.032,, +FM,BBC Radio 4 (FM) Lancashire WINTER HILL,BBC Radio 4 (FM),93000000,53.625547,-2.514794,439,2,, +FM,BBC Radio 4 (FM) North Yorkshire Woolmoor,BBC Radio 4 (FM),94400000,54.287102,-1.299034,257,5,, +FM,BBC Radio 4 (FM) London and the South East Wrotham,BBC Radio 4 (FM),93500000,51.320696,0.287702,219,125,, +FM,BBC Radio Berkshire Berkshire HANNINGTON,BBC Radio Berkshire,104100000,51.307873,-1.244777,220,2,, +FM,BBC Radio Berkshire Berkshire HENLEY,BBC Radio Berkshire,94600000,51.533409,-0.877019,109,0.125,, +FM,BBC Radio Berkshire Reading Reading - Fountain House,BBC Radio Berkshire,104400000,51.454569,-0.977254,43,0.7978,, +FM,BBC Radio Berkshire Windsor Windsor Castle,BBC Radio Berkshire,95400000,51.483228,-0.607933,38,0.5,, +FM,BBC Radio Bristol (FM) Bath,BBC Radio Bristol (FM),104600000,51.38779,-2.332925,175,0.041,, +FM,BBC Radio Bristol (FM) Bristol East Dundry,BBC Radio Bristol (FM),94900000,51.396006,-2.613844,198,0.5,, +FM,BBC Radio Bristol (FM) Weston-super-Mare Hutton,BBC Radio Bristol (FM),103600000,51.325227,-2.918301,73,0.1,, +FM,BBC Radio Cambridgeshire (FM) Cambridgeshire Madingley,BBC Radio Cambridgeshire (FM),96000000,52.215394,0.036667,62,0.4,, +FM,BBC Radio Cambridgeshire (FM) Huntingdonshire Peterborough,BBC Radio Cambridgeshire (FM),95700000,52.507615,-0.343282,56,2.6,, +FM,BBC Radio Cornwall (FM) East Cornwall Caradon Hill,BBC Radio Cornwall (FM),95200000,50.511364,-4.43683,369,2.5,, +FM,BBC Radio Cornwall (FM) Isles of Scilly,BBC Radio Cornwall (FM),96000000,49.932504,-6.305394,32,0.03,, +FM,BBC Radio Cornwall (FM) West Cornwall Redruth,BBC Radio Cornwall (FM),103900000,50.209786,-5.238489,237,8.9,, +FM,BBC Radio Cumbria (FM) South Lakes Kendal,BBC Radio Cumbria (FM),95200000,54.314559,-2.708127,172,0.1,, +FM,BBC Radio Cumbria (FM) North Lakes Keswick Forest,BBC Radio Cumbria (FM),104100000,54.608739,-3.204646,215,0,, +FM,BBC Radio Cumbria (FM) Morecambe Bay,BBC Radio Cumbria (FM),96100000,54.202505,-3.167346,259,1.6,, +FM,BBC Radio Cumbria (FM) Cumbria Sandale,BBC Radio Cumbria (FM),95600000,54.749109,-3.14076,363,7.5,, +FM,BBC Radio Cumbria (FM) West Cumbria Whitehaven - VHF,BBC Radio Cumbria (FM),104100000,54.499453,-3.558449,125,0.967164,, +FM,BBC Radio Cumbria (FM) Windermere WINDERMERE,BBC Radio Cumbria (FM),104200000,54.37436,-2.951306,216,0.032,, +FM,BBC Radio Cymru Aberdare,BBC Radio Cymru,93600000,51.702007,-3.398531,282,0.021,, +FM,BBC Radio Cymru Abergavenny,BBC Radio Cymru,103500000,51.807795,-3.097781,433,0.02,, +FM,BBC Radio Cymru Pontypool Abertillery,BBC Radio Cymru,104300000,51.714666,-3.12404,400,0.01,, +FM,BBC Radio Cymru Snowdonia Betws Y Coed,BBC Radio Cymru,92600000,53.108346,-3.75717,303,0.025,, +FM,BBC Radio Cymru Blaenavon,BBC Radio Cymru,104100000,51.750968,-3.048742,373,0.01,, +FM,BBC Radio Cymru Ceredigion Blaenplwyf,BBC Radio Cymru,93100000,52.360613,-4.102585,174,125,, +FM,BBC Radio Cymru Mid Wales Brecon VHF,BBC Radio Cymru,93300000,51.959448,-3.412497,248,0.01,, +FM,BBC Radio Cymru Carmarthen,BBC Radio Cymru,93300000,51.867126,-4.309135,137,0.01,, +FM,BBC Radio Cymru Carmarthenshire Carmel,BBC Radio Cymru,104600000,51.818501,-4.066216,255,1.6,, +FM,BBC Radio Cymru Wye Valley CLYRO,BBC Radio Cymru,92400000,52.08185,-3.162868,215,0.01,, +FM,BBC Radio Cymru Conway CONWY,BBC Radio Cymru,93500000,53.271606,-3.828796,114,0.05,, +FM,BBC Radio Cymru Croeserw,BBC Radio Cymru,93400000,51.644491,-3.652146,343,0.01,, +FM,BBC Radio Cymru Cwmafan,BBC Radio Cymru,104500000,51.628417,-3.737817,210,0.01,, +FM,BBC Radio Cymru Gwynedd DEINIOLEN,BBC Radio Cymru,92500000,53.137063,-4.129256,322,0.05,, +FM,BBC Radio Cymru Snowdonia DOLGELLAU,BBC Radio Cymru,94500000,52.749344,-3.886853,103,0.025,, +FM,BBC Radio Cymru South Wales EBBW VALE,BBC Radio Cymru,104600000,51.771714,-3.218938,442,0.015,, +FM,BBC Radio Cymru South Wales Ferndale,BBC Radio Cymru,104900000,51.662873,-3.437376,386,0.01,, +FM,BBC Radio Cymru Snowdonia FFESTINIOG,BBC Radio Cymru,92500000,52.934172,-3.921818,308,0.09,, +FM,BBC Radio Cymru Pembrokeshire Haverfordwest,BBC Radio Cymru,93700000,51.899195,-4.86653,186,9.54993,, +FM,BBC Radio Cymru Dee Estuary Holywell,BBC Radio Cymru,104700000,53.284128,-3.231408,130,0.25,, +FM,BBC Radio Cymru Swansea Kilvey Hill,BBC Radio Cymru,104200000,51.62891,-3.919745,193,0.5,, +FM,BBC Radio Cymru Anglesey LLANDDONA,BBC Radio Cymru,94200000,53.307002,-4.12825,146,10.5,, +FM,BBC Radio Cymru Snowdonia LLANDECWYN,BBC Radio Cymru,93800000,52.914419,-4.017769,268,0.1,, +FM,BBC Radio Cymru Mid Wales LLANDINAM,BBC Radio Cymru,96100000,52.479547,-3.399945,454,0.022,, +FM,BBC Radio Cymru Llandrindod Wells,BBC Radio Cymru,93500000,52.261047,-3.439354,432,1.35,, +FM,BBC Radio Cymru Ceredigion LLANDYFRIOG,BBC Radio Cymru,94500000,52.045314,-4.410045,116,0.05,, +FM,BBC Radio Cymru Llanfyllin LLANFYLLIN,BBC Radio Cymru,95700000,52.753349,-3.259797,264,0.005,, +FM,BBC Radio Cymru Bridgend Llangeinor,BBC Radio Cymru,104600000,51.586117,-3.581462,306,0.01,, +FM,BBC Radio Cymru Llangollen and Cheshire Llangollen VHF,BBC Radio Cymru,104300000,53.031048,-3.180791,556,7.80001,, +FM,BBC Radio Cymru Mid Wales LLANIDLOES,BBC Radio Cymru,92500000,52.446933,-3.549585,244,0.025,, +FM,BBC Radio Cymru Llanrhaeadr ym Mochnant LLANRHAEADR YM MOCHN,BBC Radio Cymru,103800000,52.825889,-3.226423,333,0.025,, +FM,BBC Radio Cymru Snowdonia LLWYN ONN,BBC Radio Cymru,92700000,52.737496,-4.03773,277,0.25,, +FM,BBC Radio Cymru Talgarth LLYSWEN,BBC Radio Cymru,94000000,52.016761,-3.258522,224,0.01,, +FM,BBC Radio Cymru Welshpool LONG MOUNTAIN,BBC Radio Cymru,103600000,52.644306,-3.086542,400,0.024,, +FM,BBC Radio Cymru Snowdonia MACHYNLLETH,BBC Radio Cymru,93800000,52.586449,-3.885603,100,0.06,, +FM,BBC Radio Cymru Cardigan Bay Mynydd Pencarreg,BBC Radio Cymru,94100000,52.067402,-4.076523,411,0.2,, +FM,BBC Radio Cymru Ogmore Vale,BBC Radio Cymru,95000000,51.593504,-3.546781,298,0.01,, +FM,BBC Radio Cymru Colwyn Bay Penmaen Rhos,BBC Radio Cymru,92800000,53.286941,-3.684982,125,0.1,, +FM,BBC Radio Cymru Cwmbran Pennar,BBC Radio Cymru,94700000,51.656112,-3.144564,228,0.005,, +FM,BBC Radio Cymru Pontypool,BBC Radio Cymru,93600000,51.685712,-3.036095,250,0.025,, +FM,BBC Radio Cymru Pontypridd Porth,BBC Radio Cymru,104500000,51.617787,-3.403306,247,0.01,, +FM,BBC Radio Cymru Neath Valley RHEOLA,BBC Radio Cymru,92600000,51.741661,-3.679878,409,0.01,, +FM,BBC Radio Cymru Rhymney RHYMNEY,BBC Radio Cymru,104900000,51.730587,-3.264891,419,0.02,, +FM,BBC Radio Cymru Rhondda Valley Ton Pentre,BBC Radio Cymru,93200000,51.64954,-3.504023,323,0.003,, +FM,BBC Radio Cymru Neath Valley Varteg Hill,BBC Radio Cymru,93300000,51.754672,-3.773522,351,0.024,, +FM,BBC Radio Cymru South Glamorgan Wenvoe,BBC Radio Cymru,96800000,51.459476,-3.282282,129,150,, +FM,BBC Radio Derby (FM) North Derbyshire Buxton,BBC Radio Derby (FM),96000000,53.275085,-1.911935,432,0.05,, +FM,BBC Radio Derby (FM) Derbyshire Drum Hill,BBC Radio Derby (FM),104500000,52.975288,-1.442404,156,2.2,, +FM,BBC Radio Derby (FM) Derbyshire Stanton Moor,BBC Radio Derby (FM),95300000,53.170195,-1.633756,305,0.6,, +FM,BBC Radio Devon (FM) Torbay Beacon Hill North,BBC Radio Devon (FM),104300000,50.447129,-3.610498,196,1,, +FM,BBC Radio Devon (FM) Exeter Exeter (St Thomas),BBC Radio Devon (FM),95800000,50.719122,-3.562148,102,0.226,, +FM,BBC Radio Devon (FM) North Devon Huntshaw Cross,BBC Radio Devon (FM),94800000,50.979244,-4.098818,200,0.5,, +FM,BBC Radio Devon (FM) Devon North Hessary Tor,BBC Radio Devon (FM),103400000,50.550217,-4.008413,508,7.5,, +FM,BBC Radio Devon (FM) Okehampton,BBC Radio Devon (FM),96000000,50.753618,-4.005483,208,0.05,, +FM,BBC Radio Devon (FM) Plymouth Plympton,BBC Radio Devon (FM),95700000,50.381346,-4.067802,113,1,, +FM,BBC Radio Gloucestershire (FM) Gloucestershire Churchdown Hill,BBC Radio Gloucestershire (FM),104700000,51.868264,-2.175397,147,1,, +FM,BBC Radio Gloucestershire (FM) Cirencester CIRENCESTER UHF,BBC Radio Gloucestershire (FM),95800000,51.750794,-1.994303,184,0.08,, +FM,BBC Radio Gloucestershire (FM) Gloucestershire STROUD,BBC Radio Gloucestershire (FM),95000000,51.768084,-2.238625,223,0.05,, +FM,BBC Radio Humberside (FM) Humberside High Hunsley,BBC Radio Humberside (FM),95900000,53.803052,-0.565371,164,5,, +FM,BBC Radio Jersey (FM) Jersey Les Platons,BBC Radio Jersey (FM),88800000,49.246023,-2.1012,134,1.9,, +FM,BBC Radio Kent (FM) Folkestone Creteway Down,BBC Radio Kent (FM),97600000,51.099738,1.182579,166,0.1,, +FM,BBC Radio Kent (FM) Dartford Tunnel,BBC Radio Kent (FM),96700000,51.469253,0.260425,3,0,, +FM,BBC Radio Kent (FM) Dover Swingate,BBC Radio Kent (FM),104200000,51.137832,1.335758,122,5,, +FM,BBC Radio Kent (FM) London and the South East Wrotham,BBC Radio Kent (FM),96700000,51.320696,0.287702,219,4.35,, +FM,BBC Radio Lancashire (FM) Burnley Hameldon Hill,BBC Radio Lancashire (FM),95500000,53.755909,-2.291465,397,0.8,, +FM,BBC Radio Lancashire (FM) Lancashire LANCASTER,BBC Radio Lancashire (FM),104500000,54.089336,-2.779963,88,1.05,, +FM,BBC Radio Lancashire (FM) Lancashire WINTER HILL,BBC Radio Lancashire (FM),103900000,53.625547,-2.514794,439,0.909999,, +FM,BBC Radio Leeds (FM) Leeds Beecroft Hill,BBC Radio Leeds (FM),103900000,53.811543,-1.641253,149,0.1,, +FM,BBC Radio Leeds (FM) West Yorkshire Holme Moss,BBC Radio Leeds (FM),92400000,53.533184,-1.858296,524,2.6,, +FM,BBC Radio Leeds (FM) West Yorkshire KEIGHLEY,BBC Radio Leeds (FM),102700000,53.895532,-1.896639,305,0.5,, +FM,BBC Radio Leeds (FM) Calderdale LUDDENDEN,BBC Radio Leeds (FM),95300000,53.720111,-1.92874,256,0.065929,, +FM,BBC Radio Leeds (FM) West Yorkshire WHARFEDALE,BBC Radio Leeds (FM),95300000,53.932231,-1.699923,212,0.02,, +FM,BBC Radio Leicester Leicestershire Copt Oak,BBC Radio Leicester,104900000,52.710153,-1.285042,240,4,, +FM,BBC Radio Manchester Greater Manchester Holme Moss,BBC Radio Manchester,95100000,53.533184,-1.858296,524,2.6,, +FM,BBC Radio Manchester Greater Manchester SADDLEWORTH,BBC Radio Manchester,104600000,53.541535,-2.020635,347,0.1,, +FM,BBC Radio Merseyside (FM) Merseyside Allerton Park,BBC Radio Merseyside (FM),95800000,53.373318,-2.884808,69,3.75,, +FM,BBC Radio nan Gaidheal (FM) Argyll & Bute Ardgour,BBC Radio nan Gaidheal (FM),104900000,56.712175,-5.290096,12,0.005,, +FM,BBC Radio nan Gaidheal (FM) Argyll & Bute Ballachulish,BBC Radio nan Gaidheal (FM),103700000,56.684359,-5.170394,14,0.015,, +FM,BBC Radio nan Gaidheal (FM) Central Scotland Black Hill,BBC Radio nan Gaidheal (FM),104700000,55.860005,-3.873572,275,4.88619,, +FM,BBC Radio nan Gaidheal (FM) Islay Bowmore,BBC Radio nan Gaidheal (FM),103600000,55.750128,-6.274228,46,0.061,, +FM,BBC Radio nan Gaidheal (FM) North Uist Clettraval,BBC Radio nan Gaidheal (FM),103700000,57.617267,-7.44252,123,2,, +FM,BBC Radio nan Gaidheal (FM) A CRAIGKELLY,BBC Radio nan Gaidheal (FM),104100000,56.071791,-3.233186,181,2.5,, +FM,BBC Radio nan Gaidheal (FM) South Uist Daliburgh,BBC Radio nan Gaidheal (FM),104200000,57.169643,-7.402558,6,1,, +FM,BBC Radio nan Gaidheal (FM) Isle of Lewis Eitshal,BBC Radio nan Gaidheal (FM),104300000,58.179148,-6.584974,206,2,, +FM,BBC Radio nan Gaidheal (FM) Angus Forfar,BBC Radio nan Gaidheal (FM),103700000,56.55718,-2.843663,206,2.5,, +FM,BBC Radio nan Gaidheal (FM) Great Glen Fort Augustus,BBC Radio nan Gaidheal (FM),104700000,57.105987,-4.70869,307,1,, +FM,BBC Radio nan Gaidheal (FM) Fort William,BBC Radio nan Gaidheal (FM),104200000,56.848302,-5.088621,16,2,, +FM,BBC Radio nan Gaidheal (FM) Isle of Mull Glengorm,BBC Radio nan Gaidheal (FM),103500000,56.632166,-6.133249,253,2.5,, +FM,BBC Radio nan Gaidheal (FM) Cairngorms Grantown,BBC Radio nan Gaidheal (FM),104700000,57.320173,-3.656313,389,0.36,, +FM,BBC Radio nan Gaidheal (FM) Cairngorms KINGUSSIE,BBC Radio nan Gaidheal (FM),104000000,57.061393,-4.030507,351,0.05,, +FM,BBC Radio nan Gaidheal (FM) Kinlochleven,BBC Radio nan Gaidheal (FM),104400000,56.722796,-4.978914,273,0.005,, +FM,BBC Radio nan Gaidheal (FM) Perth Kirkton Mailer,BBC Radio nan Gaidheal (FM),104500000,56.372293,-3.453361,178,0.5,, +FM,BBC Radio nan Gaidheal (FM) Mallaig,BBC Radio nan Gaidheal (FM),104300000,57.001499,-5.828475,71,0.02,, +FM,BBC Radio nan Gaidheal (FM) Aberdeenshire Meldrum,BBC Radio nan Gaidheal (FM),104200000,57.386674,-2.400197,245,75,, +FM,BBC Radio nan Gaidheal (FM) Wester Ross Melvaig,BBC Radio nan Gaidheal (FM),103900000,57.842382,-5.782182,271,17.5903,, +FM,BBC Radio nan Gaidheal (FM) Isle of Lewis Ness of Lewis,BBC Radio nan Gaidheal (FM),104900000,58.461531,-6.230808,100,0.162,, +FM,BBC Radio nan Gaidheal (FM) Oban and Mull Oban,BBC Radio nan Gaidheal (FM),104600000,56.40396,-5.485882,122,2.5,, +FM,BBC Radio nan Gaidheal (FM) Penicuik PENICUIK,BBC Radio nan Gaidheal (FM),104400000,55.819142,-3.194377,248,0.132,, +FM,BBC Radio nan Gaidheal (FM) Portree Penifiler,BBC Radio nan Gaidheal (FM),104300000,57.395531,-6.180316,52,0.005,, +FM,BBC Radio nan Gaidheal (FM) Islay PORT ELLEN,BBC Radio nan Gaidheal (FM),104900000,55.627446,-6.227617,93,0.065,, +FM,BBC Radio nan Gaidheal (FM) Highland Rosemarkie,BBC Radio nan Gaidheal (FM),104900000,57.633195,-4.075231,206,9.77237,, +FM,BBC Radio nan Gaidheal (FM) Bute ROTHESAY,BBC Radio nan Gaidheal (FM),104000000,55.878164,-4.998928,166,0.285,, +FM,BBC Radio nan Gaidheal (FM) Caithness Rumster Forest,BBC Radio nan Gaidheal (FM),104600000,58.328006,-3.371413,221,5,, +FM,BBC Radio nan Gaidheal (FM) Skye & Lochalsh Skriaig,BBC Radio nan Gaidheal (FM),104700000,57.38642,-6.24289,390,9.68481,, +FM,BBC Radio nan Gaidheal (FM) Kintyre Islay and Jura South Knapdale,BBC Radio nan Gaidheal (FM),103700000,55.91751,-5.462223,481,1.1,, +FM,BBC Radio nan Gaidheal (FM) Inveraray Strachur,BBC Radio nan Gaidheal (FM),104200000,56.179137,-5.071531,175,0.01,, +FM,BBC Radio nan Gaidheal (FM) Ullapool,BBC Radio nan Gaidheal (FM),104900000,57.892557,-5.135709,143,0.05,, +FM,BBC Radio Norfolk (FM) West Norfolk Massingham,BBC Radio Norfolk (FM),104400000,52.774139,0.649933,92,2.2,, +FM,BBC Radio Norfolk (FM) Great Yarmouth and Norwich Stoke Holy Cross,BBC Radio Norfolk (FM),95100000,52.574817,1.331664,65,2,, +FM,BBC Radio Norfolk (FM) North Norfolk WEST RUNTON,BBC Radio Norfolk (FM),95600000,52.924381,1.251171,99,0.125594,, +FM,BBC Radio Northampton North Northamptonshire Geddington,BBC Radio Northampton,103600000,52.445531,-0.666816,118,0.41,, +FM,BBC Radio Northampton Northamptonshire Northampton,BBC Radio Northampton,104200000,52.275355,-0.885481,127,2,, +FM,BBC Radio Nottingham (FM) Mansfield Fishpond Hill,BBC Radio Nottingham (FM),95500000,53.140513,-1.232922,168,1,, +FM,BBC Radio Nottingham (FM) Nottingham Mapperley Ridge,BBC Radio Nottingham (FM),103800000,52.976309,-1.132626,122,0.5,, +FM,BBC Radio Nottingham (FM) Newark,BBC Radio Nottingham (FM),95100000,53.075237,-0.787915,45,0.2,, +FM,BBC Radio Scotland (FM) Argyll & Bute Ardgour,BBC Radio Scotland (FM),93100000,56.712175,-5.290096,12,0.005,, +FM,BBC Radio Scotland (FM) Scottish Borders Ashkirk,BBC Radio Scotland (FM),93500000,55.510631,-2.840766,303,20.3208,, +FM,BBC Radio Scotland (FM) Argyll & Bute Ballachulish,BBC Radio Scotland (FM),92500000,56.684359,-5.170394,14,0.015,, +FM,BBC Radio Scotland (FM) Stirling Ben Gullipen,BBC Radio Scotland (FM),93100000,56.212308,-4.263559,415,1,, +FM,BBC Radio Scotland (FM) Central Scotland Black Hill,BBC Radio Scotland (FM),94300000,55.860005,-3.873572,275,122.155,, +FM,BBC Radio Scotland (FM) A Blair Atholl,BBC Radio Scotland (FM),93100000,56.770409,-3.810867,390,0.1,, +FM,BBC Radio Scotland (FM) Islay Bowmore,BBC Radio Scotland (FM),92500000,55.750128,-6.274228,46,0.061,, +FM,BBC Radio Scotland (FM) Shetland Bressay,BBC Radio Scotland (FM),92700000,60.130079,-1.096281,224,25,, +FM,BBC Radio Scotland (FM) Kirkcudbright CAMBRET HILL,BBC Radio Scotland (FM),93100000,54.893698,-4.302038,343,0.065,, +FM,BBC Radio Scotland (FM) Kintyre Islay and Jura CAMPBELTOWN,BBC Radio Scotland (FM),92800000,55.413714,-5.624241,83,0.2,, +FM,BBC Radio Scotland (FM) North Uist Clettraval,BBC Radio Scotland (FM),92500000,57.617267,-7.44252,123,2,, +FM,BBC Radio Scotland (FM) Crieff CRIEFF,BBC Radio Scotland (FM),93700000,56.357222,-3.920232,219,0.125,, +FM,BBC Radio Scotland (FM) A Crubenmore,BBC Radio Scotland (FM),93000000,56.984905,-4.196689,514,0.2,, +FM,BBC Radio Scotland (FM) South Uist Daliburgh,BBC Radio Scotland (FM),93700000,57.169643,-7.402558,6,1,, +FM,BBC Radio Scotland (FM) A Dalnacardoch Wood,BBC Radio Scotland (FM),94200000,56.816408,-4.098008,388,0.025,, +FM,BBC Radio Scotland (FM) Ayrshire DARVEL,BBC Radio Scotland (FM),93900000,55.579376,-4.289814,286,5,, +FM,BBC Radio Scotland (FM) Aberdeen Durris,BBC Radio Scotland (FM),93800000,56.999987,-2.389944,325,1.05,, +FM,BBC Radio Scotland (FM) Isle of Lewis Eitshal,BBC Radio Scotland (FM),94200000,58.179148,-6.584974,206,2,, +FM,BBC Radio Scotland (FM) Borders EYEMOUTH,BBC Radio Scotland (FM),94100000,55.83228,-2.085863,197,0.1,, +FM,BBC Radio Scotland (FM) Angus Forfar,BBC Radio Scotland (FM),92700000,56.55718,-2.843663,206,10,, +FM,BBC Radio Scotland (FM) Great Glen Fort Augustus,BBC Radio Scotland (FM),93000000,57.105987,-4.70869,307,1,, +FM,BBC Radio Scotland (FM) Fort William,BBC Radio Scotland (FM),93700000,56.848302,-5.088621,16,2,, +FM,BBC Radio Scotland (FM) Ayr GIRVAN,BBC Radio Scotland (FM),93300000,55.244736,-4.814596,195,0.1,, +FM,BBC Radio Scotland (FM) Isle of Mull Glengorm,BBC Radio Scotland (FM),93900000,56.632166,-6.133249,253,2.5,, +FM,BBC Radio Scotland (FM) Cairngorms Grantown,BBC Radio Scotland (FM),94200000,57.320173,-3.656313,389,0.36,, +FM,BBC Radio Scotland (FM) Borders INNERLEITHEN,BBC Radio Scotland (FM),93900000,55.620243,-3.07272,244,0.011,, +FM,BBC Radio Scotland (FM) Orkney Keelylang Hill B,BBC Radio Scotland (FM),93700000,58.975283,-3.083943,220,16,, +FM,BBC Radio Scotland (FM) Cairngorms KINGUSSIE,BBC Radio Scotland (FM),93500000,57.061393,-4.030507,351,0.05,, +FM,BBC Radio Scotland (FM) Kinlochleven,BBC Radio Scotland (FM),94100000,56.722796,-4.978914,273,0.005,, +FM,BBC Radio Scotland (FM) Borders Kirkconnel,BBC Radio Scotland (FM),93100000,55.412875,-3.983785,480,0.02,, +FM,BBC Radio Scotland (FM) Perth Kirkton Mailer,BBC Radio Scotland (FM),93400000,56.372293,-3.453361,178,0.5,, +FM,BBC Radio Scotland (FM) Moray Knockmore,BBC Radio Scotland (FM),92600000,57.532303,-3.135126,353,1,, +FM,BBC Radio Scotland (FM) East Ayrshire LETHANHILL,BBC Radio Scotland (FM),92700000,55.36421,-4.464995,300,0.11,, +FM,BBC Radio Scotland (FM) Mallaig,BBC Radio Scotland (FM),92500000,57.001499,-5.828475,71,0.02,, +FM,BBC Radio Scotland (FM) Aberdeenshire Meldrum,BBC Radio Scotland (FM),93100000,57.386674,-2.400197,245,75,, +FM,BBC Radio Scotland (FM) Wester Ross Melvaig,BBC Radio Scotland (FM),93500000,57.842382,-5.782182,271,17.5903,, +FM,BBC Radio Scotland (FM) Dumbarton Millburn Muir,BBC Radio Scotland (FM),92700000,55.982322,-4.600708,152,0.025,, +FM,BBC Radio Scotland (FM) Isle of Lewis Ness of Lewis,BBC Radio Scotland (FM),92700000,58.461531,-6.230808,100,0.162,, +FM,BBC Radio Scotland (FM) Oban and Mull Oban,BBC Radio Scotland (FM),93300000,56.40396,-5.485882,122,2.5,, +FM,BBC Radio Scotland (FM) Borders PEEBLES,BBC Radio Scotland (FM),92800000,55.661832,-3.227732,352,0.02,, +FM,BBC Radio Scotland (FM) Penicuik PENICUIK,BBC Radio Scotland (FM),93000000,55.819142,-3.194377,248,0.132,, +FM,BBC Radio Scotland (FM) Portree Penifiler,BBC Radio Scotland (FM),92500000,57.395531,-6.180316,52,0.005,, +FM,BBC Radio Scotland (FM) Pitlochry PITLOCHRY,BBC Radio Scotland (FM),93600000,56.687307,-3.759852,401,0,, +FM,BBC Radio Scotland (FM) Islay PORT ELLEN,BBC Radio Scotland (FM),93400000,55.627446,-6.227617,93,0.065,, +FM,BBC Radio Scotland (FM) Highland Rosemarkie,BBC Radio Scotland (FM),94000000,57.633195,-4.075231,206,9.77237,, +FM,BBC Radio Scotland (FM) Coupar Rosemount,BBC Radio Scotland (FM),94000000,56.576982,-3.298257,81,0.032,, +FM,BBC Radio Scotland (FM) Helensburgh Rosneath,BBC Radio Scotland (FM),93600000,55.991937,-4.793795,107,0.025,, +FM,BBC Radio Scotland (FM) Bute ROTHESAY,BBC Radio Scotland (FM),92900000,55.878164,-4.998928,166,0.285,, +FM,BBC Radio Scotland (FM) Caithness Rumster Forest,BBC Radio Scotland (FM),94500000,58.328006,-3.371413,221,5,, +FM,BBC Radio Scotland (FM) Cumbria & South West Scotland Sandale,BBC Radio Scotland (FM),94700000,54.749109,-3.14076,363,125,, +FM,BBC Radio Scotland (FM) Skye & Lochalsh Skriaig,BBC Radio Scotland (FM),92900000,57.38642,-6.24289,390,9.68481,, +FM,BBC Radio Scotland (FM) Kintyre Islay and Jura South Knapdale,BBC Radio Scotland (FM),93700000,55.91751,-5.462223,481,1.1,, +FM,BBC Radio Scotland (FM) Inveraray Strachur,BBC Radio Scotland (FM),93200000,56.179137,-5.071531,175,0.01,, +FM,BBC Radio Scotland (FM) Galloway STRANRAER,BBC Radio Scotland (FM),94100000,54.928074,-4.949004,190,0.031,, +FM,BBC Radio Scotland (FM) Tullich TULLICH,BBC Radio Scotland (FM),94500000,57.07283,-3.027203,463,0.025,, +FM,BBC Radio Scotland (FM) Ullapool,BBC Radio Scotland (FM),92700000,57.892557,-5.135709,143,0.05,, +FM,BBC Radio Scotland (FM) North Ayrshire WEST KILBRIDE,BBC Radio Scotland (FM),93500000,55.695641,-4.84184,170,0.05,, +FM,BBC Radio Sheffield (FM) South Yorkshire Chesterfield,BBC Radio Sheffield (FM),94700000,53.283991,-1.427446,179,0.4,, +FM,BBC Radio Sheffield (FM) South Yorkshire Holme Moss,BBC Radio Sheffield (FM),104100000,53.533184,-1.858296,524,2,, +FM,BBC Radio Sheffield (FM) South Yorkshire Sheffield,BBC Radio Sheffield (FM),88600000,53.379385,-1.513809,247,0.167,, +FM,BBC Radio Shropshire Shropshire CLUN,BBC Radio Shropshire,104100000,52.413167,-2.994548,413,0.04,, +FM,BBC Radio Shropshire Church Stretton HAZLER HILL,BBC Radio Shropshire,90000000,52.531128,-2.790828,343,0.01,, +FM,BBC Radio Shropshire Ludlow LUDLOW,BBC Radio Shropshire,95000000,52.362722,-2.738327,216,0.005,, +FM,BBC Radio Shropshire Shropshire and Telford The Wrekin,BBC Radio Shropshire,96000000,52.670935,-2.550228,396,2.4,, +FM,BBC Radio Solent (FM) Dorset Bincombe Hill,BBC Radio Solent (FM),103800000,50.66283,-2.443645,160,0.7,, +FM,BBC Radio Solent (FM) Hampshire and Isle of Wight ROWRIDGE,BBC Radio Solent (FM),96100000,50.676724,-1.370435,143,5,, +FM,BBC Radio Stoke (FM) Staffordshire Alsagers Bank,BBC Radio Stoke (FM),94600000,53.027231,-2.28071,220,3.1,, +FM,BBC Radio Stoke (FM) Staffordshire Stafford,BBC Radio Stoke (FM),104100000,52.806371,-2.115367,76,0.075,, +FM,BBC Radio Suffolk Suffolk Coast Aldeburgh,BBC Radio Suffolk,95900000,52.180735,1.570158,21,2,, +FM,BBC Radio Suffolk Bury St Edmunds Great Barton,BBC Radio Suffolk,104600000,52.283402,0.764679,61,0.7,, +FM,BBC Radio Suffolk Colchester and Ipswich Manningtree,BBC Radio Suffolk,103900000,51.92387,1.086097,35,2.5,, +FM,BBC Radio Suffolk Suffolk Oulton,BBC Radio Suffolk,95500000,52.486385,1.715554,8,2,, +FM,BBC Radio Ulster/Foyle (FM) North Antrim Ballycastle,BBC Radio Ulster/Foyle (FM),95100000,55.215995,-6.309339,150,0.05,, +FM,BBC Radio Ulster/Foyle (FM) Omagh and Enniskillen Brougher Mountain,BBC Radio Ulster/Foyle (FM),93800000,54.423191,-7.462812,305,5,, +FM,BBC Radio Ulster/Foyle (FM) South Newry Camlough,BBC Radio Ulster/Foyle (FM),93100000,54.160861,-6.385716,340,2,, +FM,BBC Radio Ulster/Foyle (FM) Newtownabbey Carnmoney Hill,BBC Radio Ulster/Foyle (FM),95300000,54.676355,-5.928737,207,0.02,, +FM,BBC Radio Ulster/Foyle (FM) Northern Ireland Divis A,BBC Radio Ulster/Foyle (FM),94500000,54.607538,-6.009471,368,61.0773,, +FM,BBC Radio Ulster/Foyle (FM) Kilkeel,BBC Radio Ulster/Foyle (FM),93800000,54.095375,-6.041723,267,0.1,, +FM,BBC Radio Ulster/Foyle (FM) Larne,BBC Radio Ulster/Foyle (FM),93500000,54.862024,-5.828421,119,0.016,, +FM,BBC Radio Ulster/Foyle (FM) Limavady,BBC Radio Ulster/Foyle (FM),95400000,55.109172,-6.886121,341,1.7,, +FM,BBC Radio Ulster/Foyle (FM) Londonderry,BBC Radio Ulster/Foyle (FM),93100000,55.00421,-7.368968,175,15.6,, +FM,BBC Radio Ulster/Foyle (FM) South Newry Rostrevor Forest,BBC Radio Ulster/Foyle (FM),92700000,54.092725,-6.182015,215,0.02,, +FM,BBC Radio Wales Aberdare,BBC Radio Wales,95800000,51.702007,-3.398531,282,0.021,, +FM,BBC Radio Wales Abergavenny,BBC Radio Wales,95200000,51.807795,-3.097781,433,0.02,, +FM,BBC Radio Wales Pontypool Abertillery,BBC Radio Wales,91200000,51.714666,-3.12404,400,0.01,, +FM,BBC Radio Wales Snowdonia Betws Y Coed,BBC Radio Wales,90400000,53.108346,-3.75717,303,0.025,, +FM,BBC Radio Wales Blaenavon,BBC Radio Wales,95100000,51.750968,-3.048742,373,0.01,, +FM,BBC Radio Wales Ceredigion Blaenplwyf,BBC Radio Wales,95300000,52.360613,-4.102585,174,60,, +FM,BBC Radio Wales Mid Wales Brecon VHF,BBC Radio Wales,91100000,51.959448,-3.412497,248,0.01,, +FM,BBC Radio Wales Carmarthen,BBC Radio Wales,91100000,51.867126,-4.309135,137,0.01,, +FM,BBC Radio Wales Carmarthenshire Carmel,BBC Radio Wales,95100000,51.818501,-4.066216,255,1.6,, +FM,BBC Radio Wales A Christchurch,BBC Radio Wales,95900000,51.601588,-2.935417,94,0.25,, +FM,BBC Radio Wales Wye Valley CLYRO,BBC Radio Wales,90200000,52.08185,-3.162868,215,0.01,, +FM,BBC Radio Wales Conway CONWY,BBC Radio Wales,91300000,53.271606,-3.828796,114,0.05,, +FM,BBC Radio Wales Croeserw,BBC Radio Wales,91200000,51.644491,-3.652146,343,0.01,, +FM,BBC Radio Wales Cwmafan,BBC Radio Wales,90700000,51.628417,-3.737817,210,0.01,, +FM,BBC Radio Wales Gwynedd DEINIOLEN,BBC Radio Wales,90300000,53.137063,-4.129256,322,0.05,, +FM,BBC Radio Wales Snowdonia DOLGELLAU,BBC Radio Wales,92300000,52.749344,-3.886853,103,0.025,, +FM,BBC Radio Wales South Wales EBBW VALE,BBC Radio Wales,95000000,51.771714,-3.218938,442,0.015,, +FM,BBC Radio Wales South Wales Ferndale,BBC Radio Wales,91800000,51.662873,-3.437376,386,0.01,, +FM,BBC Radio Wales Snowdonia FFESTINIOG,BBC Radio Wales,90300000,52.934172,-3.921818,308,0.082081,, +FM,BBC Radio Wales Pembrokeshire Haverfordwest,BBC Radio Wales,95900000,51.899195,-4.86653,186,9.54993,, +FM,BBC Radio Wales Swansea Kilvey Hill,BBC Radio Wales,93900000,51.62891,-3.919745,193,0.5,, +FM,BBC Radio Wales Anglesey LLANDDONA,BBC Radio Wales,94800000,53.307002,-4.12825,146,5,, +FM,BBC Radio Wales Snowdonia LLANDECWYN,BBC Radio Wales,91600000,52.914419,-4.017769,268,0.1,, +FM,BBC Radio Wales Mid Wales LLANDINAM,BBC Radio Wales,92300000,52.479547,-3.399945,454,0.022,, +FM,BBC Radio Wales Llandrindod Wells,BBC Radio Wales,91300000,52.261047,-3.439354,432,1.39,, +FM,BBC Radio Wales Ceredigion LLANDYFRIOG,BBC Radio Wales,92300000,52.045314,-4.410045,116,0.05,, +FM,BBC Radio Wales Llanfyllin LLANFYLLIN,BBC Radio Wales,91300000,52.753349,-3.259797,264,0.003972,, +FM,BBC Radio Wales Bridgend Llangeinor,BBC Radio Wales,90800000,51.586117,-3.581462,306,0.01,, +FM,BBC Radio Wales Llangollen and Cheshire Llangollen VHF,BBC Radio Wales,91100000,53.031048,-3.180791,556,7.80001,, +FM,BBC Radio Wales Mid Wales LLANIDLOES,BBC Radio Wales,90300000,52.446933,-3.549585,244,0.025,, +FM,BBC Radio Wales Llanrhaeadr ym Mochnant LLANRHAEADR YM MOCHN,BBC Radio Wales,92000000,52.825889,-3.226423,333,0.025,, +FM,BBC Radio Wales Snowdonia LLWYN ONN,BBC Radio Wales,90500000,52.737496,-4.03773,277,0.25,, +FM,BBC Radio Wales Talgarth LLYSWEN,BBC Radio Wales,91800000,52.016761,-3.258522,224,0.01,, +FM,BBC Radio Wales Welshpool LONG MOUNTAIN,BBC Radio Wales,91800000,52.644306,-3.086542,400,0.024,, +FM,BBC Radio Wales Snowdonia MACHYNLLETH,BBC Radio Wales,91600000,52.586449,-3.885603,100,0.06,, +FM,BBC Radio Wales Merthyr Tydfil,BBC Radio Wales,96100000,51.749853,-3.367124,249,0.025,, +FM,BBC Radio Wales Cardigan Bay Mynydd Pencarreg,BBC Radio Wales,91900000,52.067402,-4.076523,411,0.2,, +FM,BBC Radio Wales Ogmore Vale,BBC Radio Wales,91400000,51.593504,-3.546781,298,0.01,, +FM,BBC Radio Wales Colwyn Bay Penmaen Rhos,BBC Radio Wales,90600000,53.286941,-3.684982,125,0.1,, +FM,BBC Radio Wales Cwmbran Pennar,BBC Radio Wales,91700000,51.656112,-3.144564,228,0.005,, +FM,BBC Radio Wales Pontypool,BBC Radio Wales,91400000,51.685712,-3.036095,250,0.025,, +FM,BBC Radio Wales Pontypridd Porth,BBC Radio Wales,94600000,51.617787,-3.403306,247,0.01,, +FM,BBC Radio Wales Neath Valley RHEOLA,BBC Radio Wales,90400000,51.741661,-3.679878,409,0.01,, +FM,BBC Radio Wales Rhymney RHYMNEY,BBC Radio Wales,91500000,51.730587,-3.264891,419,0.02,, +FM,BBC Radio Wales Rhondda Valley Ton Pentre,BBC Radio Wales,94800000,51.64954,-3.504023,323,0.003,, +FM,BBC Radio Wales Neath Valley Varteg Hill,BBC Radio Wales,95500000,51.754672,-3.773522,351,0.024,, +FM,BBC Radio Wales South Glamorgan Wenvoe,BBC Radio Wales,103900000,51.459476,-3.282282,129,18.6651,, +FM,BBC Radio Wales North Wales Wrexham Rhos,BBC Radio Wales,95400000,53.076278,-3.044994,210,0.7,, +FM,BBC Radio York (FM) North Yorkshire Acklam Wold,BBC Radio York (FM),103700000,54.046924,-0.786852,224,2,, +FM,BBC Radio York (FM) Scarborough OLIVERS MOUNT,BBC Radio York (FM),95500000,54.267351,-0.404494,151,0.125,, +FM,BBC Radio York (FM) North Yorkshire Woolmoor,BBC Radio York (FM),104300000,54.287102,-1.299034,257,0.5,, +FM,BBC Somerset (FM) Somerset MENDIP,BBC Somerset (FM),95500000,51.236969,-2.625476,303,2.5,, +FM,BBC Surrey/Sussex (FM) South Downs Burton Down,BBC Surrey/Sussex (FM),104800000,50.909654,-0.627813,245,2,, +FM,BBC Surrey/Sussex (FM) Surrey GUILDFORD,BBC Surrey/Sussex (FM),104600000,51.228358,-0.605467,142,2,, +FM,BBC Surrey/Sussex (FM) East Sussex Heathfield,BBC Surrey/Sussex (FM),104500000,50.976724,0.230437,158,5,, +FM,BBC Surrey/Sussex (FM) Horsham,BBC Surrey/Sussex (FM),95100000,51.064034,-0.322554,52,0.016,, +FM,BBC Surrey/Sussex (FM) Newhaven NEWHAVEN,BBC Surrey/Sussex (FM),95000000,50.787726,0.035848,81,0.05,, +FM,BBC Surrey/Sussex (FM) Surrey REIGATE,BBC Surrey/Sussex (FM),104000000,51.254494,-0.200986,234,2,, +FM,BBC Surrey/Sussex (FM) Brighton WHITEHAWK HILL,BBC Surrey/Sussex (FM),95300000,50.824954,-0.113611,120,0.5,, +FM,BBC Tees Darlington Darlington - Harrowgate Hill,BBC Tees,104000000,54.555795,-1.560849,72,0.25,, +FM,BBC Tees Teesside Eston Nab,BBC Tees,95000000,54.556455,-1.120934,237,0.3,, +FM,BBC Tees Whitby WHITBY BUSINESS PARK,BBC Tees,95800000,54.473215,-0.595572,60,0.054,, +FM,BBC Three Counties Radio (FM) Milton Keynes Bow Brickhill,BBC Three Counties Radio (FM),104500000,51.998806,-0.669784,169,1.1,, +FM,BBC Three Counties Radio (FM) Hertfordshire Epping Green,BBC Three Counties Radio (FM),90400000,51.744172,-0.128729,122,0.1,, +FM,BBC Three Counties Radio (FM) Hertfordshire Hemel Hempstead (Pimlico),BBC Three Counties Radio (FM),92100000,51.728571,-0.426217,142,0.05,, +FM,BBC Three Counties Radio (FM) High Wycombe,BBC Three Counties Radio (FM),98000000,51.640531,-0.763678,156,0.1,, +FM,BBC Three Counties Radio (FM) Aylesbury Quainton Hill,BBC Three Counties Radio (FM),94700000,51.885386,-0.910988,186,0.4,, +FM,BBC Three Counties Radio (FM) Bedfordshire SANDY HEATH,BBC Three Counties Radio (FM),95500000,52.130132,-0.241462,54,0.6,, +FM,BBC Three Counties Radio (FM) Luton Zouches Farm,BBC Three Counties Radio (FM),103800000,51.877958,-0.483622,207,0.388,, +FM,BBC Wiltshire (FM) Swindon Blunsdon,BBC Wiltshire (FM),103600000,51.609363,-1.794027,148,0.25,, +FM,BBC Wiltshire (FM) Marlborough MARLBOROUGH,BBC Wiltshire (FM),104900000,51.417912,-1.699977,192,0.1,, +FM,BBC Wiltshire (FM) Wiltshire Naish Hill,BBC Wiltshire (FM),104300000,51.416202,-2.076595,175,0.3,, +FM,BBC Wiltshire (FM) Wiltshire Newton Barrow,BBC Wiltshire (FM),103500000,51.117705,-1.85908,152,1,, +FM,BBC WM West Midlands SUTTON COLDFIELD,BBC WM,95600000,52.600539,-1.833856,169,5.7,, +FM,BCB 106.6 FM Bradford Idle,BCB 106.6 FM,106600000,53.832945,-1.752142,215,0.1,, +FM,Beat Radio Preston Preston - Guild Tower,Beat Radio,103200000,53.759726,-2.695723,37,0.025,, +FM,Belfast 89FM Belfast Belfast - Europa Hotel,Belfast 89FM,89300000,54.59479,-5.935047,11,0.075,, +FM,Beverley FM Beverley Beverley Race Course,Beverley FM,107800000,53.843546,-0.460172,45,0.025,, +FM,Beyond Radio Lancaster Carnforth - Lancaster,Beyond Radio,107500000,54.130311,-2.76321,37,0.01,, +FM,Beyond Radio Lancaster Lancaster - Ashton Memorial,Beyond Radio,103500000,54.045283,-2.782039,105,0.05,, +FM,BFBS Aldershot Aldershot - Centre for Health,BFBS,102500000,51.254436,-0.765605,107,0.025,, +FM,BFBS Blandford Blandford Garrison,BFBS,89300000,50.870624,-2.118471,106,0.025,, +FM,BFBS Bulford Bulford Camp,BFBS,106800000,51.18517,-1.722235,184,0.025,, +FM,BFBS Inverness Cameron Barracks Inverness (LRSL),BFBS,87700000,57.477715,-4.204681,18,5e-05,, +FM,BFBS Catterick Catterick Garrison,BFBS,106900000,54.380761,-1.723761,152,0.025,, +FM,BFBS Colchester Garrison,BFBS,107000000,51.880018,0.889961,32,0.025,, +FM,BFBS Edinburgh South Dreghorn Barracks,BFBS,98500000,55.901107,-3.23816,160,0.025,, +FM,BFBS Inverness Fort George Inverness (LRSL),BFBS,87700000,57.583316,-4.070549,6,0.0005,, +FM,BFBS Edinburgh South Glencorse Barracks,BFBS,94000000,55.848416,-3.205497,183,0.005,, +FM,BFBS Blandford (Bovington) Bovington Bovington Camp - FM,BFBS Blandford (Bovington),100800000,50.699487,-2.240659,44,0.0005,, +FM,BFBS Brize Norton Brize Norton Carterton - RAF Brize Norton,BFBS Brize Norton,106100000,51.756175,-1.584595,87,0.025,, +FM,BFBS Gibraltar O'Haras Battery,BFBS Gibraltar,97800000,36.124006,-5.343083,415,0.5,, +FM,BFBS Gibraltar Oyster Cottage,BFBS Gibraltar,93500000,36.153572,-5.350287,10,1,, +FM,BFBS Gurkha Radio Dover Dover Community Centre,BFBS Gurkha Radio,90800000,51.138863,1.323536,123,0.0005,, +FM,BFBS Lisburn FM Lisburn BFBS Lisburn,BFBS Lisburn FM,100600000,54.52375,-6.050621,73,0.025,, +FM,BFBS Radio (Fort George) Inverness Fort George Inverness (LRSL),BFBS Radio (Fort George),87700000,57.583316,-4.070549,6,0.0005,, +FM,BFBS Radio 2 O'Haras Battery,BFBS Radio 2,99500000,36.124006,-5.343083,415,0.5,, +FM,BFBS Radio 2 Oyster Cottage,BFBS Radio 2,89400000,36.153572,-5.350287,10,0.5,, +FM,BFBS Shorncliffe Shorncliffe Camp Shorncliffe MoD - Tower Theatre,BFBS Shorncliffe,105400000,51.079275,1.134336,74,0.025,, +FM,BGFM Brynmawr,BGFM,97300000,51.802578,-3.181482,414,0.025,, +FM,Big City Radio Birmingham Aston Birmingham - Victor Tower,Big City Radio,89100000,52.494597,-1.872256,108,0.025,, +FM,Biggles FM Biggleswade Potton,Biggles FM,104800000,52.126108,-0.224966,47,0.05,, +FM,Bishop FM 105.9 South West Durham High Etherley - Community Centre,Bishop FM 105.9,105900000,54.648533,-1.748295,201,0.05,, +FM,Black Cat Radio St Neots Paxton Hill,Black Cat Radio,102500000,52.248446,-0.23459,46,0.1,, +FM,Black Country Radio Stourbridge Brierley Hill - St Michaels,Black Country Radio,102500000,52.479497,-2.124668,162,0.05,, +FM,Black Country Radio Dudley Dudley - King Street,Black Country Radio,92200000,52.508028,-2.08762,202,0.03,, +FM,Black Diamond Midlothian Halkerston Farm,Black Diamond,100700000,55.813657,-3.046918,263,0.025,, +FM,Black Diamond FM Midlothian Newtongrange - Newbattle MC,Black Diamond FM,107800000,55.867368,-3.049481,138,0.05,, +FM,Blackburn's 102.2FM Blackburn Blackburn - Dukes Cars,Blackburn's 102.2FM,102200000,53.754839,-2.505,197,0.05,, +FM,Blast 106 Belfast Belfast - Europa Hotel,Blast 106,106400000,54.59479,-5.935047,11,0.025,, +FM,Blast106 Coleraine and up to Portstewart Coleraine - Beresford Biz Centre,Blast106,106400000,55.128079,-6.666744,4,0.1,, +FM,Bolton FM Bolton Bolton - Dove Mill,Bolton FM,96500000,53.565454,-2.46137,140,0.1,, +FM,Bradley Stoke Radio Bradley Stoke,Bradley Stoke Radio,103400000,51.540136,-2.561134,67,0.025,, +FM,Branch FM Dewsbury,Branch FM,101800000,53.702187,-1.62128,133,0.025,, +FM,BRFM 95.6 FM Sheppey Minster,BRFM 95.6 FM,95600000,51.423502,0.830556,57,0.025,, +FM,Bridge FM Bridgend Mynydd Baedan,Bridge FM,106300000,51.556793,-3.628192,244,0.5,, +FM,Bridge FM Dundee Ninewells Hospital Dundee,Bridge FM,87700000,56.460854,-2.991471,43,5e-05,, +FM,Brill Oldies Radio Brill,Brill Oldies Radio,104700000,51.818986,-1.051142,181,5e-05,, +FM,Bristol Community FM Bristol Barton Hill,Bristol Community FM,93200000,51.454979,-2.562536,25,0.025,, +FM,BRO Radio Barry Barry - Awbery House,BRO Radio,98100000,51.410121,-3.281189,82,0.05,, +FM,BRO Radio Barry Llantwit Major Rugby Club,BRO Radio,100200000,51.407511,-3.47982,46,0.05,, +FM,BRO Radio Penarth,BRO Radio,106100000,51.42835,-3.180864,26,0.015,, +FM,BRO Radio St Athan St Athan - Community Centre,BRO Radio,98400000,51.40142,-3.419818,38,0.025,, +FM,Buchan Radio Peterhead Peterhead - Wilson Street,Buchan Radio,107900000,57.510991,-1.781674,13,0.025,, +FM,Bute Island Radio Isle of Bute Rothesay Town,Bute Island Radio,96500000,55.836375,-5.053216,9,0.025,, +FM,Cabin FM Herne Bay Herne Bay - Council Office,Cabin FM,94600000,51.370251,1.127739,6,0.025,, +FM,Caithness FM Caithness Ben Dorrery,Caithness FM,106500000,58.473372,-3.608383,244,0.630001,, +FM,Calon FM Wrexham Wrexham - Ty Pawb,Calon FM,105000000,53.046449,-2.990571,80,0.025,, +FM,Cam FM Cambridge Cambridge University Library,Cam FM,97200000,52.205053,0.108347,9,0.025,, +FM,Cambridge 105 Radio Cambridge,Cambridge 105 Radio,105000000,52.198956,0.127374,12,0.025,, +FM,CamGlen Radio Cambuslang and Rutherglen Rutherglen,CamGlen Radio,107900000,55.828534,-4.210231,23,0.025,, +FM,Canalside Radio 102.8 FM Bollington - Macclesfield Bollington - Clarence Mill,Canalside Radio 102.8 FM,102800000,53.300333,-2.100342,153,0.05,, +FM,Cando FM Barrow-in-Furness,Cando FM,106300000,54.111664,-3.227816,11,0.015,, +FM,Cando FM Barrow-in-Furness Ulverston - St Mary's Hospice,Cando FM,107300000,54.200466,-3.087573,37,0.025,, +FM,Cannock Chase Radio Burntwood,Cannock Chase Radio,89800000,52.68154,-1.925998,170,0.05,, +FM,Cannock Chase Radio Cannock,Cannock Chase Radio,94000000,52.692526,-2.029252,154,0.025,, +FM,Cannock Chase Radio Rugeley RUGELEY,Cannock Chase Radio,89600000,52.758593,-1.949725,100,0.05,, +FM,Capital Caernarfon Arfon,Capital,103000000,53.01985,-4.273475,300,2.5,, +FM,Capital Birmingham Birmingham - Metropolitan House,Capital,102200000,52.473153,-1.919722,152,0.5,, +FM,Capital Central Scotland Black Hill,Capital,106100000,55.860005,-3.873572,275,10,, +FM,Capital Blackburn Blackburn - Royal Blackburn Hospital,Capital,107000000,53.736137,-2.462328,201,0.25,, +FM,Capital North East England Burnhope,Capital,105300000,54.8219,-1.714848,240,4.2,, +FM,Capital South East Staffordshire Burton,Capital,102400000,52.801463,-1.607194,122,0.05,, +FM,Capital Chorley Chorley - St George's Church,Capital,102800000,53.652054,-2.629496,87,0.025,, +FM,Capital Newport Christchurch,Capital,97400000,51.601588,-2.935417,94,0.25,, +FM,Capital Central Scotland CRAIGKELLY,Capital,105700000,56.071791,-3.233186,181,5,, +FM,Capital London Croydon,Capital,95800000,51.40986,-0.085755,114,1.9998,, +FM,Capital Dartford Tunnel,Capital,95800000,51.469253,0.260425,3,0,, +FM,Capital Derby Drum Hill,Capital,102800000,52.975288,-1.442404,156,0.575,, +FM,Capital Yorkshire Emley Moor,Capital,105100000,53.611985,-1.66456,256,2.5,, +FM,Capital North East England Eston Nab,Capital,106400000,54.556455,-1.120934,237,0.3,, +FM,Capital Banbury Farthinghoe,Capital,107600000,52.044582,-1.224699,157,0.1,, +FM,Capital North East England Fenham,Capital,105600000,54.978039,-1.662966,120,0.05,, +FM,Capital Mansfield Fishpond Hill,Capital,96500000,53.140513,-1.232922,168,0.05,, +FM,Capital North Wales Coast Great Ormes Head,Capital,96300000,53.332716,-3.853487,200,1,, +FM,Capital North Humberside High Hunsley,Capital,105800000,53.803052,-0.565371,164,5,, +FM,Capital Bradford Idle,Capital,105600000,53.832945,-1.752142,215,0.25,, +FM,Capital Stratford Lark Stoke,Capital,102000000,52.081613,-1.728531,260,1.44,, +FM,Capital Warwick LEAMINGTON SPA,Capital,107300000,52.294288,-1.519026,90,0.1,, +FM,Capital South East Staffordshire Lichfield,Capital,101600000,52.63655,-1.759098,145,0.05,, +FM,Capital Liverpool Liverpool Cathedral,Capital,107600000,53.397439,-2.973289,40,0.17825,, +FM,Capital Manchester Manchester - City Tower,Capital,102000000,53.480177,-2.23864,43,0.5,, +FM,Capital Nottingham Mapperley Ridge,Capital,96200000,52.976309,-1.132626,122,0.5,, +FM,Capital Hexham NEWTON,Capital,105800000,54.982447,-1.945593,165,0.1,, +FM,Capital Burnley Pendle Forest,Capital,99800000,53.841893,-2.266074,276,0.5,, +FM,Capital Preston Preston - Bee Lane,Capital,106500000,53.733667,-2.694534,31,0.1,, +FM,Capital Rugby Rugby - Art Gallery,Capital,107100000,52.371554,-1.264669,113,0.1,, +FM,Capital Coventry Samuel Vale House,Capital,96200000,52.413654,-1.513171,91,0.08,, +FM,Capital Sheffield,Capital,105600000,53.379385,-1.513809,247,0.125,, +FM,Capital Leicester Skeffington,Capital,105400000,52.624763,-0.909129,210,4,, +FM,Capital Deeside Storeton,Capital,97100000,53.349848,-3.031394,61,0.5,, +FM,Capital Cardiff Wenallt,Capital,103200000,51.544539,-3.223384,226,1,, +FM,Capital Wrexham Wrexham Rhos,Capital,103400000,53.076278,-3.044994,210,0.7,, +FM,Capital FM Brighton WHITEHAWK HILL,Capital FM,107200000,50.824954,-0.113611,120,0.1,, +FM,Capital South Coast Southampton Chillerton Down,Capital South Coast,103200000,50.649245,-1.328934,166,1,, +FM,Capital Xtra North London Alexandra Palace,Capital Xtra,107100000,51.594459,-0.129257,92,0.1,, +FM,Capital Xtra Brixton Crystal Palace,Capital Xtra,96900000,51.424161,-0.074944,110,0.03,, +FM,Capital/Heart Winchester Crabwood Farm,Capital/Heart,96700000,51.063167,-1.360216,158,0.225,, +FM,Capital/Heart Portsmouth Fort Widley,Capital/Heart,97500000,50.853922,-1.067587,102,0.425,, +FM,Caroline Community Radio Maldon Maldon - Blue Boar Hotel,Caroline Community Radio,94700000,51.732016,0.675995,32,0.1,, +FM,Castledown Radio Ludgershall,Castledown Radio,104700000,51.2513,-1.63773,141,0.05,, +FM,Castledown Radio Tidworth Tidworth - Beech Hill Road,Castledown Radio,107600000,51.241283,-1.655572,127,0.025,, +FM,Celtic Music Radio Glasgow Glasgow - Grafton Place,Celtic Music Radio,95000000,55.864198,-4.245819,40,0.05,, +FM,Central FM Stirling Earls Hill,Central FM,103100000,56.070378,-4.059694,432,0.25,, +FM,CFM West Cumbria Broughton Moor,CFM,102200000,54.682913,-3.463248,125,0.5,, +FM,CFM Carlisle Caldbeck,CFM,96400000,54.773382,-3.091075,289,1,, +FM,CFM Penrith Penrith - Beacon Hill,CFM,102500000,54.674729,-2.728805,263,0.05,, +FM,CFM West Cumbria WHITEHAVEN,CFM,103400000,54.496676,-3.557726,133,0.2,, +FM,Channel 103 Jersey Fremont Point,Channel 103,103700000,49.25148,-2.131167,106,2,, +FM,CHBN Truro Treliske,CHBN,100800000,50.266723,-5.090217,94,0.025,, +FM,Chelmsford Community Radio Chelmsford Chelmsford - Soax House,Chelmsford Community Radio,104400000,51.735001,0.469917,28,0.1,, +FM,Chiltern Voice Amersham and Chesham Amersham - Beacon School,Chiltern Voice,107400000,51.687607,-0.614982,162,0.05,, +FM,Cinque Ports Radio Romney Marsh Ivychurch,Cinque Ports Radio,100200000,51.01255,0.889573,3,0.075,, +FM,Classic FM Tayside Angus,Classic FM,100100000,56.555053,-2.986125,312,8.2,, +FM,Classic FM North West Wales Arfon,Classic FM,100700000,53.01985,-4.273475,300,15,, +FM,Classic FM Bath,Classic FM,100200000,51.38779,-2.332925,175,0.1,, +FM,Classic FM Leeds Beecroft Hill,Classic FM,101600000,53.811543,-1.641253,149,0.5,, +FM,Classic FM Lincolnshire Belmont,Classic FM,100500000,53.335814,-0.171983,125,3.1,, +FM,Classic FM Glasgow Black Hill,Classic FM,101700000,55.860005,-3.873572,275,122.155,, +FM,Classic FM Cardigan Bay Blaenplwyf,Classic FM,101100000,52.360613,-4.102585,174,8,, +FM,Classic FM Swindon Blunsdon,Classic FM,100800000,51.609363,-1.794027,148,0.36,, +FM,Classic FM Bedford Bow Brickhill,Classic FM,100400000,51.998806,-0.669784,169,5,, +FM,Classic FM London Crystal Palace,Classic FM,100600000,51.424161,-0.074944,110,2,, +FM,Classic FM Dartford Tunnel,Classic FM,100900000,51.469253,0.260425,3,0,, +FM,Classic FM Ayrshire DARVEL,Classic FM,101300000,55.579376,-4.289814,286,4,, +FM,Classic FM Northern Ireland Divis A,Classic FM,101900000,54.607538,-6.009471,368,122.155,, +FM,Classic FM Isle of Man DOUGLAS,Classic FM,100200000,54.140551,-4.493075,143,0.68,, +FM,Classic FM Kent DOVER,Classic FM,101800000,51.11163,1.247272,133,4.2,, +FM,Classic FM Teesside Eston Nab,Classic FM,101600000,54.556455,-1.120934,237,0.3,, +FM,Classic FM Newcastle Fenham,Classic FM,101000000,54.978039,-1.662966,120,0.05,, +FM,Classic FM North Wales Great Ormes Head,Classic FM,101600000,53.332716,-3.853487,200,2,, +FM,Classic FM Reading Hemdean,Classic FM,101800000,51.481196,-0.978531,69,0.5,, +FM,Classic FM West Yorkshire Holme Moss,Classic FM,101100000,53.533184,-1.858296,524,125,, +FM,Classic FM Bradford Idle,Classic FM,100300000,53.832945,-1.752142,215,0.25,, +FM,Classic FM West Glamorgan Kilvey Hill,Classic FM,101300000,51.62891,-3.919745,193,0.5,, +FM,Classic FM Londonderry,Classic FM,100500000,55.00421,-7.368968,175,15.5,, +FM,Classic FM Grampian Meldrum,Classic FM,100500000,57.386674,-2.400197,245,75,, +FM,Classic FM Morecambe Bay,Classic FM,101800000,54.202505,-3.167346,259,3.2,, +FM,Classic FM Moray Firth Mounteagle,Classic FM,101400000,57.591626,-4.27683,212,8.80001,, +FM,Classic FM Devon North Hessary Tor,Classic FM,100000000,50.550217,-4.008413,508,80,, +FM,Classic FM Oxfordshire Oxford,Classic FM,101300000,51.790638,-1.179183,130,22.4765,, +FM,Classic FM Cambridgeshire Peterborough,Classic FM,101900000,52.507615,-0.343282,56,20,, +FM,Classic FM Durham PONTOP PIKE,Classic FM,100300000,54.868838,-1.771233,302,64.9999,, +FM,Classic FM South West Wales Presely,Classic FM,100500000,51.944404,-4.661027,323,5.7,, +FM,Classic FM Bristol Pur Down,Classic FM,101400000,51.48519,-2.562764,91,0.1,, +FM,Classic FM Cornwall Redruth,Classic FM,101500000,50.209786,-5.238489,237,8.1,, +FM,Classic FM Hereford RIDGE HILL,Classic FM,100400000,51.997433,-2.53989,204,3.9,, +FM,Classic FM Isle of Wight ROWRIDGE,Classic FM,100300000,50.676724,-1.370435,143,125,, +FM,Classic FM Cumbria Sandale,Classic FM,99900000,54.749109,-3.14076,363,125,, +FM,Classic FM Selkirk SELKIRK,Classic FM,100900000,55.555786,-2.793221,290,5.4,, +FM,Classic FM South Yorkshire Sheffield,Classic FM,101700000,53.379385,-1.513809,247,0.25,, +FM,Classic FM West Midlands SUTTON COLDFIELD,Classic FM,100100000,52.600539,-1.833856,169,125,, +FM,Classic FM Norfolk TACOLNESTON,Classic FM,101500000,52.518759,1.138705,64,119.374,, +FM,Classic FM South Glamorgan Wenvoe,Classic FM,101700000,51.459476,-3.282282,129,125,, +FM,Classic FM Brighton WHITEHAWK HILL,Classic FM,101900000,50.824954,-0.113611,120,0.2,, +FM,Classic FM London Wrotham,Classic FM,100900000,51.320696,0.287702,219,125,, +FM,Classic Hits Forest Gold Epping Epping - St Margaret's Hospital,Classic Hits Forest Gold,99300000,51.70488,0.123306,113,0.001,, +FM,Clyde 1 Glasgow Black Hill,Clyde 1,102500000,55.860005,-3.873572,275,13,, +FM,Clyde 1 Vale of Leven Millburn Muir,Clyde 1,97000000,55.982322,-4.600708,152,0.05,, +FM,Clyde 1 Rosneath,Clyde 1,103300000,55.991937,-4.793795,107,0.05,, +FM,Clyde 1 Rothesay ROTHESAY,Clyde 1,102300000,55.878164,-4.998928,166,0.3,, +FM,Coagh Baptist Drive-In Cookstown, NI Coagh Baptist Church,Coagh Baptist Drive-In,104200000,54.645163,-6.620914,33,5e-05,, +FM,Coast and County Radio Scarborough Oliver's Mount B,Coast and County Radio,97400000,54.267627,-0.404944,151,0.05,, +FM,Coast and County Radio Whitby Whitby - Football Club,Coast and County Radio,105500000,54.489577,-0.627277,47,0.025,, +FM,Coast FM St Just Lands End Radio Station,Coast FM,97200000,50.117779,-5.669653,156,0.1,, +FM,Coast FM Penzance Penzance Cricket Club,Coast FM,96500000,50.124239,-5.545881,64,0.025,, +FM,Coast Radio Ballygalley,Coast Radio,106400000,54.896089,-5.849348,64,0.1,, +FM,Colne Radio Wivenhoe Wivenhoe Town CC,Colne Radio,106600000,51.86508,0.966141,29,0.025,, +FM,Community Voice FM Middlesbrough,Community Voice FM,104500000,54.568152,-1.228488,7,0.025,, +FM,Cool FM/Downtown Radio Belfast Black Mountain,Cool FM/Downtown Radio,97400000,54.586989,-6.022076,301,1.85,, +FM,Cool FM/Downtown Radio Omagh and Enniskillen Brougher Mountain,Cool FM/Downtown Radio,96600000,54.423191,-7.462812,305,5,, +FM,Cool FM/Downtown Radio South Newry Camlough,Cool FM/Downtown Radio,103100000,54.160861,-6.385716,340,0.900001,, +FM,Cool FM/Downtown Radio Newtownabbey Carnmoney Hill,Cool FM/Downtown Radio,103400000,54.676355,-5.928737,207,0.01,, +FM,Cool FM/Downtown Radio Larne,Cool FM/Downtown Radio,97100000,54.862024,-5.828421,119,0.04,, +FM,Cool FM/Downtown Radio Limavady,Cool FM/Downtown Radio,96400000,55.109172,-6.886121,341,1,, +FM,Cool FM/Downtown Radio Londonderry,Cool FM/Downtown Radio,102400000,55.00421,-7.368968,175,5,, +FM,Cool FM/Downtown Radio Newcastle,Cool FM/Downtown Radio,103400000,54.203198,-5.913247,251,0.1005,, +FM,Cool FM/Downtown Radio Ballymena Portglenone,Cool FM/Downtown Radio,102300000,54.863112,-6.429581,207,0.4,, +FM,Corby Radio Corby,Corby Radio,96300000,52.487381,-0.703107,115,0.025,, +FM,Crescent Radio Rochdale Stoneyfield,Crescent Radio,97000000,53.610283,-2.157454,143,0.05,, +FM,Cross Counties Radio Blaby Town Blaby,Cross Counties Radio,95400000,52.562594,-1.169426,76,0.1,, +FM,Cross Counties Radio Lutterworth & Blaby Lutterworth - Magna Park,Cross Counties Radio,92000000,52.466091,-1.243959,125,0.0959,, +FM,Cross Rhythms City Radio Newcastle under Lyme Hanchurch - Duke Lodge,Cross Rhythms City Radio,96600000,52.957473,-2.24081,218,0.025,, +FM,Cross Rhythms City Radio Stoke On Trent Stoke-on-Trent,Cross Rhythms City Radio,101800000,53.043768,-2.123907,213,0.1,, +FM,Cross Rhythms Plymouth Plymouth Mannamead,Cross Rhythms Plymouth,96300000,50.388056,-4.129573,76,0.05,, +FM,Croydon FM Croydon,Croydon FM,97800000,51.40986,-0.085755,114,0.025,, +FM,Crystal FM Midlothian Penicuik Cricket Club,Crystal FM,107400000,55.829332,-3.218157,205,0.015,, +FM,Cumbernauld FM Cumbernauld Cumbernauld - Tay Walk,Cumbernauld FM,106800000,55.947051,-3.99036,140,0.0287,, +FM,Dales Radio Yorkshire Dales Giggleswick,Dales Radio,104900000,54.075664,-2.303636,204,0.05,, +FM,Dales Radio Settle Grassington - Spring House,Dales Radio,104900000,54.079193,-1.988349,298,0.05,, +FM,Dales Radio Ingleton,Dales Radio,103000000,54.146933,-2.471488,120,0.1,, +FM,Dales Radio Leyburn,Dales Radio,104900000,54.314104,-1.838569,239,0.03,, +FM,Dean Radio Bream,Dean Radio,95700000,51.751011,-2.573595,154,0.025,, +FM,Dean Radio Cinderford Cinderford - Belle Vue,Dean Radio,105600000,51.823198,-2.497178,214,0.025,, +FM,Dee 106.3 Chester Chester - Steam Mill,Dee 106.3,106300000,53.193393,-2.88005,25,0.1,, +FM,Deveron FM Banff and Macduff Banff (Battery Green),Deveron FM,107400000,57.67024,-2.526196,20,0.1,, +FM,Deveron FM Banff and Macduff Portsoy,Deveron FM,105900000,57.681488,-2.673424,65,0.025,, +FM,Deveron FM Banff and Macduff Turriff,Deveron FM,105300000,57.534604,-2.481317,56,0.025,, +FM,Digital Hits One North Somerset Fry's Hill,Digital Hits One,100500000,51.297615,-2.809603,238,0.1,, +FM,Diverse FM Luton Luton - Farley Hill,Diverse FM,102800000,51.878444,-0.438278,167,0.2,, +FM,Dover Community Radio Dover Guston,Dover Community Radio,104900000,51.144582,1.316654,126,0.05,, +FM,Down FM Downpatrick,Down FM,105000000,54.324166,-5.697297,62,0.025,, +FM,Drive 105 FM Londonderry Londonderry - Corrody Road,Drive 105 FM,105300000,54.979578,-7.315472,145,0.1,, +FM,Drive in Films (Banbury) Banbury DCS Stadium,Drive in Films (Banbury),95800000,52.034827,-1.319185,112,5e-05,, +FM,Drive in Films (Derby) Derby Mickleover Sports FC,Drive in Films (Derby),87700000,52.922995,-1.54059,103,5e-05,, +FM,Drive-In Theatres (Poole) Poole Farmer Palmer's Farm Park,Drive-In Theatres (Poole),87700000,50.733704,-2.088098,9,5e-05,, +FM,Drystone Radio Ilkley Ilkley - Upper Austby Farm,Drystone Radio,102000000,53.948055,-1.845316,220,0.035,, +FM,Drystone Radio Sutton in Craven Sutton in Craven - Valley Farm,Drystone Radio,103500000,53.886677,-1.981403,272,0.075,, +FM,Dunoon Community Radio Dunoon Dunoon - Argyll Street,Dunoon Community Radio,97400000,55.951328,-4.928325,16,0.025,, +FM,East Coast FM East Lothian Garleton Hills - Lothian Broadband,East Coast FM,107600000,55.974039,-2.781251,179,0.025,, +FM,East Devon Radio East Devon District Aylesbeare,East Devon Radio,94600000,50.706037,-3.349844,152,0.025,, +FM,East Devon Radio Ottery St Mary Ottery St Mary - Finnimore Ind. Estate,East Devon Radio,106100000,50.750811,-3.287199,40,0,, +FM,East Devon Radio Sidmouth Sidmouth Golf Club,East Devon Radio,95000000,50.677602,-3.254765,81,0.025,, +FM,East Devon Radio Exmouth Withycombe Raleigh Common,East Devon Radio,106400000,50.64666,-3.367095,127,0.1,, +FM,EAVA FM Leicester Leicester - Belgrave,EAVA FM,102500000,52.653973,-1.125121,52,0.025,, +FM,Eden FM Penrith Penrith - Newton Rigg,Eden FM,107500000,54.672038,-2.788379,169,0.05,, +FM,Embrace Northampton Northampton - Fire Station,Embrace,104700000,52.241724,-0.894379,88,0.05,, +FM,Endeavour FM Boston Boston - Blackfriars Theatre,Endeavour FM,107000000,52.976151,-0.022467,1,0.05,, +FM,Energy Radio Isle of Man Ballasaig,Energy Radio,105200000,54.290529,-4.348618,189,0.04,, +FM,Energy Radio Isle of Man Beary Peark,Energy Radio,102400000,54.216063,-4.616199,289,0.175,, +FM,Energy Radio Isle of Man DOUGLAS,Energy Radio,98600000,54.140551,-4.493075,143,1.1,, +FM,Energy Radio Isle of Man Jurby Control Tower,Energy Radio,93400000,54.356338,-4.521985,27,0.6,, +FM,Energy Radio Isle of Man Mull Hill,Energy Radio,105200000,54.071137,-4.768804,158,0.025,, +FM,Energy Radio Isle of Man Ramsey Town B,Energy Radio,98400000,54.322428,-4.38596,2,0.014,, +FM,Energy Radio Isle of Man Snaefell,Energy Radio,91200000,54.262536,-4.462965,610,1,, +FM,Erewash Sound Ilkeston,Erewash Sound,96800000,52.970098,-1.309798,93,0.025,, +FM,Erewash Sound Long Eaton Risley,Erewash Sound,103500000,52.931208,-1.331542,131,0.012,, +FM,Express FM Portsmouth Portsmouth - Ladywood House,Express FM,93700000,50.795443,-1.086912,4,0.025,, +FM,Fantasy Radio Devizes,Fantasy Radio,97000000,51.352297,-1.996361,129,0.025,, +FM,Felixstowe Radio Felixstowe Felixstowe - Academy,Felixstowe Radio,107500000,51.973009,1.332237,23,0.025,, +FM,Fever FM Leeds (Harehills&Chapeltown) Leeds - Potternewton Heights,Fever FM,107300000,53.825224,-1.541327,104,0.025,, +FM,Fiesta FM Southampton Midanbury - The Castle,Fiesta FM,95000000,50.92624,-1.364941,62,0.025,, +FM,First FM Oxford Oxford - Windrush Tower,First FM,105100000,51.721028,-1.202737,62,0.1467,, +FM,Flex FM London Wandsworth Wandsworth,Flex FM,101400000,51.447973,-0.215843,42,0.1,, +FM,Forest FM Verwood Cranevalley Golf Club,Forest FM,92300000,50.880448,-1.906134,58,0.025,, +FM,Forest FM Verwood Ferndown,Forest FM,98900000,50.80545,-1.904297,27,0.025,, +FM,Forth 1 Edinburgh Black Hill,Forth 1,97600000,55.860005,-3.873572,275,0.1,, +FM,Forth 1 Edinburgh CRAIGKELLY,Forth 1,97300000,56.071791,-3.233186,181,4.9,, +FM,Forth 1 Penicuik PENICUIK,Forth 1,102200000,55.819142,-3.194377,248,0.25,, +FM,Fosse 107 Hinckley,Fosse 107,107900000,52.546486,-1.371022,131,0.1,, +FM,Fosse 107 Loughborough Loughborough - University,Fosse 107,107000000,52.766045,-1.223955,48,0.08,, +FM,Free Radio Kidderminster KIDDERMINSTER,Free Radio,96700000,52.363252,-2.282794,82,0.05,, +FM,Free Radio Leamington Spa LEAMINGTON SPA,Free Radio,102900000,52.294288,-1.519026,90,0.025,, +FM,Free Radio Worcester MALVERN,Free Radio,102800000,52.115715,-2.330146,161,0.5,, +FM,Free Radio Hereford RIDGE HILL,Free Radio,97600000,51.997433,-2.53989,204,0.4,, +FM,Free Radio Coventry Shilton,Free Radio,97000000,52.450069,-1.397116,106,1,, +FM,Free Radio Birmingham SUTTON COLDFIELD,Free Radio,96400000,52.600539,-1.833856,169,5,, +FM,Free Radio Shrewsbury and Telford The Wrekin,Free Radio,103100000,52.670935,-2.550228,396,1.35,, +FM,Free Radio Wolverhampton Turners Hill B,Free Radio,97200000,52.496363,-2.049153,270,1,, +FM,Fresh FM Barnstaple Barnstaple - North Devon college,Fresh FM,87700000,51.072691,-4.068213,49,5e-05,, +FM,Frome FM Frome Frome - Memorial Theatre,Frome FM,96600000,51.22956,-2.324191,99,0.025,, +FM,Funky SX Southend Southend University Hospital,Funky SX,103700000,51.553907,0.689473,26,0.025,, +FM,fUSe FM Ballymoney Ballymoney - Baptist Church,fUSe FM,107500000,55.065952,-6.485513,65,0.025,, +FM,Future Radio Norwich Norwich - Markham Tower,Future Radio,107800000,52.65073,1.270152,25,0.05,, +FM,Gateway 97.8 Basildon Basildon - University Hospital,Gateway 97.8,97800000,51.558561,0.452552,50,0.075,, +FM,Gaydio Manchester Manchester One,Gaydio,88400000,53.478111,-2.238327,40,0.025,, +FM,Gaydio (Brighton) Brighton and Hove Brighton Race Course,Gaydio (Brighton),97800000,50.829169,-0.112873,124,0.025,, +FM,GBC Radio Gibraltar North Mole,GBC Radio Gibraltar,91300000,36.148333,-5.365833,10,0.2,, +FM,GBC Radio Gibraltar O'Haras Battery,GBC Radio Gibraltar,92600000,36.124006,-5.343083,415,1,, +FM,GEM 106 East Midlands Copt Oak,GEM 106,106000000,52.710153,-1.285042,240,4,, +FM,Glastonbury FM Glastonbury,Glastonbury FM,107100000,51.150542,-2.706801,82,0.025,, +FM,Gloucester FM Gloucester Gloucester - Clapham Court,Gloucester FM,96600000,51.869138,-2.240322,14,0.025,, +FM,Great Driffield Radio Driffield Driffield - Wold Farm,Great Driffield Radio,107200000,54.042204,-0.453755,96,0.0958,, +FM,Greatest Hits Stoke-on-Trent Alsagers Bank,Greatest Hits,102600000,53.027231,-2.28071,220,2,, +FM,Greatest Hits Stafford Pye Green,Greatest Hits,96900000,52.728576,-2.0198,234,0.1,, +FM,Greatest Hits Congleton Sutton Common,Greatest Hits,96400000,53.206041,-2.10072,401,0.125,, +FM,Greatest Hits Radio York Acklam Wold,Greatest Hits Radio,104700000,54.046924,-0.786852,224,2,, +FM,Greatest Hits Radio Liverpool Allerton Park,Greatest Hits Radio,105900000,53.373318,-2.884808,69,4,, +FM,Greatest Hits Radio Andover,Greatest Hits Radio,106400000,51.198771,-1.468544,121,0.1,, +FM,Greatest Hits Radio Barnsley Ardsley,Greatest Hits Radio,102000000,53.550702,-1.412685,94,0.2,, +FM,Greatest Hits Radio Ashbourne Ashbourne - Windmill Lane,Greatest Hits Radio,96700000,53.022497,-1.732059,167,0.2,, +FM,Greatest Hits Radio North Norfolk Aylmerton,Greatest Hits Radio,103200000,52.920542,1.253413,94,0.25,, +FM,Greatest Hits Radio Basingstoke Basingstoke - Fanum House,Greatest Hits Radio,107600000,51.269037,-1.070967,87,0.1,, +FM,Greatest Hits Radio Bath,Greatest Hits Radio,107900000,51.38779,-2.332925,175,0.1,, +FM,Greatest Hits Radio Torbay Beacon Hill,Greatest Hits Radio,105500000,50.446404,-3.610896,188,0.802201,, +FM,Greatest Hits Radio Wigan Billinge Hill,Greatest Hits Radio,102400000,53.509257,-2.71981,170,0.05,, +FM,Greatest Hits Radio Dorchester and Weymouth Bincombe Hill,Greatest Hits Radio,97200000,50.66283,-2.443645,160,0.7,, +FM,Greatest Hits Radio Blackpool Blackpool Tower,Greatest Hits Radio,96500000,53.815794,-3.055258,11,0.2,, +FM,Greatest Hits Radio Blandford Blandford - Tin Pot Lane,Greatest Hits Radio,96600000,50.868862,-2.16252,79,0.1,, +FM,Greatest Hits Radio Swindon Blunsdon,Greatest Hits Radio,107700000,51.609363,-1.794027,148,0.1,, +FM,Greatest Hits Radio Southwold Blythburgh Water Tower,Greatest Hits Radio,97400000,52.311386,1.598136,22,0.2,, +FM,Greatest Hits Radio Bridgwater Bridgwater - Westfield House,Greatest Hits Radio,107400000,51.125924,-3.010084,12,0.1,, +FM,Greatest Hits Radio Dorchester and Weymouth Bridport,Greatest Hits Radio,96000000,50.720805,-2.775678,64,0.1,, +FM,Greatest Hits Radio Alton Brockham Hill,Greatest Hits Radio,102000000,51.180141,-0.968029,213,0.05,, +FM,Greatest Hits Radio Bridlington Buckton Barn,Greatest Hits Radio,102400000,54.124979,-0.218986,105,0.1,, +FM,Greatest Hits Radio North Norfolk Bunkers Hill,Greatest Hits Radio,103200000,52.913141,0.829776,71,0.25,, +FM,Greatest Hits Radio Rutland Burley WT,Greatest Hits Radio,107200000,52.691584,-0.681529,146,0.063546,, +FM,Greatest Hits Radio Buxton,Greatest Hits Radio,106400000,53.275085,-1.911935,432,0.25,, +FM,Greatest Hits Radio Salisbury Camp Hill,Greatest Hits Radio,102000000,51.102938,-1.8427,139,1,, +FM,Greatest Hits Radio Buxton Chapel-en-le-Frith - Quentin Rd,Greatest Hits Radio,106600000,53.32452,-1.90808,221,0.04,, +FM,Greatest Hits Radio Yeovil Charlock Hill,Greatest Hits Radio,105600000,50.959935,-2.551266,143,0.25,, +FM,Greatest Hits Radio Cheltenham,Greatest Hits Radio,107500000,51.894084,-2.076391,65,0.1,, +FM,Greatest Hits Radio Chesterfield,Greatest Hits Radio,107400000,53.283991,-1.427446,179,0.1,, +FM,Greatest Hits Radio Tendring CLACTON,Greatest Hits Radio,100200000,51.803409,1.165049,7,1,, +FM,Greatest Hits Radio London Crystal Palace,Greatest Hits Radio,105800000,51.424161,-0.074944,110,1.732,, +FM,Greatest Hits Radio Dartford Tunnel,Greatest Hits Radio,105800000,51.469253,0.260425,3,0,, +FM,Greatest Hits Radio Doncaster Doncaster - Silverwood House,Greatest Hits Radio,107100000,53.515238,-1.137497,16,0.25,, +FM,Greatest Hits Radio North Norfolk Fakenham WT,Greatest Hits Radio,96200000,52.835379,0.858868,51,0.01,, +FM,Greatest Hits Radio Plymouth Fort Staddon,Greatest Hits Radio,106700000,50.346611,-4.113802,121,0.5,, +FM,Greatest Hits Radio Four Marks Four Marks WT,Greatest Hits Radio,101600000,51.119609,-1.040247,220,0.05,, +FM,Greatest Hits Radio Buxton GLOSSOP,Greatest Hits Radio,106400000,53.455242,-1.960506,262,0.25,, +FM,Greatest Hits Radio Kings Lynn Great Massingham,Greatest Hits Radio,96700000,52.773873,0.649768,92,2,, +FM,Greatest Hits Radio Great Yarmouth Great Yarmouth - Havenbridge House,Greatest Hits Radio,97400000,52.607707,1.721838,1,0.05,, +FM,Greatest Hits Radio Grimsby Grimsby- ATE,Greatest Hits Radio,96400000,53.567838,-0.079989,4,0.1,, +FM,Greatest Hits Radio Guildford GUILDFORD,Greatest Hits Radio,96400000,51.228358,-0.605467,142,1.5,, +FM,Greatest Hits Radio Littlehampton Hammerpot,Greatest Hits Radio,102300000,50.839815,-0.47846,40,0.25,, +FM,Greatest Hits Radio Harrogate Harlow Hill,Greatest Hits Radio,97200000,53.983038,-1.561859,176,0.5,, +FM,Greatest Hits Radio Haslemere,Greatest Hits Radio,97100000,51.091179,-0.736114,172,0.25,, +FM,Greatest Hits Radio Telford Heath Hill,Greatest Hits Radio,107400000,52.666946,-2.474912,198,0.1,, +FM,Greatest Hits Radio Warrington High Warren Reservoir,Greatest Hits Radio,107200000,53.354058,-2.581526,105,0.1,, +FM,Greatest Hits Radio Hindhead,Greatest Hits Radio,101600000,51.114323,-0.730911,256,0.065,, +FM,Greatest Hits Radio Buxton Hope Valley,Greatest Hits Radio,103300000,53.351313,-1.733309,196,0.035,, +FM,Greatest Hits Radio Barnsley Hoylandswaine,Greatest Hits Radio,97100000,53.536407,-1.609935,275,0.05,, +FM,Greatest Hits Radio Hungerford,Greatest Hits Radio,107400000,51.405422,-1.514458,128,0.015,, +FM,Greatest Hits Radio Kidderminster KIDDERMINSTER,Greatest Hits Radio,107200000,52.363252,-2.282794,82,0.1,, +FM,Greatest Hits Radio Midhurst MIDHURST,Greatest Hits Radio,106600000,51.017054,-0.70092,185,0.2,, +FM,Greatest Hits Radio West Somerset Minehead North,Greatest Hits Radio,102400000,51.215281,-3.484815,169,2,, +FM,Greatest Hits Radio Newbury Newbury - Water Tower,Greatest Hits Radio,105600000,51.380344,-1.349196,126,0.1,, +FM,Greatest Hits Radio Northallerton,Greatest Hits Radio,103500000,54.342393,-1.440224,36,0.2,, +FM,Greatest Hits Radio Oldham Oldham Civic Centre,Greatest Hits Radio,96200000,53.542378,-2.11676,203,0.1,, +FM,Greatest Hits Radio Scarborough OLIVERS MOUNT,Greatest Hits Radio,96200000,54.267351,-0.404494,151,0.5,, +FM,Greatest Hits Radio Great Yarmouth and Lowestoft Oulton,Greatest Hits Radio,103400000,52.486385,1.715554,8,1,, +FM,Greatest Hits Radio Pateley Bridge,Greatest Hits Radio,107100000,54.080259,-1.784452,292,0.05,, +FM,Greatest Hits Radio Petersfield,Greatest Hits Radio,101800000,51.004418,-0.936586,61,0.1,, +FM,Greatest Hits Radio West Somerset Porlock,Greatest Hits Radio,100800000,51.20432,-3.599983,160,0.1,, +FM,Greatest Hits Radio Skipton Procter Heights,Greatest Hits Radio,107800000,53.911534,-2.098304,361,0.5,, +FM,Greatest Hits Radio Bristol Pur Down,Greatest Hits Radio,107200000,51.48519,-2.562764,91,0.5,, +FM,Greatest Hits Radio Aylesbury Quainton Hill,Greatest Hits Radio,96200000,51.885386,-0.910988,186,0.5,, +FM,Greatest Hits Radio Reading Reading - Tilehurst Water Tower,Greatest Hits Radio,107000000,51.451561,-1.04712,105,0.1,, +FM,Greatest Hits Radio Oswestry Rhosfach Farm,Greatest Hits Radio,107100000,52.878444,-3.091941,305,0.05,, +FM,Greatest Hits Radio Rotherham,Greatest Hits Radio,96100000,53.416904,-1.351035,115,0.2,, +FM,Greatest Hits Radio Shaftesbury,Greatest Hits Radio,97400000,51.007936,-2.186951,218,0.1,, +FM,Greatest Hits Radio Shrewsbury Shelton,Greatest Hits Radio,106500000,52.716753,-2.793442,91,0.25,, +FM,Greatest Hits Radio Skipton Town Skipton Town - Otley Road,Greatest Hits Radio,107100000,53.961815,-2.010633,118,0.015,, +FM,Greatest Hits Radio Stamford Stamford - New College,Greatest Hits Radio,97400000,52.65786,-0.471118,41,0.025,, +FM,Greatest Hits Radio Matlock Stanton Moor,Greatest Hits Radio,102000000,53.170195,-1.633756,305,0.05,, +FM,Greatest Hits Radio Stockport Stockport - Ratcliffe Towers,Greatest Hits Radio,104900000,53.404991,-2.15549,72,0.32,, +FM,Greatest Hits Radio North Norfolk Stody,Greatest Hits Radio,96200000,52.857485,1.078719,65,1.9,, +FM,Greatest Hits Radio Norwich Stoke Holy Cross,Greatest Hits Radio,99900000,52.574817,1.331664,65,0.25,, +FM,Greatest Hits Radio West Midlands SUTTON COLDFIELD,Greatest Hits Radio,105200000,52.600539,-1.833856,169,5.5,, +FM,Greatest Hits Radio Chichester The Trundle,Greatest Hits Radio,96600000,50.89247,-0.755575,194,0.47,, +FM,Greatest Hits Radio Thirsk Thirsk - Calverts Carpets,Greatest Hits Radio,102300000,54.231543,-1.34385,36,0.1,, +FM,Greatest Hits Radio Leeds Tingley,Greatest Hits Radio,96300000,53.735659,-1.57823,136,2,, +FM,Greatest Hits Radio Bolton Tottington,Greatest Hits Radio,107400000,53.605175,-2.358734,226,0.1001,, +FM,Greatest Hits Radio Uttoxeter,Greatest Hits Radio,101800000,52.923623,-1.834558,152,0.05,, +FM,Greatest Hits Radio Wakefield Wakefield House Flats,Greatest Hits Radio,106800000,53.685368,-1.496211,44,0.5,, +FM,Greatest Hits Radio Warminster,Greatest Hits Radio,107500000,51.215032,-2.18149,204,0.1,, +FM,Greatest Hits Radio Buxton Whalley Bridge - Hawkhurst Farm,Greatest Hits Radio,103300000,53.329589,-1.998009,262,0.2,, +FM,Greatest Hits Radio Whitby WHITBY BUSINESS PARK,Greatest Hits Radio,103100000,54.473215,-0.595572,60,0.06,, +FM,Greatest Hits Radio Ilkley Windsover Farm Middleton,Greatest Hits Radio,107100000,53.946319,-1.82445,231,0.05,, +FM,Greatest Hits Radio Chard Windwhistle,Greatest Hits Radio,106600000,50.882767,-2.876735,216,0.25,, +FM,Greatest Hits Radio Wirksworth,Greatest Hits Radio,101800000,53.072948,-1.558311,254,0.045,, +FM,Greatest Hits Radio Wolverhampton Wolverhampton - Mander House,Greatest Hits Radio,107700000,52.585123,-2.128215,154,0.0849,, +FM,Greatest Hits Radio Worksop Worksop Golf Course,Greatest Hits Radio,107900000,53.291829,-1.108097,62,0.1,, +FM,Greatest Hits Radio Weston-Super-Mare Worlebury Hill,Greatest Hits Radio,107700000,51.359245,-2.970542,105,0.1,, +FM,Groove City Radio GLASGOW Glasgow - Middlesex Street,Groove City Radio,88600000,55.850935,-4.282758,9,0.027,, +FM,GTFM Aberdare Aberdare - Leisure Centre,GTFM,107100000,51.7131,-3.440416,121,0.025,, +FM,GTFM Mountain Ash Mountain Ash GC,GTFM,100700000,51.692996,-3.39261,205,0.023,, +FM,GTFM Pontypridd Pontypridd - Bryn Tail,GTFM,107900000,51.601239,-3.310241,285,0.05,, +FM,Gulshan Radio Wolverhampton Wolverhampton - Blakenhall,Gulshan Radio,106900000,52.571453,-2.133487,168,0.0412,, +FM,Hailsham FM hailsham Hailsham,Hailsham FM,95900000,50.86223,0.258648,26,0.025,, +FM,Hallam FM Barnsley Ardsley,Hallam FM,102900000,53.550702,-1.412685,94,0.2,, +FM,Hallam FM Doncaster Clifton,Hallam FM,103400000,53.457744,-1.220893,138,1,, +FM,Hallam FM Sheffield,Hallam FM,97400000,53.379385,-1.513809,247,0.2,, +FM,Halton Community Radio Halton,Halton Community Radio,92300000,53.331059,-2.696684,79,0.025,, +FM,Harborough FM (Hfm) Market Harborough Little Oxendon Farm,Harborough FM (Hfm),102300000,52.44938,-0.930682,156,0.025,, +FM,Harbour Radio Great Yarmouth Great Yarmouth - King Street,Harbour Radio,107400000,52.604326,1.729234,1,0.032,, +FM,Harrogate Hospital Radio Harrogate Hospital Harrogate District Hospital,Harrogate Hospital Radio,95300000,53.993114,-1.517643,109,0.001,, +FM,Hayes FM London Hayes St Anselms Church,Hayes FM,91800000,51.504667,-0.41939,32,0.025,, +FM,HCR FM Huntingdon,HCR FM,104000000,52.337998,-0.169522,21,0.025,, +FM,Heart Abergavenny,Heart,105200000,51.807795,-3.097781,433,0.022,, +FM,Heart Caernarfon Arfon,Heart,107200000,53.01985,-4.273475,300,2.5,, +FM,Heart Chelmsford Bakers Wood,Heart,102600000,51.705132,0.401129,90,0.9893,, +FM,Heart Bargoed,Heart,107300000,51.701289,-3.238171,292,0.1,, +FM,Heart Bath,Heart,103000000,51.38779,-2.332925,175,0.04,, +FM,Heart Torbay Beacon Hill,Heart,96400000,50.446404,-3.610896,188,0.8,, +FM,Heart Southend Benfleet,Heart,96300000,51.550856,0.5811,80,0.5644,, +FM,Heart Central Scotland Black Hill,Heart,100300000,55.860005,-3.873572,275,10,, +FM,Heart Cardigan Bay Blaenplwyf,Heart,107700000,52.360613,-4.102585,174,10,, +FM,Heart Maidstone and Medway Bluebell Hill (FM),Heart,103100000,51.324198,0.52012,192,2,, +FM,Heart Swindon Blunsdon,Heart,97200000,51.609363,-1.794027,148,0.35,, +FM,Heart Milton Keynes Bow Brickhill,Heart,103300000,51.998806,-0.669784,169,1,, +FM,Heart North East England Burnhope,Heart,101800000,54.8219,-1.714848,240,4.25,, +FM,Heart East Cornwall Caradon Hill,Heart,105100000,50.511364,-4.43683,369,2,, +FM,Heart Carmarthen Carmarthen A,Heart,106000000,51.86652,-4.307942,122,0.01,, +FM,Heart Carmarthenshire Carmel,Heart,105200000,51.818501,-4.066216,255,1.5105,, +FM,Heart Yeovil Chedington,Heart,97100000,50.842173,-2.710226,250,0.2,, +FM,Heart Newport Christchurch,Heart,105900000,51.601588,-2.935417,94,0.5,, +FM,Heart Gloucester and Cheltenham Churchdown Hill,Heart,102400000,51.868264,-2.175397,147,1,, +FM,Heart Central Scotland CRAIGKELLY,Heart,101100000,56.071791,-3.233186,181,4.8,, +FM,Heart London Croydon,Heart,106200000,51.40986,-0.085755,114,2,, +FM,Heart Dartford Tunnel,Heart,106200000,51.469253,0.260425,3,0,, +FM,Heart Dartmouth Dartmouth - Academy,Heart,100800000,50.346257,-3.594714,146,0.1,, +FM,Heart East Kent DOVER,Heart,97000000,51.11163,1.247272,133,0.25,, +FM,Heart Bristol Dundry,Heart,96300000,51.399606,-2.646667,222,0.944061,, +FM,Heart East Kent (Canterbury) Dunkirk,Heart,102800000,51.292731,0.979008,122,0.5,, +FM,Heart Ebbw Vale EBBW VALE,Heart,106100000,51.771714,-3.218938,442,0.012,, +FM,Heart South Wales Eglwysilan,Heart,102300000,51.611393,-3.302735,381,0.25,, +FM,Heart South West Yorkshire Emley Moor,Heart,106200000,53.611985,-1.66456,256,2.5,, +FM,Heart North East England Eston Nab,Heart,100700000,54.556455,-1.120934,237,0.3,, +FM,Heart Exeter Exeter (St Thomas),Heart,97000000,50.719122,-3.562148,102,0.5,, +FM,Heart Banbury Farthinghoe,Heart,97400000,52.044582,-1.224699,157,0.15,, +FM,Heart Fenham,Heart,96200000,54.978039,-1.662966,120,0.05,, +FM,Heart South Wales Fishguard,Heart,106200000,52.012591,-4.996935,123,0.5,, +FM,Heart Ipswich Foxhall Heath,Heart,97100000,52.055203,1.226159,31,2,, +FM,Heart Bury St Edmunds Great Barton,Heart,96400000,52.283402,0.764679,61,1,, +FM,Heart Colwyn Bay Great Ormes Head,Heart,105700000,53.332716,-3.853487,200,1,, +FM,Heart Peterborough Gunthorpe,Heart,102700000,52.617188,-0.247858,3,2,, +FM,Heart Rhyl Gwaenysgor,Heart,107300000,53.32441,-3.386164,226,0.035,, +FM,Heart Basingstoke and Andover HANNINGTON,Heart,102900000,51.307873,-1.244777,220,1.7,, +FM,Heart Harlow Harlow - Rye Hill,Heart,101700000,51.739903,0.097485,110,0.05,, +FM,Heart Hastings,Heart,102000000,50.861622,0.566431,76,0.1,, +FM,Heart Haverhill Haverhill Water Tower,Heart,97400000,52.078154,0.422852,106,0.01,, +FM,Heart Eastbourne Heathfield,Heart,102400000,50.976724,0.230437,158,4.1,, +FM,Heart St Albans Hemel Hempstead (Pimlico),Heart,96600000,51.728571,-0.426217,142,0.5,, +FM,Heart Sunderland Hendon,Heart,101200000,54.893895,-1.363068,10,0.05,, +FM,Heart Henley HENLEY,Heart,103400000,51.533409,-0.877019,109,0.05,, +FM,Heart Aberdare Heolgerrig,Heart,105900000,51.740421,-3.419414,451,0.025,, +FM,Heart Reigate and Crawley Horsham,Heart,97500000,51.064034,-0.322554,52,0.015,, +FM,Heart Barnstaple Huntshaw Cross,Heart,96200000,50.979244,-4.098818,200,2,, +FM,Heart Bradford Idle,Heart,107600000,53.832945,-1.752142,215,0.1,, +FM,Heart Ilfracombe,Heart,97300000,51.197967,-4.138233,203,0.05,, +FM,Heart Ivybridge,Heart,101900000,50.368664,-3.926486,142,0.25,, +FM,Heart Swansea Kilvey Hill,Heart,106000000,51.62891,-3.919745,193,0.5,, +FM,Heart Kingsbridge,Heart,101200000,50.274442,-3.794967,93,1,, +FM,Heart Powys LLANDINAM,Heart,106000000,52.479547,-3.399945,454,0.022,, +FM,Heart Welshpool LONG MOUNTAIN,Heart,102800000,52.644306,-3.086542,400,0.1,, +FM,Heart Cambridge Madingley,Heart,103000000,52.215394,0.036667,62,0.4,, +FM,Heart Marlborough MARLBOROUGH,Heart,96500000,51.417912,-1.699977,192,0.03,, +FM,Heart Somerset MENDIP,Heart,102600000,51.236969,-2.625476,303,2,, +FM,Heart North West England Mersey Tunnel,Heart,105400000,53.404457,-2.994207,3,0.001,, +FM,Heart Vale of Leven Millburn Muir,Heart,100000000,55.982322,-4.600708,152,0.05,, +FM,Heart N. Wales MOEL Y PARC,Heart,106900000,53.220944,-3.31452,336,0.445,, +FM,Heart West Wiltshire Naish Hill,Heart,102200000,51.416202,-2.076595,175,0.24,, +FM,Heart Brighton NEWHAVEN,Heart,96900000,50.787726,0.035848,81,0.05,, +FM,Heart Newmarket Newmarket - Icewell Hill,Heart,97400000,52.24549,0.401489,31,0.004,, +FM,Heart Hexham NEWTON,Heart,101200000,54.982447,-1.945593,165,0.1,, +FM,Heart Bournemouth Nine Barrow Down,Heart,102300000,50.629952,-1.987364,196,1.6,, +FM,Heart Northampton,Heart,96600000,52.275355,-0.885481,127,2,, +FM,Heart Oxford,Heart,102600000,51.790638,-1.179183,130,4.39757,, +FM,Heart Penicuik PENICUIK,Heart,103300000,55.819142,-3.194377,248,0.25,, +FM,Heart Plymouth Plympton,Heart,97000000,50.381346,-4.067802,113,1,, +FM,Heart South Wales Presely,Heart,105700000,51.944404,-4.661027,323,7.5,, +FM,Heart Reading Reading - Fountain House,Heart,97000000,51.454569,-0.977254,43,0.4,, +FM,Heart West Cornwall Redruth,Heart,107000000,50.209786,-5.238489,237,8.54,, +FM,Heart Reigate and Crawley REIGATE,Heart,102700000,51.254494,-0.200986,234,1.8,, +FM,Heart Rosneath,Heart,101100000,55.991937,-4.793795,107,0.1,, +FM,Heart Bedford SANDY HEATH,Heart,96900000,52.130132,-0.241462,54,0.450001,, +FM,Heart Sheffield,Heart,107700000,53.379385,-1.513809,247,0.1,, +FM,Heart Southend Town SOUTHEND TE,Heart,97500000,51.540835,0.698653,33,0.05,, +FM,Heart Cardiff St Hilary,Heart,105400000,51.457374,-3.402986,126,2.4883,, +FM,Heart East Devon Stockland Hill,Heart,103000000,50.807088,-3.104951,227,0.5,, +FM,Heart Great Yarmouth and Norwich Stoke Holy Cross,Heart,102400000,52.574817,1.331664,65,1.65,, +FM,Heart Gloucester and Cheltenham STROUD,Heart,103000000,51.768084,-2.238625,223,0.05,, +FM,Heart West Midlands SUTTON COLDFIELD,Heart,100700000,52.600539,-1.833856,169,5.5,, +FM,Heart Taunton Taunton MF,Heart,96500000,51.02226,-3.088245,21,0.05,, +FM,Heart Tavistock,Heart,96600000,50.524794,-4.137982,167,0.02,, +FM,Heart East Kent Thanet,Heart,95900000,51.36004,1.397292,49,0.135,, +FM,Heart Totnes Totnes - Harpers Hill,Heart,100500000,50.425305,-3.696905,150,0.25,, +FM,Heart Brighton WHITEHAWK HILL,Heart,103500000,50.824954,-0.113611,120,0.450001,, +FM,Heart North West England WINTER HILL,Heart,105400000,53.625547,-2.514794,439,2.5,, +FM,Heart Colchester Wivenhoe Park FM,Heart,96100000,51.877841,0.945039,26,0.25,, +FM,Heart Weston-Super-Mare Worlebury Hill,Heart,103000000,51.359245,-2.970542,105,0.05,, +FM,Heart Wrexham Wrexham Rhos,Heart,88000000,53.076278,-3.044994,210,0.7,, +FM,Heart East Kent (Ashford) WYE,Heart,96100000,51.187004,0.955865,98,0.1,, +FM,Heart Luton Zouches Farm,Heart,97600000,51.877958,-0.483622,207,0.5,, +FM,Heart (Morecambe Bay) Kendal,Heart (Morecambe Bay),103200000,54.314559,-2.708127,172,0.0497,, +FM,Heart (Morecambe Bay) Morecambe Bay,Heart (Morecambe Bay),96900000,54.202505,-3.167346,259,1.6,, +FM,Heart (Morecambe Bay) Windermere WINDERMERE,Heart (Morecambe Bay),102300000,54.37436,-2.951306,216,0.032,, +FM,Heart Hertfordshire Hertford Cole Green,Heart Hertfordshire,106900000,51.787539,-0.135924,71,0.25,, +FM,Heart Hertfordshire Hitchin and Letchworth Letchworth - Briar Patch Lane,Heart Hertfordshire,106900000,51.967745,-0.241566,97,0.05,, +FM,Heart Hertfordshire Stevenage Old Knebworth,Heart Hertfordshire,106700000,51.872155,-0.211306,121,0.1,, +FM,Heartland FM Pitlochry and Aberfeldy Fàire Mhòr,Heartland FM,97500000,56.705122,-3.646339,480,0.2,, +FM,Heritage CR Manchester South Manchester - Duffield Court,Heritage CR,90600000,53.463992,-2.241562,36,0.05,, +FM,Hermitage FM Coalville Coalville - Hoo Ash Farm,Hermitage FM,99200000,52.731179,-1.396357,161,0.025,, +FM,Hitmix Radio Newcastle Under Lyme Alsagers Bank (High Lane),Hitmix Radio,107500000,53.029744,-2.282367,212,0.025,, +FM,Hits Radio Bristol East Dundry,Hits Radio,106500000,51.396006,-2.613844,198,0.5,, +FM,Hits Radio Bournemouth Poole - West Howe,Hits Radio,107600000,50.757093,-1.938987,65,0.5,, +FM,Hits Radio Manchester SADDLEWORTH,Hits Radio,103000000,53.541535,-2.020635,347,2,, +FM,Holywood FM Holywood Palace Barracks,Holywood FM,101000000,54.628269,-5.845388,24,0.025,, +FM,Home Radio Cumbernauld Kilsyth - Corrie Farm,Home Radio,100800000,55.987001,-4.101109,257,0.1,, +FM,Hope FM Bournemouth Poole - Poole House,Hope FM,90100000,50.743305,-1.897054,50,0.075,, +FM,Hospital Radio Medway Gillingham Medway Maritime Hospital,Hospital Radio Medway,87900000,51.379876,0.540568,65,0.0005,, +FM,Hospital Radio Plymouth Plymouth Derriford Hospital,Hospital Radio Plymouth,107300000,50.416131,-4.114081,93,0.001,, +FM,Hot Radio 102.8 Poole CANFORD HEATH,Hot Radio 102.8,102800000,50.744958,-1.949066,63,0.05,, +FM,In2beats FM Bedford Bedford - Murdock Road,In2beats FM,106500000,52.153525,-0.475153,74,0.025,, +FM,Insanity Radio Egham,Insanity Radio,103200000,51.426938,-0.563327,51,0.025,, +FM,Inspiration FM Northampton Northampton - Fire Station,Inspiration FM,107800000,52.241724,-0.894379,88,0.01,, +FM,Inspire FM Luton Luton - Farley Hill,Inspire FM,105100000,51.878444,-0.438278,167,0.06,, +FM,Ipswich Community Radio Ipswich Ipswich - Chantry Academy,Ipswich Community Radio,105700000,52.04551,1.128753,45,0.025,, +FM,Irvine Beat FM Irvine Irvine - Cunninghame House,Irvine Beat FM,107200000,55.613121,-4.674028,4,0.025,, +FM,Island FM Alderney Fort Albert,Island FM,93700000,49.725692,-2.183499,52,0.01,, +FM,Island FM Guernsey Les Touillets,Island FM,104700000,49.457792,-2.579202,76,1,, +FM,Islands FM Isles of Scilly Isles of Scilly - Marine and Coastguard Agency,Islands FM,107900000,49.929237,-6.303709,49,0.1,, +FM,Isle of Wight Radio Isle of Wight Chillerton Down,Isle of Wight Radio,107000000,50.649245,-1.328934,166,0.08,, +FM,Isle of Wight Radio Cowes Cowes (GKN),Isle of Wight Radio,102000000,50.759894,-1.289782,1,0.007,, +FM,Isle of Wight Radio Ryde Ryde Pier,Isle of Wight Radio,102000000,50.73925,-1.160281,0,0.02,, +FM,Isle of Wight Radio Ventnor,Isle of Wight Radio,102000000,50.601933,-1.199325,227,0.025,, +FM,Isles FM Isle of Lewis Eitshal - B,Isles FM,103000000,58.180639,-6.586018,220,2,, +FM,IUR FM Newry Newry GC,IUR FM,101400000,54.151598,-6.351943,159,0.025,, +FM,Jack 2 Oxford Boars Hill A,Jack 2,107900000,51.723637,-1.302007,162,0.1,, +FM,Jack FM Bicester,Jack FM,106400000,51.903204,-1.140941,71,0.03,, +FM,Jack FM Oxford Boars Hill A,Jack FM,106800000,51.723637,-1.302007,162,0.25,, +FM,Jack FM Wallingford,Jack FM,106400000,51.607161,-1.110148,45,0.03,, +FM,Jack FM Wantage,Jack FM,106400000,51.587288,-1.427363,95,0.03,, +FM,Jack FM Witney,Jack FM,106400000,51.78673,-1.489501,80,0.023,, +FM,Jorvik Radio York York - Askrigg House,Jorvik Radio,94800000,53.958123,-1.116954,29,0.1,, +FM,Juice FM Belfast Belfast Belfast - Conway Mill,Juice FM Belfast,103800000,54.599372,-5.950909,19,0.025,, +FM,K107 FM Kirkcaldy Kirkcaldy - Glamis Road,K107 FM,107000000,56.124363,-3.195777,111,0.025,, +FM,Kane FM Guildford GUILDFORD,Kane FM,103700000,51.228358,-0.605467,142,0.025,, +FM,KCC Live Knowsley,KCC Live,99800000,53.411748,-2.840923,31,0.025,, +FM,KCR Moray Tor Sliasg,KCR,107700000,57.609887,-2.965824,293,0.47,, +FM,KeeP 106 Dorchester Bincombe Barn,KeeP 106,106300000,50.671048,-2.457873,165,0.0483,, +FM,Kemet Radio Nottingham Nottingham - Pine View,Kemet Radio,97500000,52.95996,-1.172106,54,0.025,, +FM,Kennet Radio Newbury Newbury East,Kennet Radio,106700000,51.409277,-1.298894,91,0.085,, +FM,Kingdom FM Fife Allanhill Farm,Kingdom FM,105400000,56.316831,-2.777568,96,0.1,, +FM,Kingdom FM Fife East Neuk,Kingdom FM,106300000,56.269954,-2.698467,98,0.15,, +FM,Kingdom FM Kirkcaldy Kirkcaldy - Ravens Craig,Kingdom FM,96600000,56.12053,-3.145152,32,0.01,, +FM,Kingdom FM Dunfermline Knock Hill,Kingdom FM,95200000,56.126954,-3.523569,355,0.1,, +FM,Kingdom FM Glenrothes Purin Hill,Kingdom FM,96100000,56.23998,-3.207285,337,0.25,, +FM,Kiss London Croydon,Kiss,100000000,51.40986,-0.085755,114,2,, +FM,Kiss Peterborough Gunthorpe,Kiss,107700000,52.617188,-0.247858,3,0.1,, +FM,Kiss Cambridge Madingley,Kiss,105600000,52.215394,0.036667,62,0.5,, +FM,Kiss Severn Estuary MENDIP,Kiss,101000000,51.236969,-2.625476,303,20,, +FM,Kiss Ipswich Mendlesham,Kiss,106400000,52.234496,1.107319,64,10,, +FM,Kiss Bristol Pur Down,Kiss,97200000,51.48519,-2.562764,91,0.15,, +FM,Kiss Norwich Stoke Holy Cross,Kiss,106100000,52.574817,1.331664,65,2,, +FM,KMFM Ashford Ashford - Charter House,KMFM,107600000,51.150169,0.874855,52,0.25,, +FM,KMFM Thanet Broadstairs,KMFM,107200000,51.355569,1.408741,48,0.1,, +FM,KMFM Maidstone Coxheath,KMFM,105600000,51.230029,0.495203,117,0.075,, +FM,KMFM Folkestone Creteway Down,KMFM,96400000,51.099738,1.182579,166,0.2,, +FM,KMFM Medway Gillingham,KMFM,100400000,51.371461,0.551166,78,0.15,, +FM,KMFM Dover Guston,KMFM,106800000,51.144582,1.316654,126,0.1,, +FM,KMFM Medway Hoo St Werburgh VHF,KMFM,107900000,51.429038,0.550344,61,0.3,, +FM,KMFM Canterbury Rough Common,KMFM,106000000,51.297298,1.04616,74,0.1,, +FM,KMFM Tonbridge TUNBRIDGE WELLS,KMFM,96200000,51.172557,0.297815,124,0.15,, +FM,KMFM Sevenoaks Wrotham,KMFM,101600000,51.320696,0.287702,219,0.2,, +FM,Koast Radio Ashington,Koast Radio,106600000,55.173328,-1.557073,29,0.15,, +FM,Kohinoor FM Leicester Leicester East,Kohinoor FM,97300000,52.633552,-1.099227,64,0.025,, +FM,KTCRfm Keynsham Keynsham - Burnett Business Park,KTCRfm,105800000,51.392682,-2.478266,95,0.025,, +FM,LBC 97.3 London Croydon,LBC 97.3,97300000,51.40986,-0.085755,114,2,, +FM,LCR1 Leicestershire St. Peter's Highfield Church,LCR1,94100000,52.629934,-1.116632,80,0.002,, +FM,Leeds Dance Community Radio Leeds Leeds - Burnsall Grange,Leeds Dance Community Radio,97800000,53.797363,-1.591578,77,0.0489,, +FM,Legacy 90.1 Hulme,Legacy 90.1,90100000,53.462071,-2.257667,33,0.025,, +FM,Lincoln City Radio Lincoln Lincoln Cathedral,Lincoln City Radio,103600000,53.234314,-0.536361,60,0.05,, +FM,Lincs FM Lincoln Belmont,Lincs FM,102200000,53.335814,-0.171983,125,3.2,, +FM,Lincs FM Grantham,Lincs FM,96700000,52.892406,-0.655971,115,0.035,, +FM,Lincs FM Scunthorpe Scunthorpe - Trent View House,Lincs FM,97600000,53.57127,-0.670181,42,0.025,, +FM,Link FM Sheffield Sheffield - School Road,Link FM,96700000,53.38365,-1.501733,186,0.025,, +FM,Lionheart Radio Alnwick Alnwick Moor,Lionheart Radio,107300000,55.407804,-1.722334,133,0.022,, +FM,Lisburn's 98FM Lisburn,Lisburn's 98FM,98800000,54.512999,-6.041404,35,0.025,, +FM,Liverpool Community Radio Liverpool Liverpool - Holt Road,Liverpool Community Radio,106700000,53.409869,-2.947697,57,0.1,, +FM,Lochbroom FM Ullapool Polbain,Lochbroom FM,96800000,58.044513,-5.392429,150,0.5,, +FM,Lochbroom FM Ullapool,Lochbroom FM,102200000,57.892557,-5.135709,143,0.7,, +FM,London Greek Radio Haringey Highgate - Cholmeley Lodge,London Greek Radio,103300000,51.570602,-0.144523,117,0.1,, +FM,Magic 105.4 London Croydon,Magic 105.4,105400000,51.40986,-0.085755,114,2,, +FM,Magic 105.4 Dartford Tunnel,Magic 105.4,105400000,51.469253,0.260425,3,0,, +FM,Mansfield 103.2 Mansfield Fishpond Hill,Mansfield 103.2,103200000,53.140513,-1.232922,168,0.1,, +FM,Manx Radio (FM) Isle of Man DOUGLAS,Manx Radio (FM),97200000,54.140551,-4.493075,143,5.65999,, +FM,Manx Radio (FM) Isle of Man JURBY,Manx Radio (FM),103700000,54.3611,-4.517812,27,1.95,, +FM,Manx Radio (FM) Isle of Man Peel,Manx Radio (FM),89500000,54.217653,-4.706962,96,0.05,, +FM,Manx Radio (FM) Ramsey Ramsey Town,Manx Radio (FM),89500000,54.322598,-4.386431,2,0.05,, +FM,Manx Radio (FM) Isle of Man Snaefell,Manx Radio (FM),89000000,54.262536,-4.462965,610,0.93,, +FM,Maritime Radio Greenwich Woolwich - Shrewsbury House,Maritime Radio,96500000,51.473349,0.072851,120,0.025,, +FM,Marlow FM Marlow Handy Cross Farm,Marlow FM,97500000,51.596236,-0.766902,105,0.025,, +FM,Mearns FM Inverbervie,Mearns FM,106200000,56.836148,-2.283512,41,0.1,, +FM,Mearns FM Laurencekirk,Mearns FM,107300000,56.829397,-2.416189,247,0.1,, +FM,Mearns FM Porthlethen,Mearns FM,105100000,57.059909,-2.127266,85,0.05,, +FM,Mearns FM Stonehaven,Mearns FM,105700000,56.952633,-2.206963,68,0.15,, +FM,Meridian FM East Grinstead East Grinstead VHF,Meridian FM,107000000,51.129409,-0.001901,126,0.025,, +FM,Metro Radio Tyne and Wear Burnhope,Metro Radio,97100000,54.8219,-1.714848,240,4,, +FM,Metro Radio Tyne and Wear Fenham,Metro Radio,103000000,54.978039,-1.662966,120,0.05,, +FM,Metro Radio Hexham NEWTON,Metro Radio,103200000,54.982447,-1.945593,165,0.1,, +FM,Metro Radio Alnwick Shilbottle,Metro Radio,102600000,55.366418,-1.700379,171,0.1,, +FM,Metro Radio Tyne Tunnel,Metro Radio,97100000,54.987484,-1.485827,1,0.015,, +FM,Mighty Radio Southport Southport - Holy Trinity Church,Mighty Radio,107900000,53.650278,-2.996805,6,0.025,, +FM,Mix 92.6 St Albans St Albans - St Peters Church),Mix 92.6,92600000,51.755459,-0.335019,121,0.075,, +FM,MKFM Bletchley Bletchley - MK Stadium,MKFM,102100000,52.009052,-0.734754,77,0.05,, +FM,MKFM Milton Keynes Milton Keynes - Saxon Gate,MKFM,106300000,52.043319,-0.761199,111,0.05,, +FM,MKFM Wolverton Wolverton - Stratford Road,MKFM,95000000,52.06252,-0.818579,79,0.05,, +FM,MônFM Anglesey Amlwch Nebo,MônFM,102500000,53.390547,-4.301313,160,0.2,, +FM,MônFM Anglesey Gwalchmai,MônFM,102100000,53.263035,-4.416069,97,0.1,, +FM,MônFM Anglesey Penmynydd,MônFM,96800000,53.248889,-4.228493,102,0.2,, +FM,Moonbeamers Drive-in Billericay Barleylands Craft Village,Moonbeamers Drive-in,106800000,51.600426,0.445999,25,5e-05,, +FM,Moorlands Radio Biddulph Biddulph Moor - Rails Farm,Moorlands Radio,103700000,53.123677,-2.127723,328,0.0253,, +FM,Moorlands Radio Leek Leek - Stockwell Villas,Moorlands Radio,97300000,53.107922,-2.024009,187,0.025,, +FM,Moray Firth Radio FM Caithness Ben Dorrery,Moray Firth Radio FM,102500000,58.473372,-3.608383,244,0.6004,, +FM,Moray Firth Radio FM Speyside Cairn Gorm,Moray Firth Radio FM,96600000,57.123819,-3.646048,1085,0.175,, +FM,Moray Firth Radio FM Fraserburgh Mormond Hill DAB,Moray Firth Radio FM,96700000,57.602976,-2.029937,225,0.05,, +FM,Moray Firth Radio FM Inverness Mounteagle,Moray Firth Radio FM,97400000,57.591626,-4.27683,212,5,, +FM,Moray Firth Radio FM Wick Newton Wick,Moray Firth Radio FM,96700000,58.431247,-3.118201,46,0.01,, +FM,Moray Firth Radio FM Keith Tor Sliasg,Moray Firth Radio FM,102800000,57.609887,-2.965824,293,0.506499,, +FM,More Radio Eastbourne Butts Brow,More Radio,107500000,50.791712,0.240347,184,0.1,, +FM,More Radio Worthing Findon,More Radio,107700000,50.853924,-0.410233,124,0.125,, +FM,More Radio Hastings,More Radio,107800000,50.861622,0.566431,76,0.1,, +FM,More Radio Haywards Heath,More Radio,106400000,50.99144,-0.092619,91,0.24,, +FM,More Radio Lewes Lewes County Hall,More Radio,106800000,50.871877,0.001136,40,0.05,, +FM,MSR 103.8 Burgess Hill Burgess Hill - Tower House,MSR 103.8,103800000,50.953117,-0.125228,60,0.025,, +FM,Nation Radio Cardigan Bay Blaenplwyf,Nation Radio,103300000,52.360613,-4.102585,174,4.6,, +FM,Nation Radio Swansea Carmel,Nation Radio,102900000,51.818501,-4.066216,255,1.6,, +FM,Nation Radio Solent Chillerton Down,Nation Radio,106000000,50.649245,-1.328934,166,2,, +FM,Nation Radio Winchester Crabwood Farm B,Nation Radio,106600000,51.06398,-1.360918,152,0.01,, +FM,Nation Radio Kingston-Upon-Hull Humber Bridge - North Pier,Nation Radio,99800000,53.714254,-0.450805,0,0.5,, +FM,Nation Radio Swansea Kilvey Hill,Nation Radio,107300000,51.62891,-3.919745,193,1,, +FM,Nation Radio Cardigan Bay Mynydd Pencarreg,Nation Radio,96600000,52.067402,-4.076523,411,0.2,, +FM,Nation Radio Cardigan Bay Penwaun,Nation Radio,97400000,52.065055,-4.69417,182,0.2,, +FM,Nation Radio Presely,Nation Radio,107100000,51.944404,-4.661027,323,2.0079,, +FM,Nation Radio Ipswich Warren Heath,Nation Radio,102000000,52.037726,1.200836,32,2,, +FM,Nation Radio Cardiff Wenvoe,Nation Radio,106800000,51.459476,-3.282282,129,2.5,, +FM,Nation Radio Scotland Glasgow Cathkin (S),Nation Radio Scotland,96300000,55.798001,-4.193822,206,2,, +FM,Nation Radio Scotland Helensburgh Helensburgh - East Princess St,Nation Radio Scotland,106900000,56.002997,-4.729464,8,0.025,, +FM,Nation's Easy Radio Southampton Chilworth,Nation's Easy Radio,107800000,50.960084,-1.423285,85,0.5,, +FM,Nation's Easy Radio Winchester Crabwood Farm B,Nation's Easy Radio,107200000,51.06398,-1.360918,152,0.1,, +FM,Nation's Easy Radio Portsmouth Fort Southwick,Nation's Easy Radio,107400000,50.858847,-1.110965,124,0.1,, +FM,Nation's Easy Radio Swansea Kilvey Hill,Nation's Easy Radio,102100000,51.62891,-3.919745,193,1,, +FM,Nations Radio Poole Poole - West Howe,Nations Radio,106600000,50.757093,-1.938987,65,0.25,, +FM,Nevis Radio Skye/Mallaig Cnoc Malagan,Nevis Radio,102300000,57.103288,-5.874998,185,0.8,, +FM,Nevis Radio Fort William Creag a' Chàil,Nevis Radio,96600000,56.827212,-5.054422,269,0.3,, +FM,Nevis Radio Loch Leven Glenachulish,Nevis Radio,102400000,56.681764,-5.211986,255,0.8,, +FM,Nevis Radio Glencoe,Nevis Radio,97000000,56.623839,-4.836427,680,0.2,, +FM,New Style Radio 98.7 FM Birmingham Birmingham - Cambridge Tower,New Style Radio 98.7 FM,98700000,52.480787,-1.909401,137,0.016,, +FM,Ninesprings FM Yeovil Cricket St Thomas GC,Ninesprings FM,107600000,50.880198,-2.895308,222,0.025,, +FM,Ninesprings FM Yeovil Wincanton - Racecourse,Ninesprings FM,103300000,51.065979,-2.414666,112,0.025,, +FM,Ninesprings FM Yeovil Yeovil - Huish Park,Ninesprings FM,104500000,50.950212,-2.673293,94,0.1,, +FM,NLive Northampton Northampton - Fire Station,NLive,106900000,52.241724,-0.894379,88,0.025,, +FM,Nomad Radio Hammersmith Hammersmith - Kings St,Nomad Radio,92200000,51.492361,-0.231397,6,0.1,, +FM,North Manchester FM Manchester Manchester - Kentmere Court,North Manchester FM,106600000,53.528173,-2.189733,102,0.05,, +FM,Northsound 1 Aberdeen Balgownie,Northsound 1,97600000,57.184868,-2.122071,74,0.025,, +FM,Northsound 1 Aberdeen Durris,Northsound 1,96900000,56.999987,-2.389944,325,5.1,, +FM,Northsound 1 Peterhead Stirling Hill,Northsound 1,103000000,57.460172,-1.794644,81,0.174,, +FM,Nova Radio North East 102.5 FM Newcastle Upon Tyne Newcastle upon Tyne - Westgate Community College,Nova Radio North East 102.5 FM,102500000,54.977313,-1.660472,120,0.025,, +FM,Nu Sound Radio Newham Stratford New Town,Nu Sound Radio,92000000,51.550148,-0.005011,5,0.025,, +FM,Oban FM Oban,Oban FM,103300000,56.40396,-5.485882,122,0.2364,, +FM,Oldham Community Radio Oldham Oldham Civic Centre,Oldham Community Radio,99700000,53.542378,-2.11676,203,0.025,, +FM,Original 106 Aberdeen Balgownie,Original 106,106300000,57.184868,-2.122071,74,0.025,, +FM,Original 106 Aberdeen Durris,Original 106,106800000,56.999987,-2.389944,325,9.00001,, +FM,Original 106 Peterhead Peterhead FC,Original 106,106300000,57.511465,-1.796526,26,0.175,, +FM,Park Radio Diss,Park Radio,107600000,52.379033,1.106883,42,0.2,, +FM,Park Radio Harleston,Park Radio,105200000,52.397642,1.30594,39,0.025,, +FM,Pendle Community Radio Nelson Brierfield,Pendle Community Radio,103100000,53.822137,-2.212023,265,0.025,, +FM,Penistone FM Penistone Hoylandswaine (B),Penistone FM,95700000,53.536589,-1.610386,275,0.025,, +FM,Peterborough Community Radio Peterborough Peterborough - Cumberland House,Peterborough Community Radio,103200000,52.574323,-0.237171,8,0.05,, +FM,Phoenix FM Brentwood Brentwood - Gibraltar House,Phoenix FM,98000000,51.602337,0.298078,113,0.025,, +FM,Phoenix FM Halifax,Phoenix FM,96700000,53.713832,-1.846175,233,0.025,, +FM,Phonic FM Exeter Exeter - Pheonix Arts Centre,Phonic FM,106800000,50.725287,-3.531325,45,0.025,, +FM,Pirate FM East Cornwall Caradon Hill,Pirate FM,102200000,50.511364,-4.43683,369,2,, +FM,Pirate FM West Cornwall Redruth,Pirate FM,102800000,50.209786,-5.238489,237,8.3,, +FM,Platform B Brighton Sussex Heights,Platform B,105500000,50.822557,-0.149203,19,0.025,, +FM,Pride FM NEWCASTLE Newcastle - Shieldfield House,Pride FM,89200000,54.978297,-1.599687,41,0.05,, +FM,Pulse 1 Bradford Idle,Pulse 1,97500000,53.832945,-1.752142,215,0.25,, +FM,Pulse 1 Halifax and Huddersfield Vicars Lot,Pulse 1,102500000,53.651685,-1.892848,340,1,, +FM,Pulse Community Radio Barrhead Barrhead - St Lukes School,Pulse Community Radio,98400000,55.789931,-4.385746,103,0.025,, +FM,Purbeck Coast FM Swanage Nine Barrow Down,Purbeck Coast FM,101200000,50.629952,-1.987364,196,0.05,, +FM,Pure Radio Dundee Dundee Law,Pure Radio,102000000,56.470214,-2.989443,174,0.1,, +FM,Pure Radio Perth Kirkton Mailer,Pure Radio,106600000,56.372293,-3.453361,178,0.2528,, +FM,Q 102.9 Londonderry Foyle Hill,Q 102.9,102900000,54.993579,-7.380227,183,5,, +FM,Q Radio 96.7/102.5FM Bangor,Q Radio 96.7/102.5FM,102500000,54.661435,-5.675928,21,0.04,, +FM,Q Radio 96.7/102.5FM Belfast Black Mountain,Q Radio 96.7/102.5FM,96700000,54.586989,-6.022076,301,0.3,, +FM,Q Radio 96.7/102.5FM Newtownabbey Carnmoney Hill,Q Radio 96.7/102.5FM,102500000,54.676355,-5.928737,207,0.02,, +FM,Q100.5 Armagh Armagh - City Hotel,Q100.5,107600000,54.344707,-6.650706,54,0.15,, +FM,Q100.5 Newry Camlough,Q100.5,100500000,54.160861,-6.385716,340,0.5,, +FM,Q100.5 Kilkeel,Q100.5,101100000,54.095375,-6.041723,267,0.217,, +FM,Q100.5 Newcastle Tullybrannigan Raod,Q100.5,101100000,54.217544,-5.916663,45,0.1,, +FM,Q101.2 Omagh and Enniskillen Brougher Mountain B,Q101.2,101200000,54.424515,-7.457403,293,5,, +FM,Q101.2 Enniskillen Coles Hill,Q101.2,102100000,54.342771,-7.654519,64,0.03,, +FM,Q106/7 Dungannon,Q106/7,107200000,54.505165,-6.768771,84,0.25,, +FM,Q106/7 Maghera Seefin Hill,Q106/7,106300000,54.866046,-6.726181,196,0.025,, +FM,Q106/7 Cookstown Tulnagee Quarry,Q106/7,106000000,54.728374,-6.721965,270,0.5,, +FM,Q107 Larne Gransha,Q107,107600000,54.800061,-5.698616,126,0.15,, +FM,Q107 Ballymena Portglenone,Q107,107000000,54.863112,-6.429581,207,0.5,, +FM,Q97.2 Ballycastle Drumeeny,Q97.2,97600000,55.192111,-6.23419,86,0.025,, +FM,Q97.2 Coleraine Maddybenny More,Q97.2,97200000,55.178615,-6.671493,62,0.25,, +FM,Quality Radio Paisley Paisley - Oliphant Court,Quality Radio,107500000,55.824656,-4.466108,75,0.05,, +FM,Quay FM Alderney Fort Albert,Quay FM,107100000,49.725692,-2.183499,52,0.025,, +FM,Raaj FM Sandwell West Bromwich Sandwell,Raaj FM,91300000,52.515701,-1.989789,162,0.025,, +FM,Radio Ashford Ashford Ashford - William Harvey Hospital,Radio Ashford,107100000,51.142455,0.917557,63,0.02,, +FM,Radio Borders Borders Berwick-upon-Tweed,Radio Borders,102300000,55.785854,-2.032822,129,0.01,, +FM,Radio Borders Borders EYEMOUTH,Radio Borders,103400000,55.83228,-2.085863,197,0.5,, +FM,Radio Borders Borders PEEBLES,Radio Borders,103100000,55.661832,-3.227732,352,0.035,, +FM,Radio Borders Borders SELKIRK,Radio Borders,96800000,55.555786,-2.793221,290,5,, +FM,Radio Bronglais Aberystwyth Bronglais Hospital,Radio Bronglais,87800000,52.416008,-4.071692,20,0.0005,, +FM,Radio Cardiff Cardiff Cardiff - Nelson House,Radio Cardiff,98700000,51.470082,-3.171,4,0.025,, +FM,Radio Carmarthenshire Carmarthen,Radio Carmarthenshire,97500000,51.867126,-4.309135,137,0.005,, +FM,Radio Carmarthenshire Carmarthenshire Carmel,Radio Carmarthenshire,97100000,51.818501,-4.066216,255,1.5105,, +FM,Radio Carmarthenshire Llanelli LLANELLI,Radio Carmarthenshire,97500000,51.699961,-4.157337,111,0.2,, +FM,Radio City Liverpool Allerton Park,Radio City,96700000,53.373318,-2.884808,69,4.1,, +FM,Radio City Mersey Tunnel,Radio City,96700000,53.404457,-2.994207,3,0.001,, +FM,Radio Dawn Nottingham Mapperley,Radio Dawn,107600000,52.979731,-1.133451,125,0.05,, +FM,Radio Essex Chelmsford Church Green,Radio Essex,107700000,51.715584,0.574257,110,0.325,, +FM,Radio Essex Southend SOUTHEND TE,Radio Essex,105100000,51.540835,0.698653,33,0.2378,, +FM,Radio Exe Exeter Exeter (St Thomas),Radio Exe,107300000,50.719122,-3.562148,102,1,, +FM,Radio Faza 97.1 FM Nottingham Mapperley,Radio Faza 97.1 FM,97100000,52.979731,-1.133451,125,0.05,, +FM,Radio Gibraltar Plus Upper Rock,Radio Gibraltar Plus,100500000,36.133162,-5.345733,400,0.2,, +FM,Radio Glangwili Carmarthen Glangwili General Hospital,Radio Glangwili,87700000,51.868925,-4.28613,24,5e-05,, +FM,Radio Hartlepool Hartlepool,Radio Hartlepool,102400000,54.686616,-1.216936,12,0.025,, +FM,Radio Ikhlas Derby Normanton,Radio Ikhlas,107800000,52.906966,-1.502458,95,0.025,, +FM,Radio Jackie Kingston Tolworth Tower,Radio Jackie,107800000,51.379884,-0.281001,29,0.4,, +FM,Radio LaB Luton Luton - Farley Hill,Radio LaB,97100000,51.878444,-0.438278,167,0.025,, +FM,Radio Leyland Leyland Leyland - Farington,Radio Leyland,104800000,53.702614,-2.6866,43,0.05,, +FM,Radio MAC Bradford Bradford - Richmond Building,Radio MAC,107100000,53.791802,-1.76301,127,0.025,, +FM,Radio Mount Vernon London Mount Vernon,Radio Mount Vernon,95100000,51.615389,-0.448479,105,0.0005,, +FM,Radio Newark Newark Newark - St Mary Magdalene Church,Radio Newark,107800000,53.076705,-0.808623,18,0.025,, +FM,Radio Newquay Newquay Newquay - Lane Theatre,Radio Newquay,106100000,50.400677,-5.055861,61,0.1,, +FM,Radio North Angus Arbroath Arbroath Infirmary,Radio North Angus,96600000,56.554563,-2.597638,17,0.2,, +FM,Radio North Angus Carnoustie Carnoustie High School,Radio North Angus,107500000,56.506001,-2.724898,26,0.025,, +FM,Radio North Angus Arbroath Monifieth - High School,Radio North Angus,105900000,56.47904,-2.840142,21,0.025,, +FM,Radio North Angus FM Brechin High School,Radio North Angus FM,87700000,56.736949,-2.672732,80,5e-05,, +FM,Radio North Angus FM Montrose Montrose Academy,Radio North Angus FM,87700000,56.70939,-2.463211,2,5e-05,, +FM,Radio North Angus FM Whitehills Forfar Whitehills Health Centre,Radio North Angus FM,87700000,56.651326,-2.870882,75,5e-05,, +FM,Radio Panj Coventry William Batchelor House,Radio Panj,99600000,52.413553,-1.510526,92,0.01,, +FM,Radio Pembrokeshire Pembrokeshire Fishguard,Radio Pembrokeshire,107500000,52.012591,-4.996935,123,0.09,, +FM,Radio Pembrokeshire Pembrokeshire Haverfordwest,Radio Pembrokeshire,102500000,51.899195,-4.86653,186,9.54993,, +FM,Radio Pembrokeshire Pembrokeshire Tenby VHF,Radio Pembrokeshire,107500000,51.679343,-4.701725,52,0.07,, +FM,Radio Plus Coventry Coventry - Mercia House,Radio Plus,101500000,52.408177,-1.514849,76,0.025,, +FM,Radio Redhill East Surrey East Surrey Hospital,Radio Redhill,100400000,51.218538,-0.161434,66,0.001,, +FM,Radio Reverb Brighton & Hove Brighton Race Course,Radio Reverb,97200000,50.829169,-0.112873,124,0.07,, +FM,Radio Saltire Musselburgh Carberry - Hillhead Farm,Radio Saltire,106700000,55.917198,-3.000107,152,0.025,, +FM,Radio Saltire Tranent,Radio Saltire,107200000,55.94425,-2.954051,86,0.025,, +FM,Radio Sangam Huddersfield Huddersfield - Royal Infirmary,Radio Sangam,107900000,53.655545,-1.815067,187,0.02,, +FM,Radio Scarborough Scarborough Oliver's Mount B,Radio Scarborough,107600000,54.267627,-0.404944,151,0.025,, +FM,Radio Skye Skye & Lochalsh Portree,Radio Skye,106200000,57.41693,-6.193925,51,0.03,, +FM,Radio Skye Skye & Lochalsh Sabhal Mòr Ostaig,Radio Skye,106200000,57.086557,-5.881012,42,0.1,, +FM,Radio Skye Skye & Lochalsh Skriaig,Radio Skye,102700000,57.38642,-6.24289,390,3.65,, +FM,Radio Skye Skye & Lochalsh Staffin - caravan & camping site,Radio Skye,107200000,57.62189,-6.196103,88,0.1,, +FM,Radio St Austell Bay St Austell St Austell RFC,Radio St Austell Bay,105600000,50.32788,-4.777309,90,0.025,, +FM,Radio Tamworth Tamworth Tamworth - Strode House,Radio Tamworth,106800000,52.633266,-1.698832,63,0.025,, +FM,Radio Tircoed Swansea North-West Tircoed,Radio Tircoed,106500000,51.682376,-3.997518,67,0.025,, +FM,Radio Tyneside Newcastle-Upon-Tyne Newcastle - Royal Victoria Infirmary,Radio Tyneside,93600000,54.980525,-1.622165,70,0.05,, +FM,Radio Wimborne Wimborne,Radio Wimborne,94600000,50.784535,-1.981933,42,0.025,, +FM,Radio Winchcombe Winchcombe Winchcombe - Encounter Church,Radio Winchcombe,107100000,51.955961,-1.967212,94,0.025,, +FM,Radio Wyvern Worcester Worcester - Perry Wood School,Radio Wyvern,106700000,52.193696,-2.193793,73,0.1,, +FM,Radio X London Crystal Palace,Radio X,104900000,51.424161,-0.074944,110,1.9,, +FM,Radio X Manchester Manchester - City Tower,Radio X,97700000,53.480177,-2.23864,43,0.5,, +FM,Radio2Funky Leicester Leicester - Granby Street,Radio2Funky,95000000,52.632511,-1.128357,67,0.025,, +FM,Raidió Fáilte Belfast Belfast - Divis Flats,Raidió Fáilte,107100000,54.599944,-5.942214,14,0.29,, +FM,Rainbow Radio SE_London Crystal Palace,Rainbow Radio,92400000,51.424161,-0.074944,110,0.03,, +FM,Red Kite Radio Haddenham and Thame Haddenham,Red Kite Radio,107200000,51.769341,-0.914507,71,0.04,, +FM,Redroad FM Rother Valley Wales Sheffield,Redroad FM,102400000,53.338861,-1.284487,119,0.025,, +FM,Reprezent 107.3 FM Lewisham Honor Oak - Greystead Road,Reprezent 107.3 FM,107300000,51.447582,-0.055538,88,0.08,, +FM,Resonance FM London Guys Hospital Tower,Resonance FM,104400000,51.503766,-0.087153,3,0.025,, +FM,Revival FM Glasgow Glasgow - Dundasvale Court - Block 22,Revival FM,93000000,55.867219,-4.257501,29,0.05,, +FM,Revive FM Newham Forest Gate - Mosque,Revive FM,94000000,51.54622,0.024816,7,0.1,, +FM,Revolution Radio Northampton Northampton - St James,Revolution Radio,96100000,52.238473,-0.921991,62,0.01,, +FM,Rhondda Radio Rhondda Valley Ferndale Shooting Club,Rhondda Radio,97500000,51.663145,-3.43724,392,0.0245,, +FM,Rhondda Radio Rhondda Ystrad - Nant y Gwyddon landfill,Rhondda Radio,106100000,51.637868,-3.474302,312,0.025,, +FM,Ribble FM Ribble Clitheroe,Ribble FM,106700000,53.87373,-2.39008,91,0.05,, +FM,Rinse FM London City East Peregrine House,Rinse FM,106800000,51.529704,-0.100628,22,0.025,, +FM,Riviera FM Torbay Torquay Town,Riviera FM,107900000,50.463334,-3.529328,67,0.17,, +FM,RNA FM Scotland Angus - Webster's High School,RNA FM,107900000,56.670235,-3.006984,131,0.0007,, +FM,RNIB Connect Radio West Glasgow Glasgow west - Anniesland Cort,RNIB Connect Radio,101000000,55.891042,-4.324777,28,0.025,, +FM,Rock FM Preston and Blackpool WINTER HILL,Rock FM,97400000,53.625547,-2.514794,439,1,, +FM,Rossendale Radio Rossendale Rawtenstall - Hardmans Business Centre,Rossendale Radio,104700000,53.696842,-2.297569,157,0.05,, +FM,RWS FM 103.3 Bury St Edmunds Bury St Edmunds - West Suffolk Hospital,RWS FM 103.3,103300000,52.231722,0.709179,57,0.075,, +FM,Sabras Sound FM Leicester Leicester - Corah Works,Sabras Sound FM,91000000,52.643163,-1.134352,52,0.1,, +FM,Sabras Sound FM Loughborough Loughborough - University,Sabras Sound FM,102100000,52.766045,-1.223955,48,0.1,, +FM,Salaam Radio Peterborough Peterborough - Cumberland House,Salaam Radio,106200000,52.574323,-0.237171,8,0.025,, +FM,Salford City Radio Salford Swinton Civic Centre,Salford City Radio,94400000,53.510582,-2.339991,68,0.025,, +FM,Scout Radio Chingford Gilwell Park,Scout Radio,99600000,51.649824,0.00221,75,0.001,, +FM,Seaforde - Drive in Church Downpatrick, NI Seaforde (drive in movie),Seaforde - Drive in Church,107200000,54.306741,-5.848547,57,5e-05,, +FM,Seahaven FM Eastbourne Butts Brow,Seahaven FM,95600000,50.791712,0.240347,184,0.025,, +FM,Seahaven FM Newhaven Newhaven MCA,Seahaven FM,96300000,50.782266,0.049797,50,0.1,, +FM,Seaside FM 105.3 Hornsea and District Hornsea,Seaside FM 105.3,103500000,53.898863,-0.174128,21,0.08,, +FM,Seaside FM 105.3 Withernsea,Seaside FM 105.3,105300000,53.731283,0.033575,6,0.1,, +FM,Sedgemoor FM Bridgwater Bridgwater - Westfield House,Sedgemoor FM,104200000,51.125924,-3.010084,12,0.07,, +FM,Select Radio Dulwich Dulwich - Countisbury House,Select Radio,94400000,51.433323,-0.068804,106,0.05,, +FM,SFM 106.9 Sittingbourne Sittingbourne - Swale House,SFM 106.9,106900000,51.339728,0.741529,10,0.1,, +FM,Sheffield Live 93.2 FM West Sheffield Sheffield,Sheffield Live 93.2 FM,93200000,53.379385,-1.513809,247,0.025,, +FM,Sheppey FM Sheerness,Sheppey FM,92200000,51.44156,0.7636,3,0.025,, +FM,shmu FM Aberdeen Aberdeen - Stewart Park Court,shmu FM,99800000,57.16473,-2.137712,90,0.025,, +FM,SIBC Shetland Bressay,SIBC,96200000,60.130079,-1.096281,224,4,, +FM,SIBC Shetland Lerwick,SIBC,102200000,60.155463,-1.146383,22,0.025,, +FM,Silk FM Macclesfield Sutton Common,Silk FM,106900000,53.206041,-2.10072,401,0.15,, +FM,Sine FM Doncaster Doncaster - Civic Centre,Sine FM,102600000,53.520399,-1.130303,16,0.1,, +FM,Siren FM Lincoln Lincoln University,Siren FM,107300000,53.228811,-0.549432,4,0.025,, +FM,Skylark Buckfastleigh Holne - Higher Mill Leat Farm,Skylark,107600000,50.506305,-3.812489,184,0.1,, +FM,Skylark Princetown,Skylark,105800000,50.549887,-4.001482,464,0.1,, +FM,Skyline Gold 102.5 Hedge End,Skyline Gold 102.5,102500000,50.913384,-1.317748,41,0.025,, +FM,Smooth Radio Glasgow Black Hill,Smooth Radio,105200000,55.860005,-3.873572,275,15,, +FM,Smooth Radio North East England Burnhope,Smooth Radio,97500000,54.8219,-1.714848,240,4.5,, +FM,Smooth Radio London Croydon,Smooth Radio,102200000,51.40986,-0.085755,114,2,, +FM,Smooth Radio Derby Drum Hill,Smooth Radio,101400000,52.975288,-1.442404,156,0.125,, +FM,Smooth Radio North East England Eston Nab,Smooth Radio,107700000,54.556455,-1.120934,237,2.5,, +FM,Smooth Radio North East England Fenham,Smooth Radio,107500000,54.978039,-1.662966,120,0.05,, +FM,Smooth Radio Kettering Geddington,Smooth Radio,107400000,52.445531,-0.666816,118,0.2,, +FM,Smooth Radio Peterborough Gunthorpe,Smooth Radio,106800000,52.617188,-0.247858,3,0.1,, +FM,Smooth Radio Wellingborough Irchester,Smooth Radio,97200000,52.272441,-0.64153,90,0.1,, +FM,Smooth Radio Kendal,Smooth Radio,100100000,54.314559,-2.708127,172,0.1,, +FM,Smooth Radio Kendal Keswick Forest,Smooth Radio,101400000,54.608739,-3.204646,215,0,, +FM,Smooth Radio North East England NEWTON,Smooth Radio,96400000,54.982447,-1.945593,165,0.1,, +FM,Smooth Radio West Midlands SUTTON COLDFIELD,Smooth Radio,105700000,52.600539,-1.833856,169,5.5,, +FM,Smooth Radio East Midlands WALTHAM,Smooth Radio,106600000,52.801451,-0.80088,138,6.2,, +FM,Smooth Radio Windermere WINDERMERE,Smooth Radio,100800000,54.37436,-2.951306,216,0.1,, +FM,Smooth Radio North West England WINTER HILL,Smooth Radio,100400000,53.625547,-2.514794,439,2.5,, +FM,Somer Valley FM Midsomer Norton Clandown,Somer Valley FM,97500000,51.305362,-2.458279,157,0.025,, +FM,Sound Radio Towyn & Rhyl Towyn,Sound Radio,103100000,53.301565,-3.540892,2,0.05,, +FM,Soundart Radio Totnes Dartington Hall,Soundart Radio,102500000,50.452961,-3.693809,62,0.05,, +FM,Spark FM Sunderland Monkswearmouth Sunderland,Spark FM,107000000,54.914812,-1.376309,21,0.025,, +FM,Speysound Radio Aviemore Cairn Gorm,Speysound Radio,107100000,57.123819,-3.646048,1085,0.068,, +FM,Spice FM Newcastle upon Tyne Newcastle upon Tyne - The Beacon,Spice FM,98800000,54.974313,-1.648312,110,0.025,, +FM,Stafford FM Stafford Stafford - Guildhall Shopping Centre,Stafford FM,107300000,52.807267,-2.118633,78,0.0248,, +FM,Star Radio Ely Ely Cathedral,Star Radio,107100000,52.398758,0.262657,20,0.1,, +FM,Star Radio Haverhill,Star Radio,107900000,52.094644,0.443018,105,0.01,, +FM,Star Radio Cambridge Madingley,Star Radio,100700000,52.215394,0.036667,62,0.3379,, +FM,Star Radio Saffron Walden,Star Radio,107300000,52.018807,0.236288,63,0.02,, +FM,Stoke Mandeville Hospital Radio Aylesbury Stoke Mandeville Hospital,Stoke Mandeville Hospital Radio,101800000,51.797803,-0.799261,87,0.0015,, +FM,Storm FM Bangor University of Wales Bangor,Storm FM,87700000,53.226864,-4.130115,15,5e-05,, +FM,Sun FM Darlington Darlington - Harrowgate Hill,Sun FM,103200000,54.555795,-1.560849,72,0.165,, +FM,Sun FM Sunderland Haining,Sun FM,103400000,54.851532,-1.447373,162,0.15,, +FM,Sun FM Bishop Auckland Middlestone,Sun FM,106800000,54.678457,-1.611325,189,0.025,, +FM,Sun FM Richmond Richmond - Mercury Road,Sun FM,102600000,54.411911,-1.748975,212,0.05,, +FM,Sun FM Durham Sacriston,Sun FM,102800000,54.813552,-1.619989,180,0.1648,, +FM,Sunny Govan Radio Glasgow Glasgow - Govan,Sunny Govan Radio,103500000,55.846502,-4.336973,35,0.025,, +FM,Sunrise FM Bradford Idle,Sunrise FM,103200000,53.832945,-1.752142,215,0.25,, +FM,Sunset Drive-in Cinema Portsmouth Pier Road (Car Park),Sunset Drive-in Cinema,92000000,50.786694,-1.098659,1,5e-05,, +FM,Sunshine East Belfast Belfast - Lower Braniel Road,Sunshine,104900000,54.578225,-5.850659,67,0.025,, +FM,Sunshine Radio Monmouthshire Abergavenny,Sunshine Radio,107800000,51.807795,-3.097781,433,0.02,, +FM,Sunshine Radio Ludlow Clee Hill,Sunshine Radio,107800000,52.378266,-2.596676,427,0.125,, +FM,Sunshine Radio Monmouth MONMOUTH,Sunshine Radio,107000000,51.811533,-2.688667,194,0.5,, +FM,Sunshine Radio Hereford RIDGE HILL,Sunshine Radio,106200000,51.997433,-2.53989,204,0.8,, +FM,Sunshine Radio (FM) Ludlow Woofferton,Sunshine Radio (FM),105900000,52.318073,-2.718365,81,0.5,, +FM,Susy Radio Redhill & Reigate Reigate Hill - Fort Lodge,Susy Radio,103400000,51.254634,-0.198401,226,0.05,, +FM,Swindon 105.5 Swindon Swindon - Brunel Tower,Swindon 105.5,105500000,51.561066,-1.786888,100,0.025,, +FM,Switch Radio 107.5 North East Birmingham Castle Vale,Switch Radio 107.5,107500000,52.520827,-1.794217,89,0.025,, +FM,SWUFM Bristol Bristol - Croydon House,SWUFM,103700000,51.459262,-2.569353,20,0.05,, +FM,Takeover Radio Leicester Leicester - Gorse Hill City Farm,Takeover Radio,103200000,52.65099,-1.160066,81,0.025,, +FM,Takeover Radio 106.9 Sutton in Ashfield,Takeover Radio 106.9,106900000,53.1237,-1.260867,160,0.025,, +FM,Tameside Radio Tameside Harrop Edge,Tameside Radio,103600000,53.464054,-2.026622,301,0.05,, +FM,Tay FM Dundee Angus,Tay FM,102800000,56.555053,-2.986125,312,4,, +FM,Tay FM Perth PERTH,Tay FM,96400000,56.375183,-3.444404,123,0.3,, +FM,Tay FM Dundee Tay Bridge,Tay FM,96400000,56.444566,-2.925027,100,0.05,, +FM,TD1 Radio Central Borders Galashiels - Golf Club,TD1 Radio,106500000,55.62095,-2.804223,190,0.02,, +FM,Tempo 107.4 FM Wetherby Collingham,Tempo 107.4 FM,107400000,53.897122,-1.415753,96,0.025,, +FM,TFM Radio Teeside ESTON NAB,TFM Radio,96600000,54.555831,-1.121721,234,0.3,, +FM,The Beat London 103.6FM Harlesden Stonebridge,The Beat London 103.6FM,103600000,51.541357,-0.260646,35,0.023,, +FM,The Cat Crewe & Nantwich Crewe,The Cat,107900000,53.088897,-2.45491,51,0.05,, +FM,The Eye Vale of Belvoir White Lodge Farm,The Eye,103000000,52.807355,-0.929488,167,0.042,, +FM,The Flash Waterlooville Waterlooville - Library,The Flash,104300000,50.879962,-1.029683,51,0.1,, +FM,The Hillz FM Coventry Coventry - Pioneer House,The Hillz FM,98600000,52.41396,-1.500377,87,0.05,, +FM,The Source Falmouth & Penryn Penryn - Tremough,The Source,96100000,50.170903,-5.127727,121,0.025,, +FM,The Voice Barnstaple Barnstaple - North Devon college,The Voice,106100000,51.072691,-4.068213,49,0.05,, +FM,The Voice Bideford Homlacott,The Voice,107800000,51.039847,-4.134223,116,0.05,, +FM,The Voice Ilfracombe Ilfracombe Holiday Park,The Voice,107800000,51.201277,-4.11448,130,0.0343,, +FM,The Wave Swansea Kilvey Hill,The Wave,96400000,51.62891,-3.919745,193,1,, +FM,Thornbury Radio Olveston,Thornbury Radio,107500000,51.582306,-2.561076,75,0.008,, +FM,Thornbury Radio Thornbury and District Thornbury,Thornbury Radio,105100000,51.608616,-2.488331,99,0.0511,, +FM,Time 107.5 Havering Romford - Mercury House,Time 107.5,107500000,51.57906,0.184321,18,0.4065,, +FM,TMCR Thorne-Moorends Thorne - Bridge Street,TMCR,95300000,53.610359,-0.959319,2,0.0199,, +FM,Tone FM Taunton Taunton - Market House,Tone FM,107800000,51.014569,-3.102891,19,0.025,, +FM,Torbay Hospital Radio Devon Torbay Hospital,Torbay Hospital Radio,95900000,50.482428,-3.553751,72,0.001,, +FM,Two Lochs Radio Loch Ewe Cliff Hill,Two Lochs Radio,106600000,57.757467,-5.614524,225,2,, +FM,Two Lochs Radio Gairloch Port Henderson,Two Lochs Radio,106000000,57.700504,-5.75527,64,0.6,, +FM,U105 Belfast Black Mountain,U105,105800000,54.586989,-6.022076,301,1,, +FM,Ujima Radio Bristol Bristol - Rawnsley House,Ujima Radio,98000000,51.463564,-2.572286,13,0.025,, +FM,Unity 101 Southampton Midanbury - Broadway,Unity 101,101100000,50.925696,-1.364237,61,0.075,, +FM,Unity FM Birmingham East Birmingham - Pritchett Tower,Unity FM,93500000,52.472029,-1.869083,129,0.075,, +FM,Unity Radio Manchester Manchester One,Unity Radio,92800000,53.478111,-2.238327,40,0.025,, +FM,University Radio York York University of York,University Radio York,88300000,53.947755,-1.053788,15,0.002,, +FM,UWS Radio Ayr Ayr University,UWS Radio,87700000,55.45949,-4.607931,20,5e-05,, +FM,Vanny Radio Coventry William Batchelor House,Vanny Radio,106300000,52.413553,-1.510526,92,0.01,, +FM,Vectis Radio Newport (IoW) Newport Golf Course,Vectis Radio,104600000,50.683114,-1.278202,104,0.075,, +FM,Vibe 107.6 FM Watford Watford YMCA,Vibe 107.6 FM,107600000,51.65704,-0.396312,74,0.1,, +FM,Viking FM Humberside High Hunsley,Viking FM,96900000,53.803052,-0.565371,164,5,, +FM,Vixen 101 Market Weighton,Vixen 101,101800000,53.866896,-0.628886,120,0.025,, +FM,Vixen 101 Pocklington Pocklington - Judsons Wine Bar,Vixen 101,103100000,53.930003,-0.779584,35,0.02,, +FM,Voice FM Southampton Southampton - Solent University,Voice FM,103900000,50.907544,-1.400609,9,0.05,, +FM,Wallingford Radio Wallingford and District Wallingford - Ayres Ind Est,Wallingford Radio,107300000,51.602015,-1.13133,48,0.025,, +FM,Wave 105/Greatest Hits Radio Solent Chillerton Down,Wave 105/Greatest Hits Radio,105200000,50.649245,-1.328934,166,5,, +FM,Wave 105/Greatest Hits Radio Poole POOLE,Wave 105/Greatest Hits Radio,105800000,50.729221,-1.948658,64,0.5,, +FM,Waves Radio Peterhead and Fraserburgh Mormond Hill A,Waves Radio,101200000,57.602975,-2.03228,229,1,, +FM,WCR Warminster Warminster Town,WCR,105500000,51.205499,-2.182169,117,0.025,, +FM,WCR FM Wolverhampton Wolverhampton - Mander House,WCR FM,101800000,52.585123,-2.128215,154,0.0125,, +FM,West FM Ayr DARVEL,West FM,96700000,55.579376,-4.289814,286,1.1,, +FM,West FM Ayr GIRVAN,West FM,97500000,55.244736,-4.814596,195,0.075,, +FM,West FM Rothesay ROTHESAY,West FM,106700000,55.878164,-4.998928,166,0.3,, +FM,West Hull FM West Hull Hull - Royal Infirmary,West Hull FM,106900000,53.743955,-0.359032,2,0.05,, +FM,West Kent Radio Kent Bidborough,West Kent Radio,95500000,51.16973,0.234874,121,0.015,, +FM,West Kent Radio Kent Tunbridge Wells - Sankeys,West Kent Radio,106700000,51.135332,0.260515,123,0.05,, +FM,West Somerset Radio Minehead Minehead North,West Somerset Radio,104400000,51.215281,-3.484815,169,0.05,, +FM,Westside 89.6 FM Southall Rutherford Tower,Westside 89.6 FM,89600000,51.518659,-0.364279,31,0.007,, +FM,Westsound FM Stranraer Cairn Pat,Westsound FM,96500000,54.86339,-5.049018,173,0.1,, +FM,Westsound FM Kirkcudbright CAMBRET HILL,Westsound FM,103000000,54.893698,-4.302038,343,0.532,, +FM,Westsound FM Dumfries Riddings Hill,Westsound FM,97000000,55.082733,-3.719548,230,1,, +FM,Wey Valley Radio Alton Alton - Thedden Grange,Wey Valley Radio,101100000,51.147701,-1.022937,182,0.05,, +FM,Winchester Radio Winchester Winchester - RHCH,Winchester Radio,94700000,51.061296,-1.331131,95,0.025,, +FM,Witney Radio Chilson Ascott-under-Wychwood,Witney Radio,105300000,51.86963,-1.581517,131,0.025,, +FM,Witney Radio Chipping Norton,Witney Radio,107400000,51.937095,-1.542053,220,0.025,, +FM,Witney Radio Eynsham,Witney Radio,105300000,51.782234,-1.377497,70,0.025,, +FM,Witney Radio Faringdon Faringdon Leisure Centre,Witney Radio,107100000,51.648114,-1.588473,129,0.02,, +FM,Witney Radio Stonesfield Stonesfield Sports and Social Club,Witney Radio,90200000,51.855404,-1.427162,122,0.05,, +FM,Witney Radio Witney Witney West,Witney Radio,99900000,51.791789,-1.516557,107,0.15,, +FM,Wycombe Sound High Wycombe High Wycombe (Wycombe Sound),Wycombe Sound,106600000,51.625688,-0.771594,151,0.05,, +FM,Wythenshawe FM Manchester Wythenshawe,Wythenshawe FM,97200000,53.381442,-2.258683,60,0.05,, +FM,XS Manchester Manchester Manchester - City Tower,XS Manchester,106100000,53.480177,-2.23864,43,0.5,, +FM,YO1 Radio Selby Selby Abbey,YO1 Radio,90000000,53.784006,-1.067189,2,0.2,, +FM,YO1 Radio York City York Hospital,YO1 Radio,102800000,53.970545,-1.083457,13,0.1,, +FM,Your FM Stockport Stockport - Ratcliffe Towers,Your FM,107800000,53.404991,-2.15549,72,0.025,, +FM,Your Radio Dumbarton,Your Radio,103000000,55.945232,-4.577878,8,0.1,, +FM,YourFM Portadown,YourFM,100200000,54.43016,-6.408277,63,0.025,, +FM,Zack FM 105.3 Forest Heath Mildenhall St Mary's Church,Zack FM 105.3,105300000,52.34271,0.508941,8,0.05,, +FM,Zetland FM Redcar Dunsdale - Park Farm,Zetland FM,105000000,54.570459,-1.070671,110,0.0258,, +DAB,Aberdeen 3E07,Aberdeen,220352000,56.999987,-2.389944,325,2,3E,07 +DAB,Aberdeen 3E06,Aberdeen,220352000,57.386674,-2.400197,245,8.51001,3E,06 +DAB,Aberdeen 3E01,Aberdeen,220352000,57.602976,-2.029937,225,1,3E,01 +DAB,Aberdeen 3E13,Aberdeen,220352000,57.113656,-2.096244,87,1.2,3E,13 +DAB,Ayr 400E,Ayr,218640000,55.409586,-4.699754,273,4,40,0E +DAB,Ayr 400A,Ayr,218640000,55.579376,-4.289814,286,0.760001,40,0A +DAB,Ayr 4004,Ayr,218640000,55.244736,-4.814596,195,2,40,04 +DAB,Ayr 4007,Ayr,218640000,55.695641,-4.84184,170,1.51,40,07 +DAB,Basingstoke small-scale 0308,Basingstoke small-scale,197648000,51.226848,-1.080409,176,0.25,03,08 +DAB,BBC National DAB 2412,BBC National DAB,225648000,51.702007,-3.398531,282,0.2,24,12 +DAB,BBC National DAB 2405,BBC National DAB,225648000,51.807795,-3.097781,433,0.488619,24,05 +DAB,BBC National DAB 2415,BBC National DAB,225648000,51.714666,-3.12404,400,1,24,15 +DAB,BBC National DAB 2E0D,BBC National DAB,225648000,54.046924,-0.786852,224,2,2E,0D +DAB,BBC National DAB 3A0A,BBC National DAB,225648000,53.674328,-1.813774,257,0.6,3A,0A +DAB,BBC National DAB 3111,BBC National DAB,225648000,52.180735,1.570158,21,1.8,31,11 +DAB,BBC National DAB 0B02,BBC National DAB,225648000,51.594459,-0.129257,92,3.2,0B,02 +DAB,BBC National DAB 3502,BBC National DAB,225648000,53.027231,-2.28071,220,2.5,35,02 +DAB,BBC National DAB 0C13,BBC National DAB,225648000,51.142291,-0.968873,148,0.05,0C,13 +DAB,BBC National DAB 0516,BBC National DAB,225648000,53.390547,-4.301313,160,0.6,05,16 +DAB,BBC National DAB 3C08,BBC National DAB,225648000,56.555053,-2.986125,312,10,3C,08 +DAB,BBC National DAB 3C03,BBC National DAB,225648000,56.711856,-2.671353,130,1,3C,03 +DAB,BBC National DAB 0502,BBC National DAB,225648000,53.01985,-4.273475,300,3.90895,05,02 +DAB,BBC National DAB 1908,BBC National DAB,225648000,54.342402,-6.629562,109,5,19,08 +DAB,BBC National DAB 2001,BBC National DAB,225648000,53.011257,-1.730341,170,0.5,20,01 +DAB,BBC National DAB 0E17,BBC National DAB,225648000,50.505425,-3.752519,156,0.3,0E,17 +DAB,BBC National DAB 4315,BBC National DAB,225648000,55.974965,-2.776943,175,1,43,15 +DAB,BBC National DAB 1606,BBC National DAB,225648000,51.819509,-0.811742,81,0.6,16,06 +DAB,BBC National DAB 2904,BBC National DAB,225648000,52.924963,-3.535217,309,0.3,29,04 +DAB,BBC National DAB 3E0D,BBC National DAB,225648000,57.184868,-2.122071,74,0.3,3E,0D +DAB,BBC National DAB 1912,BBC National DAB,225648000,55.191644,-6.24536,109,0.1,19,12 +DAB,BBC National DAB 0E05,BBC National DAB,225648000,51.003284,-3.473759,226,0.6,0E,05 +DAB,BBC National DAB 1914,BBC National DAB,225648000,54.661435,-5.675928,21,0.4,19,14 +DAB,BBC National DAB 0712,BBC National DAB,225648000,54.126165,-3.198099,23,0.6,07,12 +DAB,BBC National DAB 4210,BBC National DAB,225648000,54.934919,-3.857934,178,2,42,10 +DAB,BBC National DAB 0308,BBC National DAB,225648000,51.226848,-1.080409,176,1,03,08 +DAB,BBC National DAB 230E,BBC National DAB,225648000,51.38779,-2.332925,175,1.95447,23,0E +DAB,BBC National DAB 0E07,BBC National DAB,225648000,50.447129,-3.610498,196,4.66627,0E,07 +DAB,BBC National DAB 3305,BBC National DAB,225648000,52.449252,1.557261,26,0.05,33,05 +DAB,BBC National DAB 3A09,BBC National DAB,225648000,53.811543,-1.641253,149,1.85675,3A,09 +DAB,BBC National DAB 0E11,BBC National DAB,225648000,50.701549,-3.091423,93,0.05,0E,11 +DAB,BBC National DAB 3202,BBC National DAB,225648000,53.335814,-0.171983,125,4.45625,32,02 +DAB,BBC National DAB 3D14,BBC National DAB,225648000,56.212308,-4.263559,415,2,3D,14 +DAB,BBC National DAB 0514,BBC National DAB,225648000,53.108346,-3.75717,303,0.6,05,14 +DAB,BBC National DAB 4407,BBC National DAB,225648000,55.576525,-3.56198,325,0.6,44,07 +DAB,BBC National DAB 3B02,BBC National DAB,225648000,54.358848,-1.149317,381,2,3B,02 +DAB,BBC National DAB 0D03,BBC National DAB,225648000,50.66283,-2.443645,160,0.977237,0D,03 +DAB,BBC National DAB 2B08,BBC National DAB,225648000,53.372188,-1.95923,313,0.05,2B,08 +DAB,BBC National DAB 2C07,BBC National DAB,225648000,53.809005,-3.05038,6,0.12,2C,07 +DAB,BBC National DAB 280D,BBC National DAB,225648000,52.360613,-4.102585,174,4.66627,28,0D +DAB,BBC National DAB 0104,BBC National DAB,225648000,51.324006,0.520684,192,6.30001,01,04 +DAB,BBC National DAB 1A0B,BBC National DAB,225648000,51.609363,-1.794027,148,4,1A,0B +DAB,BBC National DAB 3107,BBC National DAB,225648000,52.311386,1.598136,22,1.2,31,07 +DAB,BBC National DAB 1203,BBC National DAB,225648000,51.723271,-1.300854,162,1.2,12,03 +DAB,BBC National DAB 200C,BBC National DAB,225648000,53.094354,-1.561526,315,0.3,20,0C +DAB,BBC National DAB 3203,BBC National DAB,225648000,52.962367,-0.015354,3,0.3,32,03 +DAB,BBC National DAB 1609,BBC National DAB,225648000,51.998806,-0.669784,169,1.82402,16,09 +DAB,BBC National DAB 4312,BBC National DAB,225648000,55.912262,-3.200598,202,0.977237,43,12 +DAB,BBC National DAB 060C,BBC National DAB,225648000,51.865628,0.576571,62,0.1,06,0C +DAB,BBC National DAB 310C,BBC National DAB,225648000,52.447068,0.594193,21,1.2,31,0C +DAB,BBC National DAB 280A,BBC National DAB,225648000,51.959448,-3.412497,248,0.5,28,0A +DAB,BBC National DAB 4501,BBC National DAB,225648000,60.130079,-1.096281,224,4.5,45,01 +DAB,BBC National DAB 0E0D,BBC National DAB,225648000,50.720805,-2.775678,64,0.2,0E,0D +DAB,BBC National DAB 0E0A,BBC National DAB,225648000,50.395927,-3.518996,50,0.3,0E,0A +DAB,BBC National DAB 1C04,BBC National DAB,225648000,52.355115,-2.078214,147,1.95447,1C,04 +DAB,BBC National DAB 1903,BBC National DAB,225648000,54.423191,-7.462812,305,3.2,19,03 +DAB,BBC National DAB 070F,BBC National DAB,225648000,54.682913,-3.463248,125,1.3,07,0F +DAB,BBC National DAB 400E,BBC National DAB,225648000,55.409586,-4.699754,273,2.4,40,0E +DAB,BBC National DAB 0817,BBC National DAB,225648000,51.521504,-0.138879,27,0.8,08,17 +DAB,BBC National DAB 2F0C,BBC National DAB,225648000,54.124979,-0.218986,105,1.5,2F,0C +DAB,BBC National DAB 040A,BBC National DAB,225648000,50.82768,-4.530383,47,0.293171,04,0A +DAB,BBC National DAB 0E13,BBC National DAB,225648000,50.634311,-3.354151,102,0.6,0E,13 +DAB,BBC National DAB 310B,BBC National DAB,225648000,52.440322,1.445862,34,0.05,31,0B +DAB,BBC National DAB 3308,BBC National DAB,225648000,52.952591,0.671702,60,0.977237,33,08 +DAB,BBC National DAB 0A02,BBC National DAB,225648000,50.909654,-0.627813,245,0.122155,0A,02 +DAB,BBC National DAB 200F,BBC National DAB,225648000,53.275085,-1.911935,432,0.977237,20,0F +DAB,BBC National DAB 330C,BBC National DAB,225648000,52.659164,1.715858,17,0.025,33,0C +DAB,BBC National DAB 3415,BBC National DAB,225648000,54.969789,-1.608675,15,0.025,34,15 +DAB,BBC National DAB 2007,BBC National DAB,225648000,53.265177,-1.653635,275,1,20,07 +DAB,BBC National DAB 420C,BBC National DAB,225648000,54.893698,-4.302038,343,2.4,42,0C +DAB,BBC National DAB 190A,BBC National DAB,225648000,54.160861,-6.385716,340,3,19,0A +DAB,BBC National DAB 3D09,BBC National DAB,225648000,55.413714,-5.624241,83,0.6,3D,09 +DAB,BBC National DAB 0405,BBC National DAB,225648000,50.511364,-4.43683,369,2.38748,04,05 +DAB,BBC National DAB 2708,BBC National DAB,225648000,51.867126,-4.309135,137,0.6,27,08 +DAB,BBC National DAB 2707,BBC National DAB,225648000,51.818501,-4.066216,255,4.88619,27,07 +DAB,BBC National DAB 1909,BBC National DAB,225648000,54.676355,-5.928737,207,1,19,09 +DAB,BBC National DAB 080D,BBC National DAB,225648000,51.263565,-0.086822,226,2.4,08,0D +DAB,BBC National DAB 390E,BBC National DAB,225648000,55.825353,-4.290679,67,0.01,39,0E +DAB,BBC National DAB 2104,BBC National DAB,225648000,50.874213,-2.913244,173,0.6,21,04 +DAB,BBC National DAB 0103,BBC National DAB,225648000,51.21959,0.812581,193,2,01,03 +DAB,BBC National DAB 010B,BBC National DAB,225648000,51.265673,1.013529,67,1,01,0B +DAB,BBC National DAB 340C,BBC National DAB,225648000,55.531826,-1.834261,192,3.2,34,0C +DAB,BBC National DAB 0316,BBC National DAB,225648000,51.6119,-0.734849,142,0.6,03,16 +DAB,BBC National DAB 1604,BBC National DAB,225648000,51.697895,-0.618141,127,0.1,16,04 +DAB,BBC National DAB 2D12,BBC National DAB,225648000,53.283991,-1.427446,179,1,2D,12 +DAB,BBC National DAB 2404,BBC National DAB,225648000,51.601588,-2.935417,94,2,24,04 +DAB,BBC National DAB 1B11,BBC National DAB,225648000,51.868264,-2.175397,147,2,1B,11 +DAB,BBC National DAB 0601,BBC National DAB,225648000,51.803409,1.165049,7,0.012,06,01 +DAB,BBC National DAB 3D04,BBC National DAB,225648000,57.617267,-7.44252,123,1.95447,3D,04 +DAB,BBC National DAB 2D05,BBC National DAB,225648000,53.457744,-1.220893,138,2,2D,05 +DAB,BBC National DAB 3705,BBC National DAB,225648000,52.413167,-2.994548,413,0.6,37,05 +DAB,BBC National DAB 2804,BBC National DAB,225648000,52.08185,-3.162868,215,1.2,28,04 +DAB,BBC National DAB 2116,BBC National DAB,225648000,50.916513,-2.700119,134,2,21,16 +DAB,BBC National DAB 330F,BBC National DAB,225648000,52.8029,0.84086,80,0.244309,33,0F +DAB,BBC National DAB 0513,BBC National DAB,225648000,53.271606,-3.828796,114,0.5,05,13 +DAB,BBC National DAB 0314,BBC National DAB,225648000,51.414004,-0.796112,90,0.977237,03,14 +DAB,BBC National DAB 4111,BBC National DAB,225648000,52.710153,-1.285042,240,1.5,41,11 +DAB,BBC National DAB 3D01,BBC National DAB,225648000,56.814742,-5.093559,283,1,3D,01 +DAB,BBC National DAB 0210,BBC National DAB,225648000,51.064004,-1.358677,152,0,02,10 +DAB,BBC National DAB 3F02,BBC National DAB,225648000,56.071791,-3.233186,181,5,3F,02 +DAB,BBC National DAB 0105,BBC National DAB,225648000,51.099738,1.182579,166,0.25,01,05 +DAB,BBC National DAB 3C0B,BBC National DAB,225648000,56.357222,-3.920232,219,0.6,3C,0B +DAB,BBC National DAB 1D11,BBC National DAB,225648000,51.184826,-2.177079,160,0.5,1D,11 +DAB,BBC National DAB 4406,BBC National DAB,225648000,55.40857,-2.811432,239,1,44,06 +DAB,BBC National DAB 0B01,BBC National DAB,225648000,51.424161,-0.074944,110,9.54993,0B,01 +DAB,BBC National DAB 3310,BBC National DAB,225648000,52.652502,0.872498,85,1.2,33,10 +DAB,BBC National DAB 0616,BBC National DAB,225648000,51.719492,0.58056,105,2,06,16 +DAB,BBC National DAB ,BBC National DAB,225648000,51.469253,0.260425,3,1e-05,, +DAB,BBC National DAB 400A,BBC National DAB,225648000,55.579376,-4.289814,286,9.54993,40,0A +DAB,BBC National DAB 2C02,BBC National DAB,225648000,53.696571,-2.443266,261,0.1,2C,02 +DAB,BBC National DAB 1702,BBC National DAB,225648000,52.253028,-1.139914,199,9.54993,17,02 +DAB,BBC National DAB 050E,BBC National DAB,225648000,53.137063,-4.129256,322,0.3,05,0E +DAB,BBC National DAB 1901,BBC National DAB,225648000,54.607538,-6.009471,368,9.54993,19,01 +DAB,BBC National DAB 0512,BBC National DAB,225648000,52.749344,-3.886853,103,0.649999,05,12 +DAB,BBC National DAB 2D06,BBC National DAB,225648000,53.531884,-1.091294,7,0.022,2D,06 +DAB,BBC National DAB 0C15,BBC National DAB,225648000,51.221643,-0.326426,127,0.2,0C,15 +DAB,BBC National DAB 090B,BBC National DAB,225648000,54.140551,-4.493075,143,5,09,0B +DAB,BBC National DAB 0102,BBC National DAB,225648000,51.11163,1.247272,133,1.90999,01,02 +DAB,BBC National DAB 3311,BBC National DAB,225648000,52.608286,0.367383,5,0.3,33,11 +DAB,BBC National DAB 3C17,BBC National DAB,225648000,56.308188,-2.874532,210,1.2,3C,17 +DAB,BBC National DAB 4208,BBC National DAB,225648000,55.051018,-3.613774,12,1,42,08 +DAB,BBC National DAB 010D,BBC National DAB,225648000,51.292731,0.979008,122,2,01,0D +DAB,BBC National DAB 3E07,BBC National DAB,225648000,56.999987,-2.389944,325,10,3E,07 +DAB,BBC National DAB 2306,BBC National DAB,225648000,51.665695,-2.307343,247,0.25,23,06 +DAB,BBC National DAB 3F06,BBC National DAB,225648000,56.071792,-4.055753,410,2,3F,06 +DAB,BBC National DAB 0A0C,BBC National DAB,225648000,51.107066,-0.020722,136,0.649999,0A,0C +DAB,BBC National DAB 0A01,BBC National DAB,225648000,50.764511,0.288682,11,0.3,0A,01 +DAB,BBC National DAB 2413,BBC National DAB,225648000,51.771714,-3.218938,442,0.5,24,13 +DAB,BBC National DAB 2402,BBC National DAB,225648000,51.611393,-3.302735,381,1.2,24,02 +DAB,BBC National DAB 3D05,BBC National DAB,225648000,58.179148,-6.584974,206,5,3D,05 +DAB,BBC National DAB 2E14,BBC National DAB,225648000,53.611985,-1.66456,256,7.4131,2E,14 +DAB,BBC National DAB 190B,BBC National DAB,225648000,54.359541,-7.642326,61,0.5,19,0B +DAB,BBC National DAB 1605,BBC National DAB,225648000,51.744172,-0.128729,122,2,16,05 +DAB,BBC National DAB 3B0A,BBC National DAB,225648000,54.556455,-1.120934,237,1,3B,0A +DAB,BBC National DAB 0E09,BBC National DAB,225648000,50.719122,-3.562148,102,4,0E,09 +DAB,BBC National DAB 4414,BBC National DAB,225648000,55.83228,-2.085863,197,2,44,14 +DAB,BBC National DAB 1201,BBC National DAB,225648000,52.044582,-1.224699,157,1.2,12,01 +DAB,BBC National DAB 3102,BBC National DAB,225648000,51.951883,1.331834,2,0.719999,31,02 +DAB,BBC National DAB 3403,BBC National DAB,225648000,54.978039,-1.662966,120,1.99526,34,03 +DAB,BBC National DAB 0A12,BBC National DAB,225648000,50.853924,-0.410233,124,0.28,0A,12 +DAB,BBC National DAB 2803,BBC National DAB,225648000,52.012591,-4.996935,123,0.195447,28,03 +DAB,BBC National DAB 300A,BBC National DAB,225648000,53.140513,-1.232922,168,1,30,0A +DAB,BBC National DAB 2714,BBC National DAB,225648000,51.627893,-3.760915,361,1,27,14 +DAB,BBC National DAB 0208,BBC National DAB,225648000,50.853922,-1.067587,102,1,02,08 +DAB,BBC National DAB 0213,BBC National DAB,225648000,50.877178,-1.278479,50,0.033,02,13 +DAB,BBC National DAB 3E11,BBC National DAB,225648000,57.381625,-2.756233,416,1.7,3E,11 +DAB,BBC National DAB 1705,BBC National DAB,225648000,52.445531,-0.666816,118,5,17,05 +DAB,BBC National DAB 4004,BBC National DAB,225648000,55.244736,-4.814596,195,0.6,40,04 +DAB,BBC National DAB 390B,BBC National DAB,225648000,55.886844,-4.294621,26,0.01,39,0B +DAB,BBC National DAB 3902,BBC National DAB,225648000,55.861436,-4.268991,16,0.3,39,02 +DAB,BBC National DAB 2B09,BBC National DAB,225648000,53.455242,-1.960506,262,0.5,2B,09 +DAB,BBC National DAB 3206,BBC National DAB,225648000,52.918371,-0.592549,124,0.8,32,06 +DAB,BBC National DAB 380A,BBC National DAB,225648000,57.320173,-3.656313,389,1.2,38,0A +DAB,BBC National DAB 3309,BBC National DAB,225648000,52.773873,0.649768,92,5,33,09 +DAB,BBC National DAB 3303,BBC National DAB,225648000,52.602996,1.722916,1,0.5,33,03 +DAB,BBC National DAB 2811,BBC National DAB,225648000,51.674549,-5.002613,71,1,28,11 +DAB,BBC National DAB 0C16,BBC National DAB,225648000,51.228358,-0.605467,142,4.88619,0C,16 +DAB,BBC National DAB 0404,BBC National DAB,225648000,50.13053,-5.53431,62,0.013,04,04 +DAB,BBC National DAB 030C,BBC National DAB,225648000,51.307873,-1.244777,220,5,03,0C +DAB,BBC National DAB 060B,BBC National DAB,225648000,51.739903,0.097485,110,1,06,0B +DAB,BBC National DAB 2E06,BBC National DAB,225648000,53.995727,-1.592234,150,1.2,2E,06 +DAB,BBC National DAB 1814,BBC National DAB,225648000,52.540463,-1.503661,140,0.5,18,14 +DAB,BBC National DAB 0C09,BBC National DAB,225648000,51.091179,-0.736114,172,0.630001,0C,09 +DAB,BBC National DAB 2C15,BBC National DAB,225648000,53.708491,-2.311438,336,1,2C,15 +DAB,BBC National DAB 0A06,BBC National DAB,225648000,50.861622,0.566431,76,3,0A,06 +DAB,BBC National DAB 2801,BBC National DAB,225648000,51.899195,-4.86653,186,1,28,01 +DAB,BBC National DAB 3407,BBC National DAB,225648000,54.961693,-2.298886,212,0.3,34,07 +DAB,BBC National DAB 3701,BBC National DAB,225648000,52.531128,-2.790828,343,0.649999,37,01 +DAB,BBC National DAB 3304,BBC National DAB,225648000,52.893823,0.496359,20,0.6,33,04 +DAB,BBC National DAB 1301,BBC National DAB,225648000,52.294535,-1.946877,162,0.0152,13,01 +DAB,BBC National DAB 0A07,BBC National DAB,225648000,50.976724,0.230437,158,5,0A,07 +DAB,BBC National DAB 3A0E,BBC National DAB,225648000,53.737298,-2.018458,220,0.2,3A,0E +DAB,BBC National DAB 0416,BBC National DAB,225648000,50.101857,-5.285607,71,1,04,16 +DAB,BBC National DAB 030D,BBC National DAB,225648000,51.481196,-0.978531,69,0.5,03,0D +DAB,BBC National DAB 1612,BBC National DAB,225648000,51.728571,-0.426217,142,4,16,12 +DAB,BBC National DAB 3409,BBC National DAB,225648000,54.893895,-1.363068,10,0.8,34,09 +DAB,BBC National DAB 2411,BBC National DAB,225648000,51.740421,-3.419414,451,1,24,11 +DAB,BBC National DAB 2E03,BBC National DAB,225648000,54.063816,-1.740687,298,0.25,2E,03 +DAB,BBC National DAB 2F07,BBC National DAB,225648000,53.803052,-0.565371,164,2,2F,07 +DAB,BBC National DAB 1610,BBC National DAB,225648000,51.640531,-0.763678,156,0.2,16,10 +DAB,BBC National DAB 0E0C,BBC National DAB,225648000,50.566699,-3.506408,205,0.5,0E,0C +DAB,BBC National DAB 3A04,BBC National DAB,225648000,53.533184,-1.858296,524,2,3A,04 +DAB,BBC National DAB 0506,BBC National DAB,225648000,53.307315,-4.683071,135,1,05,06 +DAB,BBC National DAB 2A09,BBC National DAB,225648000,53.265968,-3.22279,214,0.3,2A,09 +DAB,BBC National DAB 0A09,BBC National DAB,225648000,51.064289,-0.321546,53,0.024,0A,09 +DAB,BBC National DAB 0C01,BBC National DAB,225648000,51.234232,-0.820719,179,0.08,0C,01 +DAB,BBC National DAB 0E08,BBC National DAB,225648000,50.979244,-4.098818,200,2.44309,0E,08 +DAB,BBC National DAB 230C,BBC National DAB,225648000,51.325227,-2.918301,73,0.6,23,0C +DAB,BBC National DAB 1B05,BBC National DAB,225648000,51.904177,-1.707798,241,1,1B,05 +DAB,BBC National DAB 2E0B,BBC National DAB,225648000,53.832945,-1.752142,215,0.3,2E,0B +DAB,BBC National DAB 4303,BBC National DAB,225648000,55.620243,-3.07272,244,0.3,43,03 +DAB,BBC National DAB 2208,BBC National DAB,225648000,50.368664,-3.926486,142,0.25,22,08 +DAB,BBC National DAB 4402,BBC National DAB,225648000,55.494186,-2.53763,111,0.1,44,02 +DAB,BBC National DAB 4504,BBC National DAB,225648000,58.975283,-3.083943,220,10,45,04 +DAB,BBC National DAB 3A0C,BBC National DAB,225648000,53.895532,-1.896639,305,2,3A,0C +DAB,BBC National DAB 0707,BBC National DAB,225648000,54.314559,-2.708127,172,3.2,07,07 +DAB,BBC National DAB 0B14,BBC National DAB,225648000,51.317089,-0.09335,152,0.05,0B,14 +DAB,BBC National DAB 3411,BBC National DAB,225648000,55.001052,-1.665743,120,0.01,34,11 +DAB,BBC National DAB 070A,BBC National DAB,225648000,54.5916,-3.117626,178,1,07,0A +DAB,BBC National DAB 1C06,BBC National DAB,225648000,52.363252,-2.282794,82,0.092,1C,06 +DAB,BBC National DAB 2703,BBC National DAB,225648000,51.62891,-3.919745,193,4.1,27,03 +DAB,BBC National DAB 3313,BBC National DAB,225648000,52.824325,0.487473,51,0.6,33,13 +DAB,BBC National DAB 220D,BBC National DAB,225648000,50.274442,-3.794967,93,0.5,22,0D +DAB,BBC National DAB 0E10,BBC National DAB,225648000,50.349538,-3.565025,107,0.4,0E,10 +DAB,BBC National DAB 3808,BBC National DAB,225648000,57.061393,-4.030507,351,0.586342,38,08 +DAB,BBC National DAB 3F01,BBC National DAB,225648000,55.852721,-3.826093,277,9.54993,3F,01 +DAB,BBC National DAB 3207,BBC National DAB,225648000,52.832469,-0.431001,73,1.2,32,07 +DAB,BBC National DAB 4202,BBC National DAB,225648000,55.412875,-3.983785,480,1,42,02 +DAB,BBC National DAB 3C09,BBC National DAB,225648000,56.372293,-3.453361,178,3.98107,3C,09 +DAB,BBC National DAB 3805,BBC National DAB,225648000,57.532303,-3.135126,353,5,38,05 +DAB,BBC National DAB 0711,BBC National DAB,225648000,54.382063,-2.589348,349,1,07,11 +DAB,BBC National DAB 2005,BBC National DAB,225648000,53.306661,-1.960342,404,0.251189,20,05 +DAB,BBC National DAB 2C0E,BBC National DAB,225648000,54.089336,-2.779963,88,4.88619,2C,0E +DAB,BBC National DAB 4204,BBC National DAB,225648000,55.137967,-3.008447,271,0.5,42,04 +DAB,BBC National DAB 1808,BBC National DAB,225648000,52.081613,-1.728531,260,3,18,08 +DAB,BBC National DAB 190F,BBC National DAB,225648000,54.862024,-5.828421,119,0.2,19,0F +DAB,BBC National DAB 1806,BBC National DAB,225648000,52.294288,-1.519026,90,0.1,18,06 +DAB,BBC National DAB 0C0F,BBC National DAB,225648000,51.319591,-0.372374,60,0.05,0C,0F +DAB,BBC National DAB 3512,BBC National DAB,225648000,53.102442,-2.00205,265,0.6,35,12 +DAB,BBC National DAB 4108,BBC National DAB,225648000,52.637434,-1.125451,57,0.3,41,08 +DAB,BBC National DAB 1001,BBC National DAB,225648000,49.246023,-2.1012,134,2,10,01 +DAB,BBC National DAB 1102,BBC National DAB,225648000,49.457792,-2.579202,76,0.475,11,02 +DAB,BBC National DAB 160D,BBC National DAB,225648000,51.981709,-0.201702,82,0.6,16,0D +DAB,BBC National DAB 1302,BBC National DAB,225648000,52.375278,-2.013764,280,1,13,02 +DAB,BBC National DAB 1907,BBC National DAB,225648000,55.109172,-6.886121,341,6.30001,19,07 +DAB,BBC National DAB 3209,BBC National DAB,225648000,53.205578,-0.546174,15,0.6,32,09 +DAB,BBC National DAB 0A08,BBC National DAB,225648000,51.13246,-0.23632,92,1.17268,0A,08 +DAB,BBC National DAB 2B17,BBC National DAB,225648000,53.646225,-2.076355,229,0.6,2B,17 +DAB,BBC National DAB 0501,BBC National DAB,225648000,53.307002,-4.12825,146,5,05,01 +DAB,BBC National DAB 0509,BBC National DAB,225648000,52.914419,-4.017769,268,0.5,05,09 +DAB,BBC National DAB 050B,BBC National DAB,225648000,52.479547,-3.399945,454,2,05,0B +DAB,BBC National DAB 2810,BBC National DAB,225648000,52.261047,-3.439354,432,5,28,10 +DAB,BBC National DAB 280B,BBC National DAB,225648000,52.045314,-4.410045,116,1,28,0B +DAB,BBC National DAB 2709,BBC National DAB,225648000,51.699961,-4.157337,111,0.6,27,09 +DAB,BBC National DAB 290B,BBC National DAB,225648000,52.954525,-3.117972,390,0.4,29,0B +DAB,BBC National DAB 290A,BBC National DAB,225648000,53.031048,-3.180791,556,5,29,0A +DAB,BBC National DAB 0503,BBC National DAB,225648000,52.446933,-3.549585,244,0.3,05,03 +DAB,BBC National DAB 1904,BBC National DAB,225648000,55.00421,-7.368968,175,6.30001,19,04 +DAB,BBC National DAB 0511,BBC National DAB,225648000,52.644306,-3.086542,400,1,05,11 +DAB,BBC National DAB 3A11,BBC National DAB,225648000,53.720111,-1.92874,256,0.3,3A,11 +DAB,BBC National DAB 3703,BBC National DAB,225648000,52.362722,-2.738327,216,1.2,37,03 +DAB,BBC National DAB 0E06,BBC National DAB,225648000,50.737572,-2.929856,162,0.4,0E,06 +DAB,BBC National DAB 0508,BBC National DAB,225648000,52.586449,-3.885603,100,0.5,05,08 +DAB,BBC National DAB 190C,BBC National DAB,225648000,55.178615,-6.671493,62,1.5,19,0C +DAB,BBC National DAB 1506,BBC National DAB,225648000,52.215394,0.036667,62,4,15,06 +DAB,BBC National DAB 2704,BBC National DAB,225648000,51.608486,-3.6738,252,0.3,27,04 +DAB,BBC National DAB 030A,BBC National DAB,225648000,51.548667,-0.775255,85,1,03,0A +DAB,BBC National DAB 1C15,BBC National DAB,225648000,52.115715,-2.330146,161,5,1C,15 +DAB,BBC National DAB 2B06,BBC National DAB,225648000,53.480177,-2.23864,43,0.5,2B,06 +DAB,BBC National DAB 060F,BBC National DAB,225648000,51.92387,1.086097,35,5,06,0F +DAB,BBC National DAB 3008,BBC National DAB,225648000,52.976309,-1.132626,122,1,30,08 +DAB,BBC National DAB 3707,BBC National DAB,225648000,52.905093,-2.493571,100,0.033884,37,07 +DAB,BBC National DAB 3E06,BBC National DAB,225648000,57.386674,-2.400197,245,10,3E,06 +DAB,BBC National DAB 3D07,BBC National DAB,225648000,57.842382,-5.782182,271,1.5,3D,07 +DAB,BBC National DAB 1A12,BBC National DAB,225648000,51.484903,-1.558836,211,5,1A,12 +DAB,BBC National DAB 2302,BBC National DAB,225648000,51.236969,-2.625476,303,5.86342,23,02 +DAB,BBC National DAB 310E,BBC National DAB,225648000,52.234496,1.107319,64,4.3,31,0E +DAB,BBC National DAB 0A0A,BBC National DAB,225648000,51.017054,-0.70092,185,6.01646,0A,0A +DAB,BBC National DAB 390F,BBC National DAB,225648000,55.982322,-4.600708,152,0.4,39,0F +DAB,BBC National DAB 2903,BBC National DAB,225648000,53.220944,-3.31452,336,10,29,03 +DAB,BBC National DAB 4201,BBC National DAB,225648000,55.340908,-3.474167,270,0.3,42,01 +DAB,BBC National DAB 2403,BBC National DAB,225648000,51.811533,-2.688667,194,0.23,24,03 +DAB,BBC National DAB 0705,BBC National DAB,225648000,54.202505,-3.167346,259,5,07,05 +DAB,BBC National DAB 3E01,BBC National DAB,225648000,57.602976,-2.029937,225,2,3E,01 +DAB,BBC National DAB 340E,BBC National DAB,225648000,55.171858,-1.659136,60,1.1,34,0E +DAB,BBC National DAB 3803,BBC National DAB,225648000,57.591626,-4.27683,212,10,38,03 +DAB,BBC National DAB 3510,BBC National DAB,225648000,53.116187,-2.211069,325,2,35,10 +DAB,BBC National DAB 2705,BBC National DAB,225648000,51.556793,-3.628192,244,0.75,27,05 +DAB,BBC National DAB 2417,BBC National DAB,225648000,51.604177,-3.122175,360,1,24,17 +DAB,BBC National DAB 2806,BBC National DAB,225648000,52.067402,-4.076523,411,1,28,06 +DAB,BBC National DAB 280C,BBC National DAB,225648000,51.750918,-4.154115,279,1.2,28,0C +DAB,BBC National DAB 1D09,BBC National DAB,225648000,51.416202,-2.076595,175,1.90999,1D,09 +DAB,BBC National DAB 1910,BBC National DAB,225648000,54.203198,-5.913247,251,2,19,10 +DAB,BBC National DAB 0A03,BBC National DAB,225648000,50.787726,0.035848,81,1.26064,0A,03 +DAB,BBC National DAB 0414,BBC National DAB,225648000,50.402798,-5.053886,62,0.5,04,14 +DAB,BBC National DAB 340F,BBC National DAB,225648000,54.982447,-1.945593,165,1,34,0F +DAB,BBC National DAB 1913,BBC National DAB,225648000,54.590668,-5.677949,20,0.450001,19,13 +DAB,BBC National DAB 0D08,BBC National DAB,225648000,50.629952,-1.987364,196,1.5,0D,08 +DAB,BBC National DAB 220F,BBC National DAB,225648000,50.550217,-4.008413,508,3.2,22,0F +DAB,BBC National DAB 330A,BBC National DAB,225648000,52.82694,1.381376,27,0.07,33,0A +DAB,BBC National DAB 170F,BBC National DAB,225648000,52.275355,-0.885481,127,2,17,0F +DAB,BBC National DAB 3006,BBC National DAB,225648000,52.986825,-1.251436,129,0.032,30,06 +DAB,BBC National DAB 3D06,BBC National DAB,225648000,56.40396,-5.485882,122,1.17268,3D,06 +DAB,BBC National DAB 270F,BBC National DAB,225648000,51.593504,-3.546781,298,0.6,27,0F +DAB,BBC National DAB 0E02,BBC National DAB,225648000,50.753618,-4.005483,208,0.6,0E,02 +DAB,BBC National DAB 2E01,BBC National DAB,225648000,54.267351,-0.404494,151,2,2E,01 +DAB,BBC National DAB 3301,BBC National DAB,225648000,52.486385,1.715554,8,0.950001,33,01 +DAB,BBC National DAB 120E,BBC National DAB,225648000,51.790638,-1.179183,130,9.54993,12,0E +DAB,BBC National DAB 4305,BBC National DAB,225648000,55.661832,-3.227732,352,0.3,43,05 +DAB,BBC National DAB 0413,BBC National DAB,225648000,50.481389,-4.782638,116,1.2,04,13 +DAB,BBC National DAB 2C0D,BBC National DAB,225648000,53.841893,-2.266074,276,4.88619,2C,0D +DAB,BBC National DAB 3F0B,BBC National DAB,225648000,55.819142,-3.194377,248,1,3F,0B +DAB,BBC National DAB 050C,BBC National DAB,225648000,53.286941,-3.684982,125,0.6,05,0C +DAB,BBC National DAB 0706,BBC National DAB,225648000,54.674729,-2.728805,263,4,07,06 +DAB,BBC National DAB 1E01,BBC National DAB,225648000,52.507615,-0.343282,56,4.88619,1E,01 +DAB,BBC National DAB 1614,BBC National DAB,225648000,51.915442,-0.184286,133,1.2,16,14 +DAB,BBC National DAB 3C0C,BBC National DAB,225648000,56.687307,-3.759852,401,1,3C,0C +DAB,BBC National DAB 2204,BBC National DAB,225648000,50.381346,-4.067802,113,1.95447,22,04 +DAB,BBC National DAB 3401,BBC National DAB,225648000,54.868838,-1.771233,302,7.63994,34,01 +DAB,BBC National DAB 2409,BBC National DAB,225648000,51.685712,-3.036095,250,0.5,24,09 +DAB,BBC National DAB 0D07,BBC National DAB,225648000,50.729221,-1.948658,64,1.95447,0D,07 +DAB,BBC National DAB 3D0A,BBC National DAB,225648000,55.627446,-6.227617,93,1,3D,0A +DAB,BBC National DAB 0904,BBC National DAB,225648000,54.074773,-4.743515,69,0.4,09,04 +DAB,BBC National DAB 2805,BBC National DAB,225648000,51.944404,-4.661027,323,5,28,05 +DAB,BBC National DAB 230F,BBC National DAB,225648000,51.48519,-2.562764,91,1,23,0F +DAB,BBC National DAB 310A,BBC National DAB,225648000,52.290839,0.777315,69,2,31,0A +DAB,BBC National DAB 3513,BBC National DAB,225648000,52.728576,-2.0198,234,1.75,35,13 +DAB,BBC National DAB 2004,BBC National DAB,225648000,52.973416,-1.507057,125,3,20,04 +DAB,BBC National DAB 0912,BBC National DAB,225648000,54.312725,-4.381093,134,0.2,09,12 +DAB,BBC National DAB 3E13,BBC National DAB,225648000,57.113656,-2.096244,87,1.2,3E,13 +DAB,BBC National DAB 0403,BBC National DAB,225648000,50.209786,-5.238489,237,1.95447,04,03 +DAB,BBC National DAB 0B05,BBC National DAB,225648000,51.254494,-0.200986,234,0.954993,0B,05 +DAB,BBC National DAB 270E,BBC National DAB,225648000,51.741661,-3.679878,409,0.6,27,0E +DAB,BBC National DAB 2406,BBC National DAB,225648000,51.633901,-3.460735,316,1,24,06 +DAB,BBC National DAB 3702,BBC National DAB,225648000,52.878444,-3.091941,305,0.25,37,02 +DAB,BBC National DAB 2410,BBC National DAB,225648000,51.730587,-3.264891,419,1,24,10 +DAB,BBC National DAB 1C01,BBC National DAB,225648000,51.997433,-2.53989,204,10,1C,01 +DAB,BBC National DAB 3B05,BBC National DAB,225648000,54.593159,-2.041753,352,1,3B,05 +DAB,BBC National DAB 390C,BBC National DAB,225648000,55.991937,-4.793795,107,1.8,39,0C +DAB,BBC National DAB 390D,BBC National DAB,225648000,55.878164,-4.998928,166,1,39,0D +DAB,BBC National DAB 020D,BBC National DAB,225648000,50.676724,-1.370435,143,5,02,0D +DAB,BBC National DAB 3802,BBC National DAB,225648000,58.328006,-3.371413,221,10,38,02 +DAB,BBC National DAB 0A0D,BBC National DAB,225648000,50.946708,0.709647,44,0.2,0A,0D +DAB,BBC National DAB 2B12,BBC National DAB,225648000,53.541535,-2.020635,347,0.6,2B,12 +DAB,BBC National DAB 1D06,BBC National DAB,225648000,51.055855,-1.806762,107,2,1D,06 +DAB,BBC National DAB 180A,BBC National DAB,225648000,52.413654,-1.513171,91,0.5,18,0A +DAB,BBC National DAB 0702,BBC National DAB,225648000,54.749109,-3.14076,363,9.54993,07,02 +DAB,BBC National DAB 1603,BBC National DAB,225648000,52.130132,-0.241462,54,4.88619,16,03 +DAB,BBC National DAB 4502,BBC National DAB,225648000,60.140298,-1.284513,72,0.1,45,02 +DAB,BBC National DAB 3204,BBC National DAB,225648000,53.57127,-0.670181,42,0.6,32,04 +DAB,BBC National DAB 3708,BBC National DAB,225648000,52.547739,-2.115424,225,0.006,37,08 +DAB,BBC National DAB 440D,BBC National DAB,225648000,55.555786,-2.793221,290,9.77237,44,0D +DAB,BBC National DAB 3909,BBC National DAB,225648000,55.80577,-4.466669,230,1,39,09 +DAB,BBC National DAB 1517,BBC National DAB,225648000,52.029722,0.290628,120,1,15,17 +DAB,BBC National DAB 0D15,BBC National DAB,225648000,51.025199,-2.188304,244,2,0D,15 +DAB,BBC National DAB 2D03,BBC National DAB,225648000,53.379385,-1.513809,247,4,2D,03 +DAB,BBC National DAB 4105,BBC National DAB,225648000,52.763046,-1.275729,91,0.06,41,05 +DAB,BBC National DAB 340B,BBC National DAB,225648000,55.366418,-1.700379,171,0.950001,34,0B +DAB,BBC National DAB 0E01,BBC National DAB,225648000,50.684407,-3.223662,112,0.1,0E,01 +DAB,BBC National DAB 2310,BBC National DAB,225648000,51.467762,-2.478188,70,0.018197,23,10 +DAB,BBC National DAB 3205,BBC National DAB,225648000,53.191837,0.178307,60,2,32,05 +DAB,BBC National DAB 3B07,BBC National DAB,225648000,54.563232,-0.895147,69,0.3,3B,07 +DAB,BBC National DAB 2E07,BBC National DAB,225648000,53.961373,-2.140639,162,1.6,2E,07 +DAB,BBC National DAB 3D10,BBC National DAB,225648000,57.38642,-6.24289,390,5,3D,10 +DAB,BBC National DAB 2212,BBC National DAB,225648000,50.263014,-3.662366,98,0.3,22,12 +DAB,BBC National DAB 3D08,BBC National DAB,225648000,55.91751,-5.462223,481,5,3D,08 +DAB,BBC National DAB 0613,BBC National DAB,225648000,51.540835,0.698653,33,0.024,06,13 +DAB,BBC National DAB 1E09,BBC National DAB,225648000,52.773292,-0.143159,3,0.069,1E,09 +DAB,BBC National DAB 0401,BBC National DAB,225648000,50.348174,-4.801267,188,0.900001,04,01 +DAB,BBC National DAB 0D01,BBC National DAB,225648000,50.759634,-1.799193,42,0.6,0D,01 +DAB,BBC National DAB 0411,BBC National DAB,225648000,50.171587,-5.44349,18,0.1,04,11 +DAB,BBC National DAB 2A04,BBC National DAB,225648000,53.406267,-2.981914,10,0.5,2A,04 +DAB,BBC National DAB 0410,BBC National DAB,225648000,50.140528,-5.664396,199,0.6,04,10 +DAB,BBC National DAB 0113,BBC National DAB,225648000,51.11386,0.259324,144,0.02,01,13 +DAB,BBC National DAB 0415,BBC National DAB,225648000,50.168461,-5.016083,81,0.5,04,15 +DAB,BBC National DAB 1E03,BBC National DAB,225648000,52.662748,-0.501852,58,0.15,1E,03 +DAB,BBC National DAB 200D,BBC National DAB,225648000,53.170195,-1.633756,305,1.5,20,0D +DAB,BBC National DAB 1B16,BBC National DAB,225648000,51.775824,-2.234753,250,1.2,1B,16 +DAB,BBC National DAB 0E0B,BBC National DAB,225648000,50.807088,-3.104951,227,5,0E,0B +DAB,BBC National DAB 2D02,BBC National DAB,225648000,53.48869,-1.586409,301,1.2,2D,02 +DAB,BBC National DAB 3302,BBC National DAB,225648000,52.574817,1.331664,65,4.88619,33,02 +DAB,BBC National DAB 1905,BBC National DAB,225648000,54.799414,-7.388644,274,1.6,19,05 +DAB,BBC National DAB 420D,BBC National DAB,225648000,54.928074,-4.949004,190,0.8,42,0D +DAB,BBC National DAB 0610,BBC National DAB,225648000,52.004239,0.784592,71,2.4,06,10 +DAB,BBC National DAB 1312,BBC National DAB,225648000,52.600539,-1.833856,169,10,13,12 +DAB,BBC National DAB 330E,BBC National DAB,225648000,52.634975,0.72369,50,4,33,0E +DAB,BBC National DAB 0114,BBC National DAB,225648000,51.137832,1.335758,122,1,01,14 +DAB,BBC National DAB 330D,BBC National DAB,225648000,52.518759,1.138705,64,3.53973,33,0D +DAB,BBC National DAB 2110,BBC National DAB,225648000,51.058123,-3.070536,130,1,21,10 +DAB,BBC National DAB 3C16,BBC National DAB,225648000,56.444566,-2.925027,100,0.6,3C,16 +DAB,BBC National DAB 2802,BBC National DAB,225648000,51.662111,-4.735117,85,0.6,28,02 +DAB,BBC National DAB 010A,BBC National DAB,225648000,51.36004,1.397292,49,0.668495,01,0A +DAB,BBC National DAB 3709,BBC National DAB,225648000,52.670935,-2.550228,396,4.88619,37,09 +DAB,BBC National DAB 3306,BBC National DAB,225648000,52.422601,0.741906,39,0.25,33,06 +DAB,BBC National DAB 4205,BBC National DAB,225648000,55.183241,-3.798481,324,0.6,42,05 +DAB,BBC National DAB 350B,BBC National DAB,225648000,53.003549,-2.055691,265,1.95447,35,0B +DAB,BBC National DAB 370E,BBC National DAB,225648000,52.397256,-2.592523,494,2,37,0E +DAB,BBC National DAB 0E0E,BBC National DAB,225648000,50.903183,-3.509706,142,0.2,0E,0E +DAB,BBC National DAB 2C05,BBC National DAB,225648000,53.713283,-2.065416,221,1,2C,05 +DAB,BBC National DAB 0204,BBC National DAB,225648000,50.961044,-1.454599,84,1,02,04 +DAB,BBC National DAB 2203,BBC National DAB,225648000,50.353472,-4.242037,108,0.6,22,03 +DAB,BBC National DAB 0A04,BBC National DAB,225648000,50.882048,-0.257307,211,1.8,0A,04 +DAB,BBC National DAB 0402,BBC National DAB,225648000,50.258036,-5.037597,58,0.5,04,02 +DAB,BBC National DAB 3E12,BBC National DAB,225648000,57.07283,-3.027203,463,0.6,3E,12 +DAB,BBC National DAB 1911,BBC National DAB,225648000,54.838485,-6.190049,208,0.6,19,11 +DAB,BBC National DAB 0110,BBC National DAB,225648000,51.172557,0.297815,124,3.1,01,10 +DAB,BBC National DAB 130F,BBC National DAB,225648000,52.496364,-2.048417,267,9.54993,13,0F +DAB,BBC National DAB 0111,BBC National DAB,225648000,51.068688,1.072375,1,0.1,01,11 +DAB,BBC National DAB 270D,BBC National DAB,225648000,51.754672,-3.773522,351,0.6,27,0D +DAB,BBC National DAB 020E,BBC National DAB,225648000,50.601933,-1.199325,227,0.6,02,0E +DAB,BBC National DAB 4110,BBC National DAB,225648000,52.801451,-0.80088,138,4.77496,41,10 +DAB,BBC National DAB 3103,BBC National DAB,225648000,52.037726,1.200836,32,1.2,31,03 +DAB,BBC National DAB 3404,BBC National DAB,225648000,54.74117,-1.961914,404,0.8,34,04 +DAB,BBC National DAB 1706,BBC National DAB,225648000,52.297367,-0.675082,43,0.017,17,06 +DAB,BBC National DAB 3314,BBC National DAB,225648000,52.941952,0.953868,32,2,33,14 +DAB,BBC National DAB 2401,BBC National DAB,225648000,51.459476,-3.282282,129,7.8179,24,01 +DAB,BBC National DAB 3312,BBC National DAB,225648000,52.743618,0.384937,4,0.6,33,12 +DAB,BBC National DAB 3307,BBC National DAB,225648000,52.924381,1.251171,99,1,33,07 +DAB,BBC National DAB 2C06,BBC National DAB,225648000,53.812989,-2.411689,172,0.3,2C,06 +DAB,BBC National DAB 3A16,BBC National DAB,225648000,53.932231,-1.699923,212,1,3A,16 +DAB,BBC National DAB 2E05,BBC National DAB,225648000,54.473215,-0.595572,60,0.933254,2E,05 +DAB,BBC National DAB 0704,BBC National DAB,225648000,54.496676,-3.557726,133,2,07,04 +DAB,BBC National DAB 0A0E,BBC National DAB,225648000,50.824954,-0.113611,120,2,0A,0E +DAB,BBC National DAB 3405,BBC National DAB,225648000,55.035277,-1.453767,40,0.01,34,05 +DAB,BBC National DAB 2C03,BBC National DAB,225648000,53.67893,-2.17406,335,0.1,2C,03 +DAB,BBC National DAB 3108,BBC National DAB,225648000,52.190371,0.547028,125,10,31,08 +DAB,BBC National DAB 1B02,BBC National DAB,225648000,51.957393,-1.947856,148,0.1,1B,02 +DAB,BBC National DAB 070B,BBC National DAB,225648000,54.37436,-2.951306,216,0.977237,07,0B +DAB,BBC National DAB 2B11,BBC National DAB,225648000,53.625547,-2.514794,439,10,2B,11 +DAB,BBC National DAB 0D05,BBC National DAB,225648000,50.845187,-2.230756,157,0.6,0D,05 +DAB,BBC National DAB 1E0D,BBC National DAB,225648000,52.655393,0.160854,4,0.1,1E,0D +DAB,BBC National DAB 3106,BBC National DAB,225648000,52.100123,1.315423,32,0.5,31,06 +DAB,BBC National DAB 2906,BBC National DAB,225648000,53.076278,-3.044994,210,1,29,06 +DAB,BBC National DAB 0107,BBC National DAB,225648000,51.320696,0.287702,219,10,01,07 +DAB,BBC National DAB 0109,BBC National DAB,225648000,51.187004,0.955865,98,0.5,01,09 +DAB,BBC National DAB 160E,BBC National DAB,225648000,51.877958,-0.483622,207,4.00667,16,0E +DAB,Berkshire and North Hampshire 0308,Berkshire and North Hampshire,229072000,51.226848,-1.080409,176,1,03,08 +DAB,Berkshire and North Hampshire 0314,Berkshire and North Hampshire,229072000,51.414004,-0.796112,90,0.977237,03,14 +DAB,Berkshire and North Hampshire 0310,Berkshire and North Hampshire,229072000,51.325875,-0.81306,95,0.3,03,10 +DAB,Berkshire and North Hampshire 030C,Berkshire and North Hampshire,229072000,51.307873,-1.244777,220,1,03,0C +DAB,Berkshire and North Hampshire 030D,Berkshire and North Hampshire,229072000,51.481196,-0.978531,69,0.98,03,0D +DAB,Berkshire and North Hampshire 0301,Berkshire and North Hampshire,229072000,51.278623,-1.489559,215,0.4,03,01 +DAB,Berkshire and North Hampshire 030A,Berkshire and North Hampshire,229072000,51.548667,-0.775255,85,1,03,0A +DAB,Berkshire and North Hampshire 030E,Berkshire and North Hampshire,229072000,51.484903,-1.558836,211,1,03,0E +DAB,Berkshire and North Hampshire 030B,Berkshire and North Hampshire,229072000,51.414621,-1.290472,130,1,03,0B +DAB,Birmingham 1303,Birmingham,220352000,52.473153,-1.919722,152,0.3,13,03 +DAB,Birmingham 1301,Birmingham,220352000,52.294535,-1.946877,162,0.3,13,01 +DAB,Birmingham 1305,Birmingham,220352000,52.63655,-1.759098,145,2.5,13,05 +DAB,Birmingham 1302,Birmingham,220352000,52.375278,-2.013764,280,1,13,02 +DAB,Birmingham 130B,Birmingham,220352000,52.547739,-2.115424,225,0.3,13,0B +DAB,Birmingham 130F,Birmingham,220352000,52.496364,-2.048417,267,2,13,0F +DAB,Blackpool small-scale ,Blackpool small-scale,197648000,53.815794,-3.055258,11,0.1,, +DAB,Bournemouth 0D03,Bournemouth,218640000,50.66283,-2.443645,160,0.977237,0D,03 +DAB,Bournemouth 0D08,Bournemouth,218640000,50.629952,-1.987364,196,1,0D,08 +DAB,Bournemouth 0D07,Bournemouth,218640000,50.729221,-1.948658,64,0.693838,0D,07 +DAB,Bournemouth 0D15,Bournemouth,218640000,51.025199,-2.188304,244,0.5,0D,15 +DAB,Bournemouth 0D01,Bournemouth,218640000,50.759634,-1.799193,42,1,0D,01 +DAB,Bournemouth 0D05,Bournemouth,218640000,50.845187,-2.230756,157,1.20226,0D,05 +DAB,Bradford small-scale 3A06,Bradford small-scale,197648000,53.844699,-1.778397,108,0.05,3A,06 +DAB,Bradford small-scale 3A17,Bradford small-scale,197648000,53.820579,-1.728063,210,0.035,3A,17 +DAB,Bradford small-scale 3A15,Bradford small-scale,197648000,53.802804,-1.73744,183,0.1,3A,15 +DAB,Bradfrd&Huddersf 3A0A,Bradfrd&Huddersf,218640000,53.674328,-1.813774,257,1,3A,0A +DAB,Bradfrd&Huddersf 2E14,Bradfrd&Huddersf,218640000,53.611985,-1.66456,256,0.069994,2E,14 +DAB,Bradfrd&Huddersf 3A04,Bradfrd&Huddersf,218640000,53.533184,-1.858296,524,0.199526,3A,04 +DAB,Bradfrd&Huddersf 3A08,Bradfrd&Huddersf,218640000,53.832945,-1.752142,215,0.3,3A,08 +DAB,Bradfrd&Huddersf 3A0C,Bradfrd&Huddersf,218640000,53.895532,-1.896639,305,1,3A,0C +DAB,Bradfrd&Huddersf 3A11,Bradfrd&Huddersf,218640000,53.720111,-1.92874,256,1,3A,11 +DAB,Bradfrd&Huddersf 3A13,Bradfrd&Huddersf,218640000,53.761891,-1.692037,220,1.61,3A,13 +DAB,Bristol 2303,Bristol,218640000,51.441961,-2.722947,96,0.3,23,03 +DAB,Bristol 230E,Bristol,218640000,51.38779,-2.332925,175,0.977237,23,0E +DAB,Bristol 230B,Bristol,218640000,51.494699,-2.65363,90,0.3,23,0B +DAB,Bristol 230D,Bristol,218640000,51.392229,-2.630897,213,4,23,0D +DAB,Bristol 230C,Bristol,218640000,51.325227,-2.918301,73,2,23,0C +DAB,Bristol 2307,Bristol,218640000,51.608719,-2.485155,106,1.20226,23,07 +DAB,Bristol 230F,Bristol,218640000,51.48519,-2.562764,91,1,23,0F +DAB,Bristol 2310,Bristol,218640000,51.467762,-2.478188,70,0.6,23,10 +DAB,Cambridge 150F,Cambridge,220352000,52.213666,0.462135,110,2.4,15,0F +DAB,Cambridge 1507,Cambridge,220352000,52.412282,0.279524,22,1,15,07 +DAB,Cambridge 1506,Cambridge,220352000,52.215394,0.036667,62,2,15,06 +DAB,Cambridge 150B,Cambridge,220352000,52.130132,-0.241462,54,1.2,15,0B +DAB,Cambridge 1517,Cambridge,220352000,52.029722,0.290628,120,1,15,17 +DAB,Cambridge small-scale ,Cambridge small-scale,206352000,52.209429,0.14983,10,0.1,, +DAB,Cambridge small-scale ,Cambridge small-scale,206352000,52.158186,0.178647,72,0.075,, +DAB,Cardiff small-scale 240D,Cardiff small-scale,215072000,51.410121,-3.281189,82,0.07,24,0D +DAB,Cardiff small-scale 240E,Cardiff small-scale,215072000,51.50502,-3.165988,56,0.07,24,0E +DAB,Cardiff small-scale 2417,Cardiff small-scale,215072000,51.604177,-3.122175,360,0.025,24,17 +DAB,Central Scotland 3F0F,Central Scotland,222064000,55.912262,-3.200598,202,0.977237,3F,0F +DAB,Central Scotland 390E,Central Scotland,222064000,55.825353,-4.290679,67,0.01,39,0E +DAB,Central Scotland 3F02,Central Scotland,222064000,56.071791,-3.233186,181,0.780001,3F,02 +DAB,Central Scotland 3F05,Central Scotland,222064000,55.579376,-4.289814,286,0.5,3F,05 +DAB,Central Scotland 3F06,Central Scotland,222064000,56.071792,-4.055753,410,0.73,3F,06 +DAB,Central Scotland 390B,Central Scotland,222064000,55.886844,-4.294621,26,0.01,39,0B +DAB,Central Scotland 3F13,Central Scotland,222064000,55.861436,-4.268991,16,0.3,3F,13 +DAB,Central Scotland 3F01,Central Scotland,222064000,55.852721,-3.826093,277,1.79539,3F,01 +DAB,Central Scotland 3F03,Central Scotland,222064000,55.80577,-4.466669,230,0.947999,3F,03 +DAB,Channel Islands 1F01,Channel Islands,223936000,49.726408,-2.185861,52,0.2,1F,01 +DAB,Channel Islands 1001,Channel Islands,223936000,49.246023,-2.1012,134,2,10,01 +DAB,Channel Islands 1102,Channel Islands,223936000,49.457792,-2.579202,76,0.5,11,02 +DAB,Clevedon Avonmouth and Filton small-scale 2411,Clevedon Avonmouth and Filton small-scale,229072000,51.501345,-2.549924,75,0.1,24,11 +DAB,Clevedon Avonmouth and Filton small-scale 2412,Clevedon Avonmouth and Filton small-scale,229072000,51.494699,-2.65363,90,0.079816,24,12 +DAB,Cornwall 040A,Cornwall,218640000,50.82768,-4.530383,47,0.293171,04,0A +DAB,Cornwall 0405,Cornwall,218640000,50.511364,-4.43683,369,5,04,05 +DAB,Cornwall 0404,Cornwall,218640000,50.13053,-5.53431,62,0.3,04,04 +DAB,Cornwall 0416,Cornwall,218640000,50.101857,-5.285607,71,1,04,16 +DAB,Cornwall 0414,Cornwall,218640000,50.402798,-5.053886,62,0.5,04,14 +DAB,Cornwall 040B,Cornwall,218640000,50.550217,-4.008413,508,3.2,04,0B +DAB,Cornwall 0413,Cornwall,218640000,50.481389,-4.782638,116,1.2,04,13 +DAB,Cornwall 0403,Cornwall,218640000,50.209786,-5.238489,237,4.88619,04,03 +DAB,Cornwall 0401,Cornwall,218640000,50.348174,-4.801267,188,0.900001,04,01 +DAB,Cornwall 0410,Cornwall,218640000,50.140528,-5.664396,199,0.6,04,10 +DAB,Cornwall 0415,Cornwall,218640000,50.168461,-5.016083,81,0.5,04,15 +DAB,Cornwall 0402,Cornwall,218640000,50.258036,-5.037597,58,0.5,04,02 +DAB,Coventry 180D,Coventry,229072000,52.568941,-1.334553,125,0.5031,18,0D +DAB,Coventry 1803,Coventry,229072000,52.253028,-1.139914,199,1,18,03 +DAB,Coventry 1814,Coventry,229072000,52.540463,-1.503661,140,0.5,18,14 +DAB,Coventry 1817,Coventry,229072000,52.081402,-1.715399,228,2,18,17 +DAB,Coventry 1806,Coventry,229072000,52.294288,-1.519026,90,0.5,18,06 +DAB,Coventry 180C,Coventry,229072000,52.455227,-1.6222,184,0.5,18,0C +DAB,Coventry 180A,Coventry,229072000,52.413654,-1.513171,91,0.5,18,0A +DAB,Coventry 1807,Coventry,229072000,52.450069,-1.397116,106,0.5,18,07 +DAB,Coventry small-scale ,Coventry small-scale,204640000,52.413553,-1.510526,92,0.06,, +DAB,D1 National 0B02,D1 National,222064000,51.594459,-0.129257,92,2,0B,02 +DAB,D1 National 3502,D1 National,222064000,53.027231,-2.28071,220,2,35,02 +DAB,D1 National 3C08,D1 National,223936000,56.555053,-2.986125,312,10,3C,08 +DAB,D1 National 3C03,D1 National,223936000,56.711856,-2.671353,130,0.045,3C,03 +DAB,D1 National 0502,D1 National,222064000,53.01985,-4.273475,300,1.85675,05,02 +DAB,D1 National 1908,D1 National,222064000,54.342402,-6.629562,109,5,19,08 +DAB,D1 National 1606,D1 National,222064000,51.819509,-0.811742,81,0.03,16,06 +DAB,D1 National 1914,D1 National,222064000,54.661435,-5.675928,21,0.024,19,14 +DAB,D1 National 0308,D1 National,222064000,51.226848,-1.080409,176,1,03,08 +DAB,D1 National 230E,D1 National,222064000,51.38779,-2.332925,175,0.977237,23,0E +DAB,D1 National 0E07,D1 National,222064000,50.447129,-3.610498,196,2.89309,0E,07 +DAB,D1 National 3A09,D1 National,222064000,53.811543,-1.641253,149,0.977237,3A,09 +DAB,D1 National 3202,D1 National,222064000,53.335814,-0.171983,125,4.45625,32,02 +DAB,D1 National 0614,D1 National,222064000,51.550856,0.5811,80,0.089125,06,14 +DAB,D1 National 3B02,D1 National,222064000,54.358848,-1.149317,381,2,3B,02 +DAB,D1 National 0D03,D1 National,222064000,50.66283,-2.443645,160,1.56358,0D,03 +DAB,D1 National 2C07,D1 National,222064000,53.809005,-3.05038,6,0.036308,2C,07 +DAB,D1 National 280D,D1 National,222064000,52.360613,-4.102585,174,1.4,28,0D +DAB,D1 National 0115,D1 National,222064000,51.324006,0.520684,192,8.2,01,15 +DAB,D1 National 1A0B,D1 National,222064000,51.609363,-1.794027,148,3.98107,1A,0B +DAB,D1 National 3107,D1 National,222064000,52.311386,1.598136,22,0.1,31,07 +DAB,D1 National 3203,D1 National,222064000,52.962367,-0.015354,3,0.03,32,03 +DAB,D1 National 1609,D1 National,222064000,51.998806,-0.669784,169,2.37123,16,09 +DAB,D1 National 4312,D1 National,223936000,55.912262,-3.200598,202,0.977237,43,12 +DAB,D1 National 0E0A,D1 National,222064000,50.395927,-3.518996,50,0.03,0E,0A +DAB,D1 National 1C04,D1 National,222064000,52.355115,-2.078214,147,1.46585,1C,04 +DAB,D1 National 0B0A,D1 National,222064000,51.727838,-0.176464,127,1.29,0B,0A +DAB,D1 National 1903,D1 National,222064000,54.423191,-7.462812,305,3.2,19,03 +DAB,D1 National 400E,D1 National,223936000,55.409586,-4.699754,273,2.4,40,0E +DAB,D1 National 0817,D1 National,222064000,51.521504,-0.138879,27,0.8,08,17 +DAB,D1 National 2F0C,D1 National,222064000,54.124979,-0.218986,105,1.2,2F,0C +DAB,D1 National 0A02,D1 National,222064000,50.909654,-0.627813,245,0.126575,0A,02 +DAB,D1 National 330C,D1 National,222064000,52.659164,1.715858,17,0.025,33,0C +DAB,D1 National 0709,D1 National,222064000,54.773382,-3.091075,289,2.7819,07,09 +DAB,D1 National 3415,D1 National,222064000,54.969789,-1.608675,15,0.025,34,15 +DAB,D1 National 190A,D1 National,222064000,54.160861,-6.385716,340,3,19,0A +DAB,D1 National 0405,D1 National,222064000,50.511364,-4.43683,369,2,04,05 +DAB,D1 National 2707,D1 National,222064000,51.818501,-4.066216,255,1,27,07 +DAB,D1 National 1909,D1 National,222064000,54.676355,-5.928737,207,1,19,09 +DAB,D1 National 080D,D1 National,222064000,51.263565,-0.086822,226,0.14,08,0D +DAB,D1 National 390E,D1 National,223936000,55.825353,-4.290679,67,0.01,39,0E +DAB,D1 National 010B,D1 National,222064000,51.265673,1.013529,67,0.4,01,0B +DAB,D1 National 0316,D1 National,222064000,51.6119,-0.734849,142,0.6,03,16 +DAB,D1 National 1604,D1 National,222064000,51.697895,-0.618141,127,0.1,16,04 +DAB,D1 National 2D12,D1 National,222064000,53.283991,-1.427446,179,1,2D,12 +DAB,D1 National 150F,D1 National,222064000,52.213666,0.462135,110,0.1,15,0F +DAB,D1 National 0211,D1 National,222064000,50.649245,-1.328934,166,5,02,11 +DAB,D1 National 2404,D1 National,222064000,51.601588,-2.935417,94,2,24,04 +DAB,D1 National 1B11,D1 National,222064000,51.868264,-2.175397,147,3.06,1B,11 +DAB,D1 National 1B0A,D1 National,222064000,51.750794,-1.994303,184,0.035,1B,0A +DAB,D1 National 0601,D1 National,222064000,51.803409,1.165049,7,0.012,06,01 +DAB,D1 National 2D0A,D1 National,222064000,53.346243,-0.886897,62,0.07,2D,0A +DAB,D1 National 2116,D1 National,222064000,50.916513,-2.700119,134,2,21,16 +DAB,D1 National 0605,D1 National,222064000,51.917174,0.925798,45,0.4437,06,05 +DAB,D1 National 0808,D1 National,222064000,51.58316,-0.247792,46,0.3,08,08 +DAB,D1 National 0314,D1 National,222064000,51.414004,-0.796112,90,0.488619,03,14 +DAB,D1 National 4111,D1 National,222064000,52.710153,-1.285042,240,1.5,41,11 +DAB,D1 National 0210,D1 National,222064000,51.063167,-1.360216,158,0.6,02,10 +DAB,D1 National 3F02,D1 National,223936000,56.071791,-3.233186,181,6.75,3F,02 +DAB,D1 National 0B01,D1 National,222064000,51.424161,-0.074944,110,6.15971,0B,01 +DAB,D1 National 0616,D1 National,222064000,51.719492,0.58056,105,2,06,16 +DAB,D1 National ,D1 National,222064000,51.469253,0.260425,3,1e-05,, +DAB,D1 National 400A,D1 National,223936000,55.579376,-4.289814,286,7,40,0A +DAB,D1 National 2C02,D1 National,222064000,53.696571,-2.443266,261,0.028184,2C,02 +DAB,D1 National 1702,D1 National,222064000,52.253028,-1.139914,199,9.26343,17,02 +DAB,D1 National 1901,D1 National,222064000,54.607538,-6.009471,368,9.54993,19,01 +DAB,D1 National 2D06,D1 National,222064000,53.531884,-1.091294,7,0.022,2D,06 +DAB,D1 National 0102,D1 National,222064000,51.11163,1.247272,133,1.44544,01,02 +DAB,D1 National 3311,D1 National,222064000,52.608286,0.367383,5,0.0337,33,11 +DAB,D1 National 230D,D1 National,222064000,51.392229,-2.630897,213,2,23,0D +DAB,D1 National 010D,D1 National,222064000,51.292731,0.979008,122,2,01,0D +DAB,D1 National 3416,D1 National,222064000,54.775175,-1.590656,100,0.03,34,16 +DAB,D1 National 3E07,D1 National,223936000,56.999987,-2.389944,325,10,3E,07 +DAB,D1 National 3F06,D1 National,223936000,56.071792,-4.055753,410,2,3F,06 +DAB,D1 National 0A01,D1 National,222064000,50.764511,0.288682,11,1,0A,01 +DAB,D1 National 2402,D1 National,222064000,51.611393,-3.302735,381,0.6,24,02 +DAB,D1 National 1507,D1 National,222064000,52.412282,0.279524,22,0.04,15,07 +DAB,D1 National 2E14,D1 National,222064000,53.611985,-1.66456,256,6.0256,2E,14 +DAB,D1 National 0811,D1 National,222064000,51.361981,-0.349183,24,0.3,08,11 +DAB,D1 National 3B,D1 National,222064000,54.556455,-1.120934,237,0.8,3B, +DAB,D1 National 0E09,D1 National,222064000,50.719122,-3.562148,102,3.75,0E,09 +DAB,D1 National 010C,D1 National,222064000,51.305892,0.873532,33,0.691831,01,0C +DAB,D1 National 3403,D1 National,222064000,54.978039,-1.662966,120,1.4,34,03 +DAB,D1 National 300A,D1 National,222064000,53.140513,-1.232922,168,1,30,0A +DAB,D1 National 0208,D1 National,222064000,50.853922,-1.067587,102,0.5,02,08 +DAB,D1 National 0213,D1 National,222064000,50.877178,-1.278479,50,0.03,02,13 +DAB,D1 National 390B,D1 National,223936000,55.886844,-4.294621,26,0.01,39,0B +DAB,D1 National 3902,D1 National,223936000,55.861436,-4.268991,16,0.3,39,02 +DAB,D1 National 2B09,D1 National,222064000,53.455242,-1.960506,262,0.050119,2B,09 +DAB,D1 National 3309,D1 National,222064000,52.773873,0.649768,92,1.25,33,09 +DAB,D1 National 3303,D1 National,222064000,52.602996,1.722916,1,0.4,33,03 +DAB,D1 National 0C16,D1 National,222064000,51.228358,-0.605467,142,4.88619,0C,16 +DAB,D1 National 0404,D1 National,222064000,50.13053,-5.53431,62,0.013,04,04 +DAB,D1 National 030C,D1 National,222064000,51.307873,-1.244777,220,5,03,0C +DAB,D1 National 060B,D1 National,222064000,51.739903,0.097485,110,0.25,06,0B +DAB,D1 National 0803,D1 National,222064000,51.622949,-0.356487,125,0.3,08,03 +DAB,D1 National 0A06,D1 National,222064000,50.861622,0.566431,76,1,0A,06 +DAB,D1 National 2801,D1 National,222064000,51.899195,-4.86653,186,1,28,01 +DAB,D1 National 3304,D1 National,222064000,52.893823,0.496359,20,0.06,33,04 +DAB,D1 National 1301,D1 National,222064000,52.294535,-1.946877,162,0.011,13,01 +DAB,D1 National 0A07,D1 National,222064000,50.976724,0.230437,158,2.2,0A,07 +DAB,D1 National 030D,D1 National,222064000,51.481196,-0.978531,69,1,03,0D +DAB,D1 National 1612,D1 National,222064000,51.728571,-0.426217,142,3.12716,16,12 +DAB,D1 National 3409,D1 National,222064000,54.893895,-1.363068,10,0.794328,34,09 +DAB,D1 National 2F07,D1 National,222064000,53.803052,-0.565371,164,2,2F,07 +DAB,D1 National 0E0C,D1 National,222064000,50.566699,-3.506408,205,0.5,0E,0C +DAB,D1 National 3A04,D1 National,222064000,53.533184,-1.858296,524,4.68,3A,04 +DAB,D1 National 0A09,D1 National,222064000,51.064289,-0.321546,53,0.025,0A,09 +DAB,D1 National 4115,D1 National,222064000,52.63314,-1.002896,153,1.2,41,15 +DAB,D1 National 0C01,D1 National,222064000,51.234232,-0.820719,179,1,0C,01 +DAB,D1 National 0E08,D1 National,222064000,50.979244,-4.098818,200,4.55,0E,08 +DAB,D1 National 2E0B,D1 National,222064000,53.832945,-1.752142,215,1,2E,0B +DAB,D1 National 3A0C,D1 National,222064000,53.895532,-1.896639,305,3,3A,0C +DAB,D1 National 0707,D1 National,222064000,54.314559,-2.708127,172,2.9,07,07 +DAB,D1 National 3411,D1 National,222064000,55.001052,-1.665743,120,0.01,34,11 +DAB,D1 National 1C06,D1 National,222064000,52.363252,-2.282794,82,0.087,1C,06 +DAB,D1 National 2703,D1 National,222064000,51.62891,-3.919745,193,1,27,03 +DAB,D1 National 3F01,D1 National,223936000,55.852721,-3.826093,277,4.77496,3F,01 +DAB,D1 National 3C09,D1 National,223936000,56.372293,-3.453361,178,1.90561,3C,09 +DAB,D1 National 2C0E,D1 National,222064000,54.089336,-2.779963,88,1.95447,2C,0E +DAB,D1 National 1808,D1 National,222064000,52.081613,-1.728531,260,3.23594,18,08 +DAB,D1 National 0812,D1 National,222064000,51.319591,-0.372374,60,1.5,08,12 +DAB,D1 National 4108,D1 National,222064000,52.637434,-1.125451,57,0.03,41,08 +DAB,D1 National 160D,D1 National,222064000,51.981709,-0.201702,82,0.025,16,0D +DAB,D1 National 1907,D1 National,222064000,55.109172,-6.886121,341,6.30001,19,07 +DAB,D1 National 3209,D1 National,222064000,53.205578,-0.546174,15,0.06,32,09 +DAB,D1 National 0501,D1 National,222064000,53.307002,-4.12825,146,0.977237,05,01 +DAB,D1 National 1904,D1 National,222064000,55.00421,-7.368968,175,6.2,19,04 +DAB,D1 National 1506,D1 National,222064000,52.215394,0.036667,62,1,15,06 +DAB,D1 National 030A,D1 National,222064000,51.548667,-0.775255,85,0.025,03,0A +DAB,D1 National 1A05,D1 National,222064000,51.591795,-2.089739,91,0.02,1A,05 +DAB,D1 National 2B06,D1 National,222064000,53.480177,-2.23864,43,0.5,2B,06 +DAB,D1 National 060F,D1 National,222064000,51.92387,1.086097,35,1,06,0F +DAB,D1 National 3008,D1 National,222064000,52.976309,-1.132626,122,1,30,08 +DAB,D1 National 3707,D1 National,222064000,52.905093,-2.493571,100,0.033884,37,07 +DAB,D1 National 1A13,D1 National,222064000,51.417912,-1.699977,192,0.2,1A,13 +DAB,D1 National 2302,D1 National,222064000,51.236969,-2.625476,303,4.88619,23,02 +DAB,D1 National 310E,D1 National,222064000,52.234496,1.107319,64,1,31,0E +DAB,D1 National 0A0A,D1 National,222064000,51.017054,-0.70092,185,1.8,0A,0A +DAB,D1 National 2903,D1 National,222064000,53.220944,-3.31452,336,9.50001,29,03 +DAB,D1 National 0705,D1 National,222064000,54.202505,-3.167346,259,2.8,07,05 +DAB,D1 National 3A10,D1 National,222064000,53.736807,-1.572457,120,1,3A,10 +DAB,D1 National 0B0F,D1 National,222064000,51.615389,-0.448479,105,0.1,0B,0F +DAB,D1 National 3803,D1 National,223936000,57.591626,-4.27683,212,10,38,03 +DAB,D1 National 1D09,D1 National,222064000,51.416202,-2.076595,175,1.90999,1D,09 +DAB,D1 National 0A03,D1 National,222064000,50.787726,0.035848,81,0.977237,0A,03 +DAB,D1 National 1913,D1 National,222064000,54.590668,-5.677949,20,0.015,19,13 +DAB,D1 National 0D08,D1 National,222064000,50.629952,-1.987364,196,1,0D,08 +DAB,D1 National 220F,D1 National,222064000,50.550217,-4.008413,508,3.2,22,0F +DAB,D1 National 3006,D1 National,222064000,52.986825,-1.251436,129,0.03,30,06 +DAB,D1 National 0C05,D1 National,222064000,51.242524,-0.945288,115,1,0C,05 +DAB,D1 National 2E01,D1 National,222064000,54.267351,-0.404494,151,2,2E,01 +DAB,D1 National 3301,D1 National,222064000,52.486385,1.715554,8,0.501187,33,01 +DAB,D1 National 120E,D1 National,222064000,51.790638,-1.179183,130,7.4012,12,0E +DAB,D1 National 2C0D,D1 National,222064000,53.841893,-2.266074,276,2.029,2C,0D +DAB,D1 National 0706,D1 National,222064000,54.674729,-2.728805,263,2,07,06 +DAB,D1 National 1E01,D1 National,222064000,52.507615,-0.343282,56,4.88619,1E,01 +DAB,D1 National 1614,D1 National,222064000,51.915442,-0.184286,133,0.06,16,14 +DAB,D1 National 2204,D1 National,222064000,50.381346,-4.067802,113,1.95447,22,04 +DAB,D1 National 3401,D1 National,222064000,54.868838,-1.771233,302,6.01646,34,01 +DAB,D1 National 2805,D1 National,222064000,51.944404,-4.661027,323,1.69824,28,05 +DAB,D1 National 230F,D1 National,222064000,51.48519,-2.562764,91,1,23,0F +DAB,D1 National 310A,D1 National,222064000,52.290839,0.777315,69,1.8,31,0A +DAB,D1 National 3E13,D1 National,223936000,57.113656,-2.096244,87,1.2,3E,13 +DAB,D1 National 0403,D1 National,222064000,50.209786,-5.238489,237,1.28995,04,03 +DAB,D1 National 0B05,D1 National,222064000,51.254494,-0.200986,234,0.954993,0B,05 +DAB,D1 National 0813,D1 National,222064000,51.451704,-0.297446,40,0.3,08,13 +DAB,D1 National 1C01,D1 National,222064000,51.997433,-2.53989,204,5,1C,01 +DAB,D1 National 390C,D1 National,223936000,55.991937,-4.793795,107,0.6,39,0C +DAB,D1 National 2B12,D1 National,222064000,53.541535,-2.020635,347,0.6,2B,12 +DAB,D1 National 1D06,D1 National,222064000,51.055855,-1.806762,107,1.9,1D,06 +DAB,D1 National 180A,D1 National,222064000,52.413654,-1.513171,91,0.5,18,0A +DAB,D1 National 1603,D1 National,222064000,52.130132,-0.241462,54,4.59302,16,03 +DAB,D1 National 3204,D1 National,222064000,53.57127,-0.670181,42,0.077,32,04 +DAB,D1 National 3708,D1 National,222064000,52.547739,-2.115424,225,0.6,37,08 +DAB,D1 National 440D,D1 National,223936000,55.555786,-2.793221,290,9.77237,44,0D +DAB,D1 National 3909,D1 National,223936000,55.80577,-4.466669,230,0.913,39,09 +DAB,D1 National 1517,D1 National,222064000,52.029722,0.290628,120,1,15,17 +DAB,D1 National 2D03,D1 National,222064000,53.379385,-1.513809,247,2.69154,2D,03 +DAB,D1 National 4105,D1 National,222064000,52.763046,-1.275729,91,0.06,41,05 +DAB,D1 National 0E01,D1 National,222064000,50.684407,-3.223662,112,0.0302,0E,01 +DAB,D1 National 2310,D1 National,222064000,51.467762,-2.478188,70,0.02,23,10 +DAB,D1 National 0613,D1 National,222064000,51.540835,0.698653,33,0.024,06,13 +DAB,D1 National 1E09,D1 National,222064000,52.773292,-0.143159,3,0.07,1E,09 +DAB,D1 National 2A04,D1 National,222064000,53.406267,-2.981914,10,0.5,2A,04 +DAB,D1 National 0113,D1 National,222064000,51.11386,0.259324,144,0.02,01,13 +DAB,D1 National 080B,D1 National,222064000,51.43569,-0.508094,16,0.3,08,0B +DAB,D1 National 1E03,D1 National,222064000,52.662748,-0.501852,58,0.036,1E,03 +DAB,D1 National 0E0B,D1 National,222064000,50.807088,-3.104951,227,2,0E,0B +DAB,D1 National 3302,D1 National,222064000,52.574817,1.331664,65,1.07496,33,02 +DAB,D1 National 1905,D1 National,222064000,54.799414,-7.388644,274,1.6,19,05 +DAB,D1 National 0610,D1 National,222064000,52.004239,0.784592,71,2.39883,06,10 +DAB,D1 National 1312,D1 National,222064000,52.600539,-1.833856,169,8.73,13,12 +DAB,D1 National 330E,D1 National,222064000,52.634975,0.72369,50,0.17,33,0E +DAB,D1 National 0114,D1 National,222064000,51.137832,1.335758,122,1,01,14 +DAB,D1 National 330D,D1 National,222064000,52.518759,1.138705,64,2.83178,33,0D +DAB,D1 National 2110,D1 National,222064000,51.058123,-3.070536,130,1,21,10 +DAB,D1 National 010A,D1 National,222064000,51.36004,1.397292,49,0.381997,01,0A +DAB,D1 National 3709,D1 National,222064000,52.670935,-2.550228,396,7.8179,37,09 +DAB,D1 National 0204,D1 National,222064000,50.961044,-1.454599,84,0.5,02,04 +DAB,D1 National 2203,D1 National,222064000,50.353472,-4.242037,108,0.6,22,03 +DAB,D1 National 0A04,D1 National,222064000,50.882048,-0.257307,211,1.8,0A,04 +DAB,D1 National 0110,D1 National,222064000,51.172557,0.297815,124,1.46,01,10 +DAB,D1 National 130F,D1 National,222064000,52.496364,-2.048417,267,9.54993,13,0F +DAB,D1 National 0111,D1 National,222064000,51.068688,1.072375,1,0.01,01,11 +DAB,D1 National 4110,D1 National,222064000,52.801451,-0.80088,138,8.40394,41,10 +DAB,D1 National 3103,D1 National,222064000,52.037726,1.200836,32,1.2,31,03 +DAB,D1 National 1706,D1 National,222064000,52.297367,-0.675082,43,0.02,17,06 +DAB,D1 National 2401,D1 National,222064000,51.459476,-3.282282,129,8.79514,24,01 +DAB,D1 National 3312,D1 National,222064000,52.743618,0.384937,4,0.03,33,12 +DAB,D1 National 1D04,D1 National,222064000,51.336803,-2.264225,87,0.0978,1D,04 +DAB,D1 National 2C06,D1 National,222064000,53.812989,-2.411689,172,0.03,2C,06 +DAB,D1 National 2E05,D1 National,222064000,54.473215,-0.595572,60,1,2E,05 +DAB,D1 National 0704,D1 National,222064000,54.496676,-3.557726,133,1.95,07,04 +DAB,D1 National 0A0E,D1 National,222064000,50.824954,-0.113611,120,0.870964,0A,0E +DAB,D1 National 3405,D1 National,222064000,55.035277,-1.453767,40,0.01,34,05 +DAB,D1 National 2B11,D1 National,222064000,53.625547,-2.514794,439,10,2B,11 +DAB,D1 National 1E0D,D1 National,222064000,52.655393,0.160854,4,0.015,1E,0D +DAB,D1 National 0701,D1 National,222064000,54.635112,-3.548915,37,1,07,01 +DAB,D1 National 0107,D1 National,222064000,51.320696,0.287702,219,4.5,01,07 +DAB,D1 National 0109,D1 National,222064000,51.187004,0.955865,98,0.5,01,09 +DAB,D1 National 160E,D1 National,222064000,51.877958,-0.483622,207,3.12716,16,0E +DAB,Derbyshire 200C,Derbyshire,211648000,53.094354,-1.561526,315,0.6,20,0C +DAB,Derbyshire 2016,Derbyshire,211648000,52.63655,-1.759098,145,5,20,16 +DAB,Derbyshire 2004,Derbyshire,211648000,52.973416,-1.507057,125,3,20,04 +DAB,Derbyshire 200D,Derbyshire,211648000,53.170195,-1.633756,305,1.6,20,0D +DAB,Derry/Londonderry small-scale ,Derry/Londonderry small-scale,220352000,54.82055,-7.440534,123,0.1,, +DAB,Derry/Londonderry small-scale ,Derry/Londonderry small-scale,220352000,55.004444,-7.365259,160,0.2,, +DAB,Devon 0E17,Devon,220352000,50.505425,-3.752519,156,0.1,0E,17 +DAB,Devon 0E03,Devon,213360000,51.040763,-4.13826,116,0.6,0E,03 +DAB,Devon 0E07,Devon,220352000,50.447129,-3.610498,196,5,0E,07 +DAB,Devon 0E12,Devon,213360000,51.111834,-4.151684,95,0.6,0E,12 +DAB,Devon 0E0A,Devon,220352000,50.395927,-3.518996,50,0.3,0E,0A +DAB,Devon 0E13,Devon,220352000,50.634311,-3.354151,102,0.6,0E,13 +DAB,Devon 0E09,Devon,220352000,50.719122,-3.562148,102,1,0E,09 +DAB,Devon 0E0C,Devon,220352000,50.566699,-3.506408,205,0.5,0E,0C +DAB,Devon 0E08,Devon,213360000,50.979244,-4.098818,200,3.90895,0E,08 +DAB,Devon 0E14,Devon,213360000,51.197967,-4.138233,203,0.3,0E,14 +DAB,Devon 0E10,Devon,220352000,50.349538,-3.565025,107,0.4,0E,10 +DAB,Devon 0E02,Devon,213360000,50.753618,-4.005483,208,0.3,0E,02 +DAB,Devon 0E01,Devon,220352000,50.684407,-3.223662,112,0.3,0E,01 +DAB,Devon 0E0B,Devon,220352000,50.807088,-3.104951,227,5,0E,0B +DAB,Devon 0E0E,Devon,220352000,50.903183,-3.509706,142,0.3,0E,0E +DAB,Dudley-Sbridge small-scale ,Dudley-Sbridge small-scale,195936000,52.479497,-2.124668,162,0.12,, +DAB,Dudley-Sbridge small-scale ,Dudley-Sbridge small-scale,195936000,52.508028,-2.08762,202,0.05,, +DAB,East Bristol small-scale 2315,East Bristol small-scale,204640000,51.434452,-2.568038,67,0.015,23,15 +DAB,East Bristol small-scale 2313,East Bristol small-scale,204640000,51.456323,-2.608393,57,0.018,23,13 +DAB,East Bristol small-scale 2308,East Bristol small-scale,204640000,51.392699,-2.47831,98,0.057963,23,08 +DAB,Edinburgh 4315,Edinburgh,229072000,55.974965,-2.776943,175,1,43,15 +DAB,Edinburgh 4308,Edinburgh,229072000,55.860005,-3.873572,275,2.4,43,08 +DAB,Edinburgh 4312,Edinburgh,229072000,55.912262,-3.200598,202,0.977237,43,12 +DAB,Edinburgh 430C,Edinburgh,229072000,56.071791,-3.233186,181,6,43,0C +DAB,Edinburgh 430D,Edinburgh,229072000,56.071792,-4.055753,410,0.74,43,0D +DAB,Edinburgh 4309,Edinburgh,229072000,55.948651,-3.200764,121,0.0272,43,09 +DAB,Edinburgh 4307,Edinburgh,229072000,56.126954,-3.523569,355,1.58,43,07 +DAB,Edinburgh 4306,Edinburgh,229072000,55.819142,-3.194377,248,2.5,43,06 +DAB,Edinburgh small-scale ,Edinburgh small-scale,204640000,55.912262,-3.200598,202,0.109441,, +DAB,Edinburgh small-scale ,Edinburgh small-scale,204640000,55.950839,-3.272833,160,0.0816,, +DAB,Essex 0615,Essex,229072000,51.705132,0.401129,90,1,06,15 +DAB,Essex 0614,Essex,229072000,51.550856,0.5811,80,1.2,06,14 +DAB,Essex 060C,Essex,229072000,51.865628,0.576571,62,0.5,06,0C +DAB,Essex 0601,Essex,229072000,51.803409,1.165049,7,0.12,06,01 +DAB,Essex 0605,Essex,229072000,51.917174,0.925798,45,0.5,06,05 +DAB,Essex ,Essex,229072000,51.469253,0.260425,3,1e-05,, +DAB,Essex 0603,Essex,229072000,51.801904,0.710885,81,0.5,06,03 +DAB,Essex 060B,Essex,229072000,51.739903,0.097485,110,0.25,06,0B +DAB,Essex 060F,Essex,229072000,51.92387,1.086097,35,0.25,06,0F +DAB,Essex 0613,Essex,229072000,51.540835,0.698653,33,1,06,13 +DAB,Essex 0610,Essex,229072000,52.004239,0.784592,71,1,06,10 +DAB,Exeter small-scale ,Exeter small-scale,202928000,50.755187,-3.399176,27,0.1,, +DAB,Exeter small-scale ,Exeter small-scale,202928000,50.747157,-3.526652,155,0.1,, +DAB,Exeter small-scale ,Exeter small-scale,202928000,50.728418,-3.539432,37,0.1,, +DAB,Glasgow 3904,Glasgow,220352000,55.860005,-3.873572,275,4.23,39,04 +DAB,Glasgow 390E,Glasgow,220352000,55.825353,-4.290679,67,0.1,39,0E +DAB,Glasgow 3906,Glasgow,220352000,55.579376,-4.289814,286,1,39,06 +DAB,Glasgow 3907,Glasgow,220352000,56.071792,-4.055753,410,2,39,07 +DAB,Glasgow 390B,Glasgow,220352000,55.886844,-4.294621,26,0.1,39,0B +DAB,Glasgow 3902,Glasgow,220352000,55.861436,-4.268991,16,0.3,39,02 +DAB,Glasgow 390F,Glasgow,220352000,55.982322,-4.600708,152,0.4,39,0F +DAB,Glasgow 390C,Glasgow,220352000,55.991937,-4.793795,107,1.8,39,0C +DAB,Glasgow 390D,Glasgow,220352000,55.878164,-4.998928,166,1,39,0D +DAB,Glasgow 3909,Glasgow,220352000,55.80577,-4.466669,230,0.886001,39,09 +DAB,Glasgow small-scale ,Glasgow small-scale,211648000,55.867219,-4.257501,29,0.1,, +DAB,Gloucestershire 1B11,Gloucestershire,213360000,51.868264,-2.175397,147,2,1B,11 +DAB,Gloucestershire 1B0A,Gloucestershire,213360000,51.750794,-1.994303,184,1.5,1B,0A +DAB,Gloucestershire 1B05,Gloucestershire,213360000,51.904177,-1.707798,241,1,1B,05 +DAB,Gloucestershire 1B16,Gloucestershire,213360000,51.775824,-2.234753,250,1.2,1B,16 +DAB,Hereford-Worcs 1C04,Hereford-Worcs,223936000,52.355115,-2.078214,147,0.533301,1C,04 +DAB,Hereford-Worcs 1C05,Hereford-Worcs,223936000,52.024853,-2.694563,164,0.6,1C,05 +DAB,Hereford-Worcs 1C06,Hereford-Worcs,223936000,52.363252,-2.282794,82,1,1C,06 +DAB,Hereford-Worcs 1C15,Hereford-Worcs,223936000,52.115715,-2.330146,161,5,1C,15 +DAB,Hereford-Worcs 1C01,Hereford-Worcs,223936000,51.997433,-2.53989,204,4,1C,01 +DAB,Herts Beds Bucks 1606,Herts Beds Bucks,215072000,51.819509,-0.811742,81,0.6,16,06 +DAB,Herts Beds Bucks 1609,Herts Beds Bucks,215072000,51.998806,-0.669784,169,2.73603,16,09 +DAB,Herts Beds Bucks 1601,Herts Beds Bucks,215072000,51.6119,-0.734849,142,0.6,16,01 +DAB,Herts Beds Bucks 1605,Herts Beds Bucks,215072000,51.744172,-0.128729,122,2,16,05 +DAB,Herts Beds Bucks 1612,Herts Beds Bucks,215072000,51.728571,-0.426217,142,3.90895,16,12 +DAB,Herts Beds Bucks 160D,Herts Beds Bucks,215072000,51.981709,-0.201702,82,0.025,16,0D +DAB,Herts Beds Bucks 1614,Herts Beds Bucks,215072000,51.915442,-0.184286,133,0.06,16,14 +DAB,Herts Beds Bucks 1603,Herts Beds Bucks,215072000,52.130132,-0.241462,54,4.89779,16,03 +DAB,Herts Beds Bucks 160E,Herts Beds Bucks,215072000,51.877958,-0.483622,207,0.488619,16,0E +DAB,Humberside 2F0C,Humberside,215072000,54.124979,-0.218986,105,1.58489,2F,0C +DAB,Humberside 2F0F,Humberside,215072000,54.001193,-0.762246,240,1.2,2F,0F +DAB,Humberside 2F0A,Humberside,215072000,53.567838,-0.079989,4,0.2,2F,0A +DAB,Humberside 2F07,Humberside,215072000,53.803052,-0.565371,164,5,2F,07 +DAB,Humberside 2F06,Humberside,215072000,53.765567,-0.335735,2,0.2,2F,06 +DAB,Humberside 2F01,Humberside,215072000,53.679468,0.03562,26,0.6,2F,01 +DAB,Humberside 2F09,Humberside,215072000,53.57127,-0.670181,42,1,2F,09 +DAB,Inverness 3805,Inverness,218640000,57.532303,-3.135126,353,5,38,05 +DAB,Inverness 3803,Inverness,218640000,57.591626,-4.27683,212,10,38,03 +DAB,Kent 0117,Kent,220352000,51.550856,0.5811,80,1.2,01,17 +DAB,Kent 0108,Kent,220352000,51.323563,0.520373,191,1.95447,01,08 +DAB,Kent 0103,Kent,220352000,51.21959,0.812581,193,2,01,03 +DAB,Kent 010B,Kent,220352000,51.265673,1.013529,67,0.5,01,0B +DAB,Kent 0105,Kent,220352000,51.099738,1.182579,166,0.0209,01,05 +DAB,Kent ,Kent,220352000,51.469253,0.260425,3,1e-05,, +DAB,Kent 010D,Kent,220352000,51.292731,0.979008,122,2,01,0D +DAB,Kent 010E,Kent,220352000,51.107491,1.084665,174,1.2,01,0E +DAB,Kent 0113,Kent,220352000,51.11386,0.259324,144,0.2,01,13 +DAB,Kent 0114,Kent,220352000,51.137832,1.335758,122,0.34,01,14 +DAB,Kent 010A,Kent,220352000,51.36004,1.397292,49,0.116657,01,0A +DAB,Kent 0110,Kent,220352000,51.172557,0.297815,124,1,01,10 +DAB,Kent 0111,Kent,220352000,51.068688,1.072375,1,0.1,01,11 +DAB,Kent 0107,Kent,220352000,51.320696,0.287702,219,2,01,07 +DAB,Kent 0109,Kent,220352000,51.187004,0.955865,98,1,01,09 +DAB,King's Lynn small-scale 0F07,King's Lynn small-scale,206352000,52.755989,0.409029,3,0,0F,07 +DAB,King's Lynn small-scale 0F0B,King's Lynn small-scale,206352000,52.75732,0.613636,90,0,0F,0B +DAB,Lancashire 2C07,Lancashire,223936000,53.809005,-3.05038,6,1.2,2C,07 +DAB,Lancashire 2C02,Lancashire,223936000,53.696571,-2.443266,261,0.6,2C,02 +DAB,Lancashire 2C0D,Lancashire,223936000,53.841893,-2.266074,276,2.95121,2C,0D +DAB,Lancashire 2C01,Lancashire,223936000,53.625547,-2.514794,439,2.93171,2C,01 +DAB,Leeds 3A09,Leeds,229072000,53.811543,-1.641253,149,0.977237,3A,09 +DAB,Leeds 3A01,Leeds,229072000,53.611985,-1.66456,256,5,3A,01 +DAB,Leeds 3A0F,Leeds,229072000,53.959135,-1.67687,283,1.1,3A,0F +DAB,Leeds 3A10,Leeds,229072000,53.736807,-1.572457,120,1,3A,10 +DAB,Leeds small-scale 3A05,Leeds small-scale,202928000,53.791637,-1.573115,43,0.1,3A,05 +DAB,Leeds small-scale 3A02,Leeds small-scale,202928000,53.811777,-1.550434,55,0.05,3A,02 +DAB,Leeds small-scale 3A07,Leeds small-scale,202928000,53.825224,-1.541327,104,0.0391,3A,07 +DAB,Leicestershire 4102,Leicestershire,218640000,52.71359,-1.319033,271,0.6,41,02 +DAB,Leicestershire 4111,Leicestershire,218640000,52.710153,-1.285042,240,1.8,41,11 +DAB,Leicestershire 4115,Leicestershire,218640000,52.63314,-1.002896,153,1.2,41,15 +DAB,Leicestershire 4108,Leicestershire,218640000,52.637434,-1.125451,57,0.3,41,08 +DAB,Leicestershire 4105,Leicestershire,218640000,52.763046,-1.275729,91,0.6005,41,05 +DAB,Leicestershire 4110,Leicestershire,218640000,52.801451,-0.80088,138,1.14599,41,10 +DAB,Lincolnshire 3202,Lincolnshire,223936000,53.335814,-0.171983,125,4.01063,32,02 +DAB,Lincolnshire 3206,Lincolnshire,223936000,52.918371,-0.592549,124,0.6,32,06 +DAB,Lincolnshire 320B,Lincolnshire,223936000,53.803052,-0.565371,164,5,32,0B +DAB,Lincolnshire 3209,Lincolnshire,223936000,53.205578,-0.546174,15,0.6,32,09 +DAB,Liverpool 2A0A,Liverpool,213360000,53.509257,-2.71981,170,1.6,2A,0A +DAB,Liverpool 2A07,Liverpool,213360000,53.105512,-3.053022,315,0.89,2A,07 +DAB,Liverpool 2A05,Liverpool,213360000,53.220944,-3.31452,336,1.2,2A,05 +DAB,Liverpool 2A04,Liverpool,213360000,53.406267,-2.981914,10,0.6,2A,04 +DAB,Liverpool 2A0F,Liverpool,213360000,53.625547,-2.514794,439,3,2A,0F +DAB,London 1 0B02,London 1,227360000,51.594459,-0.129257,92,0.8,0B,02 +DAB,London 1 0B15,London 1,227360000,51.324006,0.520684,192,1,0B,15 +DAB,London 1 0817,London 1,227360000,51.521504,-0.138879,27,0.8,08,17 +DAB,London 1 080D,London 1,227360000,51.263565,-0.086822,226,0.14,08,0D +DAB,London 1 0B06,London 1,227360000,51.40986,-0.085755,114,5,0B,06 +DAB,London 1 ,London 1,227360000,51.469253,0.260425,3,1e-05,, +DAB,London 1 0B03,London 1,227360000,51.228358,-0.605467,142,0.977237,0B,03 +DAB,London 1 0B07,London 1,227360000,51.728571,-0.426217,142,0.390895,0B,07 +DAB,London 1 1610,London 1,227360000,51.640531,-0.763678,156,0.01,16,10 +DAB,London 1 0304,London 1,227360000,51.234232,-0.820719,179,0.08,03,04 +DAB,London 1 0B14,London 1,227360000,51.317089,-0.09335,152,0.008079,0B,14 +DAB,London 1 0812,London 1,227360000,51.319591,-0.372374,60,0.01,08,12 +DAB,London 1 0805,London 1,227360000,51.548667,-0.775255,85,0.079433,08,05 +DAB,London 1 0B0C,London 1,227360000,51.915442,-0.184286,133,0.06,0B,0C +DAB,London 1 0B05,London 1,227360000,51.254494,-0.200986,234,1.23027,0B,05 +DAB,London 1 080F,London 1,227360000,51.320696,0.287702,219,4.5,08,0F +DAB,London 1 0313,London 1,227360000,51.877958,-0.483622,207,0.488619,03,13 +DAB,London 2 0B02,London 2,223936000,51.594459,-0.129257,92,1.04713,0B,02 +DAB,London 2 0B11,London 2,223936000,51.646963,-0.238974,144,0.25,0B,11 +DAB,London 2 0B04,London 2,223936000,51.324006,0.520684,192,2,0B,04 +DAB,London 2 0817,London 2,223936000,51.521504,-0.138879,27,0.709999,08,17 +DAB,London 2 080D,London 2,223936000,51.263565,-0.086822,226,0.14,08,0D +DAB,London 2 0808,London 2,223936000,51.58316,-0.247792,46,0.003,08,08 +DAB,London 2 0B06,London 2,223936000,51.40986,-0.085755,114,2,0B,06 +DAB,London 2 0B01,London 2,223936000,51.424161,-0.074944,110,2.88403,0B,01 +DAB,London 2 ,London 2,223936000,51.469253,0.260425,3,1e-05,, +DAB,London 2 0811,London 2,223936000,51.361981,-0.349183,24,0.003,08,11 +DAB,London 2 0B03,London 2,223936000,51.228358,-0.605467,142,1.6613,0B,03 +DAB,London 2 0B09,London 2,223936000,51.739903,0.097485,110,0.1,0B,09 +DAB,London 2 0803,London 2,223936000,51.622949,-0.356487,125,0.003,08,03 +DAB,London 2 0B07,London 2,223936000,51.728571,-0.426217,142,0.25,0B,07 +DAB,London 2 1610,London 2,223936000,51.640531,-0.763678,156,0.01,16,10 +DAB,London 2 0304,London 2,223936000,51.234232,-0.820719,179,0.08,03,04 +DAB,London 2 0B14,London 2,223936000,51.317089,-0.09335,152,0.0081,0B,14 +DAB,London 2 0812,London 2,223936000,51.319591,-0.372374,60,0.01,08,12 +DAB,London 2 0801,London 2,223936000,51.13246,-0.23632,92,0.117268,08,01 +DAB,London 2 0805,London 2,223936000,51.548667,-0.775255,85,0.079433,08,05 +DAB,London 2 0B0F,London 2,223936000,51.615389,-0.448479,105,0.1,0B,0F +DAB,London 2 0B0C,London 2,223936000,51.915442,-0.184286,133,0.06,0B,0C +DAB,London 2 0B05,London 2,223936000,51.254494,-0.200986,234,1.44204,0B,05 +DAB,London 2 0813,London 2,223936000,51.451704,-0.297446,40,0.003,08,13 +DAB,London 2 080B,London 2,223936000,51.43569,-0.508094,16,0.003,08,0B +DAB,London 2 080F,London 2,223936000,51.320696,0.287702,219,4.5,08,0F +DAB,London 2 0313,London 2,223936000,51.877958,-0.483622,207,0.342033,03,13 +DAB,London 3 0B02,London 3,218640000,51.594459,-0.129257,92,0.7,0B,02 +DAB,London 3 0B12,London 3,218640000,51.323563,0.520373,191,0.900001,0B,12 +DAB,London 3 0817,London 3,218640000,51.521504,-0.138879,27,0.8,08,17 +DAB,London 3 080D,London 3,218640000,51.263565,-0.086822,226,0.14,08,0D +DAB,London 3 0316,London 3,218640000,51.6119,-0.734849,142,0.6,03,16 +DAB,London 3 0B06,London 3,218640000,51.40986,-0.085755,114,2.5,0B,06 +DAB,London 3 ,London 3,218640000,51.469253,0.260425,3,1e-05,, +DAB,London 3 0809,London 3,218640000,51.744172,-0.128729,122,1,08,09 +DAB,London 3 0B03,London 3,218640000,51.228358,-0.605467,142,0.78179,0B,03 +DAB,London 3 0B07,London 3,218640000,51.728571,-0.426217,142,0.244309,0B,07 +DAB,London 3 0812,London 3,218640000,51.319591,-0.372374,60,0.01,08,12 +DAB,London 3 0805,London 3,218640000,51.548667,-0.775255,85,0.025,08,05 +DAB,London 3 0B0C,London 3,218640000,51.915442,-0.184286,133,0.06,0B,0C +DAB,London 3 0B05,London 3,218640000,51.254494,-0.200986,234,0.381997,0B,05 +DAB,London 3 080F,London 3,218640000,51.320696,0.287702,219,4.5,08,0F +DAB,London 3 0313,London 3,218640000,51.877958,-0.483622,207,0.42658,03,13 +DAB,Manchester 2B17,Manchester,227360000,53.646225,-2.076355,229,0.6,2B,17 +DAB,Manchester 2B06,Manchester,227360000,53.480177,-2.23864,43,0.5,2B,06 +DAB,Manchester 2B12,Manchester,227360000,53.541535,-2.020635,347,0.6,2B,12 +DAB,Manchester 2B0C,Manchester,227360000,53.206041,-2.10072,401,1,2B,0C +DAB,Manchester 2B11,Manchester,227360000,53.625547,-2.514794,439,4,2B,11 +DAB,Mid & West Wales 2813,Mid & West Wales,229072000,51.867126,-4.309135,137,0.6,28,13 +DAB,Mid & West Wales 280F,Mid & West Wales,229072000,51.818501,-4.066216,255,5,28,0F +DAB,Mid & West Wales 2803,Mid & West Wales,229072000,52.012591,-4.996935,123,0.977237,28,03 +DAB,Mid & West Wales 2811,Mid & West Wales,229072000,51.674549,-5.002613,71,1,28,11 +DAB,Mid & West Wales 2815,Mid & West Wales,229072000,51.62891,-3.919745,193,1.16,28,15 +DAB,Mid & West Wales 2805,Mid & West Wales,229072000,51.944404,-4.661027,323,5,28,05 +DAB,Morecambe Bay 0707,Morecambe Bay,218640000,54.314466,-2.708587,177,1.155,07,07 +DAB,Morecambe Bay 2C0E,Morecambe Bay,218640000,54.089336,-2.779963,88,1.5,2C,0E +DAB,Morecambe Bay 0705,Morecambe Bay,218640000,54.202505,-3.167346,259,1,07,05 +DAB,Morecambe Bay 070B,Morecambe Bay,218640000,54.37436,-2.951306,216,1,07,0B +DAB,N. Ireland 1908,N. Ireland,229072000,54.342402,-6.629562,109,5,19,08 +DAB,N. Ireland 1914,N. Ireland,229072000,54.661435,-5.675928,21,0.024,19,14 +DAB,N. Ireland 1902,N. Ireland,229072000,54.586989,-6.022076,301,7,19,02 +DAB,N. Ireland 1903,N. Ireland,229072000,54.423191,-7.462812,305,4.35,19,03 +DAB,N. Ireland 190A,N. Ireland,229072000,54.160861,-6.385716,340,3,19,0A +DAB,N. Ireland 1906,N. Ireland,229072000,54.653575,-5.957634,335,1,19,06 +DAB,N. Ireland 1907,N. Ireland,229072000,55.109172,-6.886121,341,6.2,19,07 +DAB,N. Ireland 1904,N. Ireland,229072000,55.00421,-7.368968,175,6.30957,19,04 +DAB,N. Ireland 1910,N. Ireland,229072000,54.203198,-5.913247,251,2,19,10 +DAB,N. Ireland 1913,N. Ireland,229072000,54.590668,-5.677949,20,0.015,19,13 +DAB,N. Ireland 1905,N. Ireland,229072000,54.799414,-7.388644,274,3.3,19,05 +DAB,NE Wales W Chesh 2903,NE Wales W Chesh,215072000,53.220944,-3.31452,336,3.55,29,03 +DAB,NE Wales W Chesh 2908,NE Wales W Chesh,215072000,53.406267,-2.981914,10,1.5,29,08 +DAB,NE Wales W Chesh 2906,NE Wales W Chesh,215072000,53.076278,-3.044994,210,3,29,06 +DAB,Newcastle and Gateshead small-scale ,Newcastle and Gateshead small-scale,195936000,54.996393,-1.671097,105,0.1,, +DAB,Newcastle and Gateshead small-scale ,Newcastle and Gateshead small-scale,195936000,54.938423,-1.580416,160,0.1,, +DAB,Norfolk 330C,Norfolk,211648000,52.659164,1.715858,17,0.5,33,0C +DAB,Norfolk 3310,Norfolk,211648000,52.652502,0.872498,85,1.2,33,10 +DAB,Norfolk 3303,Norfolk,211648000,52.602996,1.722916,1,0.25,33,03 +DAB,Norfolk 330B,Norfolk,211648000,52.745033,1.582514,2,0.3,33,0B +DAB,Norfolk 3317,Norfolk,211648000,52.234496,1.107319,64,2,33,17 +DAB,Norfolk 330A,Norfolk,211648000,52.82694,1.381376,27,0.07,33,0A +DAB,Norfolk 3316,Norfolk,211648000,52.640225,1.305146,38,0.3,33,16 +DAB,Norfolk 3301,Norfolk,211648000,52.486385,1.715554,8,0.25,33,01 +DAB,Norfolk 3302,Norfolk,211648000,52.574817,1.331664,65,1.17268,33,02 +DAB,Norfolk 3306,Norfolk,211648000,52.422601,0.741906,39,0.25,33,06 +DAB,Norfolk 3307,Norfolk,211648000,52.924381,1.251171,99,0.2,33,07 +DAB,North and West Cumbria 070F,North and West Cumbria,218640000,54.682913,-3.463248,125,1.4,07,0F +DAB,North and West Cumbria 0709,North and West Cumbria,218640000,54.773382,-3.091075,289,5,07,09 +DAB,North and West Cumbria 070A,North and West Cumbria,218640000,54.5916,-3.117626,178,1.2,07,0A +DAB,North and West Cumbria 0706,North and West Cumbria,218640000,54.674729,-2.728805,263,1,07,06 +DAB,North and West Cumbria 0704,North and West Cumbria,218640000,54.496676,-3.557726,133,1,07,04 +DAB,North Birmingham small-scale 1313,North Birmingham small-scale,202928000,52.500637,-1.889474,117,0.08,13,13 +DAB,North Birmingham small-scale 1304,North Birmingham small-scale,202928000,52.480135,-1.73741,92,0.08,13,04 +DAB,North Birmingham small-scale 1306,North Birmingham small-scale,202928000,52.560384,-1.805848,131,0.08,13,06 +DAB,North Birmingham small-scale 1315,North Birmingham small-scale,202928000,52.520926,-1.794158,80,0.08,13,15 +DAB,North West Wales 0516,North West Wales,215072000,53.390547,-4.301313,160,0.6,05,16 +DAB,North West Wales 0502,North West Wales,215072000,53.01985,-4.273475,300,4.88619,05,02 +DAB,North West Wales 0513,North West Wales,229072000,53.271606,-3.828796,114,0.5,05,13 +DAB,North West Wales 0505,North West Wales,229072000,53.32441,-3.386164,226,0.6469,05,05 +DAB,North West Wales 0506,North West Wales,215072000,53.307315,-4.683071,135,1,05,06 +DAB,North West Wales 0501,North West Wales,215072000,53.307002,-4.12825,146,3.54913,05,01 +DAB,North West Wales 050C,North West Wales,229072000,53.286941,-3.684982,125,0.6138,05,0C +DAB,North Yorkshire 2E0D,North Yorkshire,213360000,54.046924,-0.786852,224,2,2E,0D +DAB,North Yorkshire 3B04,North Yorkshire,213360000,54.336239,-1.394168,101,0.3,3B,04 +DAB,North Yorkshire 2E08,North Yorkshire,213360000,54.358848,-1.149317,381,2,2E,08 +DAB,North Yorkshire 2E06,North Yorkshire,213360000,53.995727,-1.592234,150,2,2E,06 +DAB,North Yorkshire 2E01,North Yorkshire,213360000,54.267351,-0.404494,151,2,2E,01 +DAB,North Yorkshire 2E0E,North Yorkshire,213360000,54.252192,-1.205693,294,1,2E,0E +DAB,Northamptonshire 1702,Northamptonshire,213360000,52.253028,-1.139914,199,1.2,17,02 +DAB,Northamptonshire 1707,Northamptonshire,213360000,52.267931,-0.780155,103,0.3,17,07 +DAB,Northamptonshire 1705,Northamptonshire,213360000,52.445531,-0.666816,118,5,17,05 +DAB,Northamptonshire 170F,Northamptonshire,213360000,52.275355,-0.885481,127,1.2,17,0F +DAB,Northamptonshire 1704,Northamptonshire,213360000,52.166487,-1.029003,160,0.25,17,04 +DAB,Northamptonshire 1706,Northamptonshire,213360000,52.297367,-0.675082,43,0.3,17,06 +DAB,Nottinghamshire 300A,Nottinghamshire,227360000,53.140513,-1.232922,168,1,30,0A +DAB,Nottinghamshire 3008,Nottinghamshire,227360000,52.976309,-1.132626,122,1,30,08 +DAB,Nottinghamshire 3006,Nottinghamshire,227360000,52.986825,-1.251436,129,0.6,30,06 +DAB,Nottinghamshire 3001,Nottinghamshire,227360000,52.801451,-0.80088,138,4,30,01 +DAB,Oxfordshire 1203,Oxfordshire,211648000,51.723637,-1.302007,162,1.2,12,03 +DAB,Oxfordshire 1201,Oxfordshire,211648000,52.044582,-1.224699,157,1,12,01 +DAB,Oxfordshire 1206,Oxfordshire,211648000,51.951697,-1.551799,209,0.3,12,06 +DAB,Oxfordshire 120E,Oxfordshire,211648000,51.790638,-1.179183,130,4.77496,12,0E +DAB,Oxfordshire 1202,Oxfordshire,211648000,51.526905,-1.068756,183,0.6,12,02 +DAB,Peterborough 1E04,Peterborough,229072000,52.331465,-0.200759,25,0.25,1E,04 +DAB,Peterborough 1E0A,Peterborough,229072000,52.817122,0.029814,3,0.3,1E,0A +DAB,Peterborough 1E05,Peterborough,229072000,52.464961,0.100577,1,1.2,1E,05 +DAB,Peterborough 1E08,Peterborough,229072000,52.832469,-0.431001,73,0.5,1E,08 +DAB,Peterborough 1E01,Peterborough,229072000,52.507615,-0.343282,56,6.30001,1E,01 +DAB,Peterborough 1E06,Peterborough,229072000,52.475956,-0.497308,49,0.6,1E,06 +DAB,Peterborough 1E09,Peterborough,229072000,52.773292,-0.143159,3,1.2,1E,09 +DAB,Peterborough 1E03,Peterborough,229072000,52.662748,-0.501852,58,1.2,1E,03 +DAB,Peterborough 1E0D,Peterborough,229072000,52.655393,0.160854,4,0.3,1E,0D +DAB,Plymouth 2208,Plymouth,229072000,50.368664,-3.926486,142,0.3,22,08 +DAB,Plymouth 220D,Plymouth,229072000,50.274442,-3.794967,93,0.2,22,0D +DAB,Plymouth 220F,Plymouth,229072000,50.550217,-4.008413,508,3.2,22,0F +DAB,Plymouth 2204,Plymouth,229072000,50.381346,-4.067802,113,2.93171,22,04 +DAB,Plymouth 2203,Plymouth,229072000,50.353472,-4.242037,108,0.4,22,03 +DAB,Salisbury small-scale 1D03,Salisbury small-scale,195936000,51.18517,-1.722235,184,0.05,1D,03 +DAB,Salisbury small-scale 1D02,Salisbury small-scale,195936000,51.102938,-1.8427,139,0,1D,02 +DAB,SDL National 0B02,SDL National,216928000,51.594459,-0.129257,92,1.5,0B,02 +DAB,SDL National 3502,SDL National,216928000,53.027231,-2.28071,220,5,35,02 +DAB,SDL National 3C08,SDL National,216928000,56.555053,-2.986125,312,10,3C,08 +DAB,SDL National 1606,SDL National,216928000,51.819509,-0.811742,81,0.03,16,06 +DAB,SDL National 0E07,SDL National,216928000,50.447129,-3.610498,196,5,0E,07 +DAB,SDL National 3202,SDL National,216928000,53.335814,-0.171983,125,4.18888,32,02 +DAB,SDL National 3B02,SDL National,216928000,54.358848,-1.149317,381,2,3B,02 +DAB,SDL National 0D03,SDL National,216928000,50.66283,-2.443645,160,0.234537,0D,03 +DAB,SDL National 3F04,SDL National,216928000,55.860005,-3.873572,275,9.52796,3F,04 +DAB,SDL National 0112,SDL National,216928000,51.324006,0.520684,192,1.95547,01,12 +DAB,SDL National 1A0B,SDL National,216928000,51.609363,-1.794027,148,2.5,1A,0B +DAB,SDL National 1609,SDL National,216928000,51.998806,-0.669784,169,2.73603,16,09 +DAB,SDL National 0817,SDL National,216928000,51.521504,-0.138879,27,0.8,08,17 +DAB,SDL National 3415,SDL National,216928000,54.969789,-1.608675,15,0.025,34,15 +DAB,SDL National 0405,SDL National,216928000,50.511364,-4.43683,369,2.5001,04,05 +DAB,SDL National 390E,SDL National,216928000,55.825353,-4.290679,67,0.01,39,0E +DAB,SDL National 0211,SDL National,216928000,50.649245,-1.328934,166,5,02,11 +DAB,SDL National 1B11,SDL National,216928000,51.868264,-2.175397,147,4,1B,11 +DAB,SDL National 0605,SDL National,216928000,51.917174,0.925798,45,1,06,05 +DAB,SDL National 3F02,SDL National,216928000,56.071791,-3.233186,181,5,3F,02 +DAB,SDL National 0B01,SDL National,216928000,51.424161,-0.074944,110,5.72995,0B,01 +DAB,SDL National 0616,SDL National,216928000,51.719492,0.58056,105,3.5,06,16 +DAB,SDL National ,SDL National,216928000,51.469253,0.260425,3,1e-05,, +DAB,SDL National 400A,SDL National,216928000,55.579376,-4.289814,286,6.68495,40,0A +DAB,SDL National 1702,SDL National,216928000,52.253028,-1.139914,199,4.77496,17,02 +DAB,SDL National 1901,SDL National,216928000,54.607538,-6.009471,368,9.54993,19,01 +DAB,SDL National 2D06,SDL National,216928000,53.531884,-1.091294,7,0.022,2D,06 +DAB,SDL National 010D,SDL National,216928000,51.292731,0.979008,122,0.900001,01,0D +DAB,SDL National 2402,SDL National,216928000,51.611393,-3.302735,381,1.2,24,02 +DAB,SDL National 1507,SDL National,216928000,52.412282,0.279524,22,0.04,15,07 +DAB,SDL National 2E14,SDL National,216928000,53.611985,-1.66456,256,7.4131,2E,14 +DAB,SDL National 3B,SDL National,216928000,54.556455,-1.120934,237,0.8,3B, +DAB,SDL National 0E09,SDL National,216928000,50.719122,-3.562148,102,1,0E,09 +DAB,SDL National 390B,SDL National,216928000,55.886844,-4.294621,26,0.01,39,0B +DAB,SDL National 3902,SDL National,216928000,55.861436,-4.268991,16,0.3,39,02 +DAB,SDL National 0C16,SDL National,216928000,51.228358,-0.605467,142,4.88619,0C,16 +DAB,SDL National 030C,SDL National,216928000,51.307873,-1.244777,220,4.6,03,0C +DAB,SDL National 0A06,SDL National,216928000,50.861622,0.566431,76,0.2,0A,06 +DAB,SDL National 1612,SDL National,216928000,51.728571,-0.426217,142,3.12716,16,12 +DAB,SDL National 2F07,SDL National,216928000,53.803052,-0.565371,164,3.8,2F,07 +DAB,SDL National 2703,SDL National,216928000,51.62891,-3.919745,193,5,27,03 +DAB,SDL National 3008,SDL National,216928000,52.976309,-1.132626,122,1.55,30,08 +DAB,SDL National 3707,SDL National,216928000,52.905093,-2.493571,100,0.018665,37,07 +DAB,SDL National 2302,SDL National,216928000,51.236969,-2.625476,303,4.88619,23,02 +DAB,SDL National 310E,SDL National,216928000,52.234496,1.107319,64,2.2661,31,0E +DAB,SDL National 2903,SDL National,216928000,53.220944,-3.31452,336,10,29,03 +DAB,SDL National 1D09,SDL National,216928000,51.416202,-2.076595,175,1.90999,1D,09 +DAB,SDL National 120E,SDL National,216928000,51.790638,-1.179183,130,9.54993,12,0E +DAB,SDL National 1E01,SDL National,216928000,52.507615,-0.343282,56,8.69741,1E,01 +DAB,SDL National 2204,SDL National,216928000,50.381346,-4.067802,113,2.44309,22,04 +DAB,SDL National 3401,SDL National,216928000,54.868838,-1.771233,302,19.0999,34,01 +DAB,SDL National 0D07,SDL National,216928000,50.729221,-1.948658,64,0.390895,0D,07 +DAB,SDL National 230F,SDL National,216928000,51.48519,-2.562764,91,2,23,0F +DAB,SDL National 310a,SDL National,216928000,52.290839,0.777315,69,0.6,31,0a +DAB,SDL National 3E13,SDL National,216928000,57.113656,-2.096244,87,1.2,3E,13 +DAB,SDL National 0403,SDL National,216928000,50.209786,-5.238489,237,1.81485,04,03 +DAB,SDL National 0B05,SDL National,216928000,51.254494,-0.200986,234,0.954993,0B,05 +DAB,SDL National 1C01,SDL National,216928000,51.997433,-2.53989,204,2,1C,01 +DAB,SDL National 1603,SDL National,216928000,52.130132,-0.241462,54,7.62246,16,03 +DAB,SDL National 2D03,SDL National,216928000,53.379385,-1.513809,247,2.8,2D,03 +DAB,SDL National 0613,SDL National,216928000,51.540835,0.698653,33,0.024,06,13 +DAB,SDL National 3302,SDL National,216928000,52.574817,1.331664,65,0.390895,33,02 +DAB,SDL National 1312,SDL National,216928000,52.600539,-1.833856,169,20,13,12 +DAB,SDL National 3709,SDL National,216928000,52.670935,-2.550228,396,3.90895,37,09 +DAB,SDL National 0204,SDL National,216928000,50.961044,-1.454599,84,1,02,04 +DAB,SDL National 0A04,SDL National,216928000,50.882048,-0.257307,211,2.4,0A,04 +DAB,SDL National 130F,SDL National,216928000,52.496364,-2.048417,267,8.11744,13,0F +DAB,SDL National 4110,SDL National,216928000,52.801451,-0.80088,138,9.54993,41,10 +DAB,SDL National 2401,SDL National,216928000,51.459476,-3.282282,129,8.79514,24,01 +DAB,SDL National 0A0E,SDL National,216928000,50.824954,-0.113611,120,0.6,0A,0E +DAB,SDL National 2B11,SDL National,216928000,53.625547,-2.514794,439,20,2B,11 +DAB,SDL National 0107,SDL National,216928000,51.320696,0.287702,219,25,01,07 +DAB,SDL National 0109,SDL National,216928000,51.187004,0.955865,98,1,01,09 +DAB,SDL National 160E,SDL National,216928000,51.877958,-0.483622,207,3.32261,16,0E +DAB,Sheffield 2D15,Sheffield,220352000,53.550702,-1.412685,94,2,2D,15 +DAB,Sheffield 2D12,Sheffield,220352000,53.283991,-1.427446,179,1,2D,12 +DAB,Sheffield 2D0A,Sheffield,220352000,53.346243,-0.886897,62,1.2,2D,0A +DAB,Sheffield 2D05,Sheffield,220352000,53.457744,-1.220893,138,1,2D,05 +DAB,Sheffield 2D06,Sheffield,220352000,53.531884,-1.091294,7,0.6,2D,06 +DAB,Sheffield 2D03,Sheffield,220352000,53.379385,-1.513809,247,1.1,2D,03 +DAB,Sheffield and Rotherham small-scale 2D08,Sheffield and Rotherham small-scale,206352000,53.416904,-1.351035,115,0.198166,2D,08 +DAB,Sheffield and Rotherham small-scale 2D09,Sheffield and Rotherham small-scale,206352000,53.379385,-1.513809,247,0.06,2D,09 +DAB,Somerset 2116,Somerset,211648000,50.916513,-2.700119,134,2,21,16 +DAB,Somerset 2105,Somerset,211648000,51.325227,-2.918301,73,2,21,05 +DAB,Somerset 210A,Somerset,211648000,51.236969,-2.625476,303,3.12716,21,0A +DAB,Somerset 2110,Somerset,211648000,51.058123,-3.070536,130,1,21,10 +DAB,South Birmingham small-scale 1316,South Birmingham small-scale,206352000,52.473153,-1.919722,152,0.149311,13,16 +DAB,South Birmingham small-scale 1310,South Birmingham small-scale,206352000,52.425856,-1.77363,120,0.199081,13,10 +DAB,South East Wales 2412,South East Wales,227360000,51.702007,-3.398531,282,0.36,24,12 +DAB,South East Wales 2405,South East Wales,227360000,51.807795,-3.097781,433,0.342033,24,05 +DAB,South East Wales 2415,South East Wales,227360000,51.714666,-3.12404,400,0.6,24,15 +DAB,South East Wales 2404,South East Wales,227360000,51.601588,-2.935417,94,2,24,04 +DAB,South East Wales 2413,South East Wales,227360000,51.771714,-3.218938,442,0.36,24,13 +DAB,South East Wales 2402,South East Wales,227360000,51.611393,-3.302735,381,1.2,24,02 +DAB,South East Wales 2411,South East Wales,227360000,51.740421,-3.419414,451,1,24,11 +DAB,South East Wales 2414,South East Wales,227360000,51.236969,-2.625476,303,1.501,24,14 +DAB,South East Wales 2417,South East Wales,227360000,51.604177,-3.122175,360,0.4,24,17 +DAB,South East Wales 2407,South East Wales,227360000,51.632846,-3.151491,369,0.6,24,07 +DAB,South East Wales 2409,South East Wales,227360000,51.685712,-3.036095,250,0.5,24,09 +DAB,South East Wales 2406,South East Wales,227360000,51.633901,-3.460735,316,0.6,24,06 +DAB,South East Wales 2410,South East Wales,227360000,51.730587,-3.264891,419,0.6,24,10 +DAB,South East Wales 2408,South East Wales,227360000,51.457374,-3.402986,126,2,24,08 +DAB,South East Wales 2401,South East Wales,227360000,51.459476,-3.282282,129,3.81997,24,01 +DAB,South Hampshire 0211,South Hampshire,220352000,50.649245,-1.328934,166,5,02,11 +DAB,South Hampshire 0210,South Hampshire,220352000,51.063167,-1.360216,158,1.5,02,10 +DAB,South Hampshire 0208,South Hampshire,220352000,50.853922,-1.067587,102,0.5,02,08 +DAB,South Hampshire 0213,South Hampshire,220352000,50.877178,-1.278479,50,0.6,02,13 +DAB,South Hampshire 020B,South Hampshire,220352000,50.89247,-0.755575,194,0.4,02,0B +DAB,South Hampshire 0204,South Hampshire,220352000,50.961044,-1.454599,84,1,02,04 +DAB,South Hampshire 020E,South Hampshire,220352000,50.601933,-1.199325,227,0.3,02,0E +DAB,Stoke & Stafford 3502,Stoke & Stafford,229072000,53.027231,-2.28071,220,0.64,35,02 +DAB,Stoke & Stafford 3510,Stoke & Stafford,229072000,53.116187,-2.211069,325,2,35,10 +DAB,Stoke & Stafford 3513,Stoke & Stafford,229072000,52.728576,-2.0198,234,0.85,35,13 +DAB,Stoke & Stafford 3506,Stoke & Stafford,229072000,52.87502,-2.142442,113,0.3,35,06 +DAB,Stoke & Stafford 350E,Stoke & Stafford,229072000,53.206041,-2.10072,401,0.5,35,0E +DAB,Stoke & Stafford 350B,Stoke & Stafford,229072000,53.003549,-2.055691,265,1.95447,35,0B +DAB,Suffolk 3102,Suffolk,213360000,51.951883,1.331834,2,0.6,31,02 +DAB,Suffolk 310E,Suffolk,213360000,52.234496,1.107319,64,2.1,31,0E +DAB,Suffolk 310A,Suffolk,213360000,52.290839,0.777315,69,2,31,0A +DAB,Suffolk 3103,Suffolk,213360000,52.037726,1.200836,32,1,31,03 +DAB,Surrey 080D,Surrey,213360000,51.263565,-0.086822,226,1.2,08,0D +DAB,Surrey 0C04,Surrey,213360000,51.424161,-0.074944,110,3,0C,04 +DAB,Surrey 0C15,Surrey,213360000,51.221643,-0.326426,127,0.6,0C,15 +DAB,Surrey 0C16,Surrey,213360000,51.228358,-0.605467,142,0.586342,0C,16 +DAB,Surrey 0C01,Surrey,213360000,51.234232,-0.820719,179,0.6,0C,01 +DAB,Surrey 0C0F,Surrey,213360000,51.319591,-0.372374,60,1,0C,0F +DAB,Surrey 0C02,Surrey,213360000,51.254494,-0.200986,234,0.954993,0C,02 +DAB,Sussex 0A02,Sussex,211648000,50.909654,-0.627813,245,0.830652,0A,02 +DAB,Sussex 0A01,Sussex,211648000,50.764511,0.288682,11,0.25,0A,01 +DAB,Sussex 0A12,Sussex,211648000,50.853924,-0.410233,124,0.28,0A,12 +DAB,Sussex 0A06,Sussex,211648000,50.861622,0.566431,76,0.92,0A,06 +DAB,Sussex 0A07,Sussex,211648000,50.976724,0.230437,158,2,0A,07 +DAB,Sussex 0A09,Sussex,211648000,51.064289,-0.321546,53,0.3,0A,09 +DAB,Sussex 0A08,Sussex,211648000,51.13246,-0.23632,92,1.17268,0A,08 +DAB,Sussex 0A0A,Sussex,211648000,51.017054,-0.70092,185,0.6,0A,0A +DAB,Sussex 0A03,Sussex,211648000,50.787726,0.035848,81,0.488619,0A,03 +DAB,Sussex 0A0D,Sussex,211648000,50.946708,0.709647,44,0.6,0A,0D +DAB,Sussex 0A04,Sussex,211648000,50.882048,-0.257307,211,1.8,0A,04 +DAB,Sussex 0A0E,Sussex,211648000,50.824954,-0.113611,120,1,0A,0E +DAB,Swansea SW Wales 2708,Swansea SW Wales,223936000,51.867126,-4.309135,137,0.6,27,08 +DAB,Swansea SW Wales 2707,Swansea SW Wales,223936000,51.818501,-4.066216,255,1.95447,27,07 +DAB,Swansea SW Wales 2703,Swansea SW Wales,223936000,51.62891,-3.919745,193,2,27,03 +DAB,Swansea SW Wales 280C,Swansea SW Wales,223936000,51.750918,-4.154115,279,1.2,28,0C +DAB,Swansea SW Wales 2716,Swansea SW Wales,223936000,51.457374,-3.402986,126,1,27,16 +DAB,Swindon 1A0B,Swindon,220352000,51.609363,-1.794027,148,2.5,1A,0B +DAB,Swindon 1A03,Swindon,220352000,51.750794,-1.994303,184,0.6,1A,03 +DAB,Swindon 1A05,Swindon,220352000,51.591795,-2.089739,91,1,1A,05 +DAB,Swindon 1A13,Swindon,220352000,51.417912,-1.699977,192,0.501187,1A,13 +DAB,Swindon 1A12,Swindon,220352000,51.484903,-1.558836,211,1,1A,12 +DAB,Tayside 3C08,Tayside,218640000,56.555053,-2.986125,312,3,3C,08 +DAB,Tayside 3C0F,Tayside,218640000,56.705122,-3.646339,480,1.5,3C,0F +DAB,Tayside 3C09,Tayside,218640000,56.372293,-3.453361,178,1.44631,3C,09 +DAB,Tayside 3C10,Tayside,218640000,56.23998,-3.207285,337,1.49,3C,10 +DAB,Teesside 3B04,Teesside,218640000,54.336239,-1.394168,101,0.6,3B,04 +DAB,Teesside 3B02,Teesside,218640000,54.358848,-1.149317,381,3,3B,02 +DAB,Teesside 3B11,Teesside,218640000,54.618813,-1.682509,212,1,3B,11 +DAB,Teesside 3B03,Teesside,218640000,54.697573,-1.735424,173,0.3,3B,03 +DAB,Teesside 3B0A,Teesside,218640000,54.556455,-1.120934,237,4,3B,0A +DAB,Teesside 3B05,Teesside,218640000,54.593159,-2.041753,352,1,3B,05 +DAB,Trial Brighton ,Trial Brighton,202928000,50.822557,-0.149203,19,0.1,, +DAB,Trial Bristol ,Trial Bristol,202928000,51.439897,-2.628676,16,0.07,, +DAB,Trial London 08,Trial London,202928000,51.447582,-0.055538,88,0.2,08, +DAB,Trial London ,Trial London,202928000,51.508681,-0.198773,30,0.1,, +DAB,Trial London 08,Trial London,202928000,51.574443,-0.148552,122,0.2,08, +DAB,Trial Manchester ,Trial Manchester,211648000,53.478111,-2.238327,40,0.1,, +DAB,Trial Norwich 0F03,Trial Norwich,206352000,52.614553,1.306609,22,0.2,0F,03 +DAB,Trial Norwich 0F01,Trial Norwich,206352000,52.65073,1.270152,25,0.1,0F,01 +DAB,Trial Portsmouth 0208,Trial Portsmouth,194064000,50.853922,-1.067587,102,0.2,02,08 +DAB,Trial Portsmouth ,Trial Portsmouth,194064000,50.86903,-1.00234,12,0.005,, +DAB,Trial Woking ,Trial Woking,195936000,51.254436,-0.765605,107,0.1,, +DAB,Trial Woking ,Trial Woking,195936000,51.312761,-0.60248,56,0.1,, +DAB,Tyne & Wear 3408,Tyne & Wear,220352000,54.8219,-1.714848,240,4.38,34,08 +DAB,Tyne & Wear 3415,Tyne & Wear,220352000,54.969789,-1.608675,15,0.3,34,15 +DAB,Tyne & Wear 3416,Tyne & Wear,220352000,54.775175,-1.590656,100,0.3,34,16 +DAB,Tyne & Wear 3403,Tyne & Wear,220352000,54.978039,-1.662966,120,1.5,34,03 +DAB,Tyne & Wear 3409,Tyne & Wear,220352000,54.893895,-1.363068,10,0.8,34,09 +DAB,Tyne & Wear 3411,Tyne & Wear,220352000,55.001052,-1.665743,120,0.1,34,11 +DAB,Tyne & Wear 340E,Tyne & Wear,220352000,55.171858,-1.659136,60,1.1,34,0E +DAB,Tyne & Wear 340F,Tyne & Wear,220352000,54.982447,-1.945593,165,0.6,34,0F +DAB,Tyne & Wear 340A,Tyne & Wear,220352000,55.430165,-1.647593,107,1.2,34,0A +DAB,Tyne & Wear 340D,Tyne & Wear,220352000,54.873774,-1.899456,199,0.3,34,0D +DAB,Tyne & Wear 3404,Tyne & Wear,220352000,54.74117,-1.961914,404,1,34,04 +DAB,Tyne & Wear 3405,Tyne & Wear,220352000,55.035277,-1.453767,40,0.1,34,05 +DAB,Tynemouth and South Shields small-scale 3417,Tynemouth and South Shields small-scale,204640000,54.965374,-1.396894,80,0.2,34,17 +DAB,W & S Wiltshire 1D17,W & S Wiltshire,215072000,51.38779,-2.332925,175,0.488619,1D,17 +DAB,W & S Wiltshire 1D11,W & S Wiltshire,215072000,51.184826,-2.177079,160,0.5,1D,11 +DAB,W & S Wiltshire 1D09,W & S Wiltshire,215072000,51.416202,-2.076595,175,0.763994,1D,09 +DAB,W & S Wiltshire 1D04,W & S Wiltshire,215072000,51.336803,-2.264225,87,1,1D,04 +DAB,Winchester small-scale 0210,Winchester small-scale,206352000,51.064004,-1.358677,152,0.3,02,10 +DAB,Wolv'ton & Shrop 3704,Wolv'ton & Shrop,218640000,52.7257,-2.01965,227,0.6,37,04 +DAB,Wolv'ton & Shrop 3701,Wolv'ton & Shrop,218640000,52.531128,-2.790828,343,0.64,37,01 +DAB,Wolv'ton & Shrop 3707,Wolv'ton & Shrop,218640000,52.905093,-2.493571,100,0.554794,37,07 +DAB,Wolv'ton & Shrop 3702,Wolv'ton & Shrop,218640000,52.878444,-3.091941,305,0.64,37,02 +DAB,Wolv'ton & Shrop 3708,Wolv'ton & Shrop,218640000,52.547739,-2.115424,225,0.6,37,08 +DAB,Wolv'ton & Shrop 3709,Wolv'ton & Shrop,218640000,52.670935,-2.550228,396,1.95447,37,09 +DAB,Wolv'ton & Shrop 3711,Wolv'ton & Shrop,218640000,52.496364,-2.048417,267,4,37,11 diff --git a/plugins/feature/map/icons.qrc b/plugins/feature/map/icons.qrc index e136bbc56..46b171f4c 100644 --- a/plugins/feature/map/icons.qrc +++ b/plugins/feature/map/icons.qrc @@ -5,5 +5,7 @@ icons/ibp.png icons/muf.png icons/fof2.png + icons/controltower.png + icons/vor.png diff --git a/plugins/feature/map/icons/controltower.png b/plugins/feature/map/icons/controltower.png new file mode 100644 index 0000000000000000000000000000000000000000..d33c9d66078d71ec0fb332f73723850fbe3cdcc5 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM5!3HEhoL#F9q&N#aB8wRq*jzxEvG>W`aG>Bb zPZ!4!i{7Wfj)Khw0(Xo!9GHZZ7&$#0oKzJUIj1(Ts3i7yS8?Ay|LLwXkw(v_#cC;O%9DC1#|3VW8NDxH@{xS zX=Ax7;-}j)8>2^(PDP;yQ|eEOt_YkEaA&u4rmaAE*z|&Rx|WeeKQ3(ix_*k4+L~*& z?i-w)wXR4ii|t#y{_T&SWq+Ct%0o4FuKN9>O!NSEB;VAByEvn70)5Qj>FVdQ&MBb@ E0B&G&+5i9m literal 0 HcmV?d00001 diff --git a/plugins/feature/map/icons/vor.png b/plugins/feature/map/icons/vor.png new file mode 100644 index 0000000000000000000000000000000000000000..ac66aad04c85cc94e7bc578442882b4091e29eb6 GIT binary patch literal 365 zcmV-z0h0cSP)5nZU6uQA4x<(R5*>bRlyB|Fbq6U2Vesjp(8LtMqmU+V1zz803&n+MxcKl zjKBt01vxEHlO~{AI)O-hj&06KNJ6n1Km}mZ78T0?v;b~jOE$m;8Oc$e+Ip0aw)sY~ ziXeBW#Mc|(O0qvd?$8UzZh($tc_Kh59Pe-Gq8$dn2%rmWtY)2X7_G@Rz2d~_9g1a{ zT?P$+heSVXqi{7Y0~hhvZ<&*91$n@4J68naU;$?}CsX%$#1CpTK+E{9OlLnIFVz+$={$C-dI~G;q(1aD2v% zFTDf!Ds#Fy$y(-drjHws%wumf)#sud($>(5ua6jhg=Db4!vVYim$vAmap/antennafm.png map/antennaam.png map/ionosonde.png + map/VOR.png + map/VOR-DME.png + map/VORTAC.png + map/DVOR.png + map/DVOR-DME.png + map/DVORTAC.png + map/NDB.png + map/DME.png + map/airport_large.png + map/airport_medium.png + map/airport_small.png + map/heliport.png map/map3d.html diff --git a/plugins/feature/map/map/DME.png b/plugins/feature/map/map/DME.png new file mode 100644 index 0000000000000000000000000000000000000000..f95bac447836c8882dc3873850f219cedb0b6cd9 GIT binary patch literal 10606 zcmeHsXIN8Pw{8UKy*GgvIv9ElU0Uc>K%|8bIs}r?n-rxAHN}WZ>** zUDCwRH;a+Lm_2e)*&CaKUDR{gh21+rF9qgaC%uSB8g!J)--12#jzmn`mb&{)d$UEc z>WDmF6ipl;`&cv{JNUS9p8ej$%Ctvj@a`~!og`t^^0j>thQP-+1{ZIGE&VLsuU?6` zen0$O)sK(IrMMgN9xfR)0f8Uk`6+MnBX^Jbn+T(4x8!0SBj)DjLiipR5O&t~-@>ae zox2?Hv}(qP?U2T4kcA`h*_c~F)rASrW7cd^I?DA_Xh`mQYW|zCn#5ZN;`san1mi?u z@$YmUR>SvWKG2rB?qs_S23leE5yEjws)a$^;-gY_bO6b(JW*H4>sH{=A`i0QF%tEk zjG-d;(^6Zj5QQoUGK+cIj~nQ0uEubsTgLL$Jzc9HxCF-&b1ko{?^I*qkW!fSQ*oX3 zo>NJb-<{E}dO`yDU<}|FU*cOxRP=FYl%&;mMA{Kc3^eg|v$Mu?Z8@>}> zrTM;--p>-tuA|$JX7>v<`d{8#(kuDac|s+jkh2p=C?FjZ%S454qu#D0By6RvEuEmu zGhD%bLMlKI2PV2aXF3rOV}`fabmByC%#4|r*yWRJGc&R%^`c`TkRxqlq3Y)G^HZRU zv6~nhQZLfv_S8t1amewu)>=Jduh*)jMHzTp+OpfHgx)oOacCku2hAh`PL+zj#$ZsN zi-^Pbs*~yKPvX#NDtO}S%_TJb%_4N_QxCsb{cQHyLgqsICqtsK#EYPd<3?4149{G~ zqqN*Zgo<0-djb{KhkKRK$e1F_HP_9hO@pPN4~mSjOxq z4r@WRY$mx))$0>m5m_orv;Eq=Xs_#KC7pEpYjv;X9_I2T3B2iSmOWfIx)ecOPHVvg zi(2uGZ=mQP6L`u@Kf%%HW(>PNxnpF{bVMS%&G*4}Pj>0zv3h>i@+8wA+)^P;vN?Ue#{QcLS^XwA2b?7sRd-s|J{Ff!lj@T4GlrJ9Y>{WI1y+P>J|k|Q zdgTj)b1nUMt8bN(UGeREhVL1SgW|eM6JQ37e<8tYWUKZ%0Pl^5r*qZ3ZCVyjua3bLbl8+Y6q$;d zGI;&{>~}mq&VatdBMcmR{T(=p`ky49KwKn&v>_uH~w5yc4*4Sn) zPZivsc;EKzan2fVHzndz$hK3+NtBMN24VQ-A}^(Q`xLbA&ZjwQXhgsbt693E3o-v0o8 zXpP>6-KtcWoEYS`U1^j-ggO$)vg6UpWlyQ|A6Y5~mprjx_?FULyLjQSDS8Qh<1J0k zJp!urG^*W1Yglxna{D6w{Tc)0^VIsJyySc?vT$!82Z!HX*+*d)K843U!n4!4(9O%c zv_j&iw62*mDxc{Q{URCqMM6Vq+*WBgf}6<|=nB0YlX~aNQ`+b69R0ai}4P@ z!R^e}`wq}lD07Nje(bXixY}nb{J|>v(e{<}l<-%#(aE5vUm=Vm9@ZqXeVx1`;%1LE zw#o|v7GKapRZXkFBThIIq~_*SF;+_>uJpJPW~U#Y5Ttb(Aj3F)W7)$(`Rs97v}+D+AtiW~1v#Yn zrtaqHElZL+T6u~Kk|(Cuj(D~Q0!oF|^>pd)O!v>5yR57>$mp;F4U)LiWJ+))HtJ2O zI;c!29zWSo+Pj{`*fX*LOU{AG_EHvE;Wuk~kTxCJd34eA<8vAD@`kIZ0-+6oxb|p1 zYNLL^l;aY8RlxmIL&}4iy^(`vlJI~PT^t+#2ql4vDAGQf14M$hXB>o#)pklt&o*Mv z)Dmo1O<$Ke3c9t4k5r1jCYcd^eW;0{+jk=2tK*hgw}9TFCEjv@h_Fn_!WutSmT&j% z2<=Melai@EMIn6$lcnv7V7ZbSd7n|smYi#l{^>!H1u_+0ia3xX|WBA5a$x21L!~d{wq+;!Zr5ycrYF2eb+5>+;ov=m#Nv*xpWFpAjfNf@jpyS zAN`iT)T&&#I?zDP;l0e8*~4~O4-iL`wRY!YfBS0^?YeF$3aMDrmBy*uXc9)2w;oZ^ zsa5wf+uNhRQznA86Dax1%W8v7SCslRM*YsIGVTE%h6JA;<2QIqvYvX^K zm&AzVDkogo9R>w?b5V&t#T|4{N8Sz_A-!FoXM zD^S?QnvYBe4&upGI6*^uQ-ozXI*wbB30(s!WZM=>+c}<>+0o@058f|@Gg_ZYvch7B z%g4Qh@A>xrxY-qSyCsQ0=AlGAo#{2E&-MEduKZ_LFWat|&f+$$XCGLvS6DTfg0p=LDjs zRpmOvZ)@|YuvCK&9h^v>@ep68!v_(%C^#cae0U@71b=9G-?b=>a5Z#f_5pn`ob5^% zxg-CoR-+Ug)gR7nP|k5VL&@^C-9FDlweK-ow&}%wHR&v6J1+;g=>!I@Sd0pU#_wp^ z>f|dQl%>z>eazF+n!D=o_2=cOD{W)z&p4Hx&qL(?;ah! z1O>|(k4CBwfU{-awON#yx$qo%-)dn;=ciyrMD=Z;xkh@_fb( zW#{Uecwgwx3KFb@&Ta7X)GA(V>a+1WB#fD-mWy_3&~v-cVD`*MZqUAoXcE98{Ir8X z`Bri-MAl_pX0=$K)tHCKTPNRD$hb7ayfxY5dM!UZ&aN%&ZnHuCO`N5eNFCC~k-g~& zBxwwJPl}rTU+j%?#P z8#TwQ)Fio2&o}B^(pa2>?}Q(z5|fy2G>Dit_xvG7eVIFE2!{;W4%WUL0ILzbY+s5}8pRdU4q!lL=t}@2zS7*tGV!4nxl^FR1njFfAN3c-WOD-(agFmTTFZ)QQSA~ z+9At!uiTkf%1CeLE{nm93nd(-FD49^$P(k)HL|t=^wrjL-5fYSb}U5ZTxz|`)Hkg1 zmZhPwa*lD&Cx(dnL0`L(cZNE(35u;vJ=-{EO)orO#@&QSX4_pBa*te@SW^?NkA*bn98|!xJ zE*Jq-G|p87JT@JDXtm82_GqYR?D>@eu6Af5M;gbxh+&RZK2k1!b29vxLrAuQE`61e zFnV9jV)kS4@e`=Ow=gk3ejkYJ!HQKqD_+zfX2K#$B~u~xboJbbDUnVz}$tXC90bDjwH8pPq#~Wjqj+B z24^!@pk{ro(1S`IKHBLu>mszle*1&Xrc#07oqIIYJB!hu@{;l*+UCC98R!R133c8q zFJ3CtZ6j(YYV)m9XQ+7Grfx8+1-4(WIGXeR#_tKo`D{)=-3(3W#9PH&G!ng$UMNmT zVs|SI7(@imH@$H*<1!rSEm!{1svenfwThxzbrahM>!O4_MYjIpC9^Vp$?{I{R+i7c7+&)cTHJ!i15#iu8ed4%Ug}?vPd~ zp|y?dmIw8nFze99qix|Td8<>ejSV{Mmd&w~?S)Y&;zf{T5=b(G;hpB?tf5b8!b61H z@x={1UdRc~aZJ6S1cBE}N*_HDNM|gQtv0Z;RoeEueiRRJwc^tVs8as)gBbbkj=4?W6SXO` z`&oJ?-f{|aqFl=*+_R&+@yeq?-<;Bz5+V+Is-q62_YZi*>c|!7az1_wE(%VYlz{?o z7`n}+PYP{y2jJE*jK3h~fhK=elWaELtQ$7iL9uHXju7Xr@g+cAD2Q&Rj~H=;k>KpV z>t*@!(p&b*rr`A(1*0AU?59jIiS0k*|^{R#Mo*R}9H_maGA+y{R z+KSbCicFqLoQ~2dc#7tEXd84{<)|kA8DIUxTJ1yA`78V^savEpXkeYif$QW$Q9} zggGbOd%rC3zd-!}I z`EBy?)7V8r9~0MgqSJzo%-odelqAr5KW68`0Q$|tot>z_6Wi~*hbs8p)rZo7;+E6u zx42F4_0-2MJK?Z&@(|foZ|Y5D_Yb@j?QmaBPC8DORwb4MyUQ>59A>yKR@{HMo80+X z>9$r^ZJNO6Q)iJ3pQ8jDd@<$?f{6T2%>b_yv2VF~8|isFd0P3+gZ{HD0?8kByHqS+ z%y2J8Zx{G|(2NV+o$XI7Rk> z6H)ryW}LjyCG`);K#xT_`Q49tg?oO{hZje>`(bgs+WYG)z7N;0Ju7W7-`~r`gKp)% zkM+pLr)iZgGkAqthKCX+s&P*ZFs@REAU+mObM%z{xCP_X!LJ$E@mG}lx)l=AN(VU# zy2}yF)|Hs4hQ9E$NJ0r*c3>#G`czmJ#r{fw=<6!(r!m&$=vc)-TaJ5Cs`Kbr4j$2-b<)Utk}Z4*3knAt3N3W{JdOud}iNO@{BS@Cz5uPq{ z3N3?1Tiu5bU3Q~wudkaCm?=}90KCv>BTO>&k4##N4nNPCMBjZj-z`9qwL`_E2zgl! zf)j2il4?cQWc@A<5WdoNr$a}+z(S`T9W>kCHQq!x_*G$ldBqsTjT;2ozBEi0=NI7@ zFgzr)MH6LiXcRpd(?!|{+~ycZWX?%5T%z}L=!Wtv$EbAJ6~!+~I;g2|6@bVED%20J zb|P-=&^y*yBKiZrGT@`8v|>MtM^Uv?e}pPU-r?8cZND068c$23Ih7c25Opau+)}hb z%l4DG5*|Ni7sYpimL`dvR2mN>ATj3U#9kNS+d2|)m-^FSx9NDA)*qyn&fw7dS9ljK`v!wZRhuOmQ7`NKt) z4<9HQ!3KMcT}@5XfyetgwJq#LYpBLabM8q1fFjQsdt=00R|f(|x(mYWk#-0{KX*^; zjS~PsPSMX326shZfOZH+XAgPMR#OWI=xi?!GLz61*7a0DI5|TDPzd7yJrj6gPbrJPl%9^udlD5ub3bb zjjtz^tEZ43%u`52P*}*_{cj#< zjGE71@&2U;+5~%lO2`O-MtY;*2sIys2ZrPKpq_5t=-*>{qY-DO^Ss^c9fYt+ok#xN zM?+KB;7^}386BP7J0%t2aQL|9tNK}z&D3VoC_wi94(zh`wuWsjv26^Fqb#3d!bq7tIg zU~zkCX|Rlg6ap-1FD&8U-~fl&Nz0s5ovi~z*+5esBq}KUm&L#hhH*fm+_CND>|u}e zMgKKq;_Qwv#=y>G6Oj~?loAsbmlBth5|Ne?`wL`>K%uc^e#R;yEGYUjZF@LG9m@&B zmYcIX%n>2v>EU?pa<(uK>~yfw!p^z_%Y1If&IY1_LclOclnD~)CJ#Ex7I;Q^-c&%j zU+n_XLBh`+&%p@$vyS`Kb5~)GLg$BaLjMl@-=SL?0);J+5WIQ|BFJl7ZsI~fk}yi5wP<@wvzx$ zi%QCXWu&F-#H2)EFiC{u-`UYf2aGQag-~|H)&O>$vCVb9&OqK@^~d*jyss1Dthj_l zMZhB1m#B$|1VmH>A|ej@x!OQEp|c(KkJ-wd?LoS_kl*!?JKLimnrDS*?Ct64=8Qo7 zU8g_l=Kq5G&Hq=8{!i||!+v_JAU*xDUF?L>_x1R<;r|8jCxebN9N~dR{=3wFhy0Y~ zw-XUI=btw02@ZR{6#Dao`KwCK8tH%W@vHj&7b9S){|WLR>HCjd|H$aKe>4O(+_h>tdRhtsjEh?NJ2_MNA5-pqQ{ybnyShs=It93)l>jsQXG-IQ{Q?= ys76ttl12~bB<CCSOGMoT$K5szPX5H0cfh}sXo4H8~$IIOam00009a7bBm000C; z000C;0U7^T5C8xIK1oDDR9J=0SM5>bKn#@_{vT9u?toAM>401Zxe7=Lrh{Aup@UEX z&_U9H>3|O2&C9*@?Ko?jEb9!K*?ijCr?>Kx7g%PPX=GROs%COr6gd?C=EAViD*GZ5>THBkSS;Xkc?O&LYcWH zjie61psyMt zL`7$9GC|F|pt2-D4!WR_<16ylgr{$onZGH;f6sBGgs+0n5aG#5u3$boOy#m(hvN&9 zu%~kXNy#6HkiZs-atmOa;iHlyS$Id3Wb<=MLB!HV$5yvYu@!Z9MmPhIY$v8N733E) z`=xZ4$F+|F_S4;q_fR7i>TP@MEhyEY$FWwmq^S_!beqXkmtf~8$_kwOJ*jt3gHo$X z)DL~FYC#L^B-7Po~P*3B?0#; z=1Cix`wsJ&#;uYTB%pVm>;)x>2=iGHiHIa%u)Dpq|HEg>2<8WCn!A^nl*vB3h^0G= m$&npuFaIe$mVeF6c7Fl00;$kZ1o_(l00004=OGI literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/DVOR.png b/plugins/feature/map/map/DVOR.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6949fde5025be971f69c6b0572951528f9c26a GIT binary patch literal 415 zcmV;Q0bu@#P)00009a7bBm000C^ z000C^0ljtSLMVhnI0%Dq5DH-s3ZW1NVGzdP zO_|*3*5>0lX_iHbbh=BNm*;24IgreD00#i<)IS02N^6QPoNOX;1#l&~W(OLYh+G0N zl1p}Ufu%6{!?HU|2e4PR4%u}xfOZP#)C17Q?yAhYKouuDK@~TfnrNcA0ob=Js)Q~g zZ>qPvAlpQr4xq>`j@Hm^Wb3xhzQ~k_yaTxONC5`6;vT8-^^-&&*g8gqo*FO!*h7?O zKMn*IQpEBLl%@d2L^lZ3X&YR(mmxgQPJ@dYWXlPl7*>7urvROMUqM8}++gd+TMz!M zG_)p4npda2b4W)o&(1~OHlO>Z6#6|%kHOX+O$nSad2-fcW}}ZkKal4yT{vrzm)${M zmh9y)C%8CNMMXz(*I%)$lbxXam(eZFqp$7%?+1qh9{VeZ#1|e4Q=R*2MxFow002ov JPDHLkV1ja&s@DJj literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/DVORTAC.png b/plugins/feature/map/map/DVORTAC.png new file mode 100644 index 0000000000000000000000000000000000000000..f7df0a0b38a174b07fa25f8c808cc57200f27fbb GIT binary patch literal 847 zcmV-V1F-ywP)asXkb9dy4iWN@7~NV z1`(}w0Am0@0361CyA21>2QULLh~^#vb_jq<{t>{zErIF)_y%CX`OIUvtP9oxFbTjF zKJb6v5~u?J!l|sMT2347Y^EG*R?<9faYSje`-JGfw6kO+%RB+=2aAs7E zP_WJyb>iYU=#4q&yokI-y7@rA0K5e7LGgSSz!L!bK8?g@{{nbSM3+n7CWy#g_Z4wT z5fQ2Pl$3m9uy&^sT%w57`9;S2)?#JMS$|RoP9q{>Sr!89Wv;&@E=ACA643-bHEIJF znYr9dE>mGpT%+o7n zkf#|5R2mUY0lZK??s0s-9tcV@e7Xq{;n-dU&j6hBXVEOG5SRIiS_Ut(G+-x#5%kW5ut0T_%RF|~ zAniNh4oE1~7f|V3`0j<1%L)Zm;DQDI0r(>Py#(A@!uo#gKPe-g7#uHfl(2Jc3tE;6 z^)vOvp9GE<8YR$a%zdW{+(a_6@oB~Py?%183UI~INGb+Zrw#uOw}tDy18|v|rwUw& zCR6FK%oOdji*L+IeNJ`DwsMCeF`oVWv@s&0u5wI@nsd&tF(smoDuAXUT#&9*T$J$( z`Bz^Xs&=k0=iG=3q^oRwT+a-DlXXl)eRZJB>K1a}HQu!o2A6jaYLLirins{kb*u`O zMazIfCq&fC3T91pqiPr8BI;e6j$^iSg*oRw19-?Ypw=D$cuhn{7CG%(1Yvs*;2D5> znQ>lOCA4Q1h%a<3?6*}ymf-H3?Z7qFcHo+7J8(_49k{024qQ{sa&W#rhZ)%NKgwtg Zz<&Xuil2FEQ*!_S002ovPDHLkV1gS3cHjU2 literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/NDB.png b/plugins/feature/map/map/NDB.png new file mode 100644 index 0000000000000000000000000000000000000000..4987f3be0f4c4363eb1a55ae030a0f8601982ebc GIT binary patch literal 674 zcmV;T0$u%yP)F7cgd4#lWQ3p-WCU+e zjo=Mb5O_R1Fmx;jSKN;VP^>SCpO2JMYfZlM_v2f+zav0OX-p|)^k;48XI;B2fx4i# ziAYfr5xHZ$X+!^pwQ}jq(+wgc4qik=HD8Sh#+ig$F}Hwb{0af|;9W$fV1D8;^(WAr zTH_Vt-ONjKs^NnE9a;{g@f@6^Zra}wU^8FSoc>iZ|AhVkfd;s&n)xxmbqjz4*qk#C z$zU)zHxx%dTg(J5dy0ujfW{_cQ)9f4cDwC`;O3sAy{$)p4;NgS4m21(NZT3x2g4B7 zVwY)Pe!LU_3=CGQ)4UMVLRybRI|qBcG9?s%@V=7yY82%d@0e@oFT}L5JBV)e`9Yj? z_XxWX?g1?qVqDQ0C98tHjDltXi;S6H!VnwpywT-{#{NXyvNRl!*#KMbBbKQk~!1uCl^rfvhGW1&%;Fh`BSyU<72f2JWv!7TS}?u z8s-Mj00009a7bBm000C; z000C;0U7^T5C8xIK1oDDR9J=0SM5>bKn#@_{vT9u?toAM>401Zxe7=Lrh{Aup@UEX z&_U9H>3|O2&C9*@?Ko?jEb9!K*?ijCr?>Kx7g%PPX=GROs%COr6gd?C=EAViD*GZ5>THBkSS;Xkc?O&LYcWH zjie61psyMt zL`7$9GC|F|pt2-D4!WR_<16ylgr{$onZGH;f6sBGgs+0n5aG#5u3$boOy#m(hvN&9 zu%~kXNy#6HkiZs-atmOa;iHlyS$Id3Wb<=MLB!HV$5yvYu@!Z9MmPhIY$v8N733E) z`=xZ4$F+|F_S4;q_fR7i>TP@MEhyEY$FWwmq^S_!beqXkmtf~8$_kwOJ*jt3gHo$X z)DL~FYC#L^B-7Po~P*3B?0#; z=1Cix`wsJ&#;uYTB%pVm>;)x>2=iGHiHIa%u)Dpq|HEg>2<8WCn!A^nl*vB3h^0G= m$&npuFaIe$mVeF6c7Fl00;$kZ1o_(l00004=OGI literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/VOR.png b/plugins/feature/map/map/VOR.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6949fde5025be971f69c6b0572951528f9c26a GIT binary patch literal 415 zcmV;Q0bu@#P)00009a7bBm000C^ z000C^0ljtSLMVhnI0%Dq5DH-s3ZW1NVGzdP zO_|*3*5>0lX_iHbbh=BNm*;24IgreD00#i<)IS02N^6QPoNOX;1#l&~W(OLYh+G0N zl1p}Ufu%6{!?HU|2e4PR4%u}xfOZP#)C17Q?yAhYKouuDK@~TfnrNcA0ob=Js)Q~g zZ>qPvAlpQr4xq>`j@Hm^Wb3xhzQ~k_yaTxONC5`6;vT8-^^-&&*g8gqo*FO!*h7?O zKMn*IQpEBLl%@d2L^lZ3X&YR(mmxgQPJ@dYWXlPl7*>7urvROMUqM8}++gd+TMz!M zG_)p4npda2b4W)o&(1~OHlO>Z6#6|%kHOX+O$nSad2-fcW}}ZkKal4yT{vrzm)${M zmh9y)C%8CNMMXz(*I%)$lbxXam(eZFqp$7%?+1qh9{VeZ#1|e4Q=R*2MxFow002ov JPDHLkV1ja&s@DJj literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/VORTAC.png b/plugins/feature/map/map/VORTAC.png new file mode 100644 index 0000000000000000000000000000000000000000..f7df0a0b38a174b07fa25f8c808cc57200f27fbb GIT binary patch literal 847 zcmV-V1F-ywP)asXkb9dy4iWN@7~NV z1`(}w0Am0@0361CyA21>2QULLh~^#vb_jq<{t>{zErIF)_y%CX`OIUvtP9oxFbTjF zKJb6v5~u?J!l|sMT2347Y^EG*R?<9faYSje`-JGfw6kO+%RB+=2aAs7E zP_WJyb>iYU=#4q&yokI-y7@rA0K5e7LGgSSz!L!bK8?g@{{nbSM3+n7CWy#g_Z4wT z5fQ2Pl$3m9uy&^sT%w57`9;S2)?#JMS$|RoP9q{>Sr!89Wv;&@E=ACA643-bHEIJF znYr9dE>mGpT%+o7n zkf#|5R2mUY0lZK??s0s-9tcV@e7Xq{;n-dU&j6hBXVEOG5SRIiS_Ut(G+-x#5%kW5ut0T_%RF|~ zAniNh4oE1~7f|V3`0j<1%L)Zm;DQDI0r(>Py#(A@!uo#gKPe-g7#uHfl(2Jc3tE;6 z^)vOvp9GE<8YR$a%zdW{+(a_6@oB~Py?%183UI~INGb+Zrw#uOw}tDy18|v|rwUw& zCR6FK%oOdji*L+IeNJ`DwsMCeF`oVWv@s&0u5wI@nsd&tF(smoDuAXUT#&9*T$J$( z`Bz^Xs&=k0=iG=3q^oRwT+a-DlXXl)eRZJB>K1a}HQu!o2A6jaYLLirins{kb*u`O zMazIfCq&fC3T91pqiPr8BI;e6j$^iSg*oRw19-?Ypw=D$cuhn{7CG%(1Yvs*;2D5> znQ>lOCA4Q1h%a<3?6*}ymf-H3?Z7qFcHo+7J8(_49k{024qQ{sa&W#rhZ)%NKgwtg Zz<&Xuil2FEQ*!_S002ovPDHLkV1gS3cHjU2 literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/airport_large.png b/plugins/feature/map/map/airport_large.png new file mode 100644 index 0000000000000000000000000000000000000000..72299fcf635bd41f907c1a227e8290994c377eee GIT binary patch literal 1261 zcmVEX>4Tx04R}tkv&MmP!xqvQ>9WW3KkJj$WWauh>AE$6^me@v=v%)FnQ^Z(4-+r zad8w}3l9D)RvlcNb#-tR1i>E=Cr2km7b)?+q|hS92bcG8-aCi;?gNB+nW<*SIG}2l zk&4H}Y;HviydsEUs($oJ%+wRsRcxRP3sKrNQcR@iJm%pabo@zj$>iDq zBgZ@{P$4;f@IUxHTeC1Z;UpV2qvfWBKGxa!WWd5+TuAVafSx&aOj zfzcvmuX((?t9@?&-f7P72V?zmp-nzC`v3p{24YJ`L;(K){{a7>y{D4^000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2jmJJ3L61>WEu?s00QYrL_t(&-tC!9XcIvc$A4>5 zjg`jMfIWz|RzzAAg@WK0C`3;RUIeA22SL45JqjL*D0t{eYC&2>Jo$+rf(H*p!K$DZ z@!|(U4_1q4gj7>eYps_T9M;|G&TiZ^&HiD?WA^3Ee|Fw`GjGSj94CPlz;~dXGQ$I4 z68KtbBC{xCMC2p#l}0}9(AOl*4Yl$c1JWA37r;o^6FM%bFlgHsxs3)fMP3IS0=584 zieP&-uo|caUjOfj94L~z*jsnxHXgGLz)_%Inc!oQABk%fa0JMRycc*QaszOqROE+% zL5I~DRr&!wDDq3-xyX4#9-DyO5+e(M^T5Ta?6{Gi2h=%`uTuH}5{jHuo+VxE?gv_d z7N8Tjrv&$5MemFpz0Uy;fo5P0unJfVi~+~O4%lv`e+<|z{V7>R6T8{41NH=v6S*3= z1-u8cK)s=@J8GV<1LguBfu%(t`cw*+0pOYReFJ_2+Z4C0@o4Xt>=*q&m%NC7kN`df zbd@}?(1pYtNeEtT^#E%nEA7gE{*%;EeohR(@K|HI;@j9GVY@$=v5aH^d2JL!0cB%c_hy`R8_pyUr45`3k3)@nfhhu2`K z0(OP)=C2x%9|zhr&u;?bmZY}PGAp9y17D&qW)b-!rpi2UNplWq7Wp#Hmki5+ts>u5 z3Ud`TQnVN9-}^4(wMkj(h@`_d|5h{MUPaHXXvru#85f;KMWOi zs63y*{mN#MB5wqiIq0mk$xgQFZ8p;yE;BD+_#)2UC=2c00000NkvXXu0mjf#&k0? literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/airport_medium.png b/plugins/feature/map/map/airport_medium.png new file mode 100644 index 0000000000000000000000000000000000000000..8347f8b3d8ada0876822d9b755b6d445ec2b089e GIT binary patch literal 1015 zcmVEX>4Tx04R}tkv&MmP!xqvQ>9WW3KkJj$WWauh>AE$6^me@v=v%)FnQ^Z(4-+r zad8w}3l9D)RvlcNb#-tR1i>E=Cr2km7b)?+q|hS92bcG8-aCi;?gNB+nW<*SIG}2l zk&4H}Y;HviydsEUs($oJ%+wRsRcxRP3sKrNQcR@iJm%pabo@zj$>iDq zBgZ@{P$4;f@IUxHTeC1Z;UpV2qvfWBKGxa!WWd5+TuAVafSx&aOj zfzcvmuX((?t9@?&-f7P72V?zmp-nzC`v3p{24YJ`L;(K){{a7>y{D4^000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2jmJJ3L+>rQV-hz00Hw!L_t(o!|m8hh)!V~$MMfx z)P&hkCPm0?fk;ZSAlr$uuvGR^7FbwGNr@>7QvTHjyys%Zq4TSM=e(!?^LxL~bDry=QKLqg>PvavkCRxBM|A|>j;Rnse-orP z<@rF$eQf}1qyBIm!q!Ni+z`nb#;0)K#$AlhH0bL{pX(OaGko^oGEQK@p9GvL5HB-) z&PVdbF@lA40_?+;Sbhtau)GRjy8z&`E?%>6BcWUiV^|i;Ux6dI8Y;3g$@ESEz_U3j zJdokp#swV5^OT%Zal<9}kOS^126MiZIk;c2`$S`P+;APfW)ygb#Ra?07EG3aYhh86 z;c!ZU?^v6VKZvCXEw>L3N`Ot^8n&g?cy>y`Hwn4tLhDRoFE(Mc%Uei_FJW-(4*#F2 zB7plV^si1F45L1`iU9WFaRs<1W%dF^0AVA3B;PpqjM;OB_fSQVq`0Gt==n}v2aGqIzNE?KAXv2K8? z!u37Q)bUL676-ygQ~9VcU4!KRKh+(=%7h8Fqxn^*hZ1^)k)H757kX;ecoUfW&%ENp lk?Ei=deMB_Yt$%*egl$RIeOf1UkCsI002ovPDHLkV1jry&~E?$ literal 0 HcmV?d00001 diff --git a/plugins/feature/map/map/airport_small.png b/plugins/feature/map/map/airport_small.png new file mode 100644 index 0000000000000000000000000000000000000000..2509056d676a878c2d0bf57d2eac734a8e6779c3 GIT binary patch literal 1084 zcmV-C1jGA@P)EX>4Tx04R}tkv&MmP!xqvQ>9WW3KkJj$WWauh>AE$6^me@v=v%)FnQ^Z(4-+r zad8w}3l9D)RvlcNb#-tR1i>E=Cr2km7b)?+q|hS92bcG8-aCi;?gNB+nW<*SIG}2l zk&4H}Y;HviydsEUs($oJ%+wRsRcxRP3sKrNQcR@iJm%pabo@zj$>iDq zBgZ@{P$4;f@IUxHTeC1Z;UpV2qvfWBKGxa!WWd5+TuAVafSx&aOj zfzcvmuX((?t9@?&-f7P72V?zmp-nzC`v3p{24YJ`L;(K){{a7>y{D4^000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2jmL^6d4}o_?3A800KBkL_t(o!|m8jh>cMg2k@Vn zh8l*26@$nu7@CbD^0iTtkdld#6&sP k;_jFrYhXe^XylEo}gA~7sbj9E-E6E-qF zX3B7F9(CvT-nnzuc!CE=RD_}f6v!>-b)o#R8hrW#guq1K@%2ZHqK)pOGMY9 z7rBscIE5KmB3U1#e&!KwJxD;cn6G8MSI`Exd{D=xMtx|%0Wf1v_FGc1Ca4;{cQ@QJxMsY63T78k#ABxQPqBXCZsu&#+ z_@#(lz-}xHE7jvL-WJh+ip>-02TS1yF5_&Ji6P97QqdFA8oF%>>!)I0%rzy<-@~y; znn_`2WZN4&!n^U%AgBItZ-4cVpDL>OAMhKz6zz9r{fdPE0000EX>4Tx04R}tkv&MmP!xqvQ>9WW3KkJj$WWauh>AE$6^me@v=v%)FnQ^Z(4-+r zad8w}3l9D)RvlcNb#-tR1i>E=Cr2km7b)?+q|hS92bcG8-aCi;?gNB+nW<*SIG}2l zk&4H}Y;HviydsEUs($oJ%+wRsRcxRP3sKrNQcR@iJm%pabo@zj$>iDq zBgZ@{P$4;f@IUxHTeC1Z;UpV2qvfWBKGxa!WWd5+TuAVafSx&aOj zfzcvmuX((?t9@?&-f7P72V?zmp-nzC`v3p{24YJ`L;(K){{a7>y{D4^000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2jmJJ3j`-*@%vc-00L)8L_t(Y$L-hMi%nq^2Jjzd zG{Q`nXcQTdBtwJbBP3kVkjRDdk>uLPh1^O}{0j=>U*JN9M3j(X5{ep(k6{qaXf7N# z>(tw^&pCT$xRIxJd!N0Y^}Ns7`+e8DHvcg^j2J@BB&@(Z%#Gg-7>yqog?`kDj1Hg!)AE{Ij-Hr1tz|f`P{6eudL6;- zDidEEQ=|v$s)@}Nd@eEVLOM9uo;$v;9GT_4G`Vt;h;dkdWD9pi_tl*L4RV*zdP#e2NkyoE% z4vtIs1F_>|VM)R}nnjq5??LD+Gk8lP@^uh(wKvIIm=UkXgNT2x^cMHUTqxjbl$5N6 zRLJpaQ#?%a_LjsZ;6}uLVt+NULy6eSlC7vYis`y2uzAs%>OYW_e6RTtae?L_}QHVsy&F;?HVMCcvPMrSj95L&abCw87MiC!O0M~5`zc54E5 zM)6z{1+|EeQTgA*UfLHcy*7HpS#dOE%*hM57bNI_gq1h4G2W(=9}4`4EM=@Xvme`~nU%t 11 ? mapZoomLevel : 11 + sourceItem: Grid { + columns: 1 + Grid { + //layer.enabled: true + //layer.smooth: true + horizontalItemAlignment: Grid.AlignHCenter + Text { + id: polygonText + text: label + } + } + } + } + } + + Component { + id: polylineComponent + MapPolyline { + line.width: 1 + line.color: lineColor + path: coordinates + autoFadeIn: false + } + } + + Component { + id: polylineNameComponent + MapQuickItem { + coordinate: position + anchorPoint.x: polylineText.width/2 + anchorPoint.y: polylineText.height/2 + zoomLevel: mapZoomLevel > 11 ? mapZoomLevel : 11 + sourceItem: Grid { + columns: 1 + Grid { + //layer.enabled: true + //layer.smooth: true + horizontalItemAlignment: Grid.AlignHCenter + Text { + id: polylineText + text: label + } + } + } + } + } + Component { id: mapComponent MapQuickItem { @@ -134,8 +247,9 @@ Item { Grid { horizontalItemAlignment: Grid.AlignHCenter columnSpacing: 5 - layer.enabled: true - layer.smooth: true + // This is very slow with lots of items, so we use MSAA for the whole map instead + //layer.enabled: true + //layer.smooth: true Image { id: image rotation: mapImageRotation @@ -149,7 +263,7 @@ Item { if (mouse.button === Qt.LeftButton) { selected = !selected if (selected) { - mapModel.moveToFront(index) + mapModel.moveToFront(mapModelFiltered.mapRowToSource(index)) } } else if (mouse.button === Qt.RightButton) { if (frequency > 0) { @@ -159,6 +273,8 @@ Item { freqMenuItem.text = "No frequency available" freqMenuItem.enabled = false } + var c = mapPtr.toCoordinate(Qt.point(mouse.x, mouse.y)) + coordsMenuItem.text = "Coords: " + c.latitude.toFixed(6) + ", " + c.longitude.toFixed(6) contextMenu.popup() } } @@ -186,7 +302,7 @@ Item { if (mouse.button === Qt.LeftButton) { selected = !selected if (selected) { - mapModel.moveToFront(index) + mapModel.moveToFront(mapModelFiltered.mapRowToSource(index)) } } else if (mouse.button === Qt.RightButton) { if (frequency > 0) { @@ -196,6 +312,8 @@ Item { freqMenuItem.text = "No frequency available" freqMenuItem.enabled = false } + var c = mapPtr.toCoordinate(Qt.point(mouse.x, mouse.y)) + coordsMenuItem.text = "Coords: " + c.latitude.toFixed(6) + ", " + c.longitude.toFixed(6) contextMenu.popup() } } @@ -212,15 +330,19 @@ Item { } MenuItem { text: "Move to front" - onTriggered: mapModel.moveToFront(index) + onTriggered: mapModel.moveToFront(mapModelFiltered.mapRowToSource(index)) } MenuItem { text: "Move to back" - onTriggered: mapModel.moveToBack(index) + onTriggered: mapModel.moveToBack(mapModelFiltered.mapRowToSource(index)) } MenuItem { text: "Track on 3D map" - onTriggered: mapModel.track3D(index) + onTriggered: mapModel.track3D(mapModelFiltered.mapRowToSource(index)) + } + MenuItem { + id: coordsMenuItem + text: "" } } } diff --git a/plugins/feature/map/map/map3d.html b/plugins/feature/map/map/map3d.html index 3d7f252f8..0f0aca7f5 100644 --- a/plugins/feature/map/map/map3d.html +++ b/plugins/feature/map/map/map3d.html @@ -147,6 +147,30 @@ } } + // Polygons (such as for airspaces) should be prioritized behind other entities + function pickEntityPrioritized(e) + { + var picked = viewer.scene.drillPick(e.position); + if (Cesium.defined(picked)) { + var firstPolygon = null; + for (let i = 0; i < picked.length; i++) { + var id = Cesium.defaultValue(picked[i].id, picked[i].primitive.id); + if (id instanceof Cesium.Entity) { + if (!Cesium.defined(id.polygon)) { + return id; + } else if (firstPolygon == null) { + firstPolygon = id; + } + } + } + return firstPolygon; + } + } + + function pickEntity(e) { + viewer.selectedEntity = pickEntityPrioritized(e); + } + Cesium.Ion.defaultAccessToken = '$CESIUM_ION_API_KEY$'; const viewer = new Cesium.Viewer('cesiumContainer', { @@ -161,6 +185,7 @@ terrainProviderViewModels: [] // User should adjust terrain via dialog, so depthTestAgainstTerrain doesn't get set }); viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain + viewer.screenSpaceEventHandler.setInputAction(pickEntity, Cesium.ScreenSpaceEventType.LEFT_CLICK); var buildings = undefined; const images = new Map(); @@ -351,7 +376,7 @@ fabric: { type: 'Image', uniforms: { - image: 'data:image/png;base64,' + command.data, + image: command.data, } }, translucent: true diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index 1d85a431c..70bbec72a 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -37,6 +37,7 @@ #include "device/deviceuiset.h" #include "util/units.h" #include "util/maidenhead.h" +#include "util/morse.h" #include "maplocationdialog.h" #include "mapmaidenheaddialog.h" #include "mapsettingsdialog.h" @@ -181,7 +182,10 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur m_pluginAPI(pluginAPI), m_featureUISet(featureUISet), m_doApplySettings(true), - m_mapModel(this), + m_objectMapModel(this), + m_imageMapModel(this), + m_polygonMapModel(this), + m_polylineMapModel(this), m_beacons(nullptr), m_beaconDialog(this), m_ibpBeaconDialog(this), @@ -197,6 +201,16 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur rollupContents->arrangeRollups(); connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + // Enable MSAA antialiasing on 2D map, otherwise text is not clear + // This is much faster than using layer.smooth in the QML, when there are many items + int multisamples = MainCore::instance()->getSettings().getMapMultisampling(); + if (multisamples > 0) + { + QSurfaceFormat format; + format.setSamples(multisamples); + ui->map->setFormat(format); + } + m_osmPort = 0; m_templateServer = new OSMTemplateServer(thunderforestAPIKey(), maptilerAPIKey(), m_osmPort); @@ -206,13 +220,17 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur ui->map->setAttribute(Qt::WA_AcceptTouchEvents, true); - ui->map->rootContext()->setContextProperty("mapModel", &m_mapModel); - // 5.12 doesn't display map items when fully zoomed out -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map_5_12.qml"))); -#else + m_objectMapFilter.setSourceModel(&m_objectMapModel); + m_imageMapFilter.setSourceModel(&m_imageMapModel); + m_polygonMapFilter.setSourceModel(&m_polygonMapModel); + m_polylineMapFilter.setSourceModel(&m_polylineMapModel); + + ui->map->rootContext()->setContextProperty("mapModelFiltered", &m_objectMapFilter); + ui->map->rootContext()->setContextProperty("mapModel", &m_objectMapModel); + ui->map->rootContext()->setContextProperty("imageModelFiltered", &m_imageMapFilter); + ui->map->rootContext()->setContextProperty("polygonModelFiltered", &m_polygonMapFilter); + ui->map->rootContext()->setContextProperty("polylineModelFiltered", &m_polylineMapFilter); ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map.qml"))); -#endif m_settings.m_modelURL = QString("http://127.0.0.1:%1/3d/").arg(m_webPort); m_webServer->addPathSubstitution("3d", m_settings.m_modelDir); @@ -236,6 +254,11 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur float stationLongitude = MainCore::instance()->getSettings().getLongitude(); float stationAltitude = MainCore::instance()->getSettings().getAltitude(); m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); + QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude); + m_objectMapFilter.setPosition(stationPosition); + m_imageMapFilter.setPosition(stationPosition); + m_polygonMapFilter.setPosition(stationPosition); + m_polylineMapFilter.setPosition(stationPosition); // Centre map at My Position QQuickItem *item = ui->map->rootObject(); @@ -257,7 +280,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur m_antennaMapItem.setImageRotation(0); m_antennaMapItem.setText(new QString(MainCore::instance()->getSettings().getStationName())); m_antennaMapItem.setModel(new QString("antenna.glb")); - m_antennaMapItem.setFixedPosition(true); + m_antennaMapItem.setFixedPosition(false); + m_antennaMapItem.setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))); + m_antennaMapItem.setAvailableUntil(new QString(QDateTime::currentDateTime().addDays(365).toString(Qt::ISODateWithMs))); m_antennaMapItem.setOrientation(0); m_antennaMapItem.setLabel(new QString(MainCore::instance()->getSettings().getStationName())); m_antennaMapItem.setLabelAltitudeOffset(4.5); @@ -277,6 +302,10 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur addRadioTimeTransmitters(); addRadar(); addIonosonde(); + addBroadcast(); + addNavAids(); + addAirspace(); + addAirports(); displaySettings(); applySettings(true); @@ -317,31 +346,18 @@ void MapGUI::setWorkspaceIndex(int index) m_feature->setWorkspaceIndex(index); } -// Update a map item or image +// Update a map item void MapGUI::update(const QObject *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group) { - if (swgMapItem->getType() == 0) - { - m_mapModel.update(source, swgMapItem, group); - } - else if (m_cesium) - { - QString name = *swgMapItem->getName(); - QString image = *swgMapItem->getImage(); - if (!image.isEmpty()) - { - m_cesium->updateImage(name, - swgMapItem->getImageTileEast(), - swgMapItem->getImageTileWest(), - swgMapItem->getImageTileNorth(), - swgMapItem->getImageTileSouth(), - swgMapItem->getAltitude(), - image); - } - else - { - m_cesium->removeImage(name); - } + int type = swgMapItem->getType(); + if (type == 0) { + m_objectMapModel.update(source, swgMapItem, group); + } else if (type == 1) { + m_imageMapModel.update(source, swgMapItem, group); + } else if (type == 2) { + m_polygonMapModel.update(source, swgMapItem, group); + } else if (type == 3) { + m_polylineMapModel.update(source, swgMapItem, group); } } @@ -401,11 +417,15 @@ void MapGUI::addIBPBeacons() } const QList MapGUI::m_radioTimeTransmitters = { - {"MSF", 60000, 54.9075f, -3.27333f, 17}, - {"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, - {"TDF", 162000, 47.1694f, 2.2044f, 800}, - {"WWVB", 60000, 40.67805556f, -105.04666667f, 70}, - {"JJY", 60000, 33.465f, 130.175555f, 50} + {"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK + {"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany + {"TDF", 162000, 47.1694f, 2.2044f, 800}, // France + {"WWVB", 60000, 40.67805556f, -105.04666667f, 70}, // USA + {"JJY-40", 40000, 37.3725f, 140.848889f, 50}, // Japan + {"JJY-60", 60000, 33.465556f, 130.175556f, 50}, // Japan + {"RTZ", 50000, 52.436111f, 103.685833f, 10}, // Russia - On 22:00 to 21:00 UTC + {"RBU", 66666, 56.733333f, 37.663333f, 10}, // Russia + {"BPC", 68500, 34.457f, 115.837f, 90}, // China - On 1:00 to 21:00 UTC }; void MapGUI::addRadioTimeTransmitters() @@ -539,7 +559,111 @@ static QString arrayToString(QJsonArray array) return s; } +void MapGUI::addBroadcast() +{ + QFile file(":/map/data/transmitters.csv"); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + + QString error; + QHash colIndexes = CSV::readHeader(in, {"Type", "Id", "Name", "Frequency (Hz)", "Latitude", "Longitude", "Altitude (m)", "Power", "TII Main", "TII Sub"}, error); + if (error.isEmpty()) + { + int typeCol = colIndexes.value("Type"); + int idCol = colIndexes.value("Id"); + int nameCol = colIndexes.value("Name"); + int frequencyCol = colIndexes.value("Frequency (Hz)"); + int latCol = colIndexes.value("Latitude"); + int longCol = colIndexes.value("Longitude"); + int altCol = colIndexes.value("Altitude (m)"); + int powerCol = colIndexes.value("Power"); + int tiiMainCol = colIndexes.value("TII Main"); + int tiiSubCol = colIndexes.value("TII Sub"); + + QStringList cols; + while (CSV::readRow(in, &cols)) + { + QString type = cols[typeCol]; + QString id = cols[idCol]; + QString name = cols[nameCol]; + QString frequency = cols[frequencyCol]; + double latitude = cols[latCol].toDouble(); + double longitude = cols[longCol].toDouble(); + double altitude = cols[altCol].toDouble(); + QString power = cols[powerCol]; + + SWGSDRangel::SWGMapItem mapItem; + mapItem.setLatitude(latitude); + mapItem.setLongitude(longitude); + mapItem.setAltitude(altitude); + mapItem.setImageRotation(0); + mapItem.setModel(new QString("antenna.glb")); + mapItem.setFixedPosition(true); + mapItem.setOrientation(0); + mapItem.setLabelAltitudeOffset(4.5); + mapItem.setAltitudeReference(1); + + if (type == "AM") + { + // Name should be unique + mapItem.setName(new QString(id)); + mapItem.setImage(new QString("antennaam.png")); + mapItem.setLabel(new QString(name)); + QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 kHz\nPower (ERMP): %4 kW") + .arg(type) + .arg(name) + .arg(frequency.toDouble() / 1e3) + .arg(power) + ; + mapItem.setText(new QString(text)); + update(m_map, &mapItem, "AM"); + } + else if (type == "FM") + { + mapItem.setName(new QString(id)); + mapItem.setImage(new QString("antennafm.png")); + mapItem.setLabel(new QString(name)); + QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 MHz\nPower (ERP): %4 kW") + .arg(type) + .arg(name) + .arg(frequency.toDouble() / 1e6) + .arg(power) + ; + mapItem.setText(new QString(text)); + update(m_map, &mapItem, "FM"); + } + else if (type == "DAB") + { + mapItem.setName(new QString(id)); + mapItem.setImage(new QString("antennadab.png")); + mapItem.setLabel(new QString(name)); + QString text = QString("%1 Transmitter\nEnsemble: %2\nFrequency: %3 MHz\nPower (ERP): %4 kW\nTII: %5 %6") + .arg(type) + .arg(name) + .arg(frequency.toDouble() / 1e6) + .arg(power) + .arg(cols[tiiMainCol]) + .arg(cols[tiiSubCol]) + ; + mapItem.setText(new QString(text)); + update(m_map, &mapItem, "DAB"); + } + } + } + else + { + qCritical() << "MapGUI::addBroadcast: Failed reading transmitters.csv - " << error; + } + } + else + { + qCritical() << "MapGUI::addBroadcast: Failed to open transmitters.csv"; + } +} + // Coming soon +/* Code for FM list / DAB list, should they allow access void MapGUI::addDAB() { QFile file("stationlist_SI.json"); @@ -663,6 +787,201 @@ void MapGUI::addDAB() qDebug() << "MapGUI::addDAB: Failed to open DAB json"; } } +*/ + +void MapGUI::addNavAids() +{ + m_navAids = OpenAIP::getNavAids(); + + for (const auto navAid : *m_navAids) + { + SWGSDRangel::SWGMapItem navAidMapItem; + navAidMapItem.setName(new QString(navAid->m_name + " " + navAid->m_ident)); // Neither name or ident are unique... + navAidMapItem.setLatitude(navAid->m_latitude); + navAidMapItem.setLongitude(navAid->m_longitude); + navAidMapItem.setAltitude(Units::feetToMetres(navAid->m_elevation)); + QString image = QString("%1.png").arg(navAid->m_type); + navAidMapItem.setImage(new QString(image)); + navAidMapItem.setImageRotation(0); + QString text = QString("NAVAID\nName: %1").arg(navAid->m_name); + if (navAid->m_type == "NDB") { + text.append(QString("\nFrequency: %1 kHz").arg(navAid->m_frequencykHz, 0, 'f', 1)); + } else { + text.append(QString("\nFrequency: %1 MHz").arg(navAid->m_frequencykHz / 1000.0f, 0, 'f', 2)); + } + if (navAid->m_channel != "") { + text.append(QString("\nChannel: %1").arg(navAid->m_channel)); + } + text.append(QString("\nIdent: %1 %2").arg(navAid->m_ident).arg(Morse::toSpacedUnicodeMorse(navAid->m_ident))); + text.append(QString("\nRange: %1 nm").arg(navAid->m_range)); + if (navAid->m_alignedTrueNorth) { + text.append(QString("\nMagnetic declination: Aligned to true North")); + } else if (navAid->m_magneticDeclination != 0.0f) { + text.append(QString("\nMagnetic declination: %1%2").arg(std::round(navAid->m_magneticDeclination)).arg(QChar(0x00b0))); + } + navAidMapItem.setText(new QString(text)); + navAidMapItem.setModel(new QString("antenna.glb")); + navAidMapItem.setFixedPosition(true); + navAidMapItem.setOrientation(0); + navAidMapItem.setLabel(new QString(navAid->m_name)); + navAidMapItem.setLabelAltitudeOffset(4.5); + navAidMapItem.setAltitudeReference(1); + update(m_map, &navAidMapItem, "NavAid"); + } +} + +void MapGUI::addAirspace(const Airspace *airspace, const QString& group, int cnt) +{ + //MapSettings::MapItemSettings *itemSettings = getItemSettings(group); + + QString details; + details.append(airspace->m_name); + details.append(QString("\n%1 - %2") + .arg(airspace->getAlt(&airspace->m_bottom)) + .arg(airspace->getAlt(&airspace->m_top))); + QString name = QString("Airspace %1 (%2)").arg(airspace->m_name).arg(cnt); + + SWGSDRangel::SWGMapItem airspaceMapItem; + airspaceMapItem.setName(new QString(name)); + airspaceMapItem.setLatitude(airspace->m_position.y()); + airspaceMapItem.setLongitude(airspace->m_position.x()); + airspaceMapItem.setAltitude(airspace->bottomHeightInMetres()); + QString image = QString("none"); + airspaceMapItem.setImage(new QString(image)); + airspaceMapItem.setImageRotation(0); + airspaceMapItem.setText(new QString(details)); // Not used - label is used instead for now + airspaceMapItem.setFixedPosition(true); + airspaceMapItem.setLabel(new QString(details)); + airspaceMapItem.setAltitudeReference(0); + QList *coords = new QList(); + for (const auto p : airspace->m_polygon) + { + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(p.y()); + c->setLongitude(p.x()); + c->setAltitude(airspace->bottomHeightInMetres()); + coords->append(c); + } + airspaceMapItem.setCoordinates(coords); + airspaceMapItem.setExtrudedHeight(airspace->topHeightInMetres()); + airspaceMapItem.setType(2); + update(m_map, &airspaceMapItem, group); +} + + +void MapGUI::addAirspace() +{ + m_airspaces = OpenAIP::getAirspaces(); + + int cnt = 0; + for (const auto airspace : *m_airspaces) + { + static const QMap groupMap { + {"A", "Airspace (A)"}, + {"B", "Airspace (B)"}, + {"C", "Airspace (C)"}, + {"D", "Airspace (D)"}, + {"E", "Airspace (E)"}, + {"F", "Airspace (F)"}, + {"G", "Airspace (G)"}, + {"FIR", "Airspace (FIR)"}, + {"CTR", "Airspace (CTR)"}, + {"RMZ", "Airspace (RMZ)"}, + {"TMA", "Airspace (TMA)"}, + {"TMZ", "Airspace (TMZ)"}, + {"OTH", "Airspace (OTH)"}, + {"RESTRICTED", "Airspace (Restricted)"}, + {"GLIDING", "Airspace (Gliding)"}, + {"DANGER", "Airspace (Danger)"}, + {"PROHIBITED", "Airspace (Prohibited)"}, + {"WAVE", "Airspace (Wave)"}, + }; + + if (groupMap.contains(airspace->m_category)) + { + QString group = groupMap[airspace->m_category]; + addAirspace(airspace, group, cnt); + cnt++; + + if ( (airspace->bottomHeightInMetres() == 0) + && ((airspace->m_category == "D") || (airspace->m_category == "G") || (airspace->m_category == "CTR")) + && (!((airspace->m_country == "IT") && (airspace->m_name.startsWith("FIR")))) + && (!((airspace->m_country == "PL") && (airspace->m_name.startsWith("FIS")))) + ) + { + group = "Airspace (Airports)"; + addAirspace(airspace, group, cnt); + cnt++; + } + } + else + { + qDebug() << "MapGUI::addAirspace: No group for airspace category " << airspace->m_category; + } + } +} + +void MapGUI::addAirports() +{ + m_airportInfo = OurAirportsDB::getAirportsById(); + if (m_airportInfo) + { + QHashIterator i(*m_airportInfo); + while (i.hasNext()) + { + i.next(); + AirportInformation *airport = i.value(); + + SWGSDRangel::SWGMapItem airportMapItem; + airportMapItem.setName(new QString(airport->m_ident)); + airportMapItem.setLatitude(airport->m_latitude); + airportMapItem.setLongitude(airport->m_longitude); + airportMapItem.setAltitude(Units::feetToMetres(airport->m_elevation)); + airportMapItem.setImage(new QString(airport->getImageName())); + airportMapItem.setImageRotation(0); + QStringList list; + list.append(QString("%1: %2").arg(airport->m_ident).arg(airport->m_name)); + for (int i = 0; i < airport->m_frequencies.size(); i++) + { + const AirportInformation::FrequencyInformation *frequencyInfo = airport->m_frequencies[i]; + list.append(QString("%1: %2 MHz").arg(frequencyInfo->m_type).arg(frequencyInfo->m_frequency)); + } + airportMapItem.setText(new QString(list.join("\n"))); + airportMapItem.setModel(new QString("airport.glb")); // No such model currently, but we don't really want one + airportMapItem.setFixedPosition(true); + airportMapItem.setOrientation(0); + airportMapItem.setLabel(new QString(airport->m_ident)); + airportMapItem.setLabelAltitudeOffset(4.5); + airportMapItem.setAltitudeReference(1); + QString group; + if (airport->m_type == AirportInformation::Small) { + group = "Airport (Small)"; + } else if (airport->m_type == AirportInformation::Medium) { + group = "Airport (Medium)"; + } else if (airport->m_type == AirportInformation::Large) { + group = "Airport (Large)"; + } else { + group = "Heliport"; + } + update(m_map, &airportMapItem, group); + } + } +} + +void MapGUI::navAidsUpdated() +{ + addNavAids(); +} + +void MapGUI::airspacesUpdated() +{ + addAirspace(); +} + +void MapGUI::airportsUpdated() +{ + addAirports(); +} void MapGUI::blockApplySettings(bool block) { @@ -799,7 +1118,7 @@ void MapGUI::redrawMap() if (object) { double zoom = object->property("zoomLevel").value(); - object->setProperty("zoomLevel", QVariant::fromValue(zoom+1)); + object->setProperty("zoomLevel", QVariant::fromValue(zoom+1.0)); object->setProperty("zoomLevel", QVariant::fromValue(zoom)); } } @@ -834,6 +1153,15 @@ bool MapGUI::eventFilter(QObject *obj, QEvent *event) return FeatureGUI::eventFilter(obj, event); } +MapSettings::MapItemSettings *MapGUI::getItemSettings(const QString &group) +{ + if (m_settings.m_itemSettings.contains(group)) { + return m_settings.m_itemSettings[group]; + } else { + return nullptr; + } +} + void MapGUI::supportedMapsChanged() { QQuickItem *item = ui->map->rootObject(); @@ -893,7 +1221,8 @@ void MapGUI::applyMap3DSettings(bool reloadMap) #ifdef QT_WEBENGINE_FOUND if (m_settings.m_map3DEnabled && ((m_cesium == nullptr) || reloadMap)) { - if (m_cesium == nullptr) { + if (m_cesium == nullptr) + { m_cesium = new CesiumInterface(&m_settings); connect(m_cesium, &CesiumInterface::connected, this, &MapGUI::init3DMap); connect(m_cesium, &CesiumInterface::received, this, &MapGUI::receivedCesiumEvent); @@ -924,6 +1253,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap) m_cesium->getDateTime(); m_cesium->showMUF(m_settings.m_displayMUF); m_cesium->showfoF2(m_settings.m_displayfoF2); + m_objectMapModel.allUpdated(); + m_imageMapModel.allUpdated(); + m_polygonMapModel.allUpdated(); + m_polylineMapModel.allUpdated(); } MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations"); if (ionosondeItemSettings) { @@ -934,9 +1267,10 @@ void MapGUI::applyMap3DSettings(bool reloadMap) #else ui->displayMUF->setVisible(false); ui->displayfoF2->setVisible(false); - m_mapModel.allUpdated(); - float stationLatitude = MainCore::instance()->getSettings().getLatitude(); - float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + m_objectMapModel.allUpdated(); + m_imageMapModel.allUpdated(); + m_polygonMapModel.allUpdated(); + m_polylineMapModel.allUpdated(); #endif } @@ -947,6 +1281,11 @@ void MapGUI::init3DMap() m_cesium->initCZML(); + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + float stationAltitude = MainCore::instance()->getSettings().getLongitude(); + + m_cesium->setPosition(QGeoCoordinate(stationLatitude, stationLongitude, stationAltitude)); m_cesium->setTerrain(m_settings.m_terrain, maptilerAPIKey()); m_cesium->setBuildings(m_settings.m_buildings); m_cesium->setSunLight(m_settings.m_sunLightEnabled); @@ -954,9 +1293,10 @@ void MapGUI::init3DMap() m_cesium->setAntiAliasing(m_settings.m_antiAliasing); m_cesium->getDateTime(); - m_mapModel.allUpdated(); - float stationLatitude = MainCore::instance()->getSettings().getLatitude(); - float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + m_objectMapModel.allUpdated(); + m_imageMapModel.allUpdated(); + m_polygonMapModel.allUpdated(); + m_polylineMapModel.allUpdated(); // Set 3D view after loading initial objects m_cesium->setHomeView(stationLatitude, stationLongitude); @@ -977,10 +1317,13 @@ void MapGUI::displaySettings() ui->displayAllGroundTracks->setChecked(m_settings.m_displayAllGroundTracks); ui->displayMUF->setChecked(m_settings.m_displayMUF); ui->displayfoF2->setChecked(m_settings.m_displayfoF2); - m_mapModel.setDisplayNames(m_settings.m_displayNames); - m_mapModel.setDisplaySelectedGroundTracks(m_settings.m_displaySelectedGroundTracks); - m_mapModel.setDisplayAllGroundTracks(m_settings.m_displayAllGroundTracks); - m_mapModel.updateItemSettings(m_settings.m_itemSettings); + m_objectMapModel.setDisplayNames(m_settings.m_displayNames); + m_objectMapModel.setDisplaySelectedGroundTracks(m_settings.m_displaySelectedGroundTracks); + m_objectMapModel.setDisplayAllGroundTracks(m_settings.m_displayAllGroundTracks); + m_objectMapModel.updateItemSettings(m_settings.m_itemSettings); + m_imageMapModel.updateItemSettings(m_settings.m_itemSettings); + m_polygonMapModel.updateItemSettings(m_settings.m_itemSettings); + m_polylineMapModel.updateItemSettings(m_settings.m_itemSettings); applyMap2DSettings(true); applyMap3DSettings(true); getRollupContents()->restoreState(m_rollupState); @@ -1049,19 +1392,19 @@ void MapGUI::on_maidenhead_clicked() void MapGUI::on_displayNames_clicked(bool checked) { m_settings.m_displayNames = checked; - m_mapModel.setDisplayNames(checked); + m_objectMapModel.setDisplayNames(checked); } void MapGUI::on_displaySelectedGroundTracks_clicked(bool checked) { m_settings.m_displaySelectedGroundTracks = checked; - m_mapModel.setDisplaySelectedGroundTracks(checked); + m_objectMapModel.setDisplaySelectedGroundTracks(checked); } void MapGUI::on_displayAllGroundTracks_clicked(bool checked) { m_settings.m_displayAllGroundTracks = checked; - m_mapModel.setDisplayAllGroundTracks(checked); + m_objectMapModel.setDisplayAllGroundTracks(checked); } void MapGUI::on_displayMUF_clicked(bool checked) @@ -1183,13 +1526,15 @@ void MapGUI::find(const QString& target) } else { - MapItem *mapItem = m_mapModel.findMapItem(target); + // FIXME: Support polygon/polyline + ObjectMapItem *mapItem = m_objectMapModel.findMapItem(target); if (mapItem != nullptr) { map->setProperty("center", QVariant::fromValue(mapItem->getCoordinates())); if (m_cesium) { m_cesium->track(target); } + m_objectMapModel.moveToFront(m_objectMapModel.findMapItemIndex(target).row()); } else { @@ -1224,7 +1569,10 @@ void MapGUI::track3D(const QString& target) void MapGUI::on_deleteAll_clicked() { - m_mapModel.removeAll(); + m_objectMapModel.removeAll(); + m_imageMapModel.removeAll(); + m_polygonMapModel.removeAll(); + m_polylineMapModel.removeAll(); if (m_cesium) { m_cesium->removeAllCZMLEntities(); @@ -1235,6 +1583,9 @@ void MapGUI::on_deleteAll_clicked() void MapGUI::on_displaySettings_clicked() { MapSettingsDialog dialog(&m_settings); + connect(&dialog, &MapSettingsDialog::navAidsUpdated, this, &MapGUI::navAidsUpdated); + connect(&dialog, &MapSettingsDialog::airspacesUpdated, this, &MapGUI::airspacesUpdated); + connect(&dialog, &MapSettingsDialog::airportsUpdated, this, &MapGUI::airportsUpdated); new DialogPositioner(&dialog, true); if (dialog.exec() == QDialog::Accepted) { @@ -1244,7 +1595,10 @@ void MapGUI::on_displaySettings_clicked() applyMap2DSettings(dialog.m_map2DSettingsChanged); applyMap3DSettings(dialog.m_map3DSettingsChanged); applySettings(); - m_mapModel.allUpdated(); + m_objectMapModel.allUpdated(); + m_imageMapModel.allUpdated(); + m_polygonMapModel.allUpdated(); + m_polylineMapModel.allUpdated(); } } @@ -1289,17 +1643,17 @@ void MapGUI::receivedCesiumEvent(const QJsonObject &obj) if (event == "selected") { if (obj.contains("id")) { - m_mapModel.setSelected3D(obj.value("id").toString()); + m_objectMapModel.setSelected3D(obj.value("id").toString()); } else { - m_mapModel.setSelected3D(""); + m_objectMapModel.setSelected3D(""); } } else if (event == "tracking") { if (obj.contains("id")) { - //m_mapModel.setTarget(obj.value("id").toString()); + //m_objectMapModel.setTarget(obj.value("id").toString()); } else { - //m_mapModel.setTarget(""); + //m_objectMapModel.setTarget(""); } } else if (event == "clock") @@ -1346,9 +1700,10 @@ void MapGUI::preferenceChanged(int elementType) float stationLongitude = MainCore::instance()->getSettings().getLongitude(); float stationAltitude = MainCore::instance()->getSettings().getAltitude(); - if ( (stationLatitude != m_azEl.getLocationSpherical().m_latitude) - || (stationLongitude != m_azEl.getLocationSpherical().m_longitude) - || (stationAltitude != m_azEl.getLocationSpherical().m_altitude)) + QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude); + QGeoCoordinate previousPosition(m_azEl.getLocationSpherical().m_latitude, m_azEl.getLocationSpherical().m_longitude, m_azEl.getLocationSpherical().m_altitude); + + if (stationPosition != previousPosition) { // Update position of station m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); @@ -1356,7 +1711,24 @@ void MapGUI::preferenceChanged(int elementType) m_antennaMapItem.setLatitude(stationLatitude); m_antennaMapItem.setLongitude(stationLongitude); m_antennaMapItem.setAltitude(stationAltitude); + delete m_antennaMapItem.getPositionDateTime(); + m_antennaMapItem.setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))); update(m_map, &m_antennaMapItem, "Station"); + + m_objectMapFilter.setPosition(stationPosition); + m_imageMapFilter.setPosition(stationPosition); + m_polygonMapFilter.setPosition(stationPosition); + m_polylineMapFilter.setPosition(stationPosition); + if (m_cesium) + { + m_cesium->setPosition(stationPosition); + if (!m_lastFullUpdatePosition.isValid() || (stationPosition.distanceTo(m_lastFullUpdatePosition) >= 1000)) + { + // Update all objects so distance filter is reapplied + m_objectMapModel.allUpdated(); + m_lastFullUpdatePosition = stationPosition; + } + } } } if (pref == Preferences::StationName) @@ -1384,3 +1756,4 @@ void MapGUI::makeUIConnections() QObject::connect(ui->ibpBeacons, &QToolButton::clicked, this, &MapGUI::on_ibpBeacons_clicked); QObject::connect(ui->radiotime, &QToolButton::clicked, this, &MapGUI::on_radiotime_clicked); } + diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index 2e8a4c3fe..bf41dd065 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -33,6 +33,8 @@ #include "util/messagequeue.h" #include "util/giro.h" #include "util/azel.h" +#include "util/openaip.h" +#include "util/ourairportsdb.h" #include "settings/rollupstate.h" #include "SWGMapItem.h" @@ -152,11 +154,16 @@ public: void addRadioTimeTransmitters(); void addRadar(); void addIonosonde(); + void addBroadcast(); void addDAB(); + void addNavAids(); + void addAirspace(const Airspace *airspace, const QString& group, int cnt); + void addAirspace(); + void addAirports(); void find(const QString& target); void track3D(const QString& target); Q_INVOKABLE void supportedMapsChanged(); - MapSettings::MapItemSettings *getItemSettings(const QString &group) { return m_settings.m_itemSettings[group]; } + MapSettings::MapItemSettings *getItemSettings(const QString &group); CesiumInterface *cesium() { return m_cesium; } private: @@ -171,7 +178,14 @@ private: Map* m_map; MessageQueue m_inputMessageQueue; - MapModel m_mapModel; + ObjectMapModel m_objectMapModel; + ObjectMapFilter m_objectMapFilter; + ImageMapModel m_imageMapModel; + ImageFilter m_imageMapFilter; + PolygonMapModel m_polygonMapModel; + PolygonFilter m_polygonMapFilter; + PolylineMapModel m_polylineMapModel; + PolylineFilter m_polylineMapFilter; AzEl m_azEl; // Position of station SWGSDRangel::SWGMapItem m_antennaMapItem; QList *m_beacons; @@ -183,6 +197,10 @@ private: QTimer m_redrawMapTimer; GIRO *m_giro; QHash m_ionosondeStations; + QSharedPointer> m_navAids; + QSharedPointer> m_airspaces; + QSharedPointer> m_airportInfo; + QGeoCoordinate m_lastFullUpdatePosition; CesiumInterface *m_cesium; WebServer *m_webServer; @@ -238,6 +256,9 @@ private slots: void giroDataUpdated(const GIRO::GIROStationData& data); void mufUpdated(const QJsonDocument& document); void foF2Updated(const QJsonDocument& document); + void navAidsUpdated(); + void airspacesUpdated(); + void airportsUpdated(); }; diff --git a/plugins/feature/map/mapitem.cpp b/plugins/feature/map/mapitem.cpp new file mode 100644 index 000000000..9f1466aec --- /dev/null +++ b/plugins/feature/map/mapitem.cpp @@ -0,0 +1,288 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 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 "mapitem.h" + +MapItem::MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : + m_altitude(0.0) +{ + m_sourcePipe = sourcePipe; + m_group = group; + m_itemSettings = itemSettings; + m_name = *mapItem->getName(); + m_hashKey = m_sourcePipe->objectName() + m_name; +} + +void MapItem::update(SWGSDRangel::SWGMapItem *mapItem) +{ + if (mapItem->getLabel()) { + m_label = *mapItem->getLabel(); + } else { + m_label = ""; + } + m_latitude = mapItem->getLatitude(); + m_longitude = mapItem->getLongitude(); + m_altitude = mapItem->getAltitude(); +} + +void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem) +{ + MapItem::update(mapItem); + if (mapItem->getPositionDateTime()) { + m_positionDateTime = QDateTime::fromString(*mapItem->getPositionDateTime(), Qt::ISODateWithMs); + } else { + m_positionDateTime = QDateTime(); + } + m_useHeadingPitchRoll = mapItem->getOrientation() == 1; + m_heading = mapItem->getHeading(); + m_pitch = mapItem->getPitch(); + m_roll = mapItem->getRoll(); + if (mapItem->getOrientationDateTime()) { + m_orientationDateTime = QDateTime::fromString(*mapItem->getOrientationDateTime(), Qt::ISODateWithMs); + } else { + m_orientationDateTime = QDateTime(); + } + m_image = *mapItem->getImage(); + m_imageRotation = mapItem->getImageRotation(); + QString *text = mapItem->getText(); + if (text != nullptr) { + m_text = text->replace("\n", "
"); // Convert to HTML + } else { + m_text = ""; + } + if (mapItem->getModel()) { + m_model = *mapItem->getModel(); + } else { + m_model = ""; + } + m_labelAltitudeOffset = mapItem->getLabelAltitudeOffset(); + m_modelAltitudeOffset = mapItem->getModelAltitudeOffset(); + m_altitudeReference = mapItem->getAltitudeReference(); + m_fixedPosition = mapItem->getFixedPosition(); + QList *animations = mapItem->getAnimations(); + if (animations) + { + for (auto animation : *animations) { + m_animations.append(new CesiumInterface::Animation(animation)); + } + } + findFrequency(); + if (!m_fixedPosition) + { + updateTrack(mapItem->getTrack()); + updatePredictedTrack(mapItem->getPredictedTrack()); + } + if (mapItem->getAvailableUntil()) { + m_availableUntil = QDateTime::fromString(*mapItem->getAvailableUntil(), Qt::ISODateWithMs); + } else { + m_availableUntil = QDateTime(); + } +} + +void ImageMapItem::update(SWGSDRangel::SWGMapItem *mapItem) +{ + MapItem::update(mapItem); + + m_image = "data:image/png;base64," + *mapItem->getImage(); + m_imageZoomLevel = mapItem->getImageZoomLevel(); + + float east = mapItem->getImageTileEast(); + float west = mapItem->getImageTileWest(); + float north = mapItem->getImageTileNorth(); + float south = mapItem->getImageTileSouth(); + m_latitude = north - (north - south) / 2.0; + m_longitude = east - (east - west) / 2.0; + m_bounds = QGeoRectangle(QGeoCoordinate(north, west), QGeoCoordinate(south, east)); +} + +void PolygonMapItem::update(SWGSDRangel::SWGMapItem *mapItem) +{ + MapItem::update(mapItem); + m_extrudedHeight = mapItem->getExtrudedHeight(); + + qDeleteAll(m_points); + m_points.clear(); + QList *coords = mapItem->getCoordinates(); + if (coords) + { + for (int i = 0; i < coords->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = coords->at(i); + QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); + m_points.append(c); + } + } + + // Calculate bounds + m_polygon.clear(); + qreal latMin = 90.0, latMax = -90.0, lonMin = 180.0, lonMax = -180.0; + for (const auto p : m_points) + { + QGeoCoordinate coord = *p; + latMin = std::min(latMin, coord.latitude()); + latMax = std::max(latMax, coord.latitude()); + lonMin = std::min(lonMin, coord.longitude()); + lonMax = std::max(lonMax, coord.longitude()); + m_polygon.push_back(QVariant::fromValue(coord)); + } + m_bounds = QGeoRectangle(QGeoCoordinate(latMax, lonMin), QGeoCoordinate(latMin, lonMax)); +} + +void PolylineMapItem::update(SWGSDRangel::SWGMapItem *mapItem) +{ + MapItem::update(mapItem); + + qDeleteAll(m_points); + m_points.clear(); + QList *coords = mapItem->getCoordinates(); + if (coords) + { + for (int i = 0; i < coords->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = coords->at(i); + QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); + m_points.append(c); + } + } + + // Calculate bounds + m_polyline.clear(); + qreal latMin = 90.0, latMax = -90.0, lonMin = 180.0, lonMax = -180.0; + for (const auto p : m_points) + { + QGeoCoordinate coord = *p; + latMin = std::min(latMin, coord.latitude()); + latMax = std::max(latMax, coord.latitude()); + lonMin = std::min(lonMin, coord.longitude()); + lonMax = std::max(lonMax, coord.longitude()); + m_polyline.push_back(QVariant::fromValue(coord)); + } + m_bounds = QGeoRectangle(QGeoCoordinate(latMax, lonMin), QGeoCoordinate(latMin, lonMax)); +} + +QGeoCoordinate ObjectMapItem::getCoordinates() +{ + QGeoCoordinate coords; + coords.setLatitude(m_latitude); + coords.setLongitude(m_longitude); + return coords; +} + +void ObjectMapItem::findFrequency() +{ + // Look for a frequency in the text for this object + QRegExp re("(([0-9]+(\\.[0-9]+)?) *([kMG])?Hz)"); + if (re.indexIn(m_text) != -1) + { + QStringList capture = re.capturedTexts(); + m_frequency = capture[2].toDouble(); + if (capture.length() == 5) + { + QChar unit = capture[4][0]; + if (unit == 'k') + m_frequency *= 1000.0; + else if (unit == 'M') + m_frequency *= 1000000.0; + else if (unit == 'G') + m_frequency *= 1000000000.0; + } + m_frequencyString = capture[0]; + } + else + { + m_frequency = 0.0; + } +} + +void ObjectMapItem::updateTrack(QList *track) +{ + if (track != nullptr) + { + qDeleteAll(m_takenTrackCoords); + m_takenTrackCoords.clear(); + qDeleteAll(m_takenTrackDateTimes); + m_takenTrackDateTimes.clear(); + m_takenTrack.clear(); + m_takenTrack1.clear(); + m_takenTrack2.clear(); + for (int i = 0; i < track->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = track->at(i); + QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); + QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate)); + m_takenTrackCoords.push_back(c); + m_takenTrackDateTimes.push_back(d); + m_takenTrack.push_back(QVariant::fromValue(*c)); + } + } + else + { + // Automatically create a track + if (m_takenTrackCoords.size() == 0) + { + QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); + m_takenTrackCoords.push_back(c); + if (m_positionDateTime.isValid()) { + m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime)); + } else { + m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime())); + } + m_takenTrack.push_back(QVariant::fromValue(*c)); + } + else + { + QGeoCoordinate *prev = m_takenTrackCoords.last(); + QDateTime *prevDateTime = m_takenTrackDateTimes.last(); + if ((prev->latitude() != m_latitude) || (prev->longitude() != m_longitude) + || (prev->altitude() != m_altitude) || (*prevDateTime != m_positionDateTime)) + { + QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); + m_takenTrackCoords.push_back(c); + if (m_positionDateTime.isValid()) { + m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime)); + } else { + m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime())); + } + m_takenTrack.push_back(QVariant::fromValue(*c)); + } + } + } +} + +void ObjectMapItem::updatePredictedTrack(QList *track) +{ + if (track != nullptr) + { + qDeleteAll(m_predictedTrackCoords); + m_predictedTrackCoords.clear(); + qDeleteAll(m_predictedTrackDateTimes); + m_predictedTrackDateTimes.clear(); + m_predictedTrack.clear(); + m_predictedTrack1.clear(); + m_predictedTrack2.clear(); + for (int i = 0; i < track->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = track->at(i); + QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); + QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate)); + m_predictedTrackCoords.push_back(c); + m_predictedTrackDateTimes.push_back(d); + m_predictedTrack.push_back(QVariant::fromValue(*c)); + } + } +} + diff --git a/plugins/feature/map/mapitem.h b/plugins/feature/map/mapitem.h new file mode 100644 index 000000000..182511c50 --- /dev/null +++ b/plugins/feature/map/mapitem.h @@ -0,0 +1,184 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MAPITEM_H_ +#define INCLUDE_FEATURE_MAPITEM_H_ + +#include +#include +#include + +#include "mapsettings.h" +#include "cesiuminterface.h" + +#include "SWGMapItem.h" + +class MapModel; +class ObjectMapModel; +class PolygonMapModel; +class PolylineMapModel; +class ImageMapModel; +class CZML; + +class MapItem { + +public: + + MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem); + virtual void update(SWGSDRangel::SWGMapItem *mapItem); + +protected: + + friend CZML; + friend MapModel; + friend ObjectMapModel; + friend PolygonMapModel; + friend PolylineMapModel; + + QString m_group; + MapSettings::MapItemSettings *m_itemSettings; + const QObject *m_sourcePipe; // Channel/feature that created the item + QString m_hashKey; + + QString m_name; // Unique id + QString m_label; + float m_latitude; // Position for label + float m_longitude; + float m_altitude; // In metres + QDateTime m_availableUntil; // Date & time this item is visible until (for 3D map). Invalid date/time is forever +}; + + +// Information required about each item displayed on the map +class ObjectMapItem : public MapItem { + +public: + ObjectMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : + MapItem(sourcePipe, group, itemSettings, mapItem) + { + update(mapItem); + } + void update(SWGSDRangel::SWGMapItem *mapItem) override; + QGeoCoordinate getCoordinates(); + +protected: + void findFrequency(); + void updateTrack(QList *track); + void updatePredictedTrack(QList *track); + + friend ObjectMapModel; + friend CZML; + QDateTime m_positionDateTime; + bool m_useHeadingPitchRoll; + float m_heading; + float m_pitch; + float m_roll; + QDateTime m_orientationDateTime; + QString m_image; + int m_imageRotation; + QString m_text; + double m_frequency; // Frequency to set + QString m_frequencyString; + bool m_fixedPosition; // Don't record/display track + QList m_predictedTrackCoords; + QList m_predictedTrackDateTimes; + QVariantList m_predictedTrack; // Line showing where the object is going + QVariantList m_predictedTrack1; + QVariantList m_predictedTrack2; + QGeoCoordinate m_predictedStart1; + QGeoCoordinate m_predictedStart2; + QGeoCoordinate m_predictedEnd1; + QGeoCoordinate m_predictedEnd2; + QList m_takenTrackCoords; + QList m_takenTrackDateTimes; + QVariantList m_takenTrack; // Line showing where the object has been + QVariantList m_takenTrack1; + QVariantList m_takenTrack2; + QGeoCoordinate m_takenStart1; + QGeoCoordinate m_takenStart2; + QGeoCoordinate m_takenEnd1; + QGeoCoordinate m_takenEnd2; + + // For 3D map + QString m_model; + int m_altitudeReference; + float m_labelAltitudeOffset; + float m_modelAltitudeOffset; + QList m_animations; +}; + +class PolygonMapItem : public MapItem { + +public: + PolygonMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : + MapItem(sourcePipe, group, itemSettings, mapItem) + { + update(mapItem); + } + void update(SWGSDRangel::SWGMapItem *mapItem) override; + +protected: + friend PolygonMapModel; + friend CZML; + + QList m_points; // FIXME: Remove? + float m_extrudedHeight; // In metres + QVariantList m_polygon; + QGeoRectangle m_bounds; // Bounding boxes for the polygons, for view clipping +}; + +class PolylineMapItem : public MapItem { + +public: + PolylineMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : + MapItem(sourcePipe, group, itemSettings, mapItem) + { + update(mapItem); + } + void update(SWGSDRangel::SWGMapItem *mapItem) override; + +protected: + friend PolylineMapModel; + friend CZML; + + QList m_points; // FIXME: Remove? + QVariantList m_polyline; + QGeoRectangle m_bounds; // Bounding boxes for the polyline, for view clipping +}; + +class ImageMapItem : public MapItem { + +public: + ImageMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : + MapItem(sourcePipe, group, itemSettings, mapItem) + { + update(mapItem); + } + void update(SWGSDRangel::SWGMapItem *mapItem) override; + +protected: + friend ImageMapModel; + friend CZML; + + QString m_image; + float m_imageZoomLevel; + QGeoRectangle m_bounds; + +}; + +#endif // INCLUDE_FEATURE_MAPITEM_H_ + diff --git a/plugins/feature/map/mapmodel.cpp b/plugins/feature/map/mapmodel.cpp index 6e958925a..7827460c0 100644 --- a/plugins/feature/map/mapmodel.cpp +++ b/plugins/feature/map/mapmodel.cpp @@ -26,193 +26,54 @@ #include "SWGTargetAzimuthElevation.h" -MapItem::MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : - m_altitude(0.0) +QVariant MapModel::data(const QModelIndex &index, int role) const { - m_sourcePipe = sourcePipe; - m_group = group; - m_itemSettings = itemSettings; - m_name = *mapItem->getName(); - update(mapItem); -} - -void MapItem::update(SWGSDRangel::SWGMapItem *mapItem) -{ - if (mapItem->getLabel()) { - m_label = *mapItem->getLabel(); - } else { - m_label = ""; + int row = index.row(); + if ((row < 0) || (row >= m_items.count())) { + return QVariant(); } - m_latitude = mapItem->getLatitude(); - m_longitude = mapItem->getLongitude(); - m_altitude = mapItem->getAltitude(); - if (mapItem->getPositionDateTime()) { - m_positionDateTime = QDateTime::fromString(*mapItem->getPositionDateTime(), Qt::ISODateWithMs); - } else { - m_positionDateTime = QDateTime(); - } - m_useHeadingPitchRoll = mapItem->getOrientation() == 1; - m_heading = mapItem->getHeading(); - m_pitch = mapItem->getPitch(); - m_roll = mapItem->getRoll(); - if (mapItem->getOrientationDateTime()) { - m_orientationDateTime = QDateTime::fromString(*mapItem->getOrientationDateTime(), Qt::ISODateWithMs); - } else { - m_orientationDateTime = QDateTime(); - } - m_image = *mapItem->getImage(); - m_imageRotation = mapItem->getImageRotation(); - QString *text = mapItem->getText(); - if (text != nullptr) { - m_text = text->replace("\n", "
"); // Convert to HTML - } else { - m_text = ""; - } - if (mapItem->getModel()) { - m_model = *mapItem->getModel(); - } else { - m_model = ""; - } - m_labelAltitudeOffset = mapItem->getLabelAltitudeOffset(); - m_modelAltitudeOffset = mapItem->getModelAltitudeOffset(); - m_altitudeReference = mapItem->getAltitudeReference(); - m_fixedPosition = mapItem->getFixedPosition(); - QList *animations = mapItem->getAnimations(); - if (animations) + switch (role) { - for (auto animation : *animations) { - m_animations.append(new CesiumInterface::Animation(animation)); - } - } - findFrequency(); - updateTrack(mapItem->getTrack()); - updatePredictedTrack(mapItem->getPredictedTrack()); -} - -QGeoCoordinate MapItem::getCoordinates() -{ - QGeoCoordinate coords; - coords.setLatitude(m_latitude); - coords.setLongitude(m_longitude); - return coords; -} - -void MapItem::findFrequency() -{ - // Look for a frequency in the text for this object - QRegExp re("(([0-9]+(\\.[0-9]+)?) *([kMG])?Hz)"); - if (re.indexIn(m_text) != -1) + case itemSettingsRole: + return QVariant::fromValue(m_items[row]->m_itemSettings); + case nameRole: + return QVariant::fromValue(m_items[row]->m_name); + case labelRole: + return QVariant::fromValue(m_items[row]->m_label); + case positionRole: { - QStringList capture = re.capturedTexts(); - m_frequency = capture[2].toDouble(); - if (capture.length() == 5) - { - QChar unit = capture[4][0]; - if (unit == 'k') - m_frequency *= 1000.0; - else if (unit == 'M') - m_frequency *= 1000000.0; - else if (unit == 'G') - m_frequency *= 1000000000.0; - } - m_frequencyString = capture[0]; + // Coordinates to display the label at + QGeoCoordinate coords; + coords.setLatitude(m_items[row]->m_latitude); + coords.setLongitude(m_items[row]->m_longitude); + coords.setAltitude(m_items[row]->m_altitude); + return QVariant::fromValue(coords); } - else - { - m_frequency = 0.0; + case mapImageMinZoomRole: + // Minimum zoom level at which this is visible + return QVariant::fromValue(m_items[row]->m_itemSettings->m_2DMinZoom); + default: + return QVariant(); } } -void MapItem::updateTrack(QList *track) +bool MapModel::setData(const QModelIndex &index, const QVariant& value, int role) { - if (track != nullptr) - { - qDeleteAll(m_takenTrackCoords); - m_takenTrackCoords.clear(); - qDeleteAll(m_takenTrackDateTimes); - m_takenTrackDateTimes.clear(); - m_takenTrack.clear(); - m_takenTrack1.clear(); - m_takenTrack2.clear(); - for (int i = 0; i < track->size(); i++) - { - SWGSDRangel::SWGMapCoordinate* p = track->at(i); - QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); - QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate)); - m_takenTrackCoords.push_back(c); - m_takenTrackDateTimes.push_back(d); - m_takenTrack.push_back(QVariant::fromValue(*c)); - } - } - else - { - // Automatically create a track - if (m_takenTrackCoords.size() == 0) - { - QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); - m_takenTrackCoords.push_back(c); - if (m_positionDateTime.isValid()) { - m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime)); - } else { - m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime())); - } - m_takenTrack.push_back(QVariant::fromValue(*c)); - } - else - { - QGeoCoordinate *prev = m_takenTrackCoords.last(); - QDateTime *prevDateTime = m_takenTrackDateTimes.last(); - if ((prev->latitude() != m_latitude) || (prev->longitude() != m_longitude) - || (prev->altitude() != m_altitude) || (*prevDateTime != m_positionDateTime)) - { - QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); - m_takenTrackCoords.push_back(c); - if (m_positionDateTime.isValid()) { - m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime)); - } else { - m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime())); - } - m_takenTrack.push_back(QVariant::fromValue(*c)); - } - } + (void) value; + (void) role; + + int row = index.row(); + if ((row < 0) || (row >= m_items.count())) { + return false; } + return true; } -void MapItem::updatePredictedTrack(QList *track) -{ - if (track != nullptr) - { - qDeleteAll(m_predictedTrackCoords); - m_predictedTrackCoords.clear(); - qDeleteAll(m_predictedTrackDateTimes); - m_predictedTrackDateTimes.clear(); - m_predictedTrack.clear(); - m_predictedTrack1.clear(); - m_predictedTrack2.clear(); - for (int i = 0; i < track->size(); i++) - { - SWGSDRangel::SWGMapCoordinate* p = track->at(i); - QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); - QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate)); - m_predictedTrackCoords.push_back(c); - m_predictedTrackDateTimes.push_back(d); - m_predictedTrack.push_back(QVariant::fromValue(*c)); - } - } -} - -MapModel::MapModel(MapGUI *gui) : - m_gui(gui), - m_target(-1) -{ - connect(this, &MapModel::dataChanged, this, &MapModel::update3DMap); -} - -Q_INVOKABLE void MapModel::add(MapItem *item) +void MapModel::add(MapItem *item) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); m_items.append(item); - m_selected.append(false); + m_itemsHash.insert(item->m_hashKey, item); endInsertRows(); } @@ -237,7 +98,6 @@ void MapModel::update(const QObject *sourcePipe, SWGSDRangel::SWGMapItem *swgMap { // Update the item item->update(swgMapItem); - splitTracks(item); update(item); } } @@ -248,35 +108,311 @@ void MapModel::update(const QObject *sourcePipe, SWGSDRangel::SWGMapItem *swgMap if (!image.isEmpty()) { // Add new item - item = new MapItem(sourcePipe, group, m_gui->getItemSettings(group), swgMapItem); + item = newMapItem(sourcePipe, group, m_gui->getItemSettings(group), swgMapItem); add(item); // Add to 3D Map (we don't appear to get a dataChanged signal when adding) - CesiumInterface *cesium = m_gui->cesium(); - if (cesium) { - cesium->update(item, isTarget(item), isSelected3D(item)); - } - playAnimations(item); + update3D(item); } } } +void MapModel::update(MapItem *item) +{ + int row = m_items.indexOf(item); + if (row >= 0) + { + QModelIndex idx = index(row); + emit dataChanged(idx, idx); + } +} + +void MapModel::remove(MapItem *item) +{ + int row = m_items.indexOf(item); + if (row >= 0) + { + QString key = m_items[row]->m_hashKey; + beginRemoveRows(QModelIndex(), row, row); + m_items.removeAt(row); + m_itemsHash.remove(key); + endRemoveRows(); + } +} + +void MapModel::removeAll() +{ + if (m_items.count() > 0) + { + beginRemoveRows(QModelIndex(), 0, m_items.count() - 1); + m_items.clear(); + m_itemsHash.clear(); + endRemoveRows(); + } +} + +// After new settings are deserialised, we need to update +// pointers to item settings for all existing items +void MapModel::updateItemSettings(QHash m_itemSettings) +{ + for (auto item : m_items) + { + if (m_itemSettings.contains(item->m_group)) { + item->m_itemSettings = m_itemSettings[item->m_group]; + } + } +} + +void MapModel::allUpdated() +{ + if (m_items.count() > 0) { + emit dataChanged(index(0), index(m_items.count()-1)); + } +} + +MapItem *MapModel::findMapItem(const QObject *source, const QString& name) +{ + QString key = source->objectName() + name; + if (m_itemsHash.contains(key)) { + return m_itemsHash.value(key); + } + return nullptr; +} + +QHash MapModel::roleNames() const +{ + QHash roles; + roles[itemSettingsRole] = "itemSettings"; + roles[nameRole] = "name"; + roles[labelRole] = "label"; + roles[positionRole] = "position"; + roles[mapImageMinZoomRole] = "mapImageMinZoom"; + return roles; +} + // Slot called on dataChanged signal, to update 3D map void MapModel::update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { (void) roles; + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + update3D(m_items[row]); + } +} + +MapItem *ImageMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) +{ + return new ImageMapItem(sourcePipe, group, itemSettings, mapItem); +} + +QVariant ImageMapModel::data(const QModelIndex &index, int role) const +{ + int row = index.row(); + if ((row < 0) || (row >= m_items.count())) { + return QVariant(); + } + ImageMapItem *mapItem = (ImageMapItem*)m_items[row]; + switch (role) + { + case imageRole: + return QVariant::fromValue(mapItem->m_image); + case imageZoomLevelRole: + return QVariant::fromValue(mapItem->m_imageZoomLevel); + case boundsRole: + return QVariant::fromValue(mapItem->m_bounds); + default: + return MapModel::data(index, role); + } +} + +void ImageMapModel::update3D(MapItem *item) +{ + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) + { + ImageMapItem *imageItem = (ImageMapItem *)item; + if (!imageItem->m_image.isEmpty()) + { + /*qDebug() << "ImageMapModel::update3D - " << imageItem->m_name + << imageItem->m_bounds.right() + << imageItem->m_bounds.left() + << imageItem->m_bounds.top() + << imageItem->m_bounds.bottom() + ; */ + cesium->updateImage(imageItem->m_name, + imageItem->m_bounds.topRight().longitude(), + imageItem->m_bounds.bottomLeft().longitude(), + imageItem->m_bounds.topRight().latitude(), + imageItem->m_bounds.bottomLeft().latitude(), + imageItem->m_altitude, + imageItem->m_image); + } + else + { + qDebug() << "ImageMapModel::update3D - removeImage " << imageItem->m_name; + cesium->removeImage(imageItem->m_name); + } + } +} + +MapItem *PolygonMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) +{ + return new PolygonMapItem(sourcePipe, group, itemSettings, mapItem); +} + +QVariant PolygonMapModel::data(const QModelIndex &index, int role) const +{ + int row = index.row(); + if ((row < 0) || (row >= m_items.count())) { + return QVariant(); + } + PolygonMapItem *polygonItem = ((PolygonMapItem *)m_items[row]); + switch (role) + { + case borderColorRole: + return QVariant::fromValue(QColor(0x00, 0x00, 0x00, 0x00)); // Transparent + case fillColorRole: + if (m_items[row]->m_itemSettings->m_display2DTrack) { + return QVariant::fromValue(QColor::fromRgba(m_items[row]->m_itemSettings->m_2DTrackColor)); + } else { + return QVariant::fromValue(QColor(0x00, 0x00, 0x00, 0x00)); // Transparent + } + case polygonRole: + return polygonItem->m_polygon; + case boundsRole: + return QVariant::fromValue(polygonItem->m_bounds); + default: + return MapModel::data(index, role); + } +} + +void PolygonMapModel::update3D(MapItem *item) +{ + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) { + cesium->update((PolygonMapItem *)item); + } +} + +MapItem *PolylineMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) +{ + return new PolylineMapItem(sourcePipe, group, itemSettings, mapItem); +} + +QVariant PolylineMapModel::data(const QModelIndex &index, int role) const +{ + int row = index.row(); + if ((row < 0) || (row >= m_items.count())) { + return QVariant(); + } + PolylineMapItem *polylineItem = (PolylineMapItem *)m_items[row]; + switch (role) + { + case lineColorRole: + return QVariant::fromValue(QColor::fromRgba(m_items[row]->m_itemSettings->m_2DTrackColor)); + case coordinatesRole: + return QVariant::fromValue(polylineItem->m_polyline); + case boundsRole: + return QVariant::fromValue(polylineItem->m_bounds); + default: + return MapModel::data(index, role); + } +} + +void PolylineMapModel::update3D(MapItem *item) +{ + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) { + cesium->update((PolylineMapItem *)item); + } +} + +ObjectMapModel::ObjectMapModel(MapGUI *gui) : + MapModel(gui), + m_target(-1) +{ + //connect(this, &ObjectMapModel::dataChanged, this, &ObjectMapModel::update3DMap); +} + +Q_INVOKABLE void ObjectMapModel::add(MapItem *item) +{ + m_selected.append(false); + MapModel::add(item); +} + +/*void ObjectMapModel::update(const QObject *sourcePipe, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group) +{ + QString name = *swgMapItem->getName(); + // Add, update or delete and item + ObjectMapItem *item = (ObjectMapItem *)MapModel::findMapItem(sourcePipe, name); + if (item != nullptr) + { + QString image = *swgMapItem->getImage(); + if (image.isEmpty()) + { + // Delete the item + remove(item); + // Need to call update, for it to be removed in 3D map + // Item is set to not be available from this point in time + // It will still be available if time is set in the past + item->update(swgMapItem); + } + else + { + // Update the item + item->update(swgMapItem); + splitTracks(item); // ******** diff from base FIXME + update(item); + } + } + else + { + // Make sure not a duplicate request to delete + QString image = *swgMapItem->getImage(); + if (!image.isEmpty()) + { + // Add new item + item = (ObjectMapItem *)newMapItem(sourcePipe, group, m_gui->getItemSettings(group), swgMapItem); + add(item); + // Add to 3D Map (we don't appear to get a dataChanged signal when adding) + update3D(item); + } + } +}*/ + +MapItem *ObjectMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) +{ + return new ObjectMapItem(sourcePipe, group, itemSettings, mapItem); +} + +void ObjectMapModel::update3D(MapItem *item) +{ + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) + { + ObjectMapItem *objectMapItem = (ObjectMapItem *)item; + cesium->update(objectMapItem, isTarget(objectMapItem), isSelected3D(objectMapItem)); + playAnimations(objectMapItem); + } +} + +// Slot called on dataChanged signal, to update 3D map +/*void ObjectMapModel::update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + (void) roles; + CesiumInterface *cesium = m_gui->cesium(); if (cesium) { for (int row = topLeft.row(); row <= bottomRight.row(); row++) { - cesium->update(m_items[row], isTarget(m_items[row]), isSelected3D(m_items[row])); - playAnimations(m_items[row]); + ObjectMapItem *item = (ObjectMapItem *)m_items[row]; + cesium->update(item, isTarget(item), isSelected3D(item)); + playAnimations(item); } } -} +} */ -void MapModel::playAnimations(MapItem *item) +void ObjectMapModel::playAnimations(ObjectMapItem *item) { CesiumInterface *cesium = m_gui->cesium(); if (cesium) @@ -289,67 +425,69 @@ void MapModel::playAnimations(MapItem *item) item->m_animations.clear(); } -void MapModel::update(MapItem *item) +void ObjectMapModel::update(MapItem *item) { + splitTracks((ObjectMapItem *)item); + MapModel::update(item); int row = m_items.indexOf(item); if (row >= 0) { - QModelIndex idx = index(row); - emit dataChanged(idx, idx); if (row == m_target) { updateTarget(); } } } -void MapModel::remove(MapItem *item) +void ObjectMapModel::remove(MapItem *item) { int row = m_items.indexOf(item); if (row >= 0) { - beginRemoveRows(QModelIndex(), row, row); - m_items.removeAt(row); m_selected.removeAt(row); if (row == m_target) { m_target = -1; } else if (row < m_target) { m_target--; } - endRemoveRows(); + MapModel::remove(item); } } -void MapModel::allUpdated() +void ObjectMapModel::removeAll() { - for (int i = 0; i < m_items.count(); i++) - { - // Updates both 2D and 3D Map - QModelIndex idx = index(i); - emit dataChanged(idx, idx); - } + MapModel::removeAll(); + m_selected.clear(); } -void MapModel::removeAll() +QHash ObjectMapModel::roleNames() const { - if (m_items.count() > 0) - { - beginRemoveRows(QModelIndex(), 0, m_items.count()); - m_items.clear(); - m_selected.clear(); - endRemoveRows(); - } + QHash roles = MapModel::roleNames(); + //roles[itemSettingsRole] = "itemSettings"; + //roles[nameRole] = "name"; + //roles[positionRole] = "position"; + //roles[mapTextRole] = "mapText"; + roles[MapModel::labelRole] = "mapText"; + roles[mapTextVisibleRole] = "mapTextVisible"; + roles[mapImageVisibleRole] = "mapImageVisible"; + roles[mapImageRole] = "mapImage"; + roles[mapImageRotationRole] = "mapImageRotation"; + //roles[mapImageMinZoomRole] = "mapImageMinZoom"; + roles[bubbleColourRole] = "bubbleColour"; + roles[selectedRole] = "selected"; + roles[targetRole] = "target"; + roles[frequencyRole] = "frequency"; + roles[frequencyStringRole] = "frequencyString"; + roles[predictedGroundTrack1Role] = "predictedGroundTrack1"; + roles[predictedGroundTrack2Role] = "predictedGroundTrack2"; + roles[groundTrack1Role] = "groundTrack1"; + roles[groundTrack2Role] = "groundTrack2"; + roles[groundTrackColorRole] = "groundTrackColor"; + roles[predictedGroundTrackColorRole] = "predictedGroundTrackColor"; + roles[hasTracksRole] = "hasTracks"; + return roles; } -// After new settings are deserialised, we need to update -// pointers to item settings for all existing items -void MapModel::updateItemSettings(QHash m_itemSettings) -{ - for (auto item : m_items) { - item->m_itemSettings = m_itemSettings[item->m_group]; - } -} - -void MapModel::updateTarget() +void ObjectMapModel::updateTarget() { // Calculate range, azimuth and elevation to object from station AzEl *azEl = m_gui->getAzEl(); @@ -371,21 +509,21 @@ void MapModel::updateTarget() } } -void MapModel::setTarget(const QString& name) +void ObjectMapModel::setTarget(const QString& name) { if (name.isEmpty()) { QModelIndex idx = index(-1); - setData(idx, QVariant(-1), MapModel::targetRole); + setData(idx, QVariant(-1), ObjectMapModel::targetRole); } else { QModelIndex idx = findMapItemIndex(name); - setData(idx, QVariant(idx.row()), MapModel::targetRole); + setData(idx, QVariant(idx.row()), ObjectMapModel::targetRole); } } -bool MapModel::isTarget(const MapItem *mapItem) const +bool ObjectMapModel::isTarget(const ObjectMapItem *mapItem) const { if (m_target < 0) { return false; @@ -396,13 +534,13 @@ bool MapModel::isTarget(const MapItem *mapItem) const // FIXME: This should use Z order - rather than adding/removing // but I couldn't quite get it to work -Q_INVOKABLE void MapModel::moveToFront(int oldRow) +Q_INVOKABLE void ObjectMapModel::moveToFront(int oldRow) { // Last item in list is drawn on top, so remove than add to end of list if (oldRow < m_items.size() - 1) { bool wasTarget = m_target == oldRow; - MapItem *item = m_items[oldRow]; + ObjectMapItem *item = (ObjectMapItem *)m_items[oldRow]; bool wasSelected = m_selected[oldRow]; remove(item); add(item); @@ -416,7 +554,7 @@ Q_INVOKABLE void MapModel::moveToFront(int oldRow) } } -Q_INVOKABLE void MapModel::moveToBack(int oldRow) +Q_INVOKABLE void ObjectMapModel::moveToBack(int oldRow) { // First item in list is drawn first, so remove item then add to front of list if ((oldRow < m_items.size()) && (oldRow > 0)) @@ -440,32 +578,22 @@ Q_INVOKABLE void MapModel::moveToBack(int oldRow) } } -MapItem *MapModel::findMapItem(const QObject *source, const QString& name) -{ - // FIXME: Should consider adding a QHash for this - QListIterator i(m_items); - while (i.hasNext()) - { - MapItem *item = i.next(); - if ((item->m_name == name) && (item->m_sourcePipe == source)) - return item; - } - return nullptr; -} - -MapItem *MapModel::findMapItem(const QString& name) +// FIXME: This should potentially return a list, as we have have multiple items with the same name +// from different sources +ObjectMapItem *ObjectMapModel::findMapItem(const QString& name) { QListIterator i(m_items); while (i.hasNext()) { MapItem *item = i.next(); - if (item->m_name == name) - return item; + if (item->m_name == name) { + return (ObjectMapItem *)item; + } } return nullptr; } -QModelIndex MapModel::findMapItemIndex(const QString& name) +QModelIndex ObjectMapModel::findMapItemIndex(const QString& name) { int idx = 0; QListIterator i(m_items); @@ -480,76 +608,50 @@ QModelIndex MapModel::findMapItemIndex(const QString& name) return index(-1); } -int MapModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent) - return m_items.count(); -} - -QVariant MapModel::data(const QModelIndex &index, int role) const +QVariant ObjectMapModel::data(const QModelIndex &index, int role) const { int row = index.row(); - if ((row < 0) || (row >= m_items.count())) { return QVariant(); } - if (role == MapModel::positionRole) + ObjectMapItem *mapItem = (ObjectMapItem*)m_items[row]; + switch (role) { - // Coordinates to display the item at - QGeoCoordinate coords; - coords.setLatitude(m_items[row]->m_latitude); - coords.setLongitude(m_items[row]->m_longitude); - return QVariant::fromValue(coords); - } - else if (role == MapModel::mapTextRole) + case labelRole: // mapTextRole) { // Create the text to go in the bubble next to the image - QString name = m_items[row]->m_label.isEmpty() ? m_items[row]->m_name : m_items[row]->m_label; + QString name = mapItem->m_label.isEmpty() ? mapItem->m_name :mapItem->m_label; if (row == m_target) { AzEl *azEl = m_gui->getAzEl(); - QString text = QString("%1
Az: %2%5 El: %3%5 Dist: %4 km") - .arg(m_selected[row] ? m_items[row]->m_text : name) + QString text = QString("%1
Az: %2%5 El: %3%5 Dist: %4 km
Coords: %6, %7") + .arg(m_selected[row] ? mapItem->m_text : name) .arg(std::round(azEl->getAzimuth())) .arg(std::round(azEl->getElevation())) .arg(std::round(azEl->getDistance() / 1000.0)) - .arg(QChar(0xb0)); + .arg(QChar(0xb0)) + .arg(mapItem->m_latitude) + .arg(mapItem->m_longitude); return QVariant::fromValue(text); } else if (m_selected[row]) { - return QVariant::fromValue(m_items[row]->m_text); + return QVariant::fromValue(mapItem->m_text); } else { return QVariant::fromValue(name); } } - else if (role == MapModel::mapTextVisibleRole) - { - return QVariant::fromValue((m_selected[row] || m_displayNames) && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DLabel); - } - else if (role == MapModel::mapImageVisibleRole) - { - return QVariant::fromValue(m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DIcon); - } - else if (role == MapModel::mapImageRole) - { - // Set an image to use - return QVariant::fromValue(m_items[row]->m_image); - } - else if (role == MapModel::mapImageRotationRole) - { - // Angle to rotate image by - return QVariant::fromValue(m_items[row]->m_imageRotation); - } - else if (role == MapModel::mapImageMinZoomRole) - { - // Minimum zoom level - //return QVariant::fromValue(m_items[row]->m_imageMinZoom); - return QVariant::fromValue(m_items[row]->m_itemSettings->m_2DMinZoom); - } - else if (role == MapModel::bubbleColourRole) + case mapTextVisibleRole: + return QVariant::fromValue((m_selected[row] || m_displayNames) && mapItem->m_itemSettings->m_display2DLabel); + case mapImageVisibleRole: + return QVariant::fromValue(mapItem->m_itemSettings->m_display2DIcon); + case mapImageRole: + return QVariant::fromValue(mapItem->m_image); // Set an image to use + case mapImageRotationRole: + return QVariant::fromValue(mapItem->m_imageRotation); // Angle to rotate image by + case bubbleColourRole: { // Select a background colour for the text bubble next to the item if (m_selected[row]) { @@ -558,81 +660,81 @@ QVariant MapModel::data(const QModelIndex &index, int role) const return QVariant::fromValue(QColor("lightblue")); } } - else if (role == MapModel::selectedRole) - { + case selectedRole: return QVariant::fromValue(m_selected[row]); - } - else if (role == MapModel::targetRole) - { + case targetRole: return QVariant::fromValue(m_target == row); - } - else if (role == MapModel::frequencyRole) - { - return QVariant::fromValue(m_items[row]->m_frequency); - } - else if (role == MapModel::frequencyStringRole) - { - return QVariant::fromValue(m_items[row]->m_frequencyString); - } - else if (role == MapModel::predictedGroundTrack1Role) + case frequencyRole: + return QVariant::fromValue(mapItem->m_frequency); + case frequencyStringRole: + return QVariant::fromValue(mapItem->m_frequencyString); + case predictedGroundTrack1Role: { if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) - && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { - return m_items[row]->m_predictedTrack1; + && mapItem->m_itemSettings->m_display2DTrack) { + return mapItem->m_predictedTrack1; } else { return QVariantList(); } } - else if (role == MapModel::predictedGroundTrack2Role) + case predictedGroundTrack2Role: { if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) - && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { - return m_items[row]->m_predictedTrack2; + && mapItem->m_itemSettings->m_display2DTrack) { + return mapItem->m_predictedTrack2; } else { return QVariantList(); } } - else if (role == MapModel::groundTrack1Role) + case groundTrack1Role: { if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) - && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { - return m_items[row]->m_takenTrack1; + && mapItem->m_itemSettings->m_display2DTrack) { + return mapItem->m_takenTrack1; } else { return QVariantList(); } } - else if (role == MapModel::groundTrack2Role) + case groundTrack2Role: { if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) - && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { - return m_items[row]->m_takenTrack2; + && mapItem->m_itemSettings->m_display2DTrack) { + return mapItem->m_takenTrack2; } else { return QVariantList(); } } - else if (role == groundTrackColorRole) + case groundTrackColorRole: + return QVariant::fromValue(QColor::fromRgb(mapItem->m_itemSettings->m_2DTrackColor)); + case predictedGroundTrackColorRole: + return QVariant::fromValue(QColor::fromRgb(mapItem->m_itemSettings->m_2DTrackColor).lighter()); + case hasTracksRole: { - return QVariant::fromValue(QColor::fromRgb(m_items[row]->m_itemSettings->m_2DTrackColor)); + bool hasTracks = (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) + && ( (mapItem->m_predictedTrack1.size() > 1) + || (mapItem->m_predictedTrack2.size() > 1) + || (mapItem->m_takenTrack1.size() > 1) + || (mapItem->m_takenTrack2.size() > 1) + ); + return QVariant::fromValue(hasTracks); } - else if (role == predictedGroundTrackColorRole) - { - return QVariant::fromValue(QColor::fromRgb(m_items[row]->m_itemSettings->m_2DTrackColor).lighter()); + default: + return MapModel::data(index, role); } - return QVariant(); } -bool MapModel::setData(const QModelIndex &idx, const QVariant& value, int role) +bool ObjectMapModel::setData(const QModelIndex &idx, const QVariant& value, int role) { int row = idx.row(); if ((row < 0) || (row >= m_items.count())) return false; - if (role == MapModel::selectedRole) + if (role == selectedRole) { m_selected[row] = value.toBool(); emit dataChanged(idx, idx); return true; } - else if (role == MapModel::targetRole) + else if (role == targetRole) { if (m_target >= 0) { @@ -649,37 +751,31 @@ bool MapModel::setData(const QModelIndex &idx, const QVariant& value, int role) return true; } -Qt::ItemFlags MapModel::flags(const QModelIndex &index) const -{ - (void) index; - return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; -} - -void MapModel::setDisplayNames(bool displayNames) +void ObjectMapModel::setDisplayNames(bool displayNames) { m_displayNames = displayNames; allUpdated(); } -void MapModel::setDisplaySelectedGroundTracks(bool displayGroundTracks) +void ObjectMapModel::setDisplaySelectedGroundTracks(bool displayGroundTracks) { m_displaySelectedGroundTracks = displayGroundTracks; allUpdated(); } -void MapModel::setDisplayAllGroundTracks(bool displayGroundTracks) +void ObjectMapModel::setDisplayAllGroundTracks(bool displayGroundTracks) { m_displayAllGroundTracks = displayGroundTracks; allUpdated(); } -void MapModel::setFrequency(double frequency) +void ObjectMapModel::setFrequency(double frequency) { // Set as centre frequency ChannelWebAPIUtils::setCenterFrequency(0, frequency); } -void MapModel::track3D(int index) +void ObjectMapModel::track3D(int index) { if (index < m_items.count()) { @@ -688,17 +784,21 @@ void MapModel::track3D(int index) } } -void MapModel::splitTracks(MapItem *item) +void ObjectMapModel::splitTracks(ObjectMapItem *item) { if (item->m_takenTrackCoords.size() > 1) + { splitTrack(item->m_takenTrackCoords, item->m_takenTrack, item->m_takenTrack1, item->m_takenTrack2, item->m_takenStart1, item->m_takenStart2, item->m_takenEnd1, item->m_takenEnd2); + } if (item->m_predictedTrackCoords.size() > 1) + { splitTrack(item->m_predictedTrackCoords, item->m_predictedTrack, item->m_predictedTrack1, item->m_predictedTrack2, item->m_predictedStart1, item->m_predictedStart2, item->m_predictedEnd1, item->m_predictedEnd2); + } } -void MapModel::interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen) +void ObjectMapModel::interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen) { double x1 = c1->longitude(); double y1 = c1->latitude(); @@ -721,7 +821,7 @@ void MapModel::interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, ci->setAltitude(c1->altitude()); } -void MapModel::interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen) +void ObjectMapModel::interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen) { double x1 = c1->longitude(); double y1 = c1->latitude(); @@ -827,7 +927,7 @@ static bool crossesEdge(double lon, double prevLon, double bottomLeftLongitude, } } -void MapModel::interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen) +void ObjectMapModel::interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen) { double x1 = c1->longitude(); double x2 = c2->longitude(); @@ -849,7 +949,7 @@ void MapModel::interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottom } } -void MapModel::splitTrack(const QList& coords, const QVariantList& track, +void ObjectMapModel::splitTrack(const QList& coords, const QVariantList& track, QVariantList& track1, QVariantList& track2, QGeoCoordinate& start1, QGeoCoordinate& start2, QGeoCoordinate& end1, QGeoCoordinate& end2) @@ -968,29 +1068,297 @@ void MapModel::splitTrack(const QList& coords, const QVariantL */ } -void MapModel::viewChanged(double bottomLeftLongitude, double bottomRightLongitude) +void ObjectMapModel::viewChanged(double bottomLeftLongitude, double bottomRightLongitude) { (void) bottomRightLongitude; + if (!std::isnan(bottomLeftLongitude)) { for (int row = 0; row < m_items.size(); row++) { - MapItem *item = m_items[row]; - if (item->m_takenTrackCoords.size() > 1) + ObjectMapItem *item = (ObjectMapItem *)m_items[row]; + if (m_items[row]->m_itemSettings->m_enabled) { - splitTrack(item->m_takenTrackCoords, item->m_takenTrack, item->m_takenTrack1, item->m_takenTrack2, - item->m_takenStart1, item->m_takenStart2, item->m_takenEnd1, item->m_takenEnd2); - QModelIndex idx = index(row); - emit dataChanged(idx, idx); - } - if (item->m_predictedTrackCoords.size() > 1) - { - splitTrack(item->m_predictedTrackCoords, item->m_predictedTrack, item->m_predictedTrack1, item->m_predictedTrack2, - item->m_predictedStart1, item->m_predictedStart2, item->m_predictedEnd1, item->m_predictedEnd2); - QModelIndex idx = index(row); - emit dataChanged(idx, idx); + if (item->m_takenTrackCoords.size() > 1) + { + splitTrack(item->m_takenTrackCoords, item->m_takenTrack, item->m_takenTrack1, item->m_takenTrack2, + item->m_takenStart1, item->m_takenStart2, item->m_takenEnd1, item->m_takenEnd2); + QModelIndex idx = index(row); + QVector roles = {groundTrack1Role, groundTrack2Role}; + emit dataChanged(idx, idx, roles); + } + if (item->m_predictedTrackCoords.size() > 1) + { + splitTrack(item->m_predictedTrackCoords, item->m_predictedTrack, item->m_predictedTrack1, item->m_predictedTrack2, + item->m_predictedStart1, item->m_predictedStart2, item->m_predictedEnd1, item->m_predictedEnd2); + QModelIndex idx = index(row); + QVector roles = {predictedGroundTrack1Role, predictedGroundTrack2Role}; + emit dataChanged(idx, idx, roles); + } } } } } +bool ObjectMapFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent); + + MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, ObjectMapModel::itemSettingsRole).value(); + if (!itemSettings->m_enabled) { + return false; + } + + // Don't show tiny items when we're zoomed out + int minZoom = sourceModel()->data(index0, ObjectMapModel::mapImageMinZoomRole).toInt(); + if (minZoom - 3 >= m_zoomLevel) { + return false; + } + + // Don't show off-screen items, unless they have tracks, as these may be on screen + // In the future, we could calculate a bounding box for tracks, if helpful for performance + QGeoCoordinate coord = sourceModel()->data(index0, ObjectMapModel::positionRole).value(); + float lat = coord.latitude(); + float lon = coord.longitude(); + bool onScreen = (lat >= m_bottomRightLatitude) && (lat <= m_topLeftLatitude) && (lon >= m_topLeftLongitude) && (lon <= m_bottomRightLongitude); + if (!onScreen) + { + bool hasTracks = sourceModel()->data(index0, ObjectMapModel::hasTracksRole).toBool(); + if (!hasTracks) { + return false; + } + } + + // Apply user filter + if (!itemSettings->m_filterName.isEmpty()) + { + QString name = sourceModel()->data(index0, ObjectMapModel::nameRole).toString(); + QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name); + if (!match.hasMatch()) { + return false; + } + } + if (itemSettings->m_filterDistance > 0) + { + QGeoCoordinate position = sourceModel()->data(index0, ObjectMapModel::positionRole).value(); + if (m_position.distanceTo(position) > itemSettings->m_filterDistance) { + return false; + } + } + + return true; +} + + +void ObjectMapFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel) +{ + m_zoomLevel = zoomLevel; + if (!std::isnan(topLeftLongitude)) + { + m_topLeftLongitude = topLeftLongitude; + m_topLeftLatitude = topLeftLatitude; + m_bottomRightLongitude = bottomRightLongitude; + m_bottomRightLatitude = bottomRightLatitude; + } + invalidateFilter(); +} + +Q_INVOKABLE int ObjectMapFilter::mapRowToSource(int row) +{ + QModelIndex sourceIndex = mapToSource(index(row, 0)); + return sourceIndex.row(); +} + + +bool PolygonFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent); + + MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, PolygonMapModel::itemSettingsRole).value(); + if (!itemSettings->m_enabled) { + return false; + } + + // Don't show tiny items when we're zoomed out + int minZoom = sourceModel()->data(index0, PolygonMapModel::mapImageMinZoomRole).toInt(); + if (minZoom - 3 >= m_zoomLevel) { + return false; + } + + // Filter off-screen items + QGeoRectangle bounds = sourceModel()->data(index0, PolygonMapModel::boundsRole).value(); + if (!m_view.intersects(bounds)) { + return false; + } + + // Apply user filter + if (!itemSettings->m_filterName.isEmpty()) + { + QString name = sourceModel()->data(index0, PolygonMapModel::nameRole).toString(); + QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name); + if (!match.hasMatch()) { + return false; + } + } + if (itemSettings->m_filterDistance > 0) + { + QGeoCoordinate position = sourceModel()->data(index0, PolygonMapModel::positionRole).value(); + if (m_position.distanceTo(position) > itemSettings->m_filterDistance) { + return false; + } + } + + return true; +} + +void PolygonFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel) +{ + m_zoomLevel = zoomLevel; + if (!std::isnan(topLeftLongitude)) { + m_view = QGeoRectangle(QGeoCoordinate(topLeftLatitude, topLeftLongitude), QGeoCoordinate(bottomRightLatitude, bottomRightLongitude)); + } + invalidateFilter(); +} + +Q_INVOKABLE int PolygonFilter::mapRowToSource(int row) +{ + QModelIndex sourceIndex = mapToSource(index(row, 0)); + return sourceIndex.row(); +} + +bool PolylineFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent); + + MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, PolylineMapModel::itemSettingsRole).value(); + if (!itemSettings->m_enabled) { + return false; + } + + // Don't show tiny items when we're zoomed out + int minZoom = sourceModel()->data(index0, PolylineMapModel::mapImageMinZoomRole).toInt(); + if (minZoom - 3 >= m_zoomLevel) { + return false; + } + + // Filter off-screen items + QGeoRectangle bounds = sourceModel()->data(index0, PolylineMapModel::boundsRole).value(); + if (!m_view.intersects(bounds)) { + return false; + } + + // Apply user filter + if (!itemSettings->m_filterName.isEmpty()) + { + QString name = sourceModel()->data(index0, PolylineMapModel::nameRole).toString(); + QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name); + if (!match.hasMatch()) { + return false; + } + } + if (itemSettings->m_filterDistance > 0) + { + QGeoCoordinate position = sourceModel()->data(index0, PolylineMapModel::positionRole).value(); + if (m_position.distanceTo(position) > itemSettings->m_filterDistance) { + return false; + } + } + + return true; +} + +void PolylineFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel) +{ + m_zoomLevel = zoomLevel; + if (!std::isnan(topLeftLongitude)) { + m_view = QGeoRectangle(QGeoCoordinate(topLeftLatitude, topLeftLongitude), QGeoCoordinate(bottomRightLatitude, bottomRightLongitude)); + } + invalidateFilter(); +} + +Q_INVOKABLE int PolylineFilter::mapRowToSource(int row) +{ + QModelIndex sourceIndex = mapToSource(index(row, 0)); + return sourceIndex.row(); +} + +bool ImageFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent); + + MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, ImageMapModel::itemSettingsRole).value(); + if (!itemSettings->m_enabled || !itemSettings->m_display2DIcon) { + return false; + } + + // Don't show tiny items when we're zoomed out + int minZoom = sourceModel()->data(index0, ImageMapModel::mapImageMinZoomRole).toInt(); + if (minZoom - 3 >= m_zoomLevel) { + return false; + } + + // Filter off-screen items + QGeoRectangle bounds = sourceModel()->data(index0, ImageMapModel::boundsRole).value(); + if (!m_view.intersects(bounds)) { + return false; + } + + // Apply user filter + if (!itemSettings->m_filterName.isEmpty()) + { + QString name = sourceModel()->data(index0, ImageMapModel::nameRole).toString(); + QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name); + if (!match.hasMatch()) { + return false; + } + } + if (itemSettings->m_filterDistance > 0) + { + QGeoCoordinate position = sourceModel()->data(index0, ImageMapModel::positionRole).value(); + if (m_position.distanceTo(position) > itemSettings->m_filterDistance) { + return false; + } + } + + return true; +} + +void ImageFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel) +{ + m_zoomLevel = zoomLevel; + if (!std::isnan(topLeftLongitude)) { + m_view = QGeoRectangle(QGeoCoordinate(topLeftLatitude, topLeftLongitude), QGeoCoordinate(bottomRightLatitude, bottomRightLongitude)); + } + invalidateFilter(); +} + +Q_INVOKABLE int ImageFilter::mapRowToSource(int row) +{ + QModelIndex sourceIndex = mapToSource(index(row, 0)); + return sourceIndex.row(); +} + +void PolygonFilter::setPosition(const QGeoCoordinate& position) +{ + m_position = position; + invalidateFilter(); +} + +void PolylineFilter::setPosition(const QGeoCoordinate& position) +{ + m_position = position; + invalidateFilter(); +} + +void ObjectMapFilter::setPosition(const QGeoCoordinate& position) +{ + m_position = position; + invalidateFilter(); +} + +void ImageFilter::setPosition(const QGeoCoordinate& position) +{ + m_position = position; + invalidateFilter(); +} + diff --git a/plugins/feature/map/mapmodel.h b/plugins/feature/map/mapmodel.h index 69bb36480..3db617eb4 100644 --- a/plugins/feature/map/mapmodel.h +++ b/plugins/feature/map/mapmodel.h @@ -19,135 +19,227 @@ #define INCLUDE_FEATURE_MAPMODEL_H_ #include +#include #include +#include #include #include "util/azel.h" +#include "util/openaip.h" #include "mapsettings.h" +#include "mapitem.h" #include "cesiuminterface.h" #include "SWGMapItem.h" -class MapModel; class MapGUI; class CZML; -// Information required about each item displayed on the map -class MapItem { - -public: - MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem); - void update(SWGSDRangel::SWGMapItem *mapItem); - QGeoCoordinate getCoordinates(); - -private: - void findFrequency(); - void updateTrack(QList *track); - void updatePredictedTrack(QList *track); - - friend MapModel; - friend CZML; - QString m_group; - MapSettings::MapItemSettings *m_itemSettings; - const QObject *m_sourcePipe; // Channel/feature that created the item - QString m_name; - QString m_label; - float m_latitude; - float m_longitude; - float m_altitude; // In metres - QDateTime m_positionDateTime; - bool m_useHeadingPitchRoll; - float m_heading; - float m_pitch; - float m_roll; - QDateTime m_orientationDateTime; - QString m_image; - int m_imageRotation; - QString m_text; - double m_frequency; // Frequency to set - QString m_frequencyString; - QList m_predictedTrackCoords; - QList m_predictedTrackDateTimes; - QVariantList m_predictedTrack; // Line showing where the object is going - QVariantList m_predictedTrack1; - QVariantList m_predictedTrack2; - QGeoCoordinate m_predictedStart1; - QGeoCoordinate m_predictedStart2; - QGeoCoordinate m_predictedEnd1; - QGeoCoordinate m_predictedEnd2; - QList m_takenTrackCoords; - QList m_takenTrackDateTimes; - QVariantList m_takenTrack; // Line showing where the object has been - QVariantList m_takenTrack1; - QVariantList m_takenTrack2; - QGeoCoordinate m_takenStart1; - QGeoCoordinate m_takenStart2; - QGeoCoordinate m_takenEnd1; - QGeoCoordinate m_takenEnd2; - - // For 3D map - QString m_model; - int m_altitudeReference; - float m_labelAltitudeOffset; - float m_modelAltitudeOffset; - bool m_fixedPosition; - QList m_animations; -}; - -// Model used for each item on the map class MapModel : public QAbstractListModel { Q_OBJECT public: - using QAbstractListModel::QAbstractListModel; enum MarkerRoles { - positionRole = Qt::UserRole + 1, - mapTextRole = Qt::UserRole + 2, - mapTextVisibleRole = Qt::UserRole + 3, - mapImageVisibleRole = Qt::UserRole + 4, - mapImageRole = Qt::UserRole + 5, - mapImageRotationRole = Qt::UserRole + 6, - mapImageMinZoomRole = Qt::UserRole + 7, - bubbleColourRole = Qt::UserRole + 8, - selectedRole = Qt::UserRole + 9, - targetRole = Qt::UserRole + 10, - frequencyRole = Qt::UserRole + 11, - frequencyStringRole = Qt::UserRole + 12, - predictedGroundTrack1Role = Qt::UserRole + 13, - predictedGroundTrack2Role = Qt::UserRole + 14, - groundTrack1Role = Qt::UserRole + 15, - groundTrack2Role = Qt::UserRole + 16, - groundTrackColorRole = Qt::UserRole + 17, - predictedGroundTrackColorRole = Qt::UserRole + 18 + itemSettingsRole = Qt::UserRole + 1, + nameRole = Qt::UserRole + 2, + labelRole = Qt::UserRole + 3, + positionRole = Qt::UserRole + 4, + mapImageMinZoomRole = Qt::UserRole + 5, + lastRole = Qt::UserRole + 6 }; - MapModel(MapGUI *gui); + MapModel(MapGUI *gui) : + m_gui(gui) + { + connect(this, &MapModel::dataChanged, this, &MapModel::update3DMap); + } - void playAnimations(MapItem *item); - - Q_INVOKABLE void add(MapItem *item); + virtual void add(MapItem *item); + virtual void remove(MapItem *item); + virtual void removeAll(); void update(const QObject *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group=""); - void update(MapItem *item); - void remove(MapItem *item); - void allUpdated(); - void removeAll(); void updateItemSettings(QHash m_itemSettings); + void allUpdated(); + + MapItem *findMapItem(const QObject *source, const QString& name); + + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + (void) parent; + return m_items.count(); + } + + Qt::ItemFlags flags(const QModelIndex &index) const override + { + (void) index; + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; + } + +public slots: + void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()); + +protected: + + MapGUI *m_gui; + QList m_items; + QHash m_itemsHash; + + virtual void update(MapItem *item); + virtual void update3D(MapItem *item) = 0; + virtual MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) = 0; + +}; + +class ImageMapModel : public MapModel { + Q_OBJECT + +public: + enum MarkerRoles { + imageRole = MapModel::lastRole + 0, + imageZoomLevelRole = MapModel::lastRole + 1, + boundsRole = MapModel::lastRole + 2 + }; + + ImageMapModel(MapGUI *gui) : + MapModel(gui) + {} + + QHash roleNames() const override + { + QHash roles = MapModel::roleNames(); + roles[imageRole] = "imageData"; + roles[imageZoomLevelRole] = "imageZoomLevel"; + roles[boundsRole] = "bounds"; + return roles; + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +protected: + MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override; + void update3D(MapItem *item) override; + +}; + +class PolygonMapModel : public MapModel { + Q_OBJECT + +public: + enum MarkerRoles { + borderColorRole = MapModel::lastRole + 0, + fillColorRole = MapModel::lastRole + 1, + polygonRole = MapModel::lastRole + 2, + boundsRole = MapModel::lastRole + 3 + }; + + PolygonMapModel(MapGUI *gui) : + MapModel(gui) + {} + + QHash roleNames() const override + { + QHash roles = MapModel::roleNames(); + roles[borderColorRole] = "borderColor"; + roles[fillColorRole] = "fillColor"; + roles[polygonRole] = "polygon"; + roles[boundsRole] = "bounds"; + return roles; + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +protected: + void update3D(MapItem *item) override; + MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override; + +}; + +class PolylineMapModel : public MapModel { + Q_OBJECT + +public: + + enum MarkerRoles { + lineColorRole = MapModel::lastRole + 0, + coordinatesRole = MapModel::lastRole + 1, + boundsRole = MapModel::lastRole + 2, + }; + + PolylineMapModel(MapGUI *gui) : + MapModel(gui) + {} + + QHash roleNames() const override + { + QHash roles = MapModel::roleNames(); + roles[lineColorRole] = "lineColor"; + roles[coordinatesRole] = "coordinates"; + roles[boundsRole] = "bounds"; + return roles; + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +protected: + void update3D(MapItem *item) override; + MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override; +}; + +// Model used for each item on the map +class ObjectMapModel : public MapModel { + Q_OBJECT + +public: + + enum MarkerRoles { + //itemSettingsRole = Qt::UserRole + 1, + //nameRole = Qt::UserRole + 2, + //positionRole = Qt::UserRole + 3, + // label? mapTextRole = Qt::UserRole + 4, + mapTextVisibleRole =MapModel::lastRole + 0, + mapImageVisibleRole = MapModel::lastRole + 1, + mapImageRole = MapModel::lastRole + 2, + mapImageRotationRole = MapModel::lastRole + 3, + //mapImageMinZoomRole = MapModel::lastRole + 9, + bubbleColourRole = MapModel::lastRole + 4, + selectedRole = MapModel::lastRole + 5, + targetRole = MapModel::lastRole + 6, + frequencyRole = MapModel::lastRole + 7, + frequencyStringRole = MapModel::lastRole + 8, + predictedGroundTrack1Role = MapModel::lastRole + 9, + predictedGroundTrack2Role = MapModel::lastRole + 10, + groundTrack1Role = MapModel::lastRole + 11, + groundTrack2Role = MapModel::lastRole + 12, + groundTrackColorRole = MapModel::lastRole + 13, + predictedGroundTrackColorRole = MapModel::lastRole + 14, + hasTracksRole = MapModel::lastRole + 15 + }; + + ObjectMapModel(MapGUI *gui); + + Q_INVOKABLE void add(MapItem *item) override; + //void update(const QObject *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group=""); + using MapModel::update; + void remove(MapItem *item) override; + void removeAll() override; + QHash roleNames() const override; void updateTarget(); void setTarget(const QString& name); - bool isTarget(const MapItem *mapItem) const; + bool isTarget(const ObjectMapItem *mapItem) const; Q_INVOKABLE void moveToFront(int oldRow); Q_INVOKABLE void moveToBack(int oldRow); - MapItem *findMapItem(const QObject *source, const QString& name); - MapItem *findMapItem(const QString& name); + ObjectMapItem *findMapItem(const QString& name); QModelIndex findMapItemIndex(const QString& name); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override; - Qt::ItemFlags flags(const QModelIndex &index) const override; void setDisplayNames(bool displayNames); void setDisplaySelectedGroundTracks(bool displayGroundTracks); @@ -155,48 +247,9 @@ public: Q_INVOKABLE void setFrequency(double frequency); Q_INVOKABLE void track3D(int index); - void interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); - void interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); - void interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen); - - void splitTracks(MapItem *item); - void splitTrack(const QList& coords, const QVariantList& track, - QVariantList& track1, QVariantList& track2, - QGeoCoordinate& start1, QGeoCoordinate& start2, - QGeoCoordinate& end1, QGeoCoordinate& end2); Q_INVOKABLE void viewChanged(double bottomLeftLongitude, double bottomRightLongitude); - QHash roleNames() const - { - QHash roles; - roles[positionRole] = "position"; - roles[mapTextRole] = "mapText"; - roles[mapTextVisibleRole] = "mapTextVisible"; - roles[mapImageVisibleRole] = "mapImageVisible"; - roles[mapImageRole] = "mapImage"; - roles[mapImageRotationRole] = "mapImageRotation"; - roles[mapImageMinZoomRole] = "mapImageMinZoom"; - roles[bubbleColourRole] = "bubbleColour"; - roles[selectedRole] = "selected"; - roles[targetRole] = "target"; - roles[frequencyRole] = "frequency"; - roles[frequencyStringRole] = "frequencyString"; - roles[predictedGroundTrack1Role] = "predictedGroundTrack1"; - roles[predictedGroundTrack2Role] = "predictedGroundTrack2"; - roles[groundTrack1Role] = "groundTrack1"; - roles[groundTrack2Role] = "groundTrack2"; - roles[groundTrackColorRole] = "groundTrackColor"; - roles[predictedGroundTrackColorRole] = "predictedGroundTrackColor"; - return roles; - } - - // Linear interpolation - double interpolate(double x0, double y0, double x1, double y1, double x) - { - return (y0*(x1-x) + y1*(x-x0)) / (x1-x0); - } - - bool isSelected3D(const MapItem *item) const + bool isSelected3D(const ObjectMapItem *item) const { return m_selected3D == item->m_name; } @@ -206,13 +259,24 @@ public: m_selected3D = selected; } -public slots: - void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()); +//public slots: +// void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override; + +protected: + void playAnimations(ObjectMapItem *item); + MapItem *newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) override; + void update(MapItem *item) override; + void update3D(MapItem *item) override; + + // Linear interpolation + double interpolate(double x0, double y0, double x1, double y1, double x) const + { + return (y0*(x1-x) + y1*(x-x0)) / (x1-x0); + } private: - MapGUI *m_gui; - QList m_items; - QList m_selected; + QList m_selected; // Whether each item on 2D map is selected + QString m_selected3D; // Name of item selected on 3D map - only supports 1 item, unlike 2D map int m_target; // Row number of current target, or -1 for none bool m_displayNames; bool m_displaySelectedGroundTracks; @@ -221,8 +285,112 @@ private: double m_bottomLeftLongitude; double m_bottomRightLongitude; - QString m_selected3D; // Name of item selected on 3D map - only supports 1 item, unlike 2D map + void interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); + void interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); + void interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen); + + void splitTracks(ObjectMapItem *item); + void splitTrack(const QList& coords, const QVariantList& track, + QVariantList& track1, QVariantList& track2, + QGeoCoordinate& start1, QGeoCoordinate& start2, + QGeoCoordinate& end1, QGeoCoordinate& end2); }; +class PolygonFilter : public QSortFilterProxyModel { + Q_OBJECT +public: + PolygonFilter() : + m_zoomLevel(10), + m_settings(nullptr) + { + } + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel); + Q_INVOKABLE int mapRowToSource(int row); + void setPosition(const QGeoCoordinate& position); + +private: + QGeoRectangle m_view; + double m_zoomLevel; + MapSettings *m_settings; + QGeoCoordinate m_position;; + +}; + +class PolylineFilter : public QSortFilterProxyModel { + Q_OBJECT +public: + PolylineFilter() : + m_zoomLevel(10), + m_settings(nullptr) + { + } + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel); + Q_INVOKABLE int mapRowToSource(int row); + void setPosition(const QGeoCoordinate& position); + +private: + QGeoRectangle m_view; + double m_zoomLevel; + MapSettings *m_settings; + QGeoCoordinate m_position;; + +}; + +class ObjectMapFilter : public QSortFilterProxyModel { + Q_OBJECT +public: + ObjectMapFilter() : + m_topLeftLongitude(0), + m_topLeftLatitude(0), + m_bottomRightLongitude(0), + m_bottomRightLatitude(0), + m_zoomLevel(10), + m_settings(nullptr) + { + } + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel); + void applySettings(MapSettings *settings); + Q_INVOKABLE int mapRowToSource(int row); + void setPosition(const QGeoCoordinate& position); + +private: + double m_topLeftLongitude; + double m_topLeftLatitude; + double m_bottomRightLongitude; + double m_bottomRightLatitude; + double m_zoomLevel; + MapSettings *m_settings; + QGeoCoordinate m_position; + +}; + +class ImageFilter : public QSortFilterProxyModel { + Q_OBJECT +public: + ImageFilter() : + m_zoomLevel(10), + m_settings(nullptr) + { + } + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + Q_INVOKABLE void viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel); + Q_INVOKABLE int mapRowToSource(int row); + void setPosition(const QGeoCoordinate& position); + +private: + QGeoRectangle m_view; + double m_zoomLevel; + MapSettings *m_settings; + QGeoCoordinate m_position;; + +}; #endif // INCLUDE_FEATURE_MAPMODEL_H_ + diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index ac3c883ab..0a3930c0e 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -26,25 +26,31 @@ #include "mapsettings.h" const QStringList MapSettings::m_pipeTypes = { + QStringLiteral("ACARSDemod"), QStringLiteral("ADSBDemod"), QStringLiteral("AIS"), QStringLiteral("APRS"), QStringLiteral("APTDemod"), QStringLiteral("FT8Demod"), + QStringLiteral("HeatMap"), QStringLiteral("Radiosonde"), QStringLiteral("StarTracker"), - QStringLiteral("SatelliteTracker") + QStringLiteral("SatelliteTracker"), + QStringLiteral("VORLocalizer") }; const QStringList MapSettings::m_pipeURIs = { + QStringLiteral("sdrangel.channel.acarsdemod"), QStringLiteral("sdrangel.channel.adsbdemod"), QStringLiteral("sdrangel.feature.ais"), QStringLiteral("sdrangel.feature.aprs"), QStringLiteral("sdrangel.channel.aptdemod"), QStringLiteral("sdrangel.channel.ft8demod"), + QStringLiteral("sdrangel.channel.heatmap"), QStringLiteral("sdrangel.feature.radiosonde"), QStringLiteral("sdrangel.feature.startracker"), - QStringLiteral("sdrangel.feature.satellitetracker") + QStringLiteral("sdrangel.feature.satellitetracker"), + QStringLiteral("sdrangel.feature.vorlocalizer") }; // GUI combo box should match ordering in this list @@ -62,24 +68,70 @@ MapSettings::MapSettings() : // Source names should match m_pipeTypes // Colors currently match color of rollup widget for that plugin int modelMinPixelSize = 50; - m_itemSettings.insert("ADSBDemod", new MapItemSettings("ADSBDemod", QColor(244, 151, 57), false, 11, modelMinPixelSize)); - m_itemSettings.insert("AIS", new MapItemSettings("AIS", QColor(102, 0, 0), false, 11, modelMinPixelSize)); - m_itemSettings.insert("APRS", new MapItemSettings("APRS", QColor(255, 255, 0), false, 11)); - m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", QColor(230, 230, 230), true, 3)); - m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", QColor(0, 0, 255), false, 0, modelMinPixelSize)); - m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", QColor(255, 0, 0), true, 8)); - m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", QColor(102, 0, 102), false, 11, modelMinPixelSize)); - m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", QColor(255, 0, 0), true, 8)); - m_itemSettings.insert("Radar", new MapItemSettings("Radar", QColor(255, 0, 0), true, 8)); - m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", QColor(0, 192, 255), true, 8)); + MapItemSettings *aptSettings = new MapItemSettings("APTDemod", true, QColor(216, 112, 169), true, false, 11); + aptSettings->m_display2DIcon = false; // Disabled as 2D projection is wrong + m_itemSettings.insert("APTDemod", aptSettings); + m_itemSettings.insert("ACARSDemod", new MapItemSettings("ACARSDemod", true, QColor(244, 151, 57), true, false, 11, modelMinPixelSize)); + m_itemSettings.insert("ADSBDemod", new MapItemSettings("ADSBDemod", true, QColor(244, 151, 57), true, false, 11, modelMinPixelSize)); + m_itemSettings.insert("AIS", new MapItemSettings("AIS", true, QColor(102, 0, 0), true, false, 11, modelMinPixelSize)); + MapItemSettings *aprsSettings = new MapItemSettings("APRS", true, QColor(255, 255, 0), true, false, 11); + aprsSettings->m_extrapolate = 0; + m_itemSettings.insert("APRS", aprsSettings); + m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", true, QColor(230, 230, 230), true, true, 3)); + m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", true, QColor(0, 0, 255), true, false, 0, modelMinPixelSize)); + m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", true, QColor(255, 0, 0), false, true, 8)); + m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", true, QColor(102, 0, 102), true, false, 11, modelMinPixelSize)); + m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", true, QColor(255, 0, 0), false, true, 8)); + m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8)); + m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", true, QColor(0, 192, 255), true, true, 8)); + m_itemSettings.insert("HeatMap", new MapItemSettings("HeatMap", true, QColor(102, 40, 220), true, false, 11)); - MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", QColor(255, 255, 0), true, 4); + m_itemSettings.insert("AM", new MapItemSettings("AM", false, QColor(255, 0, 0), false, true, 10)); + MapItemSettings *fmSettings = new MapItemSettings("FM", false, QColor(255, 0, 0), false, true, 12); + fmSettings->m_filterDistance = 150000; + m_itemSettings.insert("FM", fmSettings); + MapItemSettings *dabSettings = new MapItemSettings("DAB", false, QColor(255, 0, 0), false, true, 12); + dabSettings->m_filterDistance = 75000; + m_itemSettings.insert("DAB", dabSettings); + + MapItemSettings *navAidSettings = new MapItemSettings("NavAid", false, QColor(255, 0, 255), false, true, 11); + navAidSettings->m_filterDistance = 500000; + m_itemSettings.insert("NavAid", navAidSettings); + + m_itemSettings.insert("Airport (Large)", new MapItemSettings("Airport (Large)", false, QColor(255, 0, 255), false, true, 11)); + m_itemSettings.insert("Airport (Medium)", new MapItemSettings("Airport (Medium)", false, QColor(255, 0, 255), false, true, 11)); + m_itemSettings.insert("Airport (Small)", new MapItemSettings("Airport (Small)", false, QColor(255, 0, 255), false, true, 13)); + m_itemSettings.insert("Heliport", new MapItemSettings("Heliport", false, QColor(255, 0, 255), false, true, 12)); + MapItemSettings *stationSettings = new MapItemSettings("Station", true, QColor(255, 0, 0), false, true, 11); + stationSettings->m_extrapolate = 0; + stationSettings->m_display3DTrack = false; + m_itemSettings.insert("Station", stationSettings); + m_itemSettings.insert("VORLocalizer", new MapItemSettings("VORLocalizer", true, QColor(255, 255, 0), false, true, 11)); + + MapItemSettings *ionosondeItemSettings = new MapItemSettings("Ionosonde Stations", true, QColor(255, 255, 0), false, true, 4); ionosondeItemSettings->m_display2DIcon = false; m_itemSettings.insert("Ionosonde Stations", ionosondeItemSettings); - MapItemSettings *stationItemSettings = new MapItemSettings("Station", QColor(255, 0, 0), true, 11); - stationItemSettings->m_display2DTrack = false; - m_itemSettings.insert("Station", stationItemSettings); + m_itemSettings.insert("Airspace (A)", new MapItemSettings("Airspace (A)", false, QColor(255, 0, 0, 0x20), false, false, 7)); + m_itemSettings.insert("Airspace (B)", new MapItemSettings("Airspace (B)", false, QColor(255, 0, 0, 0x20), false, false, 7)); + m_itemSettings.insert("Airspace (C)", new MapItemSettings("Airspace (C)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (D)", new MapItemSettings("Airspace (D)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (E)", new MapItemSettings("Airspace (E)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (F)", new MapItemSettings("Airspace (F)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (G)", new MapItemSettings("Airspace (G)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (FIR)", new MapItemSettings("Airspace (FIR)", false, QColor(255, 0, 0, 0x20), false, false, 7)); + m_itemSettings.insert("Airspace (CTR)", new MapItemSettings("Airspace (CTR)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (RMZ)", new MapItemSettings("Airspace (RMZ)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (TMA)", new MapItemSettings("Airspace (TMA)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (TMZ)", new MapItemSettings("Airspace (TMZ)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (OTH)", new MapItemSettings("Airspace (OTH)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (Restricted)", new MapItemSettings("Airspace (Restricted)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (Gliding)", new MapItemSettings("Airspace (Gliding)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (Danger)", new MapItemSettings("Airspace (Danger)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (Prohibited)", new MapItemSettings("Airspace (Prohibited)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (Wave)", new MapItemSettings("Airspace (Wave)", false, QColor(255, 0, 0, 0x20), false, false, 11)); + m_itemSettings.insert("Airspace (Airports)", new MapItemSettings("Airspace (Airports)", false, QColor(0, 0, 255, 0x20), false, false, 11)); + resetToDefaults(); } @@ -104,7 +156,7 @@ void MapSettings::resetToDefaults() m_displaySelectedGroundTracks = true; m_displayAllGroundTracks = true; m_title = "Map"; - m_displayAllGroundTracks = QColor(225, 25, 99).rgb(); + m_displayAllGroundTracks = QColor(225, 25, 99).rgba(); m_useReverseAPI = false; m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; @@ -121,6 +173,7 @@ void MapSettings::resetToDefaults() m_displayMUF = false; m_displayfoF2 = false; m_workspaceIndex = 0; + m_checkWXAPIKey = ""; } QByteArray MapSettings::serialize() const @@ -165,6 +218,8 @@ QByteArray MapSettings::serialize() const s.writeBool(35, m_displayMUF); s.writeBool(36, m_displayfoF2); + s.writeString(46, m_checkWXAPIKey); + return s.final(); } @@ -184,6 +239,7 @@ bool MapSettings::deserialize(const QByteArray& data) uint32_t utmp; QString strtmp; QByteArray blob; + QString string; d.readBool(1, &m_displayNames, true); d.readString(2, &m_mapProvider, "osm"); @@ -195,7 +251,7 @@ bool MapSettings::deserialize(const QByteArray& data) d.readString(3, &m_mapBoxAPIKey, ""); d.readString(4, &m_mapBoxStyles, ""); d.readString(8, &m_title, "Map"); - d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb()); + d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgba()); d.readBool(10, &m_useReverseAPI, false); d.readString(11, &m_reverseAPIAddress, "127.0.0.1"); d.readU32(12, &utmp, 0); @@ -241,6 +297,8 @@ bool MapSettings::deserialize(const QByteArray& data) d.readBool(35, &m_displayMUF, false); d.readBool(36, &m_displayfoF2, false); + d.readString(46, &m_checkWXAPIKey, ""); + return true; } else @@ -251,16 +309,20 @@ bool MapSettings::deserialize(const QByteArray& data) } MapSettings::MapItemSettings::MapItemSettings(const QString& group, + bool enabled, const QColor color, + bool display2DTrack, bool display3DPoint, int minZoom, int modelMinPixelSize) { m_group = group; resetToDefaults(); - m_3DPointColor = color.rgb(); - m_2DTrackColor = color.darker().rgb(); - m_3DTrackColor = color.darker().rgb(); + m_enabled = enabled, + m_3DPointColor = color.rgba(); + m_2DTrackColor = color.darker().rgba(); + m_3DTrackColor = color.darker().rgba(); + m_display2DTrack = display2DTrack; m_display3DPoint = display3DPoint; m_2DMinZoom = minZoom; m_3DModelMinPixelSize = modelMinPixelSize; @@ -277,16 +339,19 @@ void MapSettings::MapItemSettings::resetToDefaults() m_display2DIcon = true; m_display2DLabel = true; m_display2DTrack = true; - m_2DTrackColor = QColor(150, 0, 20).rgb(); + m_2DTrackColor = QColor(150, 0, 20).rgba(); m_2DMinZoom = 1; m_display3DModel = true; m_display3DPoint = false; - m_3DPointColor = QColor(225, 0, 0).rgb(); + m_3DPointColor = QColor(225, 0, 0).rgba(); m_display3DLabel = true; m_display3DTrack = true; - m_3DTrackColor = QColor(150, 0, 20).rgb(); + m_3DTrackColor = QColor(150, 0, 20).rgba(); m_3DModelMinPixelSize = 0; m_3DLabelScale = 0.5f; + m_filterName = ""; + m_filterDistance = 0; + m_extrapolate = 60; } QByteArray MapSettings::MapItemSettings::serialize() const @@ -308,6 +373,9 @@ QByteArray MapSettings::MapItemSettings::serialize() const s.writeU32(13, m_3DTrackColor); s.writeS32(14, m_3DModelMinPixelSize); s.writeFloat(15, m_3DLabelScale); + s.writeString(16, m_filterName); + s.writeS32(17, m_filterDistance); + s.writeS32(18, m_extrapolate); return s.final(); } @@ -329,16 +397,21 @@ bool MapSettings::MapItemSettings::deserialize(const QByteArray& data) d.readBool(3, &m_display2DIcon, true); d.readBool(4, &m_display2DLabel, true); d.readBool(5, &m_display2DTrack, true); - d.readU32(6, &m_2DTrackColor, QColor(150, 0, 0).rgb()); + d.readU32(6, &m_2DTrackColor, QColor(150, 0, 0).rgba()); d.readS32(7, &m_2DMinZoom, 1); d.readBool(8, &m_display3DModel, true); d.readBool(9, &m_display3DLabel, true); d.readBool(10, &m_display3DPoint, true); - d.readU32(11, &m_3DPointColor, QColor(255, 0, 0).rgb()); + d.readU32(11, &m_3DPointColor, QColor(255, 0, 0).rgba()); d.readBool(12, &m_display3DTrack, true); - d.readU32(13, &m_3DTrackColor, QColor(150, 0, 20).rgb()); + d.readU32(13, &m_3DTrackColor, QColor(150, 0, 20).rgba()); d.readS32(14, &m_3DModelMinPixelSize, 0); d.readFloat(15, &m_3DLabelScale, 0.5f); + d.readString(16, &m_filterName, ""); + d.readS32(17, &m_filterDistance, 0); + d.readS32(18, &m_extrapolate, 60); + m_filterNameRE.setPattern(m_filterName); + m_filterNameRE.optimize(); return true; } else @@ -567,3 +640,4 @@ QString MapSettings::getDebugString(const QStringList& settingsKeys, bool force) return QString(ostr.str().c_str()); } + diff --git a/plugins/feature/map/mapsettings.h b/plugins/feature/map/mapsettings.h index 71804e26d..06754f552 100644 --- a/plugins/feature/map/mapsettings.h +++ b/plugins/feature/map/mapsettings.h @@ -22,6 +22,7 @@ #include #include #include +#include class Serializable; @@ -32,7 +33,7 @@ struct MapSettings bool m_enabled; // Whether enabled at all on 2D or 3D map bool m_display2DIcon; // Display image 2D map bool m_display2DLabel; // Display label on 2D map - bool m_display2DTrack; // Display tracks on 2D map + bool m_display2DTrack; // Display tracks (or polygon) on 2D map quint32 m_2DTrackColor; int m_2DMinZoom; bool m_display3DModel; // Draw 3D model for item @@ -43,14 +44,26 @@ struct MapSettings quint32 m_3DTrackColor; int m_3DModelMinPixelSize; float m_3DLabelScale; + QString m_filterName; + QRegularExpression m_filterNameRE; + int m_filterDistance; // Filter items > this distance in metres away from My Position. <= 0 don't filter + int m_extrapolate; // Extrapolate duration in seconds on 3D map - MapItemSettings(const QString& group, const QColor color, bool display3DPoint=true, int minZoom=11, int modelMinPixelSize=0); + MapItemSettings(const QString& group, bool enabled, const QColor color, bool display2DTrack=true, bool display3DPoint=true, int minZoom=11, int modelMinPixelSize=0); MapItemSettings(const QByteArray& data); void resetToDefaults(); QByteArray serialize() const; bool deserialize(const QByteArray& data); }; + struct MapPolygonSettings { + QString m_group; + bool m_enabled; + quint32 m_2DColor; + int m_2DMinZoom; + quint32 m_3DColor; + }; + struct AvailableChannelOrFeature { QString m_kind; //!< "R" for channel, "F" for feature @@ -107,6 +120,8 @@ struct MapSettings bool m_displayMUF; // Plot MUF contours bool m_displayfoF2; // Plot foF2 contours + QString m_checkWXAPIKey; //!< checkwxapi.com API key + // Per source settings QHash m_itemSettings; @@ -127,4 +142,7 @@ struct MapSettings static const QStringList m_mapProviders; }; +Q_DECLARE_METATYPE(MapSettings::MapItemSettings *); + #endif // INCLUDE_FEATURE_MAPSETTINGS_H_ + diff --git a/plugins/feature/map/mapsettingsdialog.cpp b/plugins/feature/map/mapsettingsdialog.cpp index 859cc176a..9ce212513 100644 --- a/plugins/feature/map/mapsettingsdialog.cpp +++ b/plugins/feature/map/mapsettingsdialog.cpp @@ -109,6 +109,7 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : QDialog(parent), m_settings(settings), m_downloadDialog(this), + m_progressDialog(nullptr), ui(new Ui::MapSettingsDialog) { ui->setupUi(this); @@ -117,6 +118,7 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey); ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey); ui->cesiumIonAPIKey->setText(settings->m_cesiumIonAPIKey); + ui->checkWXAPIKey->setText(settings->m_checkWXAPIKey); ui->osmURL->setText(settings->m_osmURL); ui->mapBoxStyles->setText(settings->m_mapBoxStyles); ui->map2DEnabled->setChecked(m_settings->m_map2DEnabled); @@ -129,10 +131,10 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : // Sort groups in table alphabetically QList itemSettings = m_settings->m_itemSettings.values(); - std::sort(itemSettings.begin(), itemSettings.end(), - [](const MapSettings::MapItemSettings* a, const MapSettings::MapItemSettings* b) -> bool { - return a->m_group < b->m_group; - }); + std::sort(itemSettings.begin(), itemSettings.end(), + [](const MapSettings::MapItemSettings* a, const MapSettings::MapItemSettings* b) -> bool { + return a->m_group < b->m_group; + }); QListIterator itr(itemSettings); while (itr.hasNext()) { @@ -163,6 +165,14 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : item->setCheckState(itemSettings->m_display3DLabel ? Qt::Checked : Qt::Unchecked); ui->mapItemSettings->setItem(row, COL_3D_LABEL, item); + item = new QTableWidgetItem(itemSettings->m_filterName); + ui->mapItemSettings->setItem(row, COL_FILTER_NAME, item); + item = new QTableWidgetItem(); + if (itemSettings->m_filterDistance > 0) { + item->setText(QString::number(itemSettings->m_filterDistance / 1000)); + } + ui->mapItemSettings->setItem(row, COL_FILTER_DISTANCE, item); + MapItemSettingsGUI *gui = new MapItemSettingsGUI(ui->mapItemSettings, row, itemSettings); m_mapItemSettingsGUIs.append(gui); } @@ -172,6 +182,15 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : on_map3DEnabled_clicked(m_settings->m_map3DEnabled); connect(&m_dlm, &HttpDownloadManagerGUI::downloadComplete, this, &MapSettingsDialog::downloadComplete); + connect(&m_openAIP, &OpenAIP::downloadingURL, this, &MapSettingsDialog::downloadingURL); + connect(&m_openAIP, &OpenAIP::downloadError, this, &MapSettingsDialog::downloadError); + connect(&m_openAIP, &OpenAIP::downloadAirspaceFinished, this, &MapSettingsDialog::downloadAirspaceFinished); + connect(&m_openAIP, &OpenAIP::downloadNavAidsFinished, this, &MapSettingsDialog::downloadNavAidsFinished); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadingURL, this, &MapSettingsDialog::downloadingURL); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadProgress, this, &MapSettingsDialog::downloadProgress); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadError, this, &MapSettingsDialog::downloadError); + connect(&m_ourAirportsDB, &OurAirportsDB::downloadAirportInformationFinished, this, &MapSettingsDialog::downloadAirportInformationFinished); + #ifndef QT_WEBENGINE_FOUND ui->map3DSettings->setVisible(false); ui->downloadModels->setVisible(false); @@ -257,6 +276,16 @@ void MapSettingsDialog::accept() itemSettings->m_3DTrackColor = gui->m_track3D.m_color; itemSettings->m_3DModelMinPixelSize = gui->m_minPixels->value(); itemSettings->m_3DLabelScale = gui->m_labelScale->value(); + itemSettings->m_filterName = ui->mapItemSettings->item(row, COL_FILTER_NAME)->text(); + itemSettings->m_filterNameRE.setPattern(itemSettings->m_filterName); + itemSettings->m_filterNameRE.optimize(); + bool ok; + int filterDistance = ui->mapItemSettings->item(row, COL_FILTER_DISTANCE)->text().toInt(&ok); + if (ok && filterDistance > 0) { + itemSettings->m_filterDistance = filterDistance * 1000; + } else { + itemSettings->m_filterDistance = 0; + } } QDialog::accept(); @@ -307,6 +336,7 @@ void MapSettingsDialog::on_map3DEnabled_clicked(bool checked) ui->buildings->setEnabled(checked); ui->sunLightEnabled->setEnabled(checked); ui->eciCamera->setEnabled(checked); + ui->antiAliasing->setEnabled(checked); } // Models have individual licensing. See LICENSE on github @@ -423,3 +453,95 @@ void MapSettingsDialog::downloadComplete(const QString &filename, bool success, QMessageBox::warning(this, "Download failed", QString("Failed to download %1 to %2\n%3").arg(url).arg(filename).arg(errorMessage)); } } + +void MapSettingsDialog::on_getAirportDB_clicked() +{ + // Don't try to download while already in progress + if (m_progressDialog == nullptr) + { + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); + m_ourAirportsDB.downloadAirportInformation(); + } +} + +void MapSettingsDialog::on_getAirspacesDB_clicked() +{ + // Don't try to download while already in progress + if (m_progressDialog == nullptr) + { + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setMaximum(OpenAIP::m_countryCodes.size()); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setWindowFlag(Qt::WindowCloseButtonHint, false); + m_openAIP.downloadAirspaces(); + } +} + +void MapSettingsDialog::downloadingURL(const QString& url) +{ + if (m_progressDialog) + { + m_progressDialog->setLabelText(QString("Downloading %1.").arg(url)); + m_progressDialog->setValue(m_progressDialog->value() + 1); + } +} + +void MapSettingsDialog::downloadProgress(qint64 bytesRead, qint64 totalBytes) +{ + if (m_progressDialog) + { + m_progressDialog->setMaximum(totalBytes); + m_progressDialog->setValue(bytesRead); + } +} + +void MapSettingsDialog::downloadError(const QString& error) +{ + QMessageBox::critical(this, "Map", error); + if (m_progressDialog) + { + m_progressDialog->close(); + delete m_progressDialog; + m_progressDialog = nullptr; + } +} + +void MapSettingsDialog::downloadAirspaceFinished() +{ + if (m_progressDialog) { + m_progressDialog->setLabelText("Reading airspaces."); + } + emit airspacesUpdated(); + m_openAIP.downloadNavAids(); +} + +void MapSettingsDialog::downloadNavAidsFinished() +{ + if (m_progressDialog) { + m_progressDialog->setLabelText("Reading NAVAIDs."); + } + emit navAidsUpdated(); + if (m_progressDialog) + { + m_progressDialog->close(); + delete m_progressDialog; + m_progressDialog = nullptr; + } +} + +void MapSettingsDialog::downloadAirportInformationFinished() +{ + if (m_progressDialog) { + m_progressDialog->setLabelText("Reading airports."); + } + emit airportsUpdated(); + if (m_progressDialog) + { + m_progressDialog->close(); + delete m_progressDialog; + m_progressDialog = nullptr; + } +} + diff --git a/plugins/feature/map/mapsettingsdialog.h b/plugins/feature/map/mapsettingsdialog.h index c8d60b9e7..6eade0e69 100644 --- a/plugins/feature/map/mapsettingsdialog.h +++ b/plugins/feature/map/mapsettingsdialog.h @@ -21,8 +21,11 @@ #include #include #include +#include #include "gui/httpdownloadmanagergui.h" +#include "util/openaip.h" +#include "util/ourairportsdb.h" #include "ui_mapsettingsdialog.h" #include "mapsettings.h" @@ -78,7 +81,9 @@ public: COL_3D_LABEL, COL_3D_POINT, COL_3D_TRACK, - COL_3D_LABEL_SCALE + COL_3D_LABEL_SCALE, + COL_FILTER_NAME, + COL_FILTER_DISTANCE }; public: @@ -92,6 +97,9 @@ private: HttpDownloadManagerGUI m_dlm; int m_fileIdx; QMessageBox m_downloadDialog; + QProgressDialog *m_progressDialog; + OpenAIP m_openAIP; + OurAirportsDB m_ourAirportsDB; void unzip(const QString &filename); @@ -100,7 +108,20 @@ private slots: void on_map2DEnabled_clicked(bool checked=false); void on_map3DEnabled_clicked(bool checked=false); void on_downloadModels_clicked(); + void on_getAirportDB_clicked(); + void on_getAirspacesDB_clicked(); void downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage); + void downloadingURL(const QString& url); + void downloadProgress(qint64 bytesRead, qint64 totalBytes); + void downloadError(const QString& error); + void downloadAirspaceFinished(); + void downloadNavAidsFinished(); + void downloadAirportInformationFinished(); + +signals: + void navAidsUpdated(); + void airspacesUpdated(); + void airportsUpdated(); private: Ui::MapSettingsDialog* ui; diff --git a/plugins/feature/map/mapsettingsdialog.ui b/plugins/feature/map/mapsettingsdialog.ui index 30cc95f37..6728e47c5 100644 --- a/plugins/feature/map/mapsettingsdialog.ui +++ b/plugins/feature/map/mapsettingsdialog.ui @@ -6,8 +6,8 @@ 0 0 - 1016 - 720 + 1267 + 648
@@ -35,68 +35,6 @@ Maps - - - - QAbstractItemView::NoSelection - - - - Enabled - - - - - 2D Icon - - - - - 2D Label - - - - - 2D Min Zoom - - - - - 2D Track - - - - - 3D Model - - - - - 3D Min Pixels - - - - - 3D Label - - - - - 3D Point - - - - - 3D Track - - - - - 3D Label Scale - - - - @@ -346,6 +284,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -358,6 +309,34 @@ + + + + Download the latest OurAirports airport databases (10MB) + + + + + + + :/map/icons/controltower.png:/map/icons/controltower.png + + + + + + + Download airspaces and NAVAIDs from OpenAIP (40MB) + + + + + + + :/map/icons/vor.png:/map/icons/vor.png + + + @@ -375,6 +354,115 @@ + + + Map Items + + + + + + QAbstractItemView::NoSelection + + + + Enabled + + + Whether items of this type will be displayed on the maps + + + + + 2D Icon + + + Whether to display icon on 2D map + + + + + 2D Label + + + Whether to display label on 2D map + + + + + 2D Min Zoom + + + + + 2D Colour + + + Track colour or 2D map + + + + + 3D Model + + + Whether to display model on 3D map + + + + + 3D Min Pixels + + + + + 3D Label + + + Whether to display label on 3D map + + + + + 3D Point + + + Whether to display a point on 3D map + + + + + 3D Colour + + + + + 3D Label Scale + + + Scale factor for label on 3D map + + + + + Filter Name + + + Regular expression to filter by name (Only items with names matching the pattern will be displayed) + + + + + Filter Distance (km) + + + Filter objects further than this distance in km away from My Position + + + + + + API Keys @@ -466,6 +554,20 @@ + + + + CheckWX API key + + + + + + + checkwxapi.com API key for accessing airport weather (METARs) + + + @@ -489,8 +591,6 @@ - tabWidget - mapItemSettings map2DEnabled mapProvider osmURL @@ -507,7 +607,10 @@ mapBoxAPIKey cesiumIonAPIKey - + + + + buttonBox diff --git a/plugins/feature/map/osmtemplateserver.h b/plugins/feature/map/osmtemplateserver.h index fcada37fe..13b80d881 100644 --- a/plugins/feature/map/osmtemplateserver.h +++ b/plugins/feature/map/osmtemplateserver.h @@ -94,6 +94,25 @@ private slots: \"DataCopyRight\" : \"Maptiler\"\ }").arg(hiresURL).arg(m_maptilerAPIKey); } + else if (tokens[1].contains("transit")) + { + QStringList map({"/night-transit", "/night-transit-hires", "/transit", "/transit-hires"}); + QStringList mapId({"thf-nighttransit", "thf-nighttransit-hires", "thf-transit", "thf-transit-hires"}); + QStringList mapUrl({"dark_nolabels", "dark_nolabels", "light_nolabels", "light_nolabels"}); + + // Use CartoDB maps without labels for aviation maps + int idx = map.indexOf(tokens[1]); + xml = QString("\ + {\ + \"UrlTemplate\" : \"http://1.basemaps.cartocdn.com/%2/%z/%x/%y.png%1\",\ + \"ImageFormat\" : \"png\",\ + \"QImageFormat\" : \"Indexed8\",\ + \"ID\" : \"%3\",\ + \"MaximumZoomLevel\" : 20,\ + \"MapCopyRight\" : \"CartoDB\",\ + \"DataCopyRight\" : \"CartoDB\"\ + }").arg(hiresURL).arg(mapUrl[idx]).arg(mapId[idx]); + } else { int idx = map.indexOf(tokens[1]); diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index c138fdb2a..1a65ddf80 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -11,10 +11,13 @@ On top of this, it can plot data from other plugins, such as: * Satellites from the Satellite Tracker, * Weather imagery from APT Demodulator, * The Sun, Moon and Stars from the Star Tracker, -* Weather ballons from the RadioSonde feature. +* Weather ballons from the RadioSonde feature, +* Radials and estimated position from the VOR localizer feature. -As well as other other data sources: +As well as other data sources: +* AM, FM and DAB transmitters in the UK, +* Airports, NavAids and airspaces, * Beacons based on the IARU Region 1 beacon database and International Beacon Project, * Radio time transmitters, * GRAVES radar, diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 146a63129..12fd1f08f 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -197,6 +197,7 @@ set(sdrbase_SOURCES util/morse.cpp util/openaip.cpp util/osndb.cpp + util/ourairportsdb.cpp util/peakfinder.cpp util/planespotters.cpp util/png.cpp @@ -424,6 +425,7 @@ set(sdrbase_HEADERS util/movingaverage.h util/openaip.h util/osndb.h + util/outairportsdb.h util/peakfinder.h util/planespotters.h util/png.h @@ -466,6 +468,7 @@ include_directories( ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client ${Boost_INCLUDE_DIRS} ${OPUS_INCLUDE_DIRS} + ${Qt${QT_DEFAULT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS} ) add_library(sdrbase SHARED diff --git a/sdrbase/settings/mainsettings.h b/sdrbase/settings/mainsettings.h index 27573666f..b76b3c2f3 100644 --- a/sdrbase/settings/mainsettings.h +++ b/sdrbase/settings/mainsettings.h @@ -196,6 +196,13 @@ public: emit preferenceChanged(Preferences::Multisampling); } + int getMapMultisampling() const { return m_preferences.getMapMultisampling(); } + void setMapMultisampling(int samples) + { + m_preferences.setMapMultisampling(samples); + emit preferenceChanged(Preferences::MapMultisampling); + } + signals: void preferenceChanged(int); diff --git a/sdrbase/settings/preferences.cpp b/sdrbase/settings/preferences.cpp index dbef62e1e..4f92f4ede 100644 --- a/sdrbase/settings/preferences.cpp +++ b/sdrbase/settings/preferences.cpp @@ -23,6 +23,7 @@ void Preferences::resetToDefaults() m_consoleMinLogLevel = QtDebugMsg; m_fileMinLogLevel = QtDebugMsg; m_multisampling = 0; + m_mapMultisampling = 16; } QByteArray Preferences::serialize() const @@ -43,6 +44,7 @@ QByteArray Preferences::serialize() const s.writeS32((int) SourceItemIndex, m_sourceItemIndex); s.writeS32((int) Multisampling, m_multisampling); s.writeBool((int) AutoUpdatePosition, m_autoUpdatePosition); + s.writeS32((int) MapMultisampling, m_mapMultisampling); return s.final(); } @@ -98,6 +100,7 @@ bool Preferences::deserialize(const QByteArray& data) d.readS32((int) Multisampling, &m_multisampling, 0); d.readBool((int) AutoUpdatePosition, &m_autoUpdatePosition, true); + d.readS32((int) MapMultisampling, &m_mapMultisampling, 16); return true; } diff --git a/sdrbase/settings/preferences.h b/sdrbase/settings/preferences.h index 87c1d15a0..9298a5bda 100644 --- a/sdrbase/settings/preferences.h +++ b/sdrbase/settings/preferences.h @@ -23,7 +23,8 @@ public: Altitude, SourceItemIndex, Multisampling, - AutoUpdatePosition + AutoUpdatePosition, + MapMultisampling }; Preferences(); @@ -79,6 +80,9 @@ public: int getMultisampling() const { return m_multisampling; } void setMultisampling(int samples) { m_multisampling = samples; } + int getMapMultisampling() const { return m_mapMultisampling; } + void setMapMultisampling(int samples) { m_mapMultisampling = samples; } + protected: QString m_sourceDevice; //!< Identification of the source used in R0 tab (GUI flavor) at startup int m_sourceIndex; //!< Index of the source used in R0 tab (GUI flavor) at startup @@ -98,7 +102,8 @@ protected: bool m_useLogFile; QString m_logFileName; - int m_multisampling; //!< Number of samples to use for multisampling anti-aliasing (typically 0 or 4) + int m_multisampling; //!< Number of samples to use for multisampling anti-aliasing for spectrums (typically 0 or 4) + int m_mapMultisampling; //!< Number of samples to use for multisampling anti-aliasing for 2D maps (16 gives best text) }; #endif // INCLUDE_PREFERENCES_H diff --git a/sdrbase/util/openaip.cpp b/sdrbase/util/openaip.cpp index 0950ed568..95b94f245 100644 --- a/sdrbase/util/openaip.cpp +++ b/sdrbase/util/openaip.cpp @@ -17,6 +17,251 @@ #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", @@ -268,6 +513,12 @@ const QStringList OpenAIP::m_countryCodes = { "zw" }; +QSharedPointer> OpenAIP::m_airspaces; +QSharedPointer> OpenAIP::m_navAids; + +QDateTime OpenAIP::m_airspacesModifiedDateTime; +QDateTime OpenAIP::m_navAidsModifiedDateTime; + OpenAIP::OpenAIP(QObject *parent) : QObject(parent) { @@ -390,11 +641,11 @@ void OpenAIP::downloadFinished(const QString& filename, bool success) } // Read airspaces for all countries -QList OpenAIP::readAirspaces() +QList *OpenAIP::readAirspaces() { - QList airspaces; + QList *airspaces = new QList(); for (const auto& countryCode : m_countryCodes) { - airspaces.append(readAirspaces(countryCode)); + airspaces->append(readAirspaces(countryCode)); } return airspaces; } @@ -406,11 +657,11 @@ QList OpenAIP::readAirspaces(const QString& countryCode) } // Read NavAids for all countries -QList OpenAIP::readNavAids() +QList *OpenAIP::readNavAids() { - QList navAids; + QList *navAids = new QList(); for (const auto& countryCode : m_countryCodes) { - navAids.append(readNavAids(countryCode)); + navAids->append(readNavAids(countryCode)); } return navAids; } @@ -420,3 +671,60 @@ 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; +} + diff --git a/sdrbase/util/openaip.h b/sdrbase/util/openaip.h index 1d4d372d1..ca5e2355f 100644 --- a/sdrbase/util/openaip.h +++ b/sdrbase/util/openaip.h @@ -118,116 +118,7 @@ struct SDRBASE_API Airspace { } // Read OpenAIP XML file - static QList 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; - } + static QList readXML(const QString &filename); }; @@ -251,140 +142,7 @@ struct SDRBASE_API NavAid { } // OpenAIP XML file - static QList 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; - } - + static QList readXML(const QString &filename); }; class SDRBASE_API OpenAIP : public QObject { @@ -396,18 +154,27 @@ public: static const QStringList m_countryCodes; - static QList readAirspaces(); - static QList readAirspaces(const QString& countryCode); - static QList readNavAids(); - static QList readNavAids(const QString& countryCode); - void downloadAirspaces(); void downloadNavAids(); + static QSharedPointer> getAirspaces(); + static QSharedPointer> getNavAids(); + private: HttpDownloadManager m_dlm; int m_countryIndex; + static QSharedPointer> m_airspaces; + static QSharedPointer> m_navAids; + + static QDateTime m_airspacesModifiedDateTime; + static QDateTime m_navAidsModifiedDateTime; + + static QList *readAirspaces(); + static QList readAirspaces(const QString& countryCode); + static QList *readNavAids(); + static QList readNavAids(const QString& countryCode); + static QString getDataDir(); static QString getAirspaceFilename(int i); static QString getAirspaceFilename(const QString& countryCode); @@ -415,6 +182,8 @@ private: static QString getNavAidsFilename(int i); static QString getNavAidsFilename(const QString& countryCode); static QString getNavAidsURL(int i); + static QDateTime getAirspacesModifiedDateTime(); + static QDateTime getNavAidsModifiedDateTime(); void downloadAirspace(); void downloadNavAid(); @@ -431,3 +200,4 @@ signals: }; #endif // INCLUDE_OPENAIP_H + diff --git a/sdrbase/util/osndb.cpp b/sdrbase/util/osndb.cpp index a8ac992cd..5c6998a7f 100644 --- a/sdrbase/util/osndb.cpp +++ b/sdrbase/util/osndb.cpp @@ -15,6 +15,11 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// +#include +#include + +#include + #include "util/osndb.h" QHash AircraftInformation::m_airlineIcons; @@ -23,3 +28,544 @@ QHash AircraftInformation::m_flagIcons; QHash *AircraftInformation::m_prefixMap; QHash *AircraftInformation::m_militaryMap; QMutex AircraftInformation::m_mutex; + +QSharedPointer> OsnDB::m_aircraftInformation; +QSharedPointer> OsnDB::m_aircraftInformationByReg; +QDateTime OsnDB::m_modifiedDateTime; + + +OsnDB::OsnDB(QObject *parent) : + QObject(parent) +{ + connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OsnDB::downloadFinished); +} + +OsnDB::~OsnDB() +{ + disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OsnDB::downloadFinished); +} + +void OsnDB::downloadAircraftInformation() +{ + QString filename = OsnDB::getOSNDBZipFilename(); + QString urlString = OSNDB_URL; + QUrl dbURL(urlString); + qDebug() << "OsnDB::downloadAircraftInformation: Downloading " << urlString; + emit downloadingURL(urlString); + QNetworkReply *reply = m_dlm.download(dbURL, filename); + connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) { + emit downloadProgress(bytesRead, totalBytes); + }); +} + +void OsnDB::downloadFinished(const QString& filename, bool success) +{ + if (!success) + { + qWarning() << "OsnDB::downloadFinished: Failed to download: " << filename; + emit downloadError(QString("Failed to download: %1").arg(filename)); + } + else if (filename == OsnDB::getOSNDBZipFilename()) + { + // Extract .csv file from .zip file + QZipReader reader(filename); + QByteArray database = reader.fileData("media/data/samples/metadata/aircraftDatabase.csv"); + if (database.size() > 0) + { + QFile file(OsnDB::getOSNDBFilename()); + if (file.open(QIODevice::WriteOnly)) + { + file.write(database); + file.close(); + emit downloadAircraftInformationFinished(); + } + else + { + qWarning() << "OsnDB::downloadFinished - Failed to open " << file.fileName() << " for writing"; + emit downloadError(QString("Failed to open %1 for writing").arg(file.fileName())); + } + } + else + { + qWarning() << "OsnDB::downloadFinished - aircraftDatabase.csv not in expected dir. Extracting all."; + if (reader.extractAll(getDataDir())) + { + emit downloadAircraftInformationFinished(); + } + else + { + qWarning() << "OsnDB::downloadFinished - Failed to extract files from " << filename; + emit downloadError(QString("Failed to extract files from ").arg(filename)); + } + } + } + else + { + qDebug() << "OsnDB::downloadFinished: Unexpected filename: " << filename; + emit downloadError(QString("Unexpected filename: %1").arg(filename)); + } +} + +QSharedPointer> OsnDB::getAircraftInformation() +{ + QFileInfo fastFileInfo(getFastDBFilename()); + QFileInfo fullFileInfo(getOSNDBFilename()); + QDateTime fastModifiedDateTime = fastFileInfo.lastModified(); + QDateTime fullModifiedDateTime = fullFileInfo.lastModified(); + + // Update fast database, if full database is newer + if (fullModifiedDateTime > fastModifiedDateTime) + { + qDebug() << "AircraftInformation::getAircraftInformation: Creating fast database"; + m_aircraftInformation = QSharedPointer>(OsnDB::readOSNDB(getOSNDBFilename())); + if (m_aircraftInformation) + { + OsnDB::writeFastDB(OsnDB::getFastDBFilename(), m_aircraftInformation.get()); + fastModifiedDateTime = fastFileInfo.lastModified(); + m_modifiedDateTime = fastModifiedDateTime; + m_aircraftInformationByReg = QSharedPointer> (OsnDB::registrationHash(m_aircraftInformation.get())); + } + } + + if (!m_aircraftInformation || (fastModifiedDateTime > m_modifiedDateTime)) + { + m_aircraftInformation = QSharedPointer>(OsnDB::readFastDB(getFastDBFilename())); + if (m_aircraftInformation) + { + m_modifiedDateTime = fastModifiedDateTime; + m_aircraftInformationByReg = QSharedPointer> (OsnDB::registrationHash(m_aircraftInformation.get())); + } + } + + return m_aircraftInformation; +} + +QSharedPointer> OsnDB::getAircraftInformationByReg() +{ + getAircraftInformation(); + return m_aircraftInformationByReg; +} + +QHash *OsnDB::readOSNDB(const QString &filename) +{ + int cnt = 0; + QHash *aircraftInfo = nullptr; + + // Column numbers used for the data as of 2020/10/28 + int icaoCol = 0; + int registrationCol = 1; + int manufacturerNameCol = 3; + int modelCol = 4; + int ownerCol = 13; + int operatorCol = 9; + int operatorICAOCol = 11; + int registeredCol = 15; + + qDebug() << "AircraftInformation::readOSNDB: " << filename; + + FILE *file; + QByteArray utfFilename = filename.toUtf8(); + if ((file = fopen(utfFilename.constData(), "r")) != NULL) + { + char row[2048]; + + if (fgets(row, sizeof(row), file)) + { + aircraftInfo = new QHash(); + aircraftInfo->reserve(500000); + // Read header + int idx = 0; + char *p = strtok(row, ","); + while (p != NULL) + { + if (!strcmp(p, "icao24")) + icaoCol = idx; + else if (!strcmp(p, "registration")) + registrationCol = idx; + else if (!strcmp(p, "manufacturername")) + manufacturerNameCol = idx; + else if (!strcmp(p, "model")) + modelCol = idx; + else if (!strcmp(p, "owner")) + ownerCol = idx; + else if (!strcmp(p, "operator")) + operatorCol = idx; + else if (!strcmp(p, "operatoricao")) + operatorICAOCol = idx; + else if (!strcmp(p, "registered")) + registeredCol = idx; + p = strtok(NULL, ","); + idx++; + } + // Read data + while (fgets(row, sizeof(row), file)) + { + int icao = 0; + char *icaoString = NULL; + char *registration = NULL; + size_t registrationLen = 0; + char *manufacturerName = NULL; + size_t manufacturerNameLen = 0; + char *model = NULL; + size_t modelLen = 0; + char *owner = NULL; + size_t ownerLen = 0; + char *operatorName = NULL; + size_t operatorNameLen = 0; + char *operatorICAO = NULL; + size_t operatorICAOLen = 0; + char *registered = NULL; + size_t registeredLen = 0; + + p = strtok(row, ","); + idx = 0; + while (p != NULL) + { + // Read strings, stripping quotes + if (idx == icaoCol) + { + icaoString = p+1; + icaoString[strlen(icaoString)-1] = '\0'; + icao = strtol(icaoString, NULL, 16); + // Ignore entries with uppercase - might be better to try and merge? + // See: https://opensky-network.org/forum/bug-reports/652-are-the-aircraft-database-dumps-working + for (int i = 0; icaoString[i] != '\0'; i++) + { + char c = icaoString[i]; + if ((c >= 'A') && (c <= 'F')) + { + icao = 0; + break; + } + } + } + else if (idx == registrationCol) + { + registration = p+1; + registrationLen = strlen(registration)-1; + registration[registrationLen] = '\0'; + } + else if (idx == manufacturerNameCol) + { + manufacturerName = p+1; + manufacturerNameLen = strlen(manufacturerName)-1; + manufacturerName[manufacturerNameLen] = '\0'; + } + else if (idx == modelCol) + { + model = p+1; + modelLen = strlen(model)-1; + model[modelLen] = '\0'; + } + else if (idx == ownerCol) + { + owner = p+1; + ownerLen = strlen(owner)-1; + owner[ownerLen] = '\0'; + } + else if (idx == operatorCol) + { + operatorName = p+1; + operatorNameLen = strlen(operatorName)-1; + operatorName[operatorNameLen] = '\0'; + } + else if (idx == operatorICAOCol) + { + operatorICAO = p+1; + operatorICAOLen = strlen(operatorICAO)-1; + operatorICAO[operatorICAOLen] = '\0'; + } + else if (idx == registeredCol) + { + registered = p+1; + registeredLen = strlen(registered)-1; + registered[registeredLen] = '\0'; + } + p = strtok(NULL, ","); + idx++; + } + + // Only create the entry if we have some interesting data + if ((icao != 0) && (registrationLen > 0 || modelLen > 0 || ownerLen > 0 || operatorNameLen > 0 || operatorICAOLen > 0)) + { + QString modelQ = QString(model); + // Tidy up the model names + if (modelQ.endsWith(" (Boeing)")) + modelQ = modelQ.left(modelQ.size() - 9); + else if (modelQ.startsWith("BOEING ")) + modelQ = modelQ.right(modelQ.size() - 7); + else if (modelQ.startsWith("Boeing ")) + modelQ = modelQ.right(modelQ.size() - 7); + else if (modelQ.startsWith("AIRBUS ")) + modelQ = modelQ.right(modelQ.size() - 7); + else if (modelQ.startsWith("Airbus ")) + modelQ = modelQ.right(modelQ.size() - 7); + else if (modelQ.endsWith(" (Cessna)")) + modelQ = modelQ.left(modelQ.size() - 9); + + AircraftInformation *aircraft = new AircraftInformation(); + aircraft->m_icao = icao; + aircraft->m_registration = QString(registration); + aircraft->m_manufacturerName = QString(manufacturerName); + aircraft->m_model = modelQ; + aircraft->m_owner = QString(owner); + aircraft->m_operator = QString(operatorName); + aircraft->m_operatorICAO = QString(operatorICAO); + aircraft->m_registered = QString(registered); + aircraftInfo->insert(icao, aircraft); + cnt++; + } + + } + } + fclose(file); + } + else + qDebug() << "AircraftInformation::readOSNDB: Failed to open " << filename; + + qDebug() << "AircraftInformation::readOSNDB: Read " << cnt << " aircraft"; + + return aircraftInfo; +} + +QHash *OsnDB::registrationHash(const QHash *in) +{ + QHash *out = new QHash(); + QHashIterator i(*in); + while (i.hasNext()) + { + i.next(); + AircraftInformation *info = i.value(); + out->insert(info->m_registration, info); + } + return out; +} + +bool OsnDB::writeFastDB(const QString &filename, const QHash *aircraftInfo) +{ + QFile file(filename); + if (file.open(QIODevice::WriteOnly)) + { + file.write("icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n"); + QHash::const_iterator i = aircraftInfo->begin(); + while (i != aircraftInfo->end()) + { + AircraftInformation *info = i.value(); + file.write(QString("%1").arg(info->m_icao, 1, 16).toUtf8()); + file.write(","); + file.write(info->m_registration.toUtf8()); + file.write(","); + file.write(info->m_manufacturerName.toUtf8()); + file.write(","); + file.write(info->m_model.toUtf8()); + file.write(","); + file.write(info->m_owner.toUtf8()); + file.write(","); + file.write(info->m_operator.toUtf8()); + file.write(","); + file.write(info->m_operatorICAO.toUtf8()); + file.write(","); + file.write(info->m_registered.toUtf8()); + file.write("\n"); + ++i; + } + file.close(); + return true; + } + else + { + qCritical() << "AircraftInformation::writeFastDB failed to open " << filename << " for writing: " << file.errorString(); + return false; + } +} + +QHash *OsnDB::readFastDB(const QString &filename) +{ + int cnt = 0; + QHash *aircraftInfo = nullptr; + + qDebug() << "AircraftInformation::readFastDB: " << filename; + + FILE *file; + QByteArray utfFilename = filename.toUtf8(); + if ((file = fopen(utfFilename.constData(), "r")) != NULL) + { + char row[2048]; + + if (fgets(row, sizeof(row), file)) + { + // Check header + if (!strcmp(row, "icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n")) + { + aircraftInfo = new QHash(); + aircraftInfo->reserve(500000); + // Read data + while (fgets(row, sizeof(row), file)) + { + char *p = row; + AircraftInformation *aircraft = new AircraftInformation(); + char *icaoString = csvNext(&p); + int icao = strtol(icaoString, NULL, 16); + aircraft->m_icao = icao; + aircraft->m_registration = QString(csvNext(&p)); + aircraft->m_manufacturerName = QString(csvNext(&p)); + aircraft->m_model = QString(csvNext(&p)); + aircraft->m_owner = QString(csvNext(&p)); + aircraft->m_operator = QString(csvNext(&p)); + aircraft->m_operatorICAO = QString(csvNext(&p)); + aircraft->m_registered = QString(csvNext(&p)); + aircraftInfo->insert(icao, aircraft); + cnt++; + } + } + else + qDebug() << "AircraftInformation::readFastDB: Unexpected header"; + } + else + qDebug() << "AircraftInformation::readFastDB: Empty file"; + fclose(file); + } + else + qDebug() << "AircraftInformation::readFastDB: Failed to open " << filename; + + qDebug() << "AircraftInformation::readFastDB - read " << cnt << " aircraft"; + + return aircraftInfo; +} + +QString AircraftInformation::getFlag() const +{ + QString flag; + if (m_prefixMap) + { + int idx = m_registration.indexOf('-'); + if (idx >= 0) + { + QString prefix; + + // Some countries use AA-A - try these first as first letters are common + prefix = m_registration.left(idx + 2); + if (m_prefixMap->contains(prefix)) + { + flag = m_prefixMap->value(prefix); + } + else + { + // Try letters before '-' + prefix = m_registration.left(idx); + if (m_prefixMap->contains(prefix)) { + flag = m_prefixMap->value(prefix); + } + } + } + else + { + // No '-' Could be one of a few countries or military. + // See: https://en.wikipedia.org/wiki/List_of_aircraft_registration_prefixes + if (m_registration.startsWith("N")) { + flag = m_prefixMap->value("N"); // US + } else if (m_registration.startsWith("JA")) { + flag = m_prefixMap->value("JA"); // Japan + } else if (m_registration.startsWith("HL")) { + flag = m_prefixMap->value("HL"); // Korea + } else if (m_registration.startsWith("YV")) { + flag = m_prefixMap->value("YV"); // Venezuela + } else if ((m_militaryMap != nullptr) && (m_militaryMap->contains(m_operator))) { + flag = m_militaryMap->value(m_operator); + } + } + } + return flag; +} + +QString AircraftInformation::getAirlineIconPath(const QString &operatorICAO) +{ + QString endPath = QString("/airlinelogos/%1.bmp").arg(operatorICAO); + // Try in user directory first, so they can customise + QString userIconPath = OsnDB::getDataDir() + endPath; + QFile file(userIconPath); + if (file.exists()) + { + return userIconPath; + } + else + { + // Try in resources + QString resourceIconPath = ":" + endPath; + QResource resource(resourceIconPath); + if (resource.isValid()) + { + return resourceIconPath; + } + } + return QString(); +} + +QIcon *AircraftInformation::getAirlineIcon(const QString &operatorICAO) +{ + if (m_airlineIcons.contains(operatorICAO)) + { + return m_airlineIcons.value(operatorICAO); + } + else + { + QIcon *icon = nullptr; + QString path = getAirlineIconPath(operatorICAO); + if (!path.isEmpty()) + { + icon = new QIcon(path); + m_airlineIcons.insert(operatorICAO, icon); + } + else + { + if (!m_airlineMissingIcons.contains(operatorICAO)) + { + qDebug() << "ADSBDemodGUI: No airline logo for " << operatorICAO; + m_airlineMissingIcons.insert(operatorICAO, true); + } + } + return icon; + } +} +QString AircraftInformation::getFlagIconPath(const QString &country) +{ + QString endPath = QString("/flags/%1.bmp").arg(country); + // Try in user directory first, so they can customise + QString userIconPath = OsnDB::getDataDir() + endPath; + QFile file(userIconPath); + if (file.exists()) + { + return userIconPath; + } + else + { + // Try in resources + QString resourceIconPath = ":" + endPath; + QResource resource(resourceIconPath); + if (resource.isValid()) + { + return resourceIconPath; + } + } + return QString(); +} + +QIcon *AircraftInformation::getFlagIcon(const QString &country) +{ + if (m_flagIcons.contains(country)) + { + return m_flagIcons.value(country); + } + else + { + QIcon *icon = nullptr; + QString path = getFlagIconPath(country); + if (!path.isEmpty()) + { + icon = new QIcon(path); + m_flagIcons.insert(country, icon); + } + return icon; + } +} + diff --git a/sdrbase/util/osndb.h b/sdrbase/util/osndb.h index 67486b60c..9a6ede114 100644 --- a/sdrbase/util/osndb.h +++ b/sdrbase/util/osndb.h @@ -19,20 +19,19 @@ #define INCLUDE_OSNDB_H #include -#include -#include #include #include #include #include #include #include -#include +#include #include #include #include "util/csv.h" +#include "util/httpdownloadmanager.h" #include "export.h" #define OSNDB_URL "https://opensky-network.org/datasets/metadata/aircraftDatabase.zip" @@ -48,6 +47,34 @@ struct SDRBASE_API AircraftInformation { QString m_operatorICAO; QString m_registered; + static void init() + { + QMutexLocker locker(&m_mutex); + if (!m_prefixMap) + { + // Read registration prefix to country map + m_prefixMap = CSV::hash(":/flags/regprefixmap.csv"); + // Read operator air force to military map + m_militaryMap = CSV::hash(":/flags/militarymap.csv"); + } + } + + + // Get flag based on registration + QString getFlag() const; + + static QString getAirlineIconPath(const QString &operatorICAO); + + // Try to find an airline logo based on ICAO + static QIcon *getAirlineIcon(const QString &operatorICAO); + + static QString getFlagIconPath(const QString &country); + + // Try to find an flag logo based on a country + static QIcon *getFlagIcon(const QString &country); + +private: + static QHash m_airlineIcons; // Hashed on airline ICAO static QHash m_airlineMissingIcons; // Hash containing which ICAOs we don't have icons for static QHash m_flagIcons; // Hashed on country @@ -55,345 +82,20 @@ struct SDRBASE_API AircraftInformation { static QHash *m_militaryMap; // Operator airforce to military (flag name) static QMutex m_mutex; - static void init() - { - QMutexLocker locker(&m_mutex); +}; - // Read registration prefix to country map - m_prefixMap = CSV::hash(":/flags/regprefixmap.csv"); - // Read operator air force to military map - m_militaryMap = CSV::hash(":/flags/militarymap.csv"); - } +class SDRBASE_API OsnDB : public QObject { + Q_OBJECT - // Read OpenSky Network CSV file - // This is large and contains lots of data we don't want, so we convert to - // a smaller version to speed up loading time - // Note that we use C file functions rather than QT, as these are ~30% faster - // and the QT version seemed to occasionally crash - static QHash *readOSNDB(const QString &filename) - { - int cnt = 0; - QHash *aircraftInfo = nullptr; +public: - // Column numbers used for the data as of 2020/10/28 - int icaoCol = 0; - int registrationCol = 1; - int manufacturerNameCol = 3; - int modelCol = 4; - int ownerCol = 13; - int operatorCol = 9; - int operatorICAOCol = 11; - int registeredCol = 15; + OsnDB(QObject* parent=nullptr); + ~OsnDB(); - qDebug() << "AircraftInformation::readOSNDB: " << filename; + void downloadAircraftInformation(); - FILE *file; - QByteArray utfFilename = filename.toUtf8(); - if ((file = fopen(utfFilename.constData(), "r")) != NULL) - { - char row[2048]; - - if (fgets(row, sizeof(row), file)) - { - aircraftInfo = new QHash(); - aircraftInfo->reserve(500000); - // Read header - int idx = 0; - char *p = strtok(row, ","); - while (p != NULL) - { - if (!strcmp(p, "icao24")) - icaoCol = idx; - else if (!strcmp(p, "registration")) - registrationCol = idx; - else if (!strcmp(p, "manufacturername")) - manufacturerNameCol = idx; - else if (!strcmp(p, "model")) - modelCol = idx; - else if (!strcmp(p, "owner")) - ownerCol = idx; - else if (!strcmp(p, "operator")) - operatorCol = idx; - else if (!strcmp(p, "operatoricao")) - operatorICAOCol = idx; - else if (!strcmp(p, "registered")) - registeredCol = idx; - p = strtok(NULL, ","); - idx++; - } - // Read data - while (fgets(row, sizeof(row), file)) - { - int icao = 0; - char *icaoString = NULL; - char *registration = NULL; - size_t registrationLen = 0; - char *manufacturerName = NULL; - size_t manufacturerNameLen = 0; - char *model = NULL; - size_t modelLen = 0; - char *owner = NULL; - size_t ownerLen = 0; - char *operatorName = NULL; - size_t operatorNameLen = 0; - char *operatorICAO = NULL; - size_t operatorICAOLen = 0; - char *registered = NULL; - size_t registeredLen = 0; - - p = strtok(row, ","); - idx = 0; - while (p != NULL) - { - // Read strings, stripping quotes - if (idx == icaoCol) - { - icaoString = p+1; - icaoString[strlen(icaoString)-1] = '\0'; - icao = strtol(icaoString, NULL, 16); - } - else if (idx == registrationCol) - { - registration = p+1; - registrationLen = strlen(registration)-1; - registration[registrationLen] = '\0'; - } - else if (idx == manufacturerNameCol) - { - manufacturerName = p+1; - manufacturerNameLen = strlen(manufacturerName)-1; - manufacturerName[manufacturerNameLen] = '\0'; - } - else if (idx == modelCol) - { - model = p+1; - modelLen = strlen(model)-1; - model[modelLen] = '\0'; - } - else if (idx == ownerCol) - { - owner = p+1; - ownerLen = strlen(owner)-1; - owner[ownerLen] = '\0'; - } - else if (idx == operatorCol) - { - operatorName = p+1; - operatorNameLen = strlen(operatorName)-1; - operatorName[operatorNameLen] = '\0'; - } - else if (idx == operatorICAOCol) - { - operatorICAO = p+1; - operatorICAOLen = strlen(operatorICAO)-1; - operatorICAO[operatorICAOLen] = '\0'; - } - else if (idx == registeredCol) - { - registered = p+1; - registeredLen = strlen(registered)-1; - registered[registeredLen] = '\0'; - } - p = strtok(NULL, ","); - idx++; - } - - // Only create the entry if we have some interesting data - if ((icao != 0) && (registrationLen > 0 || modelLen > 0 || ownerLen > 0 || operatorNameLen > 0 || operatorICAOLen > 0)) - { - QString modelQ = QString(model); - // Tidy up the model names - if (modelQ.endsWith(" (Boeing)")) - modelQ = modelQ.left(modelQ.size() - 9); - else if (modelQ.startsWith("BOEING ")) - modelQ = modelQ.right(modelQ.size() - 7); - else if (modelQ.startsWith("Boeing ")) - modelQ = modelQ.right(modelQ.size() - 7); - else if (modelQ.startsWith("AIRBUS ")) - modelQ = modelQ.right(modelQ.size() - 7); - else if (modelQ.startsWith("Airbus ")) - modelQ = modelQ.right(modelQ.size() - 7); - else if (modelQ.endsWith(" (Cessna)")) - modelQ = modelQ.left(modelQ.size() - 9); - - AircraftInformation *aircraft = new AircraftInformation(); - aircraft->m_icao = icao; - aircraft->m_registration = QString(registration); - aircraft->m_manufacturerName = QString(manufacturerName); - aircraft->m_model = modelQ; - aircraft->m_owner = QString(owner); - aircraft->m_operator = QString(operatorName); - aircraft->m_operatorICAO = QString(operatorICAO); - aircraft->m_registered = QString(registered); - aircraftInfo->insert(icao, aircraft); - cnt++; - } - - } - } - fclose(file); - } - else - qDebug() << "AircraftInformation::readOSNDB: Failed to open " << filename; - - qDebug() << "AircraftInformation::readOSNDB: Read " << cnt << " aircraft"; - - return aircraftInfo; - } - - // Create hash table using registration as key - static QHash *registrationHash(const QHash *in) - { - QHash *out = new QHash(); - QHashIterator i(*in); - while (i.hasNext()) - { - i.next(); - AircraftInformation *info = i.value(); - out->insert(info->m_registration, info); - } - return out; - } - - // Write a reduced size and validated version of the DB, so it loads quicker - static bool writeFastDB(const QString &filename, QHash *aircraftInfo) - { - QFile file(filename); - if (file.open(QIODevice::WriteOnly)) - { - file.write("icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n"); - QHash::iterator i = aircraftInfo->begin(); - while (i != aircraftInfo->end()) - { - AircraftInformation *info = i.value(); - file.write(QString("%1").arg(info->m_icao, 1, 16).toUtf8()); - file.write(","); - file.write(info->m_registration.toUtf8()); - file.write(","); - file.write(info->m_manufacturerName.toUtf8()); - file.write(","); - file.write(info->m_model.toUtf8()); - file.write(","); - file.write(info->m_owner.toUtf8()); - file.write(","); - file.write(info->m_operator.toUtf8()); - file.write(","); - file.write(info->m_operatorICAO.toUtf8()); - file.write(","); - file.write(info->m_registered.toUtf8()); - file.write("\n"); - ++i; - } - file.close(); - return true; - } - else - { - qCritical() << "AircraftInformation::writeFastDB failed to open " << filename << " for writing: " << file.errorString(); - return false; - } - } - - // Read smaller CSV file with no validation. Takes about 0.5s instead of 2s. - static QHash *readFastDB(const QString &filename) - { - int cnt = 0; - QHash *aircraftInfo = nullptr; - - qDebug() << "AircraftInformation::readFastDB: " << filename; - - FILE *file; - QByteArray utfFilename = filename.toUtf8(); - if ((file = fopen(utfFilename.constData(), "r")) != NULL) - { - char row[2048]; - - if (fgets(row, sizeof(row), file)) - { - // Check header - if (!strcmp(row, "icao24,registration,manufacturername,model,owner,operator,operatoricao,registered\n")) - { - aircraftInfo = new QHash(); - aircraftInfo->reserve(500000); - // Read data - while (fgets(row, sizeof(row), file)) - { - char *p = row; - AircraftInformation *aircraft = new AircraftInformation(); - char *icaoString = csvNext(&p); - int icao = strtol(icaoString, NULL, 16); - aircraft->m_icao = icao; - aircraft->m_registration = QString(csvNext(&p)); - aircraft->m_manufacturerName = QString(csvNext(&p)); - aircraft->m_model = QString(csvNext(&p)); - aircraft->m_owner = QString(csvNext(&p)); - aircraft->m_operator = QString(csvNext(&p)); - aircraft->m_operatorICAO = QString(csvNext(&p)); - aircraft->m_registered = QString(csvNext(&p)); - aircraftInfo->insert(icao, aircraft); - cnt++; - } - } - else - qDebug() << "AircraftInformation::readFastDB: Unexpected header"; - } - else - qDebug() << "AircraftInformation::readFastDB: Empty file"; - fclose(file); - } - else - qDebug() << "AircraftInformation::readFastDB: Failed to open " << filename; - - qDebug() << "AircraftInformation::readFastDB - read " << cnt << " aircraft"; - - return aircraftInfo; - } - - // Get flag based on registration - QString getFlag() const - { - QString flag; - if (m_prefixMap) - { - int idx = m_registration.indexOf('-'); - if (idx >= 0) - { - QString prefix; - - // Some countries use AA-A - try these first as first letters are common - prefix = m_registration.left(idx + 2); - if (m_prefixMap->contains(prefix)) - { - flag = m_prefixMap->value(prefix); - } - else - { - // Try letters before '-' - prefix = m_registration.left(idx); - if (m_prefixMap->contains(prefix)) { - flag = m_prefixMap->value(prefix); - } - } - } - else - { - // No '-' Could be one of a few countries or military. - // See: https://en.wikipedia.org/wiki/List_of_aircraft_registration_prefixes - if (m_registration.startsWith("N")) { - flag = m_prefixMap->value("N"); // US - } else if (m_registration.startsWith("JA")) { - flag = m_prefixMap->value("JA"); // Japan - } else if (m_registration.startsWith("HL")) { - flag = m_prefixMap->value("HL"); // Korea - } else if (m_registration.startsWith("YV")) { - flag = m_prefixMap->value("YV"); // Venezuela - } else if ((m_militaryMap != nullptr) && (m_militaryMap->contains(m_operator))) { - flag = m_militaryMap->value(m_operator); - } - } - } - return flag; - } + static QSharedPointer> getAircraftInformation(); + static QSharedPointer> getAircraftInformationByReg(); static QString getOSNDBZipFilename() { @@ -405,11 +107,6 @@ struct SDRBASE_API AircraftInformation { return getDataDir() + "/aircraftDatabase.csv"; } - static QString getFastDBFilename() - { - return getDataDir() + "/aircraftDatabaseFast.csv"; - } - static QString getDataDir() { // Get directory to store app data in (aircraft & airport databases and user-definable icons) @@ -418,100 +115,44 @@ struct SDRBASE_API AircraftInformation { return locations[0]; } - static QString getAirlineIconPath(const QString &operatorICAO) +private: + HttpDownloadManager m_dlm; + + static QSharedPointer> m_aircraftInformation; + static QSharedPointer> m_aircraftInformationByReg; + static QDateTime m_modifiedDateTime; + + // Write a reduced size and validated version of the DB, so it loads quicker + static bool writeFastDB(const QString &filename, const QHash *aircraftInfo); + + // Read smaller CSV file with no validation. Takes about 0.5s instead of 2s. + static QHash *readFastDB(const QString &filename); + + // Read OpenSky Network CSV file + // This is large and contains lots of data we don't want, so we convert to + // a smaller version to speed up loading time + // Note that we use C file functions rather than QT, as these are ~30% faster + // and the QT version seemed to occasionally crash + static QHash *readOSNDB(const QString &filename); + + static QString getFastDBFilename() { - QString endPath = QString("/airlinelogos/%1.bmp").arg(operatorICAO); - // Try in user directory first, so they can customise - QString userIconPath = getDataDir() + endPath; - QFile file(userIconPath); - if (file.exists()) - { - return userIconPath; - } - else - { - // Try in resources - QString resourceIconPath = ":" + endPath; - QResource resource(resourceIconPath); - if (resource.isValid()) - { - return resourceIconPath; - } - } - return QString(); + return getDataDir() + "/aircraftDatabaseFast.csv"; } - // Try to find an airline logo based on ICAO - static QIcon *getAirlineIcon(const QString &operatorICAO) - { - if (m_airlineIcons.contains(operatorICAO)) - { - return m_airlineIcons.value(operatorICAO); - } - else - { - QIcon *icon = nullptr; - QString path = getAirlineIconPath(operatorICAO); - if (!path.isEmpty()) - { - icon = new QIcon(path); - m_airlineIcons.insert(operatorICAO, icon); - } - else - { - if (!m_airlineMissingIcons.contains(operatorICAO)) - { - qDebug() << "ADSBDemodGUI: No airline logo for " << operatorICAO; - m_airlineMissingIcons.insert(operatorICAO, true); - } - } - return icon; - } - } + // Create hash table using registration as key + static QHash *registrationHash(const QHash *in); - static QString getFlagIconPath(const QString &country) - { - QString endPath = QString("/flags/%1.bmp").arg(country); - // Try in user directory first, so they can customise - QString userIconPath = getDataDir() + endPath; - QFile file(userIconPath); - if (file.exists()) - { - return userIconPath; - } - else - { - // Try in resources - QString resourceIconPath = ":" + endPath; - QResource resource(resourceIconPath); - if (resource.isValid()) - { - return resourceIconPath; - } - } - return QString(); - } +private slots: + void downloadFinished(const QString& filename, bool success); - // Try to find an flag logo based on a country - static QIcon *getFlagIcon(const QString &country) - { - if (m_flagIcons.contains(country)) - { - return m_flagIcons.value(country); - } - else - { - QIcon *icon = nullptr; - QString path = getFlagIconPath(country); - if (!path.isEmpty()) - { - icon = new QIcon(path); - m_flagIcons.insert(country, icon); - } - return icon; - } - } +signals: + void downloadingURL(const QString& url); + void downloadError(const QString& error); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadAircraftInformationFinished(); }; #endif + diff --git a/sdrbase/util/ourairportsdb.cpp b/sdrbase/util/ourairportsdb.cpp new file mode 100644 index 000000000..cd993e83f --- /dev/null +++ b/sdrbase/util/ourairportsdb.cpp @@ -0,0 +1,419 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 +#include +#include +#include +#include +#include +#include + +#include "util/ourairportsdb.h" + +QMutex OurAirportsDB::m_mutex; +QSharedPointer> OurAirportsDB::m_airportsById; +QSharedPointer> OurAirportsDB::m_airportsByIdent; +QDateTime OurAirportsDB::m_modifiedDateTime; + +AirportInformation::~AirportInformation() +{ + qDeleteAll(m_frequencies); +} + +QString AirportInformation::getImageName() const +{ + switch (m_type) + { + case AirportType::Large: + return "airport_large.png"; + case AirportType::Medium: + return "airport_medium.png"; + case AirportType::Heliport: + return "heliport.png"; + default: + return "airport_small.png"; + } +} + +OurAirportsDB::OurAirportsDB(QObject *parent) : + QObject(parent) +{ + connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OurAirportsDB::downloadFinished); +} + +OurAirportsDB::~OurAirportsDB() +{ + disconnect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &OurAirportsDB::downloadFinished); +} + +void OurAirportsDB::downloadAirportInformation() +{ + // Download airport database + QString urlString = AIRPORTS_URL; + QUrl dbURL(urlString); + qDebug() << "OurAirportsDB::downloadAirportInformation: Downloading " << urlString; + emit downloadingURL(urlString); + QNetworkReply *reply = m_dlm.download(dbURL, OurAirportsDB::getAirportDBFilename()); + connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) { + emit downloadProgress(bytesRead, totalBytes); + }); +} + +void OurAirportsDB::downloadFinished(const QString& filename, bool success) +{ + if (!success) + { + qWarning() << "OurAirportsDB::downloadFinished: Failed to download: " << filename; + emit downloadError(QString("Failed to download: %1").arg(filename)); + } + else if (filename == OurAirportsDB::getAirportDBFilename()) + { + // Now download airport frequencies + QString urlString = AIRPORT_FREQUENCIES_URL; + QUrl dbURL(urlString); + qDebug() << "OurAirportsDB::downloadFinished: Downloading " << urlString; + emit downloadingURL(urlString); + QNetworkReply *reply = m_dlm.download(dbURL, OurAirportsDB::getAirportFrequenciesDBFilename()); + connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesRead, qint64 totalBytes) { + emit downloadProgress(bytesRead, totalBytes); + }); + } + else if (filename == OurAirportsDB::getAirportFrequenciesDBFilename()) + { + emit downloadAirportInformationFinished(); + } + else + { + qDebug() << "OurAirportsDB::downloadFinished: Unexpected filename: " << filename; + emit downloadError(QString("Unexpected filename: %1").arg(filename)); + } +} + +QSharedPointer> OurAirportsDB::getAirportsById() +{ + QMutexLocker locker(&m_mutex); + + readDB(); + return m_airportsById; +} + +QSharedPointer> OurAirportsDB::getAirportsByIdent() +{ + QMutexLocker locker(&m_mutex); + + readDB(); + return m_airportsByIdent; +} + +void OurAirportsDB::readDB() +{ + QFileInfo airportDBFileInfo(getAirportDBFilename()); + QDateTime airportDBModifiedDateTime = airportDBFileInfo.lastModified(); + + if (!m_airportsById || (airportDBModifiedDateTime > m_modifiedDateTime)) + { + // Using shared pointer, so old object, if it exists, will be deleted, when no longer user + m_airportsById = QSharedPointer>(OurAirportsDB::readAirportsDB(getAirportDBFilename())); + if (m_airportsById != nullptr) + { + OurAirportsDB::readFrequenciesDB(getAirportFrequenciesDBFilename(), m_airportsById.get()); + m_airportsByIdent = QSharedPointer>(identHash(m_airportsById.get())); + } + + m_modifiedDateTime = airportDBModifiedDateTime; + } +} + +QString OurAirportsDB::getDataDir() +{ + // Get directory to store app data in + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + // First dir is writable + return locations[0]; +} + +QString OurAirportsDB::getAirportDBFilename() +{ + return getDataDir() + "/airportDatabase.csv"; +} + +QString OurAirportsDB::getAirportFrequenciesDBFilename() +{ + return getDataDir() + "/airportFrequenciesDatabase.csv"; +} + +QString OurAirportsDB::trimQuotes(const QString s) +{ + if (s.startsWith('\"') && s.endsWith('\"')) { + return s.mid(1, s.size() - 2); + } else { + return s; + } +} + +// Read OurAirport's airport CSV file +// See comments for readOSNDB +QHash *OurAirportsDB::readAirportsDB(const QString &filename) +{ + int cnt = 0; + QHash *airportInfo = nullptr; + + // Column numbers used for the data as of 2020/10/28 + int idCol = 0; + int identCol = 1; + int typeCol = 2; + int nameCol = 3; + int latitudeCol = 4; + int longitudeCol = 5; + int elevationCol = 6; + + qDebug() << "OurAirportsDB::readAirportsDB: " << filename; + + FILE *file; + QByteArray utfFilename = filename.toUtf8(); + QLocale cLocale(QLocale::C); + if ((file = fopen(utfFilename.constData(), "r")) != NULL) + { + char row[2048]; + + if (fgets(row, sizeof(row), file)) + { + airportInfo = new QHash(); + airportInfo->reserve(70000); + + // Read header + int idx = 0; + char *p = strtok(row, ","); + while (p != NULL) + { + if (!strcmp(p, "id")) + idCol = idx; + else if (!strcmp(p, "ident")) + identCol = idx; + else if (!strcmp(p, "type")) + typeCol = idx; + else if (!strcmp(p, "name")) + nameCol = idx; + else if (!strcmp(p, "latitude_deg")) + latitudeCol = idx; + else if (!strcmp(p, "longitude_deg")) + longitudeCol = idx; + else if (!strcmp(p, "elevation_ft")) + elevationCol = idx; + p = strtok(NULL, ","); + idx++; + } + // Read data + while (fgets(row, sizeof(row), file)) + { + int id = 0; + char *idString = NULL; + char *ident = NULL; + size_t identLen = 0; + char *type = NULL; + size_t typeLen = 0; + char *name = NULL; + size_t nameLen = 0; + float latitude = 0.0f; + char *latitudeString = NULL; + size_t latitudeLen = 0; + float longitude = 0.0f; + char *longitudeString = NULL; + size_t longitudeLen = 0; + float elevation = 0.0f; + char *elevationString = NULL; + size_t elevationLen = 0; + + p = strtok(row, ","); + idx = 0; + while (p != NULL) + { + // Read strings, stripping quotes + if (idx == idCol) + { + idString = p; + idString[strlen(idString)] = '\0'; + id = strtol(idString, NULL, 10); + } + else if (idx == identCol) + { + ident = p+1; + identLen = strlen(ident)-1; + ident[identLen] = '\0'; + } + else if (idx == typeCol) + { + type = p+1; + typeLen = strlen(type)-1; + type[typeLen] = '\0'; + } + else if (idx == nameCol) + { + name = p+1; + nameLen = strlen(name)-1; + name[nameLen] = '\0'; + } + else if (idx == latitudeCol) + { + latitudeString = p; + latitudeLen = strlen(latitudeString)-1; + latitudeString[latitudeLen] = '\0'; + latitude = cLocale.toFloat(latitudeString); + } + else if (idx == longitudeCol) + { + longitudeString = p; + longitudeLen = strlen(longitudeString)-1; + longitudeString[longitudeLen] = '\0'; + longitude = cLocale.toFloat(longitudeString); + } + else if (idx == elevationCol) + { + elevationString = p; + elevationLen = strlen(elevationString)-1; + elevationString[elevationLen] = '\0'; + elevation = cLocale.toFloat(elevationString); + } + p = strtok(NULL, ","); + idx++; + } + + // Only create the entry if we have some interesting data + if (((latitude != 0.0f) || (longitude != 0.0f)) && (type && strcmp(type, "closed"))) + { + AirportInformation *airport = new AirportInformation(); + airport->m_id = id; + airport->m_ident = QString(ident); + if (!strcmp(type, "small_airport")) { + airport->m_type = AirportInformation::AirportType::Small; + } else if (!strcmp(type, "medium_airport")) { + airport->m_type = AirportInformation::AirportType::Medium; + } else if (!strcmp(type, "large_airport")) { + airport->m_type = AirportInformation::AirportType::Large; + } else if (!strcmp(type, "heliport")) { + airport->m_type = AirportInformation::AirportType::Heliport; + } + airport->m_name = QString(name); + airport->m_latitude = latitude; + airport->m_longitude = longitude; + airport->m_elevation = elevation; + airportInfo->insert(id, airport); + cnt++; + } + } + } + fclose(file); + } + else + qDebug() << "OurAirportsDB::readAirportsDB: Failed to open " << filename; + + qDebug() << "OurAirportsDB::readAirportsDB: Read " << cnt << " airports"; + + return airportInfo; +} + +// Create hash table using ICAO identifier as key +QHash *OurAirportsDB::identHash(QHash *in) +{ + QHash *out = new QHash(); + QHashIterator i(*in); + while (i.hasNext()) + { + i.next(); + AirportInformation *info = i.value(); + out->insert(info->m_ident, info); + } + return out; +} + +// Read OurAirport's airport frequencies CSV file +bool OurAirportsDB::readFrequenciesDB(const QString &filename, QHash *airportInfo) +{ + int cnt = 0; + + // Column numbers used for the data as of 2020/10/28 + int airportRefCol = 1; + int typeCol = 3; + int descriptionCol = 4; + int frequencyCol = 5; + + qDebug() << "OurAirportsDB::readFrequenciesDB: " << filename; + + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) + { + QList colNames; + int idx; + + // Read header + if (!file.atEnd()) + { + QByteArray row = file.readLine().trimmed(); + colNames = row.split(','); + // Work out which columns the data is in, based on the headers + idx = colNames.indexOf("airport_ref"); + if (idx >= 0) + airportRefCol = idx; + idx = colNames.indexOf("type"); + if (idx >= 0) + typeCol = idx; + idx = colNames.indexOf("descrption"); + if (idx >= 0) + descriptionCol = idx; + idx = colNames.indexOf("frequency_mhz"); + if (idx >= 0) + frequencyCol = idx; + } + // Read data + while (!file.atEnd()) + { + QByteArray row = file.readLine(); + QList cols = row.split(','); + + bool ok = false; + int airportRef = cols[airportRefCol].toInt(&ok, 10); + if (ok) + { + if (airportInfo->contains(airportRef)) + { + QString type = trimQuotes(cols[typeCol]); + QString description = trimQuotes(cols[descriptionCol]); + float frequency = cols[frequencyCol].toFloat(); + + AirportInformation::FrequencyInformation *frequencyInfo = new AirportInformation::FrequencyInformation(); + frequencyInfo->m_type = type; + frequencyInfo->m_description = description; + frequencyInfo->m_frequency = frequency; + airportInfo->value(airportRef)->m_frequencies.append(frequencyInfo); + cnt++; + } + } + } + file.close(); + } + else + { + qDebug() << "Failed to open " << filename << " " << file.errorString(); + return false; + } + + qDebug() << "OurAirportsDB::readFrequenciesDB: - read " << cnt << " airports"; + + return true; +} + diff --git a/sdrbase/util/ourairportsdb.h b/sdrbase/util/ourairportsdb.h new file mode 100644 index 000000000..14d2600d9 --- /dev/null +++ b/sdrbase/util/ourairportsdb.h @@ -0,0 +1,119 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_UTIL_OURAIRPORTSDB_H +#define INCLUDE_UTIL_OURAIRPORTSDB_H + +#include +#include +#include +#include + +#include +#include + +#include "util/csv.h" +#include "util/httpdownloadmanager.h" +#include "export.h" + +#define AIRPORTS_URL "https://davidmegginson.github.io/ourairports-data/airports.csv" +#define AIRPORT_FREQUENCIES_URL "https://davidmegginson.github.io/ourairports-data/airport-frequencies.csv" + +class SDRBASE_API AirportInformation { + +public: + enum AirportType { + Small, + Medium, + Large, + Heliport + }; + + struct FrequencyInformation { + QString m_type; + QString m_description; + float m_frequency; // In MHz + }; + + int m_id; + QString m_ident; + AirportType m_type; + QString m_name; + float m_latitude; + float m_longitude; + float m_elevation; + QVector m_frequencies; + + ~AirportInformation(); + QString getImageName() const; + +}; + +class SDRBASE_API OurAirportsDB : public QObject { + Q_OBJECT + +public: + + OurAirportsDB(QObject *parent=nullptr); + ~OurAirportsDB(); + + void downloadAirportInformation(); + + static QSharedPointer> getAirportsById(); + static QSharedPointer> getAirportsByIdent(); + +private: + HttpDownloadManager m_dlm; + + static QSharedPointer> m_airportsById; + static QSharedPointer> m_airportsByIdent; + static QDateTime m_modifiedDateTime; + + static QMutex m_mutex; + + static QString getDataDir(); + + static void readDB(); + + // Read OurAirport's airport CSV file + // See comments for readOSNDB + static QHash *readAirportsDB(const QString &filename); + + // Create hash table using ICAO identifier as key + static QHash *identHash(QHash *in); + + // Read OurAirport's airport frequencies CSV file + static bool readFrequenciesDB(const QString &filename, QHash *airportInfo); + + static QString trimQuotes(const QString s); + + static QString getAirportDBFilename(); + static QString getAirportFrequenciesDBFilename(); + +private slots: + void downloadFinished(const QString& filename, bool success); + +signals: + void downloadingURL(const QString& url); + void downloadError(const QString& error); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadAirportInformationFinished(); + +}; + +#endif + diff --git a/sdrgui/gui/graphicsdialog.cpp b/sdrgui/gui/graphicsdialog.cpp index 7da623336..f4a821397 100644 --- a/sdrgui/gui/graphicsdialog.cpp +++ b/sdrgui/gui/graphicsdialog.cpp @@ -32,6 +32,12 @@ GraphicsDialog::GraphicsDialog(MainSettings& mainSettings, QWidget* parent) : } else { ui->multisampling->setCurrentText(QString::number(samples)); } + samples = m_mainSettings.getMapMultisampling(); + if (samples == 0) { + ui->mapMultisampling->setCurrentText("Off"); + } else { + ui->mapMultisampling->setCurrentText(QString::number(samples)); + } } GraphicsDialog::~GraphicsDialog() @@ -42,5 +48,6 @@ GraphicsDialog::~GraphicsDialog() void GraphicsDialog::accept() { m_mainSettings.setMultisampling(ui->multisampling->currentText().toInt()); + m_mainSettings.setMapMultisampling(ui->mapMultisampling->currentText().toInt()); QDialog::accept(); } diff --git a/sdrgui/gui/graphicsdialog.ui b/sdrgui/gui/graphicsdialog.ui index 9dc2642c2..f37a06b25 100644 --- a/sdrgui/gui/graphicsdialog.ui +++ b/sdrgui/gui/graphicsdialog.ui @@ -6,8 +6,8 @@ 0 0 - 277 - 98 + 282 + 131 @@ -23,49 +23,106 @@ - - - - 150 - 0 - + + + Anti-aliasing - - Multisampling (MSAA) - - - - - - - - 80 - 0 - - - - Number of samples to use for mulitsampling anti-aliasing (MSAA) - Requires windows to be reopened - - - - Off - - - - - 2 - - - - - 4 - - - - - 8 - - + + + + + + 150 + 0 + + + + Spectrum (MSAA) + + + + + + + + 80 + 0 + + + + Number of samples to use for mulitsampling anti-aliasing (MSAA) for spectrums + +Requires windows to be reopened to take effect + + + + Off + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + + + + Number of samples to use for mulitsampling anti-aliasing (MSAA) for 2D maps + +Requires windows to be reopened to take effect + + + 2D Map (MSAA) + + + + + + + + Off + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + diff --git a/swagger/sdrangel/api/swagger/include/Map.yaml b/swagger/sdrangel/api/swagger/include/Map.yaml index 5909000b5..e8c9b306c 100644 --- a/swagger/sdrangel/api/swagger/include/Map.yaml +++ b/swagger/sdrangel/api/swagger/include/Map.yaml @@ -118,7 +118,7 @@ MapItem: type: number format: float altitudeReference: - description: "0 - NONE (Absolule), 1 - CLAMP_TO_GROUND, 2 - RELATIVE_TO_GROUND, 3 - CLIP_TO_GROUND" + description: "0 - NONE (Absolute), 1 - CLAMP_TO_GROUND, 2 - RELATIVE_TO_GROUND, 3 - CLIP_TO_GROUND" type: integer animations: description: "Animations to play" @@ -126,7 +126,7 @@ MapItem: items: $ref: "http://swgserver:8081/api/swagger/include/Map.yaml#/MapAnimation" type: - description: "(0 - Map Item, 1 - Image Tile)" + description: "(0 - Map Item, 1 - Image Tile, 2 - Polygon, 3 - Polyline)" type: integer imageTileWest: type: number @@ -140,6 +140,22 @@ MapItem: imageTileNorth: type: number format: float + imageZoomLevel: + description: "For 2D map" + type: number + format: float + coordinates: + description: "Polygon/polyline coordinates" + type: array + items: + $ref: "http://swgserver:8081/api/swagger/include/Map.yaml#/MapCoordinate" + extrudedHeight: + description: "Extruded height (from surface) for polygons" + type: number + format: float + availableUntil: + description: "Date and time until after which this item should no longer appear on 3D map" + type: string MapAnimation: description: "Animation to play in the model on the 3D map" diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp b/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp index 9608112a6..0b00c0769 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp @@ -82,6 +82,14 @@ SWGMapItem::SWGMapItem() { m_image_tile_east_isSet = false; image_tile_north = 0.0f; m_image_tile_north_isSet = false; + image_zoom_level = 0.0f; + m_image_zoom_level_isSet = false; + coordinates = nullptr; + m_coordinates_isSet = false; + extruded_height = 0.0f; + m_extruded_height_isSet = false; + available_until = nullptr; + m_available_until_isSet = false; } SWGMapItem::~SWGMapItem() { @@ -144,6 +152,14 @@ SWGMapItem::init() { m_image_tile_east_isSet = false; image_tile_north = 0.0f; m_image_tile_north_isSet = false; + image_zoom_level = 0.0f; + m_image_zoom_level_isSet = false; + coordinates = new QList(); + m_coordinates_isSet = false; + extruded_height = 0.0f; + m_extruded_height_isSet = false; + available_until = new QString(""); + m_available_until_isSet = false; } void @@ -207,6 +223,18 @@ SWGMapItem::cleanup() { + + if(coordinates != nullptr) { + auto arr = coordinates; + for(auto o: *arr) { + delete o; + } + delete coordinates; + } + + if(available_until != nullptr) { + delete available_until; + } } SWGMapItem* @@ -274,6 +302,14 @@ SWGMapItem::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&image_tile_north, pJson["imageTileNorth"], "float", ""); + ::SWGSDRangel::setValue(&image_zoom_level, pJson["imageZoomLevel"], "float", ""); + + + ::SWGSDRangel::setValue(&coordinates, pJson["coordinates"], "QList", "SWGMapCoordinate"); + ::SWGSDRangel::setValue(&extruded_height, pJson["extrudedHeight"], "float", ""); + + ::SWGSDRangel::setValue(&available_until, pJson["availableUntil"], "QString", "QString"); + } QString @@ -371,6 +407,18 @@ SWGMapItem::asJsonObject() { if(m_image_tile_north_isSet){ obj->insert("imageTileNorth", QJsonValue(image_tile_north)); } + if(m_image_zoom_level_isSet){ + obj->insert("imageZoomLevel", QJsonValue(image_zoom_level)); + } + if(coordinates && coordinates->size() > 0){ + toJsonArray((QList*)coordinates, obj, "coordinates", "SWGMapCoordinate"); + } + if(m_extruded_height_isSet){ + obj->insert("extrudedHeight", QJsonValue(extruded_height)); + } + if(available_until != nullptr && *available_until != QString("")){ + toJsonValue(QString("availableUntil"), available_until, obj, QString("QString")); + } return obj; } @@ -645,6 +693,46 @@ SWGMapItem::setImageTileNorth(float image_tile_north) { this->m_image_tile_north_isSet = true; } +float +SWGMapItem::getImageZoomLevel() { + return image_zoom_level; +} +void +SWGMapItem::setImageZoomLevel(float image_zoom_level) { + this->image_zoom_level = image_zoom_level; + this->m_image_zoom_level_isSet = true; +} + +QList* +SWGMapItem::getCoordinates() { + return coordinates; +} +void +SWGMapItem::setCoordinates(QList* coordinates) { + this->coordinates = coordinates; + this->m_coordinates_isSet = true; +} + +float +SWGMapItem::getExtrudedHeight() { + return extruded_height; +} +void +SWGMapItem::setExtrudedHeight(float extruded_height) { + this->extruded_height = extruded_height; + this->m_extruded_height_isSet = true; +} + +QString* +SWGMapItem::getAvailableUntil() { + return available_until; +} +void +SWGMapItem::setAvailableUntil(QString* available_until) { + this->available_until = available_until; + this->m_available_until_isSet = true; +} + bool SWGMapItem::isSet(){ @@ -731,6 +819,18 @@ SWGMapItem::isSet(){ if(m_image_tile_north_isSet){ isObjectUpdated = true; break; } + if(m_image_zoom_level_isSet){ + isObjectUpdated = true; break; + } + if(coordinates && (coordinates->size() > 0)){ + isObjectUpdated = true; break; + } + if(m_extruded_height_isSet){ + isObjectUpdated = true; break; + } + if(available_until && *available_until != QString("")){ + isObjectUpdated = true; break; + } }while(false); return isObjectUpdated; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem.h b/swagger/sdrangel/code/qt5/client/SWGMapItem.h index 871f682bc..b807fcaa5 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem.h @@ -126,6 +126,18 @@ public: float getImageTileNorth(); void setImageTileNorth(float image_tile_north); + float getImageZoomLevel(); + void setImageZoomLevel(float image_zoom_level); + + QList* getCoordinates(); + void setCoordinates(QList* coordinates); + + float getExtrudedHeight(); + void setExtrudedHeight(float extruded_height); + + QString* getAvailableUntil(); + void setAvailableUntil(QString* available_until); + virtual bool isSet() override; @@ -211,6 +223,18 @@ private: float image_tile_north; bool m_image_tile_north_isSet; + float image_zoom_level; + bool m_image_zoom_level_isSet; + + QList* coordinates; + bool m_coordinates_isSet; + + float extruded_height; + bool m_extruded_height_isSet; + + QString* available_until; + bool m_available_until_isSet; + }; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp index 3a26b3c18..5d357c82b 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp @@ -82,6 +82,14 @@ SWGMapItem_2::SWGMapItem_2() { m_image_tile_east_isSet = false; image_tile_north = 0.0f; m_image_tile_north_isSet = false; + image_zoom_level = 0.0f; + m_image_zoom_level_isSet = false; + coordinates = nullptr; + m_coordinates_isSet = false; + extruded_height = 0.0f; + m_extruded_height_isSet = false; + available_until = nullptr; + m_available_until_isSet = false; } SWGMapItem_2::~SWGMapItem_2() { @@ -144,6 +152,14 @@ SWGMapItem_2::init() { m_image_tile_east_isSet = false; image_tile_north = 0.0f; m_image_tile_north_isSet = false; + image_zoom_level = 0.0f; + m_image_zoom_level_isSet = false; + coordinates = new QList(); + m_coordinates_isSet = false; + extruded_height = 0.0f; + m_extruded_height_isSet = false; + available_until = new QString(""); + m_available_until_isSet = false; } void @@ -207,6 +223,18 @@ SWGMapItem_2::cleanup() { + + if(coordinates != nullptr) { + auto arr = coordinates; + for(auto o: *arr) { + delete o; + } + delete coordinates; + } + + if(available_until != nullptr) { + delete available_until; + } } SWGMapItem_2* @@ -274,6 +302,14 @@ SWGMapItem_2::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&image_tile_north, pJson["imageTileNorth"], "float", ""); + ::SWGSDRangel::setValue(&image_zoom_level, pJson["imageZoomLevel"], "float", ""); + + + ::SWGSDRangel::setValue(&coordinates, pJson["coordinates"], "QList", "SWGMapCoordinate"); + ::SWGSDRangel::setValue(&extruded_height, pJson["extrudedHeight"], "float", ""); + + ::SWGSDRangel::setValue(&available_until, pJson["availableUntil"], "QString", "QString"); + } QString @@ -371,6 +407,18 @@ SWGMapItem_2::asJsonObject() { if(m_image_tile_north_isSet){ obj->insert("imageTileNorth", QJsonValue(image_tile_north)); } + if(m_image_zoom_level_isSet){ + obj->insert("imageZoomLevel", QJsonValue(image_zoom_level)); + } + if(coordinates && coordinates->size() > 0){ + toJsonArray((QList*)coordinates, obj, "coordinates", "SWGMapCoordinate"); + } + if(m_extruded_height_isSet){ + obj->insert("extrudedHeight", QJsonValue(extruded_height)); + } + if(available_until != nullptr && *available_until != QString("")){ + toJsonValue(QString("availableUntil"), available_until, obj, QString("QString")); + } return obj; } @@ -645,6 +693,46 @@ SWGMapItem_2::setImageTileNorth(float image_tile_north) { this->m_image_tile_north_isSet = true; } +float +SWGMapItem_2::getImageZoomLevel() { + return image_zoom_level; +} +void +SWGMapItem_2::setImageZoomLevel(float image_zoom_level) { + this->image_zoom_level = image_zoom_level; + this->m_image_zoom_level_isSet = true; +} + +QList* +SWGMapItem_2::getCoordinates() { + return coordinates; +} +void +SWGMapItem_2::setCoordinates(QList* coordinates) { + this->coordinates = coordinates; + this->m_coordinates_isSet = true; +} + +float +SWGMapItem_2::getExtrudedHeight() { + return extruded_height; +} +void +SWGMapItem_2::setExtrudedHeight(float extruded_height) { + this->extruded_height = extruded_height; + this->m_extruded_height_isSet = true; +} + +QString* +SWGMapItem_2::getAvailableUntil() { + return available_until; +} +void +SWGMapItem_2::setAvailableUntil(QString* available_until) { + this->available_until = available_until; + this->m_available_until_isSet = true; +} + bool SWGMapItem_2::isSet(){ @@ -731,6 +819,18 @@ SWGMapItem_2::isSet(){ if(m_image_tile_north_isSet){ isObjectUpdated = true; break; } + if(m_image_zoom_level_isSet){ + isObjectUpdated = true; break; + } + if(coordinates && (coordinates->size() > 0)){ + isObjectUpdated = true; break; + } + if(m_extruded_height_isSet){ + isObjectUpdated = true; break; + } + if(available_until && *available_until != QString("")){ + isObjectUpdated = true; break; + } }while(false); return isObjectUpdated; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h index 809f1d932..fdf07802f 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h @@ -126,6 +126,18 @@ public: float getImageTileNorth(); void setImageTileNorth(float image_tile_north); + float getImageZoomLevel(); + void setImageZoomLevel(float image_zoom_level); + + QList* getCoordinates(); + void setCoordinates(QList* coordinates); + + float getExtrudedHeight(); + void setExtrudedHeight(float extruded_height); + + QString* getAvailableUntil(); + void setAvailableUntil(QString* available_until); + virtual bool isSet() override; @@ -211,6 +223,18 @@ private: float image_tile_north; bool m_image_tile_north_isSet; + float image_zoom_level; + bool m_image_zoom_level_isSet; + + QList* coordinates; + bool m_coordinates_isSet; + + float extruded_height; + bool m_extruded_height_isSet; + + QString* available_until; + bool m_available_until_isSet; + }; }