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.