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 000000000..d33c9d660 Binary files /dev/null and b/plugins/feature/map/icons/controltower.png differ diff --git a/plugins/feature/map/icons/vor.png b/plugins/feature/map/icons/vor.png new file mode 100644 index 000000000..ac66aad04 Binary files /dev/null and b/plugins/feature/map/icons/vor.png differ diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc index 98f30fc48..b8a82c70f 100644 --- a/plugins/feature/map/map.qrc +++ b/plugins/feature/map/map.qrc @@ -8,6 +8,18 @@ map/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 000000000..f95bac447 Binary files /dev/null and b/plugins/feature/map/map/DME.png differ diff --git a/plugins/feature/map/map/DVOR-DME.png b/plugins/feature/map/map/DVOR-DME.png new file mode 100644 index 000000000..c2e2c412d Binary files /dev/null and b/plugins/feature/map/map/DVOR-DME.png differ diff --git a/plugins/feature/map/map/DVOR.png b/plugins/feature/map/map/DVOR.png new file mode 100644 index 000000000..0d6949fde Binary files /dev/null and b/plugins/feature/map/map/DVOR.png differ diff --git a/plugins/feature/map/map/DVORTAC.png b/plugins/feature/map/map/DVORTAC.png new file mode 100644 index 000000000..f7df0a0b3 Binary files /dev/null and b/plugins/feature/map/map/DVORTAC.png differ diff --git a/plugins/feature/map/map/NDB.png b/plugins/feature/map/map/NDB.png new file mode 100644 index 000000000..4987f3be0 Binary files /dev/null and b/plugins/feature/map/map/NDB.png differ diff --git a/plugins/feature/map/map/VOR-DME.png b/plugins/feature/map/map/VOR-DME.png new file mode 100644 index 000000000..c2e2c412d Binary files /dev/null and b/plugins/feature/map/map/VOR-DME.png differ diff --git a/plugins/feature/map/map/VOR.png b/plugins/feature/map/map/VOR.png new file mode 100644 index 000000000..0d6949fde Binary files /dev/null and b/plugins/feature/map/map/VOR.png differ diff --git a/plugins/feature/map/map/VORTAC.png b/plugins/feature/map/map/VORTAC.png new file mode 100644 index 000000000..f7df0a0b3 Binary files /dev/null and b/plugins/feature/map/map/VORTAC.png differ diff --git a/plugins/feature/map/map/airport_large.png b/plugins/feature/map/map/airport_large.png new file mode 100644 index 000000000..72299fcf6 Binary files /dev/null and b/plugins/feature/map/map/airport_large.png differ diff --git a/plugins/feature/map/map/airport_medium.png b/plugins/feature/map/map/airport_medium.png new file mode 100644 index 000000000..8347f8b3d Binary files /dev/null and b/plugins/feature/map/map/airport_medium.png differ diff --git a/plugins/feature/map/map/airport_small.png b/plugins/feature/map/map/airport_small.png new file mode 100644 index 000000000..2509056d6 Binary files /dev/null and b/plugins/feature/map/map/airport_small.png differ diff --git a/plugins/feature/map/map/heliport.png b/plugins/feature/map/map/heliport.png new file mode 100644 index 000000000..8241b4282 Binary files /dev/null and b/plugins/feature/map/map/heliport.png differ diff --git a/plugins/feature/map/map/map.qml b/plugins/feature/map/map/map.qml index b1ab8c4ec..ab8c81775 100644 --- a/plugins/feature/map/map/map.qml +++ b/plugins/feature/map/map/map.qml @@ -67,40 +67,73 @@ Item { gesture.enabled: true gesture.acceptedGestures: MapGestureArea.PinchGesture | MapGestureArea.PanGesture + MapItemView { + model: imageModelFiltered + delegate: imageComponent + } + + MapItemView { + model: polygonModelFiltered + delegate: polygonComponent + } + + MapItemView { + model: polygonModelFiltered + delegate: polygonNameComponent + } + + MapItemView { + model: polylineModelFiltered + delegate: polylineComponent + } + + MapItemView { + model: polylineModelFiltered + delegate: polylineNameComponent + } + // Tracks first, so drawn under other items MapItemView { - model: mapModel + model: mapModelFiltered delegate: groundTrack1Component } MapItemView { - model: mapModel + model: mapModelFiltered delegate: groundTrack2Component } MapItemView { - model: mapModel + model: mapModelFiltered delegate: predictedGroundTrack1Component } MapItemView { - model: mapModel + model: mapModelFiltered delegate: predictedGroundTrack2Component } MapItemView { - model: mapModel + model: mapModelFiltered delegate: mapComponent } onZoomLevelChanged: { mapZoomLevel = zoomLevel + mapModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); + imageModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); + polygonModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); + polylineModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); } // The map displays MapPolyLines in the wrong place (+360 degrees) if // they start to the left of the visible region, so we need to // split them so they don't, each time the visible region is changed. meh. onCenterChanged: { + polylineModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); + polygonModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); + imageModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); + mapModelFiltered.viewChanged(visibleRegion.boundingGeoRectangle().topLeft.longitude, visibleRegion.boundingGeoRectangle().topLeft.latitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude, visibleRegion.boundingGeoRectangle().bottomRight.latitude, zoomLevel); mapModel.viewChanged(visibleRegion.boundingGeoRectangle().bottomLeft.longitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude); } @@ -118,6 +151,86 @@ Item { return null; } + Component { + id: imageComponent + MapQuickItem { + coordinate: position + anchorPoint.x: imageId.width/2 + anchorPoint.y: imageId.height/2 + zoomLevel: imageZoomLevel + sourceItem: Image { + id: imageId + source: imageData + } + autoFadeIn: false + } + } + + Component { + id: polygonComponent + MapPolygon { + border.width: 1 + border.color: borderColor + color: fillColor + path: polygon + autoFadeIn: false + } + } + + Component { + id: polygonNameComponent + MapQuickItem { + coordinate: position + anchorPoint.x: polygonText.width/2 + anchorPoint.y: polygonText.height/2 + zoomLevel: mapZoomLevel > 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; + }; }