diff --git a/plugins/feature/map/CMakeLists.txt b/plugins/feature/map/CMakeLists.txt index 536ba6e22..6f04e0e39 100644 --- a/plugins/feature/map/CMakeLists.txt +++ b/plugins/feature/map/CMakeLists.txt @@ -5,6 +5,7 @@ set(map_SOURCES mapsettings.cpp mapplugin.cpp mapwebapiadapter.cpp + osmtemplateserver.cpp ) set(map_HEADERS @@ -13,6 +14,7 @@ set(map_HEADERS mapplugin.h mapreport.h mapwebapiadapter.h + osmtemplateserver.h beacon.h ) diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc index 9f6072bb1..c71cd4ae7 100644 --- a/plugins/feature/map/map.qrc +++ b/plugins/feature/map/map.qrc @@ -4,5 +4,8 @@ map/map_5_12.qml map/antenna.png map/antennatime.png + map/antennadab.png + map/antennafm.png + map/antennaam.png diff --git a/plugins/feature/map/map/antennaam.png b/plugins/feature/map/map/antennaam.png new file mode 100644 index 000000000..ffd03d24f Binary files /dev/null and b/plugins/feature/map/map/antennaam.png differ diff --git a/plugins/feature/map/map/antennadab.png b/plugins/feature/map/map/antennadab.png new file mode 100644 index 000000000..b9ba310b5 Binary files /dev/null and b/plugins/feature/map/map/antennadab.png differ diff --git a/plugins/feature/map/map/antennafm.png b/plugins/feature/map/map/antennafm.png new file mode 100644 index 000000000..95dd87cec Binary files /dev/null and b/plugins/feature/map/map/antennafm.png differ diff --git a/plugins/feature/map/map/map.qml b/plugins/feature/map/map/map.qml index 0b0816009..5fa290553 100644 --- a/plugins/feature/map/map/map.qml +++ b/plugins/feature/map/map/map.qml @@ -8,33 +8,28 @@ Item { id: qmlMap property int mapZoomLevel: 11 property string mapProvider: "osm" - property variant mapParameters property variant mapPtr + property variant guiPtr - function createMap(pluginParameters) { - var parameters = new Array() + function createMap(pluginParameters, gui) { + guiPtr = gui + var paramString = "" for (var prop in pluginParameters) { - var parameter = Qt.createQmlObject('import QtLocation 5.14; PluginParameter{ name: "'+ prop + '"; value: "' + pluginParameters[prop]+'"}', qmlMap) - parameters.push(parameter) + var parameter = 'PluginParameter { name: "' + prop + '"; value: "' + pluginParameters[prop] + '"}' + paramString = paramString + parameter } - qmlMap.mapParameters = parameters + var pluginString = 'import QtLocation 5.14; Plugin{ name:"' + mapProvider + '"; ' + paramString + '}' + var plugin = Qt.createQmlObject (pluginString, qmlMap) - var plugin - if (mapParameters && mapParameters.length > 0) - plugin = Qt.createQmlObject ('import QtLocation 5.14; Plugin{ name:"' + mapProvider + '"; parameters: qmlMap.mapParameters}', qmlMap) - else - plugin = Qt.createQmlObject ('import QtLocation 5.14; Plugin{ name:"' + mapProvider + '"}', qmlMap) if (mapPtr) { - // Objects aren't destroyed immediately, so rename the old - // map, so any C++ code that calls findChild("map") doesn't find - // the old map - mapPtr.objectName = "oldMap"; + // Objects aren't destroyed immediately, so don't call findChild("map") mapPtr.destroy() + mapPtr = null } mapPtr = actualMapComponent.createObject(page) mapPtr.plugin = plugin; mapPtr.forceActiveFocus() - mapPtr.objectName = "map"; + return mapPtr } function getMapTypes() { @@ -48,8 +43,11 @@ Item { } function setMapType(mapTypeIndex) { - if (mapPtr) - mapPtr.activeMapType = mapPtr.supportedMapTypes[mapTypeIndex] + if (mapPtr && (mapTypeIndex < mapPtr.supportedMapTypes.length)) { + if (mapPtr.supportedMapTypes[mapTypeIndex] !== undefined) { + mapPtr.activeMapType = mapPtr.supportedMapTypes[mapTypeIndex] + } + } } Item { @@ -62,6 +60,7 @@ Item { Map { id: map + objectName: "map" anchors.fill: parent center: QtPositioning.coordinate(51.5, 0.125) // London zoomLevel: 10 @@ -103,6 +102,10 @@ Item { mapModel.viewChanged(visibleRegion.boundingGeoRectangle().bottomLeft.longitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude); } + onSupportedMapTypesChanged : { + guiPtr.supportedMapsChanged() + } + } } diff --git a/plugins/feature/map/map/map_5_12.qml b/plugins/feature/map/map/map_5_12.qml index 34c97404c..39a3be447 100644 --- a/plugins/feature/map/map/map_5_12.qml +++ b/plugins/feature/map/map/map_5_12.qml @@ -8,33 +8,28 @@ Item { id: qmlMap property int mapZoomLevel: 11 property string mapProvider: "osm" - property variant mapParameters property variant mapPtr + property variant guiPtr - function createMap(pluginParameters) { - var parameters = new Array() + function createMap(pluginParameters, gui) { + guiPtr = gui + var paramString = "" for (var prop in pluginParameters) { - var parameter = Qt.createQmlObject('import QtLocation 5.6; PluginParameter{ name: "'+ prop + '"; value: "' + pluginParameters[prop]+'"}', qmlMap) - parameters.push(parameter) + var parameter = 'PluginParameter { name: "' + prop + '"; value: "' + pluginParameters[prop] + '"}' + paramString = paramString + parameter } - qmlMap.mapParameters = parameters + var pluginString = 'import QtLocation 5.12; Plugin{ name:"' + mapProvider + '"; ' + paramString + '}' + var plugin = Qt.createQmlObject (pluginString, qmlMap) - var plugin - if (mapParameters && mapParameters.length > 0) - plugin = Qt.createQmlObject ('import QtLocation 5.12; Plugin{ name:"' + mapProvider + '"; parameters: qmlMap.mapParameters}', qmlMap) - else - plugin = Qt.createQmlObject ('import QtLocation 5.12; Plugin{ name:"' + mapProvider + '"}', qmlMap) if (mapPtr) { - // Objects aren't destroyed immediately, so rename the old - // map, so any C++ code that calls findChild("map") doesn't find - // the old map - mapPtr.objectName = "oldMap"; + // Objects aren't destroyed immediately, so don't call findChild("map") mapPtr.destroy() + mapPtr = null } mapPtr = actualMapComponent.createObject(page) mapPtr.plugin = plugin; mapPtr.forceActiveFocus() - mapPtr.objectName = "map"; + return mapPtr } function getMapTypes() { @@ -48,8 +43,11 @@ Item { } function setMapType(mapTypeIndex) { - if (mapPtr) - mapPtr.activeMapType = mapPtr.supportedMapTypes[mapTypeIndex] + if (mapPtr && (mapTypeIndex < mapPtr.supportedMapTypes.length)) { + if (mapPtr.supportedMapTypes[mapTypeIndex] !== undefined) { + mapPtr.activeMapType = mapPtr.supportedMapTypes[mapTypeIndex] + } + } } Item { @@ -62,6 +60,7 @@ Item { Map { id: map + objectName: "map" anchors.fill: parent center: QtPositioning.coordinate(51.5, 0.125) // London zoomLevel: 10 @@ -103,6 +102,10 @@ Item { mapModel.viewChanged(visibleRegion.boundingGeoRectangle().bottomLeft.longitude, visibleRegion.boundingGeoRectangle().bottomRight.longitude); } + onSupportedMapTypesChanged : { + guiPtr.supportedMapsChanged() + } + } } diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index c3cce4ed8..17ed97b27 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -669,6 +669,9 @@ void MapGUI::onWidgetRolled(QWidget* widget, bool rollDown) { (void) widget; (void) rollDown; + + m_settings.m_rollupState = saveState(); + applySettings(); } MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : @@ -684,6 +687,12 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur { ui->setupUi(this); + quint16 port = 0; // Pick a free port + // Free keys, so no point in stealing them :) + QString tfKey = m_settings.m_thunderforestAPIKey.isEmpty() ? "3e1f614f78a345459931ba3c898e975e" : m_settings.m_thunderforestAPIKey; + QString mtKey = m_settings.m_maptilerAPIKey.isEmpty() ? "q2RVNAe3eFKCH4XsrE3r" : m_settings.m_maptilerAPIKey; + m_templateServer = new OSMTemplateServer(tfKey, mtKey, m_osmPort); + 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) @@ -745,6 +754,11 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur MapGUI::~MapGUI() { + if (m_templateServer) + { + m_templateServer->close(); + delete m_templateServer; + } delete ui; } @@ -804,42 +818,252 @@ void MapGUI::addRadioTimeTransmitters() } } +static QString arrayToString(QJsonArray array) +{ + QString s; + for (int i = 0; i < array.size(); i++) + { + s = s.append(array[i].toString()); + s = s.append(" "); + } + return s; +} + +// Coming soon +void MapGUI::addDAB() +{ + QFile file("stationlist_SI.json"); + if (file.open(QIODevice::ReadOnly)) + { + QByteArray bytes = file.readAll(); + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(bytes, &error); + if (!json.isNull()) + { + if (json.isObject()) + { + QJsonObject obj = json.object(); + QJsonValue stations = obj.value("stations"); + QJsonArray stationsArray = stations.toArray(); + for (int i = 0; i < stationsArray.size(); i++) + { + QJsonObject station = stationsArray[i].toObject(); + // "txs" contains array of transmitters + QString stationName = station.value("stationName").toString(); + QJsonArray txs = station.value("txs").toArray(); + QString languages = arrayToString(station.value("language").toArray()); + QString format = arrayToString(station.value("format").toArray()); + for (int j = 0; j < txs.size(); j++) + { + QJsonObject tx = txs[j].toObject(); + QString band = tx.value("band").toString(); + double lat = tx.value("latitude").toString().toDouble(); + double lon = tx.value("longitude").toString().toDouble(); + double alt = tx.value("haat").toString().toDouble(); // This is height above terrain - not actual height - Check "haatUnits" is m + double frequency = tx.value("frequency").toString().toDouble(); // Can be MHz or kHz for AM + double erp = tx.value("erp").toString().toDouble(); + SWGSDRangel::SWGMapItem mapItem; + mapItem.setLatitude(lat); + mapItem.setLongitude(lon); + mapItem.setAltitude(alt); + mapItem.setImageRotation(0); + mapItem.setImageMinZoom(8); + if (band == "DAB") + { + // Name should be unique - can we use TII code for this? can it repeat across countries? + QString name = QString("%1").arg(tx.value("tsId").toString()); + mapItem.setName(new QString(name)); + mapItem.setImage(new QString("antennadab.png")); + // Need tiicode? + QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 %4\nPower: %5 %6\nLanguage(s): %7\nType: %8\nService: %9\nEnsemble: %10") + .arg(band) + .arg(stationName) + .arg(frequency) + .arg(tx.value("frequencyUnits").toString()) + .arg(erp) + .arg(tx.value("erpUnits").toString()) + .arg(languages) + .arg(format) + .arg(tx.value("serviceLabel").toString()) + .arg(tx.value("ensembleLabel").toString()) + ; + mapItem.setText(new QString(text)); + m_mapModel.update(m_map, &mapItem, MapSettings::SOURCE_DAB); + } + else if (band == "FM") + { + // Name should be unique + QString name = QString("%1").arg(tx.value("tsId").toString()); + mapItem.setName(new QString(name)); + mapItem.setImage(new QString("antennafm.png")); + QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 %4\nPower: %5 %6\nLanguage(s): %7\nType: %8") + .arg(band) + .arg(stationName) + .arg(frequency) + .arg(tx.value("frequencyUnits").toString()) + .arg(erp) + .arg(tx.value("erpUnits").toString()) + .arg(languages) + .arg(format) + ; + mapItem.setText(new QString(text)); + m_mapModel.update(m_map, &mapItem, MapSettings::SOURCE_FM); + } + else if (band == "AM") + { + // Name should be unique + QString name = QString("%1").arg(tx.value("tsId").toString()); + mapItem.setName(new QString(name)); + mapItem.setImage(new QString("antennaam.png")); + QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 %4\nPower: %5 %6\nLanguage(s): %7\nType: %8") + .arg(band) + .arg(stationName) + .arg(frequency) + .arg(tx.value("frequencyUnits").toString()) + .arg(erp) + .arg(tx.value("erpUnits").toString()) + .arg(languages) + .arg(format) + ; + mapItem.setText(new QString(text)); + m_mapModel.update(m_map, &mapItem, MapSettings::SOURCE_AM); + } + } + } + } + else + { + qDebug() << "MapGUI::addDAB: Expecting an object in DAB json:"; + } + } + else + { + qDebug() << "MapGUI::addDAB: Failed to parse DAB json: " << error.errorString(); + } + } + else + { + qDebug() << "MapGUI::addDAB: Failed to open DAB json"; + } +} + void MapGUI::blockApplySettings(bool block) { m_doApplySettings = !block; } +QString MapGUI::osmCachePath() +{ + return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/QtLocation/5.8/tiles/osm/sdrangel_map"; +} + +void MapGUI::clearOSMCache() +{ + // Delete all cached custom tiles when user changes the URL. Is there a better way to do this? + QDir dir(osmCachePath()); + if (dir.exists()) + { + QStringList filenames = dir.entryList({"osm_100-l-8-*.png"}); + for (const auto& filename : filenames) + { + QFile file(dir.filePath(filename)); + if (!file.remove()) { + qDebug() << "MapGUI::clearOSMCache: Failed to remove " << file; + } + } + } +} + void MapGUI::applyMapSettings() { + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + float stationAltitude = MainCore::instance()->getSettings().getAltitude(); + QQuickItem *item = ui->map->rootObject(); - // Save existing position of map QObject *object = item->findChild("map"); QGeoCoordinate coords; + double zoom; if (object != nullptr) + { + // Save existing position of map coords = object->property("center").value(); + zoom = object->property("zoomLevel").value(); + } + else + { + // Center on my location when map is first opened + coords.setLatitude(stationLatitude); + coords.setLongitude(stationLongitude); + coords.setAltitude(stationAltitude); + zoom = 10.0; + } // Create the map using the specified provider QQmlProperty::write(item, "mapProvider", m_settings.m_mapProvider); QVariantMap parameters; - if (!m_settings.m_mapBoxApiKey.isEmpty() && m_settings.m_mapProvider == "mapbox") + if (!m_settings.m_mapBoxAPIKey.isEmpty() && m_settings.m_mapProvider == "mapbox") { parameters["mapbox.map_id"] = "mapbox.satellite"; // The only one that works - parameters["mapbox.access_token"] = m_settings.m_mapBoxApiKey; + parameters["mapbox.access_token"] = m_settings.m_mapBoxAPIKey; } - if (!m_settings.m_mapBoxApiKey.isEmpty() && m_settings.m_mapProvider == "mapboxgl") + if (!m_settings.m_mapBoxAPIKey.isEmpty() && m_settings.m_mapProvider == "mapboxgl") { - parameters["mapboxgl.access_token"] = m_settings.m_mapBoxApiKey; + parameters["mapboxgl.access_token"] = m_settings.m_mapBoxAPIKey; if (!m_settings.m_mapBoxStyles.isEmpty()) parameters["mapboxgl.mapping.additional_style_urls"] = m_settings.m_mapBoxStyles; } - //QQmlProperty::write(item, "mapParameters", parameters); - QMetaObject::invokeMethod(item, "createMap", Q_ARG(QVariant, QVariant::fromValue(parameters))); + if (m_settings.m_mapProvider == "osm") + { + // Allow user to specify URL + if (!m_settings.m_osmURL.isEmpty()) { + parameters["osm.mapping.custom.host"] = m_settings.m_osmURL; // E.g: "http://a.tile.openstreetmap.fr/hot/" + } + // Use our repo, so we can append API key + parameters["osm.mapping.providersrepository.address"] = QString("http://127.0.0.1:%1/").arg(m_osmPort); + // Use application specific cache, as other apps may not use API key so will have different images + QString cachePath = osmCachePath(); + parameters["osm.mapping.cache.directory"] = cachePath; + // On Linux, we need to create the directory + QDir dir(cachePath); + if (!dir.exists()) { + dir.mkpath(cachePath); + } + } + + QVariant retVal; + if (!QMetaObject::invokeMethod(item, "createMap", Qt::DirectConnection, + Q_RETURN_ARG(QVariant, retVal), + Q_ARG(QVariant, QVariant::fromValue(parameters)), + //Q_ARG(QVariant, mapType), + Q_ARG(QVariant, QVariant::fromValue(this)))) + { + qCritical() << "MapGUI::applyMapSettings - Failed to invoke createMap"; + } + QObject *newMap = retVal.value(); // Restore position of map - object = item->findChild("map"); - if ((object != nullptr) && coords.isValid()) - object->setProperty("center", QVariant::fromValue(coords)); + if (newMap != nullptr) + { + if (coords.isValid()) + { + newMap->setProperty("zoomLevel", QVariant::fromValue(zoom)); + newMap->setProperty("center", QVariant::fromValue(coords)); + } + } + else + { + qCritical() << "MapGUI::applyMapSettings - createMap returned a nullptr"; + } + + supportedMapsChanged(); +} + +void MapGUI::supportedMapsChanged() +{ + QQuickItem *item = ui->map->rootObject(); + QObject *object = item->findChild("map"); // Get list of map types ui->mapTypes->clear(); @@ -847,14 +1071,17 @@ void MapGUI::applyMapSettings() { // Mapbox plugin only works for Satellite imagary, despite what is indicated if (m_settings.m_mapProvider == "mapbox") + { ui->mapTypes->addItem("Satellite"); + } else { QVariant mapTypesVariant; QMetaObject::invokeMethod(item, "getMapTypes", Q_RETURN_ARG(QVariant, mapTypesVariant)); QStringList mapTypes = mapTypesVariant.value(); - for (int i = 0; i < mapTypes.size(); i++) + for (int i = 0; i < mapTypes.size(); i++) { ui->mapTypes->addItem(mapTypes[i]); + } } } } @@ -880,6 +1107,7 @@ void MapGUI::displaySettings() m_mapModel.setGroundTrackColor(m_settings.m_groundTrackColor); m_mapModel.setPredictedGroundTrackColor(m_settings.m_predictedGroundTrackColor); applyMapSettings(); + restoreState(m_settings.m_rollupState); blockApplySettings(false); } @@ -1052,11 +1280,16 @@ void MapGUI::on_displaySettings_clicked() MapSettingsDialog dialog(&m_settings); if (dialog.exec() == QDialog::Accepted) { - if (dialog.m_mapSettingsChanged) + if (dialog.m_osmURLChanged) { + clearOSMCache(); + } + if (dialog.m_mapSettingsChanged) { applyMapSettings(); + } applySettings(); - if (dialog.m_sourcesChanged) + if (dialog.m_sourcesChanged) { m_mapModel.setSources(m_settings.m_sources); + } m_mapModel.setGroundTrackColor(m_settings.m_groundTrackColor); m_mapModel.setPredictedGroundTrackColor(m_settings.m_predictedGroundTrackColor); } diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index 8b2c02005..f88304cec 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -32,6 +32,7 @@ #include "SWGMapItem.h" #include "mapbeacondialog.h" #include "mapradiotimedialog.h" +#include "osmtemplateserver.h" class PluginAPI; class FeatureUISet; @@ -482,7 +483,9 @@ public: void setBeacons(QList *beacons); QList getRadioTimeTransmitters() { return m_radioTimeTransmitters; } void addRadioTimeTransmitters(); + void addDAB(); void find(const QString& target); + Q_INVOKABLE void supportedMapsChanged(); private: Ui::MapGUI* ui; @@ -499,6 +502,8 @@ private: QList *m_beacons; MapBeaconDialog m_beaconDialog; MapRadioTimeDialog m_radioTimeDialog; + quint16 m_osmPort; + OSMTemplateServer *m_templateServer; explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); virtual ~MapGUI(); @@ -506,6 +511,8 @@ private: void blockApplySettings(bool block); void applySettings(bool force = false); void applyMapSettings(); + QString osmCachePath(); + void clearOSMCache(); void displaySettings(); bool handleMessage(const Message& message); void geoReply(); diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index aec102391..0659bb0c6 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -56,7 +56,10 @@ void MapSettings::resetToDefaults() { m_displayNames = true; m_mapProvider = "osm"; - m_mapBoxApiKey = ""; + m_thunderforestAPIKey = ""; + m_maptilerAPIKey = ""; + m_mapBoxAPIKey = ""; + m_osmURL = ""; m_mapBoxStyles = ""; m_sources = -1; m_displaySelectedGroundTracks = true; @@ -78,7 +81,7 @@ QByteArray MapSettings::serialize() const s.writeBool(1, m_displayNames); s.writeString(2, m_mapProvider); - s.writeString(3, m_mapBoxApiKey); + s.writeString(3, m_mapBoxAPIKey); s.writeString(4, m_mapBoxStyles); s.writeU32(5, m_sources); s.writeU32(6, m_groundTrackColor); @@ -92,6 +95,10 @@ QByteArray MapSettings::serialize() const s.writeU32(14, m_reverseAPIFeatureIndex); s.writeBool(15, m_displaySelectedGroundTracks); s.writeBool(16, m_displayAllGroundTracks); + s.writeString(17, m_thunderforestAPIKey); + s.writeString(18, m_maptilerAPIKey); + s.writeBlob(19, m_rollupState); + s.writeString(20, m_osmURL); return s.final(); } @@ -114,7 +121,7 @@ bool MapSettings::deserialize(const QByteArray& data) d.readBool(1, &m_displayNames, true); d.readString(2, &m_mapProvider, "osm"); - d.readString(3, &m_mapBoxApiKey, ""); + d.readString(3, &m_mapBoxAPIKey, ""); d.readString(4, &m_mapBoxStyles, ""); d.readU32(5, &m_sources, -1); d.readU32(6, &m_groundTrackColor, QColor(150, 0, 20).rgb()); @@ -138,6 +145,11 @@ bool MapSettings::deserialize(const QByteArray& data) d.readBool(15, &m_displaySelectedGroundTracks, true); d.readBool(16, &m_displayAllGroundTracks, true); + d.readString(17, &m_thunderforestAPIKey, ""); + d.readString(18, &m_maptilerAPIKey, ""); + d.readBlob(19, &m_rollupState); + d.readString(20, &m_osmURL, ""); + return true; } else diff --git a/plugins/feature/map/mapsettings.h b/plugins/feature/map/mapsettings.h index 04c0265aa..31d5e0cc9 100644 --- a/plugins/feature/map/mapsettings.h +++ b/plugins/feature/map/mapsettings.h @@ -31,7 +31,10 @@ struct MapSettings { bool m_displayNames; QString m_mapProvider; - QString m_mapBoxApiKey; + QString m_thunderforestAPIKey; + QString m_maptilerAPIKey; + QString m_mapBoxAPIKey; + QString m_osmURL; QString m_mapBoxStyles; quint32 m_sources; // Bitmask of SOURCE_* bool m_displayAllGroundTracks; @@ -45,6 +48,7 @@ struct MapSettings uint16_t m_reverseAPIPort; uint16_t m_reverseAPIFeatureSetIndex; uint16_t m_reverseAPIFeatureIndex; + QByteArray m_rollupState; MapSettings(); void resetToDefaults(); @@ -64,7 +68,10 @@ struct MapSettings static const quint32 SOURCE_SATELLITE_TRACKER = 0x10; static const quint32 SOURCE_BEACONS = 0x20; static const quint32 SOURCE_RADIO_TIME = 0x40; - static const quint32 SOURCE_STATION = 0x80; + static const quint32 SOURCE_AM = 0x80; + static const quint32 SOURCE_FM = 0x100; + static const quint32 SOURCE_DAB = 0x200; + static const quint32 SOURCE_STATION = 0x400; // Antenna at "My Position" }; #endif // INCLUDE_FEATURE_MAPSETTINGS_H_ diff --git a/plugins/feature/map/mapsettingsdialog.cpp b/plugins/feature/map/mapsettingsdialog.cpp index 992e3b8b1..599458ddd 100644 --- a/plugins/feature/map/mapsettingsdialog.cpp +++ b/plugins/feature/map/mapsettingsdialog.cpp @@ -42,10 +42,14 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : { ui->setupUi(this); ui->mapProvider->setCurrentIndex(MapSettings::m_mapProviders.indexOf(settings->m_mapProvider)); - ui->mapBoxApiKey->setText(settings->m_mapBoxApiKey); + ui->thunderforestAPIKey->setText(settings->m_thunderforestAPIKey); + ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey); + ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey); + ui->osmURL->setText(settings->m_osmURL); ui->mapBoxStyles->setText(settings->m_mapBoxStyles); - for (int i = 0; i < ui->sourceList->count(); i++) + for (int i = 0; i < ui->sourceList->count(); i++) { ui->sourceList->item(i)->setCheckState((m_settings->m_sources & (1 << i)) ? Qt::Checked : Qt::Unchecked); + } ui->groundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_groundTrackColor)); ui->predictedGroundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_predictedGroundTrackColor)); } @@ -58,23 +62,36 @@ MapSettingsDialog::~MapSettingsDialog() void MapSettingsDialog::accept() { QString mapProvider = MapSettings::m_mapProviders[ui->mapProvider->currentIndex()]; - QString mapBoxApiKey = ui->mapBoxApiKey->text(); + QString mapBoxAPIKey = ui->mapBoxAPIKey->text(); + QString osmURL = ui->osmURL->text(); QString mapBoxStyles = ui->mapBoxStyles->text(); + QString thunderforestAPIKey = ui->thunderforestAPIKey->text(); + QString maptilerAPIKey = ui->maptilerAPIKey->text(); + m_osmURLChanged = osmURL != m_settings->m_osmURL; if ((mapProvider != m_settings->m_mapProvider) - || (mapBoxApiKey != m_settings->m_mapBoxApiKey) - || (mapBoxStyles != m_settings->m_mapBoxStyles)) + || (thunderforestAPIKey != m_settings->m_thunderforestAPIKey) + || (maptilerAPIKey != m_settings->m_maptilerAPIKey) + || (mapBoxAPIKey != m_settings->m_mapBoxAPIKey) + || (mapBoxStyles != m_settings->m_mapBoxStyles) + || (osmURL != m_settings->m_osmURL)) { m_settings->m_mapProvider = mapProvider; - m_settings->m_mapBoxApiKey = mapBoxApiKey; + m_settings->m_thunderforestAPIKey = thunderforestAPIKey; + m_settings->m_maptilerAPIKey = maptilerAPIKey; + m_settings->m_mapBoxAPIKey = mapBoxAPIKey; + m_settings->m_osmURL = osmURL; m_settings->m_mapBoxStyles = mapBoxStyles; m_mapSettingsChanged = true; } else + { m_mapSettingsChanged = false; + } m_settings->m_sources = 0; quint32 sources = MapSettings::SOURCE_STATION; - for (int i = 0; i < ui->sourceList->count(); i++) + for (int i = 0; i < ui->sourceList->count(); i++) { sources |= (ui->sourceList->item(i)->checkState() == Qt::Checked) << i; + } m_sourcesChanged = sources != m_settings->m_sources; m_settings->m_sources = sources; QDialog::accept(); diff --git a/plugins/feature/map/mapsettingsdialog.h b/plugins/feature/map/mapsettingsdialog.h index d3b49a9c7..34d1de67b 100644 --- a/plugins/feature/map/mapsettingsdialog.h +++ b/plugins/feature/map/mapsettingsdialog.h @@ -30,6 +30,7 @@ public: MapSettings *m_settings; bool m_mapSettingsChanged; + bool m_osmURLChanged; bool m_sourcesChanged; private slots: diff --git a/plugins/feature/map/mapsettingsdialog.ui b/plugins/feature/map/mapsettingsdialog.ui index d2de8745f..ec5fb8b9f 100644 --- a/plugins/feature/map/mapsettingsdialog.ui +++ b/plugins/feature/map/mapsettingsdialog.ui @@ -7,7 +7,7 @@ 0 0 436 - 491 + 520 @@ -182,34 +182,76 @@ - - + + Mapbox API Key - - + + - Enter a Mapbox API key in order to use Mapbox maps + Enter a Mapbox API key in order to use Mapbox maps: https://www.mapbox.com/ - + MapboxGL Styles - + Comma separated list of MapBox styles + + + + Thunderforest API Key + + + + + + + Maptiler API Key + + + + + + + Enter a Thunderforest API key in order to use non-watermarked Thunderforest maps: https://www.thunderforest.com/ + + + + + + + Enter a Maptiler API key in order to use Maptiler maps: https://www.maptiler.com/ + + + + + + + OSM Custom URL + + + + + + + URL of custom map for use with OpenStreetMap provider + + + diff --git a/plugins/feature/map/osmtemplateserver.cpp b/plugins/feature/map/osmtemplateserver.cpp new file mode 100644 index 000000000..c9364eb8e --- /dev/null +++ b/plugins/feature/map/osmtemplateserver.cpp @@ -0,0 +1,18 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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 "osmtemplateserver.h" diff --git a/plugins/feature/map/osmtemplateserver.h b/plugins/feature/map/osmtemplateserver.h new file mode 100644 index 000000000..fcada37fe --- /dev/null +++ b/plugins/feature/map/osmtemplateserver.h @@ -0,0 +1,137 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 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_OSMTEMPLATE_SERVER_H_ +#define INCLUDE_OSMTEMPLATE_SERVER_H_ + +#include +#include +#include + +class OSMTemplateServer : public QTcpServer +{ + Q_OBJECT +private: + QString m_thunderforestAPIKey; + QString m_maptilerAPIKey; + +public: + // port - port to listen on / is listening on. Use 0 for any free port. + OSMTemplateServer(const QString &thunderforestAPIKey, const QString &maptilerAPIKey, quint16 &port, QObject* parent = 0) : + QTcpServer(parent), + m_thunderforestAPIKey(thunderforestAPIKey), + m_maptilerAPIKey(maptilerAPIKey) + { + listen(QHostAddress::Any, port); + port = serverPort(); + } + + void incomingConnection(qintptr socket) override + { + QTcpSocket* s = new QTcpSocket(this); + connect(s, SIGNAL(readyRead()), this, SLOT(readClient())); + connect(s, SIGNAL(disconnected()), this, SLOT(discardClient())); + s->setSocketDescriptor(socket); + //addPendingConnection(socket); + } + +private slots: + void readClient() + { + QStringList map({"/cycle", "/cycle-hires", "/hiking", "/hiking-hires", "/night-transit", "/night-transit-hires", "/terrain", "/terrain-hires", "/transit", "/transit-hires"}); + QStringList mapId({"thf-cycle", "thf-cycle-hires", "thf-hike", "thf-hike-hires", "thf-nighttransit", "thf-nighttransit-hires", "thf-landsc", "thf-landsc-hires", "thf-transit", "thf-transit-hires"}); + QStringList mapUrl({"cycle", "cycle", "outdoors", "outdoors", "transport-dark", "transport-dark", "landscape", "landscape", "transport", "transport"}); + + QTcpSocket* socket = (QTcpSocket*)sender(); + if (socket->canReadLine()) + { + QString line = socket->readLine(); + qDebug() << "HTTP Request: " << line; + QStringList tokens = QString(line).split(QRegExp("[ \r\n][ \r\n]*")); + if (tokens[0] == "GET") + { + bool hires = tokens[1].contains("hires"); + QString hiresURL = hires ? "@2x" : ""; + QString xml; + if ((tokens[1] == "/street") || (tokens[1] == "/street-hires")) + { + xml = QString("\ + {\ + \"UrlTemplate\" : \"https://maps.wikimedia.org/osm-intl/%z/%x/%y%1.png\",\ + \"ImageFormat\" : \"png\",\ + \"QImageFormat\" : \"Indexed8\",\ + \"ID\" : \"wmf-intl-%2x\",\ + \"MaximumZoomLevel\" : 18,\ + \"MapCopyRight\" : \"WikiMedia Foundation\",\ + \"DataCopyRight\" : \"OpenStreetMap contributors\"\ + }").arg(hiresURL).arg(hires ? 1 : 2); + } + else if (tokens[1] == "/satellite") + { + xml = QString("\ + {\ + \"Enabled\" : true,\ + \"UrlTemplate\" : \"https://api.maptiler.com/tiles/satellite/%z/%x/%y%1.jpg?key=%2\",\ + \"ImageFormat\" : \"jpg\",\ + \"QImageFormat\" : \"RGB888\",\ + \"ID\" : \"usgs-l7\",\ + \"MaximumZoomLevel\" : 20,\ + \"MapCopyRight\" : \"Maptiler\",\ + \"DataCopyRight\" : \"Maptiler\"\ + }").arg(hiresURL).arg(m_maptilerAPIKey); + } + else + { + int idx = map.indexOf(tokens[1]); + if (idx != -1) + { + xml = QString("\ + {\ + \"UrlTemplate\" : \"http://a.tile.thunderforest.com/%1/%z/%x/%y%4.png?apikey=%2\",\ + \"ImageFormat\" : \"png\",\ + \"QImageFormat\" : \"Indexed8\",\ + \"ID\" : \"%3\",\ + \"MaximumZoomLevel\" : 20,\ + \"MapCopyRight\" : \"Thunderforest\",\ + \"DataCopyRight\" : \"OpenStreetMap contributors\"\ + }").arg(mapUrl[idx]).arg(m_thunderforestAPIKey).arg(mapId[idx]).arg(hiresURL); + } + } + QTextStream os(socket); + os.setAutoDetectUnicode(true); + os << "HTTP/1.0 200 Ok\r\n" + "Content-Type: text/html; charset=\"utf-8\"\r\n" + "\r\n" + << xml << "\n"; + socket->close(); + + if (socket->state() == QTcpSocket::UnconnectedState) { + delete socket; + } + } + } + } + + void discardClient() + { + QTcpSocket* socket = (QTcpSocket*)sender(); + socket->deleteLater(); + } + +}; + +#endif diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 57fa8059e..94d1fa860 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -89,6 +89,10 @@ Free API keys are available by signing up for an accounts with: * [Maptiler](https://www.maptiler.com/) * [Mapbox](https://www.mapbox.com/) +If API keys are not specified, a default key will be used, but this may not work if too many users use it. + +When OpenStreetMap is used as the provider, a custom map URL can be entered. For example, http://a.tile.openstreetmap.fr/hot/ or http://1.basemaps.cartocdn.com/light_nolabels/ +

Map

The map displays objects reported by other SDRangel channels and features, as well as beacon locations.