/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2021 Jon Beniston, M7RCE // // Copyright (C) 2020 Edouard Griffiths, F4EXB // // // // 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 #include #include #include #include "feature/featureuiset.h" #include "gui/basicfeaturesettingsdialog.h" #include "channel/channelwebapiutils.h" #include "mainwindow.h" #include "device/deviceuiset.h" #include "util/units.h" #include "util/maidenhead.h" #include "maplocationdialog.h" #include "mapmaidenheaddialog.h" #include "mapsettingsdialog.h" #include "ibpbeacon.h" #include "ui_mapgui.h" #include "map.h" #include "mapgui.h" #include "SWGMapItem.h" #include "SWGTargetAzimuthElevation.h" 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) { 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; } QVariant MapModel::data(const QModelIndex &index, int role) const { int row = index.row(); if ((row < 0) || (row >= m_items.count())) return QVariant(); if (role == MapModel::positionRole) { // 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) { // Create the text to go in the bubble next to the image if (row == m_target) { AzEl *azEl = m_gui->getAzEl(); QString text = QString("%1\nAz: %2 El: %3") .arg(m_selected[row] ? m_items[row]->m_text : m_items[row]->m_name) .arg(std::round(azEl->getAzimuth())) .arg(std::round(azEl->getElevation())); return QVariant::fromValue(text); } else if (m_selected[row]) return QVariant::fromValue(m_items[row]->m_text); else return QVariant::fromValue(m_items[row]->m_name); } else if (role == MapModel::mapTextVisibleRole) { return QVariant::fromValue((m_selected[row] || m_displayNames) && (m_sources & m_items[row]->m_sourceMask)); } else if (role == MapModel::mapImageVisibleRole) { return QVariant::fromValue((m_sources & m_items[row]->m_sourceMask) != 0); } 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); } else if (role == MapModel::bubbleColourRole) { // Select a background colour for the text bubble next to the item if (m_selected[row]) return QVariant::fromValue(QColor("lightgreen")); else return QVariant::fromValue(QColor("lightblue")); } else if (role == MapModel::selectedRole) return QVariant::fromValue(m_selected[row]); else if (role == MapModel::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) { if ((m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) && (m_sources & m_items[row]->m_sourceMask)) return m_items[row]->m_predictedTrack1; else return QVariantList(); } else if (role == MapModel::predictedGroundTrack2Role) { if ((m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) && (m_sources & m_items[row]->m_sourceMask)) return m_items[row]->m_predictedTrack2; else return QVariantList(); } else if (role == MapModel::groundTrack1Role) { if ((m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) && (m_sources & m_items[row]->m_sourceMask)) return m_items[row]->m_takenTrack1; else return QVariantList(); } else if (role == MapModel::groundTrack2Role) { if ((m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) && (m_sources & m_items[row]->m_sourceMask)) return m_items[row]->m_takenTrack2; else return QVariantList(); } else if (role == groundTrackColorRole) { return m_groundTrackColor; } else if (role == predictedGroundTrackColorRole) { return m_predictedGroundTrackColor; } return QVariant(); } bool MapModel::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) { m_selected[row] = value.toBool(); emit dataChanged(idx, idx); return true; } else if (role == MapModel::targetRole) { if (m_target >= 0) { // Update text bubble for old target QModelIndex oldIdx = index(m_target); m_target = -1; emit dataChanged(oldIdx, oldIdx); } m_target = row; updateTarget(); emit dataChanged(idx, idx); return true; } return true; } void MapModel::setFrequency(double frequency) { // Set as centre frequency ChannelWebAPIUtils::setCenterFrequency(0, frequency); } void MapModel::update(const PipeEndPoint *sourcePipe, SWGSDRangel::SWGMapItem *swgMapItem, quint32 sourceMask) { QString name = *swgMapItem->getName(); // Add, update or delete and item MapItem *item = findMapItem(sourcePipe, name); if (item != nullptr) { QString image = *swgMapItem->getImage(); if (image.isEmpty()) { // Delete the item remove(item); } else { // Update the item item->update(swgMapItem); splitTracks(item); update(item); } } else { // Make sure not a duplicate request to delete QString image = *swgMapItem->getImage(); if (!image.isEmpty()) { if (!sourceMask) sourceMask = m_gui->getSourceMask(sourcePipe); // Add new item add(new MapItem(sourcePipe, sourceMask, swgMapItem)); } } } void MapModel::updateTarget() { // Calculate range, azimuth and elevation to object from station AzEl *azEl = m_gui->getAzEl(); azEl->setTarget(m_items[m_target]->m_latitude, m_items[m_target]->m_longitude, m_items[m_target]->m_altitude); azEl->calculate(); // Send to Rotator Controllers MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); QList *mapMessageQueues = messagePipes.getMessageQueues(m_gui->getMap(), "target"); if (mapMessageQueues) { QList::iterator it = mapMessageQueues->begin(); for (; it != mapMessageQueues->end(); ++it) { SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation(); swgTarget->setName(new QString(m_items[m_target]->m_name)); swgTarget->setAzimuth(azEl->getAzimuth()); swgTarget->setElevation(azEl->getElevation()); (*it)->push(MainCore::MsgTargetAzimuthElevation::create(m_gui->getMap(), swgTarget)); } } } void MapModel::splitTracks(MapItem *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) { double x1 = c1->longitude(); double y1 = c1->latitude(); double x2 = c2->longitude(); double y2 = c2->latitude(); double y; if (x2 < x1) x2 += 360.0; if (x < x1) x += 360.0; y = interpolate(x1, y1, x2, y2, x); if (x > 180) x -= 360.0; if (offScreen) x -= 0.000000001; else x += 0.000000001; ci->setLongitude(x); ci->setLatitude(y); ci->setAltitude(c1->altitude()); } void MapModel::interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen) { double x1 = c1->longitude(); double y1 = c1->latitude(); double x2 = c2->longitude(); double y2 = c2->latitude(); double y; if (x2 > x1) x2 -= 360.0; if (x > x1) x -= 360.0; y = interpolate(x1, y1, x2, y2, x); if (x < -180) x += 360.0; if (offScreen) x += 0.000000001; else x -= 0.000000001; ci->setLongitude(x); ci->setLatitude(y); ci->setAltitude(c1->altitude()); } static bool isOnScreen(double lon, double bottomLeftLongitude, double bottomRightLongitude, double width, bool antimed) { bool onScreen = false; if (width == 360) onScreen = true; else if (!antimed) onScreen = (lon > bottomLeftLongitude) && (lon <= bottomRightLongitude); else onScreen = (lon > bottomLeftLongitude) || (lon <= bottomRightLongitude); return onScreen; } static bool crossesAntimeridian(double prevLon, double lon) { bool crosses = false; if ((prevLon > 90) && (lon < -90)) crosses = true; // West to East else if ((prevLon < -90) && (lon > 90)) crosses = true; // East to West return crosses; } static bool crossesAntimeridianEast(double prevLon, double lon) { bool crosses = false; if ((prevLon > 90) && (lon < -90)) crosses = true; // West to East return crosses; } static bool crossesAntimeridianWest(double prevLon, double lon) { bool crosses = false; if ((prevLon < -90) && (lon > 90)) crosses = true; // East to West return crosses; } static bool crossesEdge(double lon, double prevLon, double bottomLeftLongitude, double bottomRightLongitude) { // Determine if antimerdian is between the two points if (!crossesAntimeridian(prevLon, lon)) { bool crosses = false; if ((prevLon <= bottomRightLongitude) && (lon > bottomRightLongitude)) crosses = true; // Crosses right edge East else if ((prevLon >= bottomRightLongitude) && (lon < bottomRightLongitude)) crosses = true; // Crosses right edge West else if ((prevLon >= bottomLeftLongitude) && (lon < bottomLeftLongitude)) crosses = true; // Crosses left edge West else if ((prevLon <= bottomLeftLongitude) && (lon > bottomLeftLongitude)) crosses = true; // Crosses left edge East return crosses; } else { // Determine which point and the edge the antimerdian is between bool prevLonToRightCrossesAnti = crossesAntimeridianEast(prevLon, bottomRightLongitude); bool rightToLonCrossesAnti = crossesAntimeridianEast(bottomRightLongitude, lon); bool prevLonToLeftCrossesAnti = crossesAntimeridianWest(prevLon, bottomLeftLongitude); bool leftToLonCrossesAnti = crossesAntimeridianWest(bottomLeftLongitude, lon); bool crosses = false; if ( ((prevLon > bottomRightLongitude) && prevLonToRightCrossesAnti && (lon > bottomRightLongitude)) || ((prevLon <= bottomRightLongitude) && (lon <= bottomRightLongitude) && rightToLonCrossesAnti) ) crosses = true; // Crosses right edge East else if ( ((prevLon < bottomRightLongitude) && prevLonToRightCrossesAnti && (lon < bottomRightLongitude)) || ((prevLon >= bottomRightLongitude) && (lon >= bottomRightLongitude) && rightToLonCrossesAnti) ) crosses = true; // Crosses right edge West else if ( ((prevLon < bottomLeftLongitude) && prevLonToLeftCrossesAnti && (lon < bottomLeftLongitude)) || ((prevLon >= bottomLeftLongitude) && (lon >= bottomLeftLongitude) && leftToLonCrossesAnti) ) crosses = true; // Crosses left edge West else if ( ((prevLon > bottomLeftLongitude) && prevLonToLeftCrossesAnti && (lon > bottomLeftLongitude)) || ((prevLon <= bottomLeftLongitude) && (lon <= bottomLeftLongitude) && leftToLonCrossesAnti) ) crosses = true; // Crosses left edge East return crosses; } } void MapModel::interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen) { double x1 = c1->longitude(); double x2 = c2->longitude(); double crossesAnti = crossesAntimeridian(x1, x2); double x; // Need to work out which edge we're interpolating too // and whether antimeridian is in the way, as that flips x1x2 if (((x1 < x2) && !crossesAnti) || ((x1 > x2) && crossesAnti)) { x = offScreen ? bottomRightLongitude : bottomLeftLongitude; interpolateEast(c1, c2, x, ci, offScreen); } else { x = offScreen ? bottomLeftLongitude : bottomRightLongitude; interpolateWest(c1, c2, x, ci, offScreen); } } void MapModel::splitTrack(const QList& coords, const QVariantList& track, QVariantList& track1, QVariantList& track2, QGeoCoordinate& start1, QGeoCoordinate& start2, QGeoCoordinate& end1, QGeoCoordinate& end2) { /* QStringList l; for (int i = 0; i < track.size(); i++) { QGeoCoordinate c = track[i].value(); l.append(QString("%1").arg((int)c.longitude())); } qDebug() << "Init T: " << l; */ QQuickItem* map = m_gui->getMapItem(); QVariant rectVariant; QMetaObject::invokeMethod(map, "mapRect", Q_RETURN_ARG(QVariant, rectVariant)); QGeoRectangle rect = qvariant_cast(rectVariant); double bottomLeftLongitude = rect.bottomLeft().longitude(); double bottomRightLongitude = rect.bottomRight().longitude(); int width = round(rect.width()); bool antimed = (width == 360) || (bottomLeftLongitude > bottomRightLongitude); /* qDebug() << "Anitmed visible: " << antimed; qDebug() << "bottomLeftLongitude: " << bottomLeftLongitude; qDebug() << "bottomRightLongitude: " << bottomRightLongitude; */ track1.clear(); track2.clear(); double lon, prevLon; bool onScreen, prevOnScreen; QList tracks({&track1, &track2}); QList ends({&end1, &end2}); QList starts({&start1, &start2}); int trackIdx = 0; for (int i = 0; i < coords.size(); i++) { lon = coords[i]->longitude(); if (i == 0) { prevLon = lon; prevOnScreen = true; // To avoid interpolation for first point } // Can be onscreen after having crossed edge from other side // Or can be onscreen after previously having been off screen onScreen = isOnScreen(lon, bottomLeftLongitude, bottomRightLongitude, width, antimed); bool crossedEdge = crossesEdge(lon, prevLon, bottomLeftLongitude, bottomRightLongitude); if ((onScreen && !crossedEdge) || (onScreen && !prevOnScreen)) { if ((i > 0) && (tracks[trackIdx]->size() == 0)) // Could also use (onScreen && !prevOnScreen)? { if (trackIdx >= starts.size()) break; // Interpolate from edge of screen interpolate(coords[i-1], coords[i], bottomLeftLongitude, bottomRightLongitude, starts[trackIdx], false); tracks[trackIdx]->append(QVariant::fromValue(*starts[trackIdx])); } tracks[trackIdx]->append(track[i]); } else if (tracks[trackIdx]->size() > 0) { // Either we've crossed to the other side, or have gone off screen if (trackIdx >= ends.size()) break; // Interpolate to edge of screen interpolate(coords[i-1], coords[i], bottomLeftLongitude, bottomRightLongitude, ends[trackIdx], true); tracks[trackIdx]->append(QVariant::fromValue(*ends[trackIdx])); // Start new track trackIdx++; if (trackIdx >= tracks.size()) { // This can happen with highly retrograde orbits, where trace 90% of period // will cover more than 360 degrees - delete last point as Map // will not be able to display it properly tracks[trackIdx-1]->removeLast(); break; } if (onScreen) { // Interpolate from edge of screen interpolate(coords[i-1], coords[i], bottomLeftLongitude, bottomRightLongitude, starts[trackIdx], false); tracks[trackIdx]->append(QVariant::fromValue(*starts[trackIdx])); tracks[trackIdx]->append(track[i]); } } prevLon = lon; prevOnScreen = onScreen; } /* l.clear(); for (int i = 0; i < track1.size(); i++) { QGeoCoordinate c = track1[i].value(); if (!c.isValid()) l.append("Invalid!"); else l.append(QString("%1").arg(c.longitude(), 0, 'f', 1)); } qDebug() << "T1: " << l; l.clear(); for (int i = 0; i < track2.size(); i++) { QGeoCoordinate c = track2[i].value(); if (!c.isValid()) l.append("Invalid!"); else l.append(QString("%1").arg(c.longitude(), 0, 'f', 1)); } qDebug() << "T2: " << l; */ } void MapModel::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) { 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); } } } } MapGUI* MapGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) { MapGUI* gui = new MapGUI(pluginAPI, featureUISet, feature); return gui; } void MapGUI::destroy() { delete this; } void MapGUI::resetToDefaults() { m_settings.resetToDefaults(); displaySettings(); applySettings(true); } QByteArray MapGUI::serialize() const { return m_settings.serialize(); } bool MapGUI::deserialize(const QByteArray& data) { if (m_settings.deserialize(data)) { displaySettings(); applySettings(true); return true; } else { resetToDefaults(); return false; } } bool MapGUI::handleMessage(const Message& message) { if (Map::MsgConfigureMap::match(message)) { qDebug("MapGUI::handleMessage: Map::MsgConfigureMap"); const Map::MsgConfigureMap& cfg = (Map::MsgConfigureMap&) message; m_settings = cfg.getSettings(); blockApplySettings(true); displaySettings(); blockApplySettings(false); return true; } else if (PipeEndPoint::MsgReportPipes::match(message)) { PipeEndPoint::MsgReportPipes& report = (PipeEndPoint::MsgReportPipes&) message; m_availablePipes = report.getAvailablePipes(); return true; } else if (Map::MsgFind::match(message)) { Map::MsgFind& msgFind = (Map::MsgFind&) message; find(msgFind.getTarget()); return true; } else if (MainCore::MsgMapItem::match(message)) { MainCore::MsgMapItem& msgMapItem = (MainCore::MsgMapItem&) message; SWGSDRangel::SWGMapItem *swgMapItem = msgMapItem.getSWGMapItem(); m_mapModel.update(msgMapItem.getPipeSource(), swgMapItem); return true; } return false; } void MapGUI::handleInputMessages() { Message* message; while ((message = getInputMessageQueue()->pop())) { if (handleMessage(*message)) { delete message; } } } 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) : FeatureGUI(parent), ui(new Ui::MapGUI), m_pluginAPI(pluginAPI), m_featureUISet(featureUISet), m_doApplySettings(true), m_mapModel(this), m_beacons(nullptr), m_beaconDialog(this), m_ibpBeaconDialog(this), m_radioTimeDialog(this) { ui->setupUi(this); m_helpURL = "plugins/feature/map/readme.md"; // 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) ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map_5_12.qml"))); #else ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map.qml"))); #endif setAttribute(Qt::WA_DeleteOnClose, true); setChannelWidget(false); connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); m_map = reinterpret_cast(feature); m_map->setMessageQueueToGUI(&m_inputMessageQueue); m_featureUISet->addRollupWidget(this); connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); displaySettings(); applySettings(true); // Get station position float stationLatitude = MainCore::instance()->getSettings().getLatitude(); float stationLongitude = MainCore::instance()->getSettings().getLongitude(); float stationAltitude = MainCore::instance()->getSettings().getAltitude(); m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); // Centre map at My Position QQuickItem *item = ui->map->rootObject(); QObject *object = item->findChild("map"); if (object != nullptr) { QGeoCoordinate coords = object->property("center").value(); coords.setLatitude(stationLatitude); coords.setLongitude(stationLongitude); object->setProperty("center", QVariant::fromValue(coords)); } // Create antenna at My Position SWGSDRangel::SWGMapItem antennaMapItem; antennaMapItem.setName(new QString(MainCore::instance()->getSettings().getStationName())); antennaMapItem.setLatitude(stationLatitude); antennaMapItem.setLongitude(stationLongitude); antennaMapItem.setAltitude(stationAltitude); antennaMapItem.setImage(new QString("antenna.png")); antennaMapItem.setImageRotation(0); antennaMapItem.setImageMinZoom(11); antennaMapItem.setText(new QString(MainCore::instance()->getSettings().getStationName())); m_mapModel.update(m_map, &antennaMapItem, MapSettings::SOURCE_STATION); // Read beacons, if they exist QList *beacons = Beacon::readIARUCSV(MapGUI::getBeaconFilename()); if (beacons != nullptr) setBeacons(beacons); addIBPBeacons(); addRadioTimeTransmitters(); addRadar(); } MapGUI::~MapGUI() { if (m_templateServer) { m_templateServer->close(); delete m_templateServer; } delete ui; } void MapGUI::setBeacons(QList *beacons) { delete m_beacons; m_beacons = beacons; m_beaconDialog.updateTable(); // Add to Map QListIterator i(*m_beacons); while (i.hasNext()) { Beacon *beacon = i.next(); SWGSDRangel::SWGMapItem beaconMapItem; // Need to suffix frequency, as there are multiple becaons with same callsign at different locations QString name = QString("%1-%2").arg(beacon->m_callsign).arg(beacon->getFrequencyShortText()); beaconMapItem.setName(new QString(name)); beaconMapItem.setLatitude(beacon->m_latitude); beaconMapItem.setLongitude(beacon->m_longitude); beaconMapItem.setAltitude(beacon->m_altitude); beaconMapItem.setImage(new QString("antenna.png")); beaconMapItem.setImageRotation(0); beaconMapItem.setImageMinZoom(8); beaconMapItem.setText(new QString(beacon->getText())); m_mapModel.update(m_map, &beaconMapItem, MapSettings::SOURCE_BEACONS); } } void MapGUI::addIBPBeacons() { // Add to Map for (const auto beacon : IBPBeacon::m_beacons) { SWGSDRangel::SWGMapItem beaconMapItem; beaconMapItem.setName(new QString(beacon.m_callsign)); beaconMapItem.setLatitude(beacon.m_latitude); beaconMapItem.setLongitude(beacon.m_longitude); beaconMapItem.setAltitude(0); beaconMapItem.setImage(new QString("antenna.png")); beaconMapItem.setImageRotation(0); beaconMapItem.setImageMinZoom(8); beaconMapItem.setText(new QString(beacon.getText())); m_mapModel.update(m_map, &beaconMapItem, MapSettings::SOURCE_BEACONS); } } 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} }; void MapGUI::addRadioTimeTransmitters() { for (int i = 0; i < m_radioTimeTransmitters.size(); i++) { SWGSDRangel::SWGMapItem timeMapItem; // Need to suffix frequency, as there are multiple becaons with same callsign at different locations QString name = QString("%1").arg(m_radioTimeTransmitters[i].m_callsign); timeMapItem.setName(new QString(name)); timeMapItem.setLatitude(m_radioTimeTransmitters[i].m_latitude); timeMapItem.setLongitude(m_radioTimeTransmitters[i].m_longitude); timeMapItem.setAltitude(0.0); timeMapItem.setImage(new QString("antennatime.png")); timeMapItem.setImageRotation(0); timeMapItem.setImageMinZoom(8); QString text = QString("Radio Time Transmitter\nCallsign: %1\nFrequency: %2 kHz\nPower: %3 kW") .arg(m_radioTimeTransmitters[i].m_callsign) .arg(m_radioTimeTransmitters[i].m_frequency/1000.0) .arg(m_radioTimeTransmitters[i].m_power); timeMapItem.setText(new QString(text)); m_mapModel.update(m_map, &timeMapItem, MapSettings::SOURCE_RADIO_TIME); } } void MapGUI::addRadar() { SWGSDRangel::SWGMapItem radarMapItem; radarMapItem.setName(new QString("GRAVES")); radarMapItem.setLatitude(47.3480); radarMapItem.setLongitude(5.5151); radarMapItem.setAltitude(0.0); radarMapItem.setImage(new QString("antenna.png")); radarMapItem.setImageRotation(0); radarMapItem.setImageMinZoom(8); QString text = QString("Radar\nCallsign: %1\nFrequency: %2 MHz") .arg("GRAVES") .arg("143.050"); radarMapItem.setText(new QString(text)); m_mapModel.update(m_map, &radarMapItem, MapSettings::SOURCE_RADAR); } 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(); 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") { parameters["mapbox.map_id"] = "mapbox.satellite"; // The only one that works parameters["mapbox.access_token"] = m_settings.m_mapBoxAPIKey; } if (!m_settings.m_mapBoxAPIKey.isEmpty() && m_settings.m_mapProvider == "mapboxgl") { parameters["mapboxgl.access_token"] = m_settings.m_mapBoxAPIKey; if (!m_settings.m_mapBoxStyles.isEmpty()) parameters["mapboxgl.mapping.additional_style_urls"] = m_settings.m_mapBoxStyles; } 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 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(); if (object != nullptr) { // 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++) { ui->mapTypes->addItem(mapTypes[i]); } } } } void MapGUI::on_mapTypes_currentIndexChanged(int index) { QVariant mapType = index; QMetaObject::invokeMethod(ui->map->rootObject(), "setMapType", Q_ARG(QVariant, mapType)); } void MapGUI::displaySettings() { setTitleColor(m_settings.m_rgbColor); setWindowTitle(m_settings.m_title); blockApplySettings(true); ui->displayNames->setChecked(m_settings.m_displayNames); ui->displaySelectedGroundTracks->setChecked(m_settings.m_displaySelectedGroundTracks); ui->displayAllGroundTracks->setChecked(m_settings.m_displayAllGroundTracks); m_mapModel.setDisplayNames(m_settings.m_displayNames); m_mapModel.setDisplaySelectedGroundTracks(m_settings.m_displaySelectedGroundTracks); m_mapModel.setDisplayAllGroundTracks(m_settings.m_displayAllGroundTracks); m_mapModel.setSources(m_settings.m_sources); m_mapModel.setGroundTrackColor(m_settings.m_groundTrackColor); m_mapModel.setPredictedGroundTrackColor(m_settings.m_predictedGroundTrackColor); applyMapSettings(); restoreState(m_settings.m_rollupState); blockApplySettings(false); } void MapGUI::leaveEvent(QEvent*) { } void MapGUI::enterEvent(QEvent*) { } void MapGUI::onMenuDialogCalled(const QPoint &p) { if (m_contextMenuType == ContextMenuChannelSettings) { BasicFeatureSettingsDialog dialog(this); dialog.setTitle(m_settings.m_title); dialog.setColor(m_settings.m_rgbColor); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); dialog.move(p); dialog.exec(); m_settings.m_rgbColor = dialog.getColor().rgb(); m_settings.m_title = dialog.getTitle(); m_settings.m_useReverseAPI = dialog.useReverseAPI(); m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); setWindowTitle(m_settings.m_title); setTitleColor(m_settings.m_rgbColor); applySettings(); } resetContextMenuType(); } void MapGUI::applySettings(bool force) { if (m_doApplySettings) { Map::MsgConfigureMap* message = Map::MsgConfigureMap::create(m_settings, force); m_map->getInputMessageQueue()->push(message); } } void MapGUI::on_maidenhead_clicked() { MapMaidenheadDialog dialog; dialog.exec(); } void MapGUI::on_displayNames_clicked(bool checked) { m_settings.m_displayNames = checked; m_mapModel.setDisplayNames(checked); } void MapGUI::on_displaySelectedGroundTracks_clicked(bool checked) { m_settings.m_displaySelectedGroundTracks = checked; m_mapModel.setDisplaySelectedGroundTracks(checked); } void MapGUI::on_displayAllGroundTracks_clicked(bool checked) { m_settings.m_displayAllGroundTracks = checked; m_mapModel.setDisplayAllGroundTracks(checked); } void MapGUI::on_find_returnPressed() { find(ui->find->text().trimmed()); } void MapGUI::geoReply() { QGeoCodeReply *pQGeoCode = dynamic_cast(sender()); if ((pQGeoCode != nullptr) && (pQGeoCode->error() == QGeoCodeReply::NoError)) { QList qGeoLocs = pQGeoCode->locations(); QQuickItem *item = ui->map->rootObject(); QObject *map = item->findChild("map"); if (qGeoLocs.size() == 1) { // Only one result, so centre map on that map->setProperty("center", QVariant::fromValue(qGeoLocs.at(0).coordinate())); } else if (qGeoLocs.size() == 0) { qDebug() << "MapGUI::geoReply: No location found for address"; QApplication::beep(); } else { // Show dialog allowing user to select from the results MapLocationDialog dialog(qGeoLocs); if (dialog.exec() == QDialog::Accepted) map->setProperty("center", QVariant::fromValue(dialog.m_selectedLocation.coordinate())); } } else qWarning() << "MapGUI::geoReply: GeoCode error: " << pQGeoCode->error(); pQGeoCode->deleteLater(); } void MapGUI::find(const QString& target) { if (!target.isEmpty()) { QQuickItem *item = ui->map->rootObject(); QObject *map = item->findChild("map"); if (map != nullptr) { // Search as: // latitude and longitude // Maidenhead locator // object name // address float latitude, longitude; if (Units::stringToLatitudeAndLongitude(target, latitude, longitude)) { map->setProperty("center", QVariant::fromValue(QGeoCoordinate(latitude, longitude))); } else if (Maidenhead::fromMaidenhead(target, latitude, longitude)) { map->setProperty("center", QVariant::fromValue(QGeoCoordinate(latitude, longitude))); } else { MapItem *mapItem = m_mapModel.findMapItem(target); if (mapItem != nullptr) map->setProperty("center", QVariant::fromValue(mapItem->getCoordinates())); else { QGeoServiceProvider* geoSrv = new QGeoServiceProvider("osm"); if (geoSrv != nullptr) { QLocale qLocaleC(QLocale::C, QLocale::AnyCountry); geoSrv->setLocale(qLocaleC); QGeoCodeReply *pQGeoCode = geoSrv->geocodingManager()->geocode(target); if (pQGeoCode) QObject::connect(pQGeoCode, &QGeoCodeReply::finished, this, &MapGUI::geoReply); else qDebug() << "MapGUI::find: GeoCoding failed"; } else qDebug() << "MapGUI::find: osm not available"; } } } } } void MapGUI::on_deleteAll_clicked() { m_mapModel.removeAll(); } void MapGUI::on_displaySettings_clicked() { MapSettingsDialog dialog(&m_settings); if (dialog.exec() == QDialog::Accepted) { if (dialog.m_osmURLChanged) { clearOSMCache(); } if (dialog.m_mapSettingsChanged) { applyMapSettings(); } applySettings(); 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); } } void MapGUI::on_beacons_clicked() { m_beaconDialog.show(); } void MapGUI::on_ibpBeacons_clicked() { m_ibpBeaconDialog.show(); } void MapGUI::on_radiotime_clicked() { m_radioTimeDialog.updateTable(); m_radioTimeDialog.show(); } quint32 MapGUI::getSourceMask(const PipeEndPoint *sourcePipe) { for (int i = 0; i < m_availablePipes.size(); i++) { if (m_availablePipes[i].m_source == sourcePipe) { for (int j = 0; j < MapSettings::m_pipeTypes.size(); j++) { if (m_availablePipes[i].m_id == MapSettings::m_pipeTypes[j]) return 1 << j; } } } return 0; } QString MapGUI::getDataDir() { // Get directory to store app data in (aircraft & airport databases and user-definable icons) QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); // First dir is writable return locations[0]; } QString MapGUI::getBeaconFilename() { return MapGUI::getDataDir() + "/iaru_beacons.csv"; } QQuickItem *MapGUI::getMapItem() { return ui->map->rootObject(); }