diff --git a/plugins/feature/map/CMakeLists.txt b/plugins/feature/map/CMakeLists.txt index 26b8103c6..fe578a004 100644 --- a/plugins/feature/map/CMakeLists.txt +++ b/plugins/feature/map/CMakeLists.txt @@ -7,6 +7,7 @@ set(map_SOURCES mapwebapiadapter.cpp osmtemplateserver.cpp ibpbeacon.cpp + webserver.cpp ) set(map_HEADERS @@ -18,10 +19,12 @@ set(map_HEADERS osmtemplateserver.h beacon.h ibpbeacon.h + webserver.h ) include_directories( ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${Qt5Gui_PRIVATE_INCLUDE_DIRS} ) if(NOT SERVER_MODE) @@ -41,8 +44,14 @@ if(NOT SERVER_MODE) mapibpbeacondialog.ui mapradiotimedialog.cpp mapradiotimedialog.ui + mapcolordialog.cpp + mapmodel.cpp + mapwebsocketserver.cpp + cesiuminterface.cpp + czml.cpp map.qrc icons.qrc + cesium.qrc ) set(map_HEADERS ${map_HEADERS} @@ -53,10 +62,15 @@ if(NOT SERVER_MODE) mapbeacondialog.h mapibpbeacon.h mapradiotimedialog.h + mapcolordialog.h + mapmodel.h + mapwebsocketserver.h + cesiuminterface.h + czml.h ) set(TARGET_NAME map) - set(TARGET_LIB "Qt5::Widgets" Qt5::Quick Qt5::QuickWidgets Qt5::Positioning Qt5::Location) + set(TARGET_LIB "Qt5::Widgets" Qt5::Quick Qt5::QuickWidgets Qt5::Positioning Qt5::Location Qt5::WebEngine Qt5::WebEngineCore Qt5::WebEngineWidgets) set(TARGET_LIB_GUI "sdrgui") set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) else() @@ -84,3 +98,8 @@ if(WIN32) include(DeployQt) windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/map) endif() + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/feature/map/cesiuminterface.cpp b/plugins/feature/map/cesiuminterface.cpp new file mode 100644 index 000000000..b9a81b8e3 --- /dev/null +++ b/plugins/feature/map/cesiuminterface.cpp @@ -0,0 +1,219 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "cesiuminterface.h" + +CesiumInterface::CesiumInterface(const MapSettings *settings, QObject *parent) : + m_czml(settings), + MapWebSocketServer(parent) +{ +} + +// Set the view displayed when the Home button is pressed +// Angle in degrees of longitude or latitude either side of central location +void CesiumInterface::setHomeView(float latitude, float longitude, float angle) +{ + QJsonObject obj { + {"command", "setHomeView"}, + {"latitude", latitude}, + {"longitude", longitude}, + {"angle", angle} + }; + send(obj); +} + +// Set the current camera view to a given location +void CesiumInterface::setView(float latitude, float longitude, float altitude) +{ + QJsonObject obj { + {"command", "setView"}, + {"latitude", latitude}, + {"longitude", longitude}, + {"altitude", altitude} + }; + send(obj); +} + +// Play glTF model animation for the map item with the specified name +void CesiumInterface::playAnimation(const QString &name, Animation *animation) +{ + QJsonObject obj { + {"command", "playAnimation"}, + {"id", name}, + {"animation", animation->m_name}, + {"startDateTime", animation->m_startDateTime}, + {"reverse", animation->m_reverse}, + {"loop", animation->m_loop}, + {"stop", animation->m_stop}, + {"startOffset", animation->m_startOffset}, + {"duration", animation->m_duration}, + {"multiplier", animation->m_multiplier} + }; + send(obj); +} + +// Set date and time for the map +void CesiumInterface::setDateTime(QDateTime dateTime) +{ + QJsonObject obj { + {"command", "setDateTime"}, + {"dateTime", dateTime.toString(Qt::ISODate)} + }; + send(obj); +} + +// Get current date and time from the map +void CesiumInterface::getDateTime() +{ + QJsonObject obj { + {"command", "getDateTime"} + }; + send(obj); +} + +// Set the camera to track the map item with the specified name +void CesiumInterface::track(const QString& name) +{ + QJsonObject obj { + {"command", "trackId"}, + {"id", name} + }; + send(obj); +} + +void CesiumInterface::setTerrain(const QString &terrain, const QString &maptilerAPIKey) +{ + QString provider; + QString url; + if (terrain == "Maptiler") + { + provider = "CesiumTerrainProvider"; + url = "https://api.maptiler.com/tiles/terrain-quantized-mesh/?key=" + maptilerAPIKey; + } + else if (terrain == "ArcGIS") + { + provider = "ArcGISTiledElevationTerrainProvider"; + url = "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"; + } + else + { + provider = terrain; + } + QJsonObject obj { + {"command", "setTerrain"}, + {"provider", provider}, + {"url", url} + }; + send(obj); +} + +void CesiumInterface::setBuildings(const QString &buildings) +{ + QJsonObject obj { + {"command", "setBuildings"}, + {"buildings", buildings} + }; + send(obj); +} + +void CesiumInterface::setSunLight(bool useSunLight) +{ + QJsonObject obj { + {"command", "setSunLight"}, + {"useSunLight", useSunLight} + }; + send(obj); +} + +void CesiumInterface::setCameraReferenceFrame(bool eci) +{ + QJsonObject obj { + {"command", "setCameraReferenceFrame"}, + {"eci", eci} + }; + send(obj); +} + +void CesiumInterface::setAntiAliasing(const QString &antiAliasing) +{ + QJsonObject obj { + {"command", "setAntiAliasing"}, + {"antiAliasing", antiAliasing} + }; + send(obj); +} + +void CesiumInterface::updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data) +{ + QJsonObject obj { + {"command", "updateImage"}, + {"name", name}, + {"east", east}, + {"west", west}, + {"north", north}, + {"south", south}, + {"altitude", altitude}, + {"data", data}, + }; + send(obj); +} + +void CesiumInterface::removeImage(const QString &name) +{ + QJsonObject obj { + {"command", "removeImage"}, + {"name", name} + }; + send(obj); +} + +void CesiumInterface::removeAllImages() +{ + QJsonObject obj { + {"command", "removeAllImages"} + }; + send(obj); +} + +// Remove all entities created by CZML +void CesiumInterface::removeAllCZMLEntities() +{ + QJsonObject obj { + {"command", "removeAllCZMLEntities"} + }; + send(obj); +} + +void CesiumInterface::initCZML() +{ + czml(m_czml.init()); +} + +// Send and process CZML +void CesiumInterface::czml(QJsonObject &obj) +{ + obj.insert("command", "czml"); + send(obj); +} + +void CesiumInterface::update(MapItem *mapItem, bool isTarget, bool isSelected) +{ + QJsonObject obj = m_czml.update(mapItem, isTarget, isSelected); + czml(obj); +} diff --git a/plugins/feature/map/cesiuminterface.h b/plugins/feature/map/cesiuminterface.h new file mode 100644 index 000000000..6f21b9b84 --- /dev/null +++ b/plugins/feature/map/cesiuminterface.h @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_CESIUMINTERFACE_H_ +#define INCLUDE_FEATURE_CESIUMINTERFACE_H_ + +#include "mapwebsocketserver.h" +#include "czml.h" +#include "swgmapanimation.h" + +class MapItem; + +class CesiumInterface : public MapWebSocketServer +{ +public: + + struct Animation { + Animation(SWGSDRangel::SWGMapAnimation *swgAnimation) + { + m_name = *swgAnimation->getName(); + m_startDateTime = *swgAnimation->getStartDateTime(); + m_reverse = swgAnimation->getReverse(); + m_loop = swgAnimation->getLoop(); + m_stop = swgAnimation->getStop(); + m_startOffset = swgAnimation->getStartOffset(); + m_duration = swgAnimation->getDuration(); + m_multiplier = swgAnimation->getMultiplier(); + } + + QString m_name; + QString m_startDateTime; // No need to convert to QDateTime, as we don't use it in c++ + bool m_reverse; + bool m_loop; + bool m_stop; // Stop looped animation + float m_delay; // Delay in seconds before animation starts + float m_startOffset; // [0..1] What point to start playing animation + float m_duration; // How long to play animation for + float m_multiplier; // Speed to play animation at + }; + + CesiumInterface(const MapSettings *settings, QObject *parent = nullptr); + void setHomeView(float latitude, float longitude, float angle=1.0f); + void setView(float latitude, float longitude, float altitude=60000); + void playAnimation(const QString &name, Animation *animation); + void setDateTime(QDateTime dateTime); + void getDateTime(); + void track(const QString &name); + void setTerrain(const QString &terrain, const QString &maptilerAPIKey); + void setBuildings(const QString &buildings); + void setCameraReferenceFrame(bool eci); + void setSunLight(bool useSunLight); + void setAntiAliasing(const QString &antiAliasing); + void updateImage(const QString &name, float east, float west, float north, float south, float altitude, const QString &data); + void removeImage(const QString &name); + void removeAllImages(); + void removeAllCZMLEntities(); + void initCZML(); + void czml(QJsonObject &obj); + void update(MapItem *mapItem, bool isTarget, bool isSelected); + +protected: + + CZML m_czml; +}; + +#endif // INCLUDE_FEATURE_CESIUMINTERFACE_H_ diff --git a/plugins/feature/map/czml.cpp b/plugins/feature/map/czml.cpp new file mode 100644 index 000000000..2ce94adaa --- /dev/null +++ b/plugins/feature/map/czml.cpp @@ -0,0 +1,531 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "czml.h" +#include "mapsettings.h" +#include "mapmodel.h" + +#include "util/units.h" + +CZML::CZML(const MapSettings *settings, QObject *parent) : + m_settings(settings) +{ +} + +QJsonObject CZML::init() +{ + QString start = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + QString stop = QDateTime::currentDateTimeUtc().addSecs(60*60).toString(Qt::ISODate); + QString interval = QString("%1/%2").arg(start).arg(stop); + + QJsonObject spec { + {"interval", interval}, + {"currentTime", start}, + {"range", "UNBOUNDED"} + }; + QJsonObject doc { + {"id", "document"}, + {"version", "1.0"}, + {"clock", spec} + }; + + return doc; +} + +// Convert a position specified in longitude, latitude in degrees and height in metres above WGS84 ellipsoid in to +// Earth Centered Earth Fixed frame cartesian coordinates +// See Cesium.Cartesian3.fromDegrees +QVector3D CZML::cartesian3FromDegrees(double longitude, double latitude, double height) const +{ + return cartesianFromRadians(Units::degreesToRadians(longitude), Units::degreesToRadians(latitude), height); +} + +// FIXME: QVector3D is only float! +// See Cesium.Cartesian3.fromRadians +QVector3D CZML::cartesianFromRadians(double longitude, double latitude, double height) const +{ + QVector3D wgs84RadiiSquared(6378137.0 * 6378137.0, 6378137.0 * 6378137.0, 6356752.3142451793 * 6356752.3142451793); + + double cosLatitude = cos(latitude); + QVector3D n; + n.setX(cosLatitude * cos(longitude)); + n.setY(cosLatitude * sin(longitude)); + n.setZ(sin(latitude)); + n.normalize(); + QVector3D k; + k = wgs84RadiiSquared * n; + double gamma = sqrt(QVector3D::dotProduct(n, k)); + k = k / gamma; + n = n * height; + return k + n; +} + +// Convert heading, pitch and roll in degrees to a quaternoin +// See: Cesium.Quaternion.fromHeadingPitchRoll +QQuaternion CZML::fromHeadingPitchRoll(double heading, double pitch, double roll) const +{ + QVector3D xAxis(1, 0, 0); + QVector3D yAxis(0, 1, 0); + QVector3D zAxis(0, 0, 1); + + QQuaternion rollQ = QQuaternion::fromAxisAndAngle(xAxis, roll); + + QQuaternion pitchQ = QQuaternion::fromAxisAndAngle(yAxis, -pitch); + + QQuaternion headingQ = QQuaternion::fromAxisAndAngle(zAxis, -heading); + + QQuaternion temp = rollQ * pitchQ; + + return headingQ * temp; +} + +// Calculate a transformation matrix from a East, North, Up frame at the given position to Earth Centered Earth Fixed frame +// See: Cesium.Transforms.eastNorthUpToFixedFrame +QMatrix4x4 CZML::eastNorthUpToFixedFrame(QVector3D origin) const +{ + // TODO: Handle special case at centre of earth and poles + QVector3D up = origin.normalized(); + QVector3D east(-origin.y(), origin.x(), 0.0); + east.normalize(); + QVector3D north = QVector3D::crossProduct(up, east); + QMatrix4x4 result( + east.x(), north.x(), up.x(), origin.x(), + east.y(), north.y(), up.y(), origin.y(), + east.z(), north.z(), up.z(), origin.z(), + 0.0, 0.0, 0.0, 1.0 + ); + return result; +} + +// Convert 3x3 rotation matrix to a quaternoin +// Although there is a method for this in Qt: QQuaternion::fromRotationMatrix, it seems to +// result in different signs, so the following is based on Cesium code +QQuaternion CZML::fromRotation(QMatrix3x3 mat) const +{ + QQuaternion q; + + double trace = mat(0, 0) + mat(1, 1) + mat(2, 2); + + if (trace > 0.0) + { + double root = sqrt(trace + 1.0); + q.setScalar(0.5 * root); + root = 0.5 / root; + + q.setX((mat(2,1) - mat(1,2)) * root); + q.setY((mat(0,2) - mat(2,0)) * root); + q.setZ((mat(1,0) - mat(0,1)) * root); + } + else + { + double next[] = {1, 2, 0}; + int i = 0; + if (mat(1,1) > mat(0,0)) { + i = 1; + } + if (mat(2,2) > mat(0,0) && mat(2,2) > mat(1,1)) { + i = 2; + } + int j = next[i]; + int k = next[j]; + + double root = sqrt(mat(i,i) - mat(j,j) - mat(k,k) + 1); + double quat[] = {0.0, 0.0, 0.0}; + quat[i] = 0.5 * root; + root = 0.5 / root; + + q.setScalar((mat(j,k) - mat(k,j)) * root); + quat[j] = (mat(i,j) + mat(j,i)) * root; + quat[k] = (mat(i,k) + mat(k,i)) * root; + q.setX(-quat[0]); + q.setY(-quat[1]); + q.setZ(-quat[2]); + } + return q; +} + +// Calculate orientation quaternion for a model (such as an aircraft) based on position and (HPR) heading, pitch and roll (in degrees) +// While Cesium supports specifying orientation as HPR, CZML doesn't currently. See https://github.com/CesiumGS/cesium/issues/5184 +// CZML requires the orientation to be in the Earth Centered Earth Fixed (geocentric) reference frame (https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates) +// The orientation therefore depends not only on HPR but also on position +// +// glTF uses a right-handed axis convention; that is, the cross product of right and forward yields up. glTF defines +Y as up, +Z as forward, and -X as right. +// Cesium.Quaternion.fromHeadingPitchRoll Heading is the rotation about the negative z axis. Pitch is the rotation about the negative y axis. Roll is the rotation about the positive x axis. +QQuaternion CZML::orientation(double longitude, double latitude, double altitude, double heading, double pitch, double roll) const +{ + // Forward direction for gltf models in Cesium seems to be Eastward, rather than Northward, so we adjust heading by -90 degrees + heading = -90 + heading; + + // Convert position to Earth Centered Earth Fixed (ECEF) frame + QVector3D positionECEF = cartesian3FromDegrees(longitude, latitude, altitude); + + // Calculate matrix to transform from East, North, Up (ENU) frame to ECEF frame + QMatrix4x4 enuToECEFTransform = eastNorthUpToFixedFrame(positionECEF); + + // Calculate rotation based on HPR in ENU frame + QQuaternion hprENU = fromHeadingPitchRoll(heading, pitch, roll); + + // Transform rotation from ENU to ECEF + QMatrix3x3 hprENU3 = hprENU.toRotationMatrix(); + QMatrix4x4 hprENU4(hprENU3); + QMatrix4x4 transform = enuToECEFTransform * hprENU4; + + // Convert from 4x4 matrix to 3x3 matrix then to a quaternion + QQuaternion oq = fromRotation(transform.toGenericMatrix<3,3>()); + + return oq; +} + +QJsonObject CZML::update(MapItem *mapItem, bool isTarget, bool isSelected) +{ + // Don't currently use CLIP_TO_GROUND in Cesium due to Jitter bug + // https://github.com/CesiumGS/cesium/issues/4049 + // Instead we implement our own clipping code in map3d.html + const QStringList heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "NONE"}; + QString dt; + + if (mapItem->m_takenTrackDateTimes.size() > 0) { + dt = mapItem->m_takenTrackDateTimes.last()->toString(Qt::ISODateWithMs); + } else { + dt = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs); + } + + QString id = mapItem->m_name; + + // Keep a hash of the time we first saw each item + bool existingId = m_ids.contains(id); + if (!existingId) { + m_ids.insert(id, dt); + } + + bool removeObj = false; + bool fixedPosition = mapItem->m_fixedPosition; + + float displayDistanceMax = std::numeric_limits::max(); + QString image = mapItem->m_image; + if ((image == "antenna.png") || (image == "antennaam.png") || (image == "antennadab.png") || (image == "antennafm.png") || (image == "antennatime.png")) { + displayDistanceMax = 1000000; + } + if (image == "") { + // Need to remove this from the map + removeObj = true; + } + + QJsonArray coords; + if (!removeObj) + { + if (!fixedPosition && (mapItem->m_predictedTrackCoords.size() > 0)) + { + QListIterator i(mapItem->m_takenTrackCoords); + QListIterator j(mapItem->m_takenTrackDateTimes); + while (i.hasNext()) + { + QGeoCoordinate *c = i.next(); + coords.append(j.next()->toString(Qt::ISODateWithMs)); + coords.append(c->longitude()); + coords.append(c->latitude()); + coords.append(c->altitude()); + } + if (mapItem->m_predictedTrackCoords.size() > 0) + { + QListIterator k(mapItem->m_predictedTrackCoords); + QListIterator l(mapItem->m_predictedTrackDateTimes); + k.toBack(); + l.toBack(); + while (k.hasPrevious()) + { + QGeoCoordinate *c = k.previous(); + coords.append(l.previous()->toString(Qt::ISODateWithMs)); + coords.append(c->longitude()); + coords.append(c->latitude()); + coords.append(c->altitude()); + } + } + } + else + { + // Only send latest position, to reduce processing + if (!fixedPosition && mapItem->m_positionDateTime.isValid()) { + coords.push_back(mapItem->m_positionDateTime.toString(Qt::ISODateWithMs)); + } + coords.push_back(mapItem->m_longitude); + coords.push_back(mapItem->m_latitude); + coords.push_back(mapItem->m_altitude); + } + } + else + { + coords = m_lastPosition.value(id); + } + QJsonObject position { + {"cartographicDegrees", coords}, + }; + if (!fixedPosition) + { + // Don't use forward extrapolation for satellites (with predicted tracks), as + // it seems to jump about. We use it for AIS and ADS-B that don't have predicted tracks + if (mapItem->m_predictedTrackCoords.size() == 0) + { + // Need 2 different positions to enable extrapolation, otherwise entity may not appear + bool hasMoved = m_hasMoved.contains(id); + if (!hasMoved && m_lastPosition.contains(id) && (m_lastPosition.value(id) != coords)) + { + hasMoved = true; + m_hasMoved.insert(id, true); + } + if (hasMoved) + { + position.insert("forwardExtrapolationType", "EXTRAPOLATE"); + position.insert("forwardExtrapolationDuration", 60); + // Use linear interpolation for now - other two can go crazy with aircraft on the ground + //position.insert("interpolationAlgorithm", "HERMITE"); + //position.insert("interpolationDegree", "2"); + //position.insert("interpolationAlgorithm", "LAGRANGE"); + //position.insert("interpolationDegree", "5"); + } + else + { + position.insert("forwardExtrapolationType", "HOLD"); + } + } + else + { + // Interpolation goes wrong at end points + //position.insert("interpolationAlgorithm", "LAGRANGE"); + //position.insert("interpolationDegree", "5"); + //position.insert("interpolationAlgorithm", "HERMITE"); + //position.insert("interpolationDegree", "2"); + } + } + + QQuaternion q = orientation(mapItem->m_longitude, mapItem->m_latitude, mapItem->m_altitude, + mapItem->m_heading, mapItem->m_pitch, mapItem->m_roll); + QJsonArray quaternion; + if (!fixedPosition && mapItem->m_orientationDateTime.isValid()) { + quaternion.push_back(mapItem->m_orientationDateTime.toString(Qt::ISODateWithMs)); + } + quaternion.push_back(q.x()); + quaternion.push_back(q.y()); + quaternion.push_back(q.z()); + quaternion.push_back(q.scalar()); + + QJsonObject orientation { + {"unitQuaternion", quaternion}, + {"forwardExtrapolationType", "HOLD"}, // If we extrapolate, aircraft tend to spin around + {"forwardExtrapolationDuration", 60}, + // {"interpolationAlgorithm", "LAGRANGE"} + }; + QJsonObject orientationPosition { + {"velocityReference", "#position"}, + }; + QJsonObject noPosition { + {"cartographicDegrees", coords}, + {"forwardExtrapolationType", "NONE"} + }; + + // Point + QColor pointColor = QColor::fromRgba(mapItem->m_itemSettings->m_3DPointColor); + QJsonArray pointRGBA { + pointColor.red(), pointColor.green(), pointColor.blue(), pointColor.alpha() + }; + QJsonObject pointColorObj { + {"rgba", pointRGBA} + }; + QJsonObject point { + {"pixelSize", 8}, + {"color", pointColorObj}, + {"heightReference", heightReferences[mapItem->m_altitudeReference]}, + {"show", mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DPoint} + }; + // If clamping to ground, we need to disable depth test, so part of the point isn't clipped + // However, when the point isn't clamped to ground, we shouldn't use this, otherwise + // the point will become visible through the globe + if (mapItem->m_altitudeReference == 1) { + point.insert("disableDepthTestDistance", 100000000); + } + + // Model + QJsonArray node0Cartesian { + {0.0, mapItem->m_modelAltitudeOffset, 0.0} + }; + QJsonObject node0Translation { + {"cartesian", node0Cartesian} + }; + QJsonObject node0Transform { + {"translation", node0Translation} + }; + QJsonObject nodeTransforms { + {"node0", node0Transform}, + }; + QJsonObject model { + {"gltf", m_settings->m_modelURL + mapItem->m_model}, + {"incrementallyLoadTextures", false}, // Aircraft will flash as they appear without textures if this is the default of true + {"heightReference", heightReferences[mapItem->m_altitudeReference]}, + {"runAnimations", false}, + {"show", mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DModel}, + {"minimumPixelSize", mapItem->m_itemSettings->m_3DModelMinPixelSize}, + {"maximumScale", 20000} // Stop it getting too big when zoomed really far out + }; + if (mapItem->m_modelAltitudeOffset != 0.0) { + model.insert("nodeTransformations", nodeTransforms); + } + + // Path + QColor pathColor = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + QJsonArray pathColorRGBA { + pathColor.red(), pathColor.green(), pathColor.blue(), pathColor.alpha() + }; + QJsonObject pathColorObj { + {"rgba", pathColorRGBA} + }; + // Paths can't be clamped to ground, so AIS paths can be underground if terrain is used + // See: https://github.com/CesiumGS/cesium/issues/7133 + QJsonObject pathSolidColorMaterial { + {"color", pathColorObj} + }; + QJsonObject pathMaterial { + {"solidColor", pathSolidColorMaterial} + }; + bool showPath = mapItem->m_itemSettings->m_enabled + && mapItem->m_itemSettings->m_display3DTrack + && ( m_settings->m_displayAllGroundTracks + || (m_settings->m_displaySelectedGroundTracks && isSelected)); + QJsonObject path { + // We want full paths for sat tracker, so leadTime and trailTime should be 0 + // Should be configurable.. 6000=100mins ~> 1 orbit for LEO + //{"leadTime", "6000"}, + //{"trailTime", "6000"}, + {"width", "3"}, + {"material", pathMaterial}, + {"show", showPath} + }; + + // Label + QJsonArray labelPixelOffsetArray { + 20, 0 + }; + QJsonObject labelPixelOffset { + {"cartesian2", labelPixelOffsetArray} + }; + QJsonArray labelEyeOffsetArray { + 0, mapItem->m_labelAltitudeOffset, 0 // Position above the object, dependent on the height of the model + }; + QJsonObject labelEyeOffset { + {"cartesian", labelEyeOffsetArray} + }; + QJsonObject labelHorizontalOrigin { + {"horizontalOrigin", "LEFT"} + }; + QJsonArray labelDisplayDistance { + 0, displayDistanceMax + }; + QJsonObject labelDistanceDisplayCondition { + {"distanceDisplayCondition", labelDisplayDistance} + }; + QJsonObject label { + {"text", mapItem->m_label}, + {"show", m_settings->m_displayNames && mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DLabel}, + {"scale", 0.5}, + {"pixelOffset", labelPixelOffset}, + {"eyeOffset", labelEyeOffset}, + {"verticalOrigin", "BASELINE"}, + {"horizontalOrigin", "LEFT"}, + {"heightReference", heightReferences[mapItem->m_altitudeReference]}, + }; + if (displayDistanceMax != std::numeric_limits::max()) + { + label.insert("disableDepthTestDistance", 100000000.0); + label.insert("distanceDisplayCondition", labelDistanceDisplayCondition); + } + + // Use billboard for APRS as we don't currently have 3D objects + QString imageURL = mapItem->m_image; + if (imageURL.startsWith("qrc://")) { + imageURL = imageURL.mid(6); // Redirect to our embedded webserver, which will check resources + } + QJsonObject billboard { + {"image", imageURL}, + {"heightReference", heightReferences[mapItem->m_altitudeReference]}, + {"verticalOrigin", "BOTTOM"} // To stop it being cut in half when zoomed out + }; + if (mapItem->m_altitudeReference == 1) { + billboard.insert("disableDepthTestDistance", 100000000); + } + + QJsonObject obj { + {"id", id} // id must be unique + }; + + if (!removeObj) + { + obj.insert("position", position); + if (!fixedPosition) + { + if (mapItem->m_useHeadingPitchRoll) { + obj.insert("orientation", orientation); + } else { + obj.insert("orientation", orientationPosition); + } + } + obj.insert("point", point); + if (!mapItem->m_model.isEmpty()) { + obj.insert("model", model); + } else { + obj.insert("billboard", billboard); + } + obj.insert("label", label); + obj.insert("description", mapItem->m_text); + if (!fixedPosition) { + obj.insert("path", path); + } + + if (!fixedPosition) + { + if (mapItem->m_takenTrackDateTimes.size() > 0 && mapItem->m_predictedTrackDateTimes.size() > 0) + { + QString availability = QString("%1/%2") + .arg(mapItem->m_takenTrackDateTimes.last()->toString(Qt::ISODateWithMs)) + .arg(mapItem->m_predictedTrackDateTimes.last()->toString(Qt::ISODateWithMs)); + obj.insert("availability", availability); + } + else + { + QString oneMin = QDateTime::currentDateTimeUtc().addSecs(60).toString(Qt::ISODateWithMs); + QString createdToNow = QString("%1/%2").arg(m_ids[id]).arg(oneMin); // From when object was created to now + obj.insert("availability", createdToNow); + } + } + m_lastPosition.insert(id, coords); + } + else + { + // Disable forward extrapolation + obj.insert("position", noPosition); + } + + // Use our own clipping routine, due to + // https://github.com/CesiumGS/cesium/issues/4049 + if (mapItem->m_altitudeReference == 3) { + obj.insert("altitudeReference", "CLIP_TO_GROUND"); + } + + //qDebug() << obj; + + return obj; +} diff --git a/plugins/feature/map/czml.h b/plugins/feature/map/czml.h new file mode 100644 index 000000000..f989b0191 --- /dev/null +++ b/plugins/feature/map/czml.h @@ -0,0 +1,57 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_CZML_H_ +#define INCLUDE_FEATURE_CZML_H_ + +#include +#include +#include +#include +#include +#include +#include + +struct MapSettings; +class MapItem; + +class CZML +{ +private: + const MapSettings *m_settings; + QHash m_ids; + QHash m_lastPosition; + QHash m_hasMoved; + +public: + CZML(const MapSettings *settings, QObject *parent = nullptr); + QJsonObject init(); + QJsonObject update(MapItem *mapItem, bool isTarget, bool isSelected); + +protected: + QVector3D cartesian3FromDegrees(double longitude, double latitude, double height=0.0) const; + QVector3D cartesianFromRadians(double longitude, double latitude, double height=0.0) const; + QQuaternion fromHeadingPitchRoll(double heading, double pitch, double roll) const; + QMatrix4x4 eastNorthUpToFixedFrame(QVector3D origin) const; + QQuaternion fromRotation(QMatrix3x3 mat) const; + QQuaternion orientation(double longitude, double latitude, double altitude, double heading, double pitch, double roll) const; + +signals: + void connected(); +}; + +#endif // INCLUDE_FEATURE_CZML_H_ diff --git a/plugins/feature/map/map.cpp b/plugins/feature/map/map.cpp index 082b05214..4445b8a3f 100644 --- a/plugins/feature/map/map.cpp +++ b/plugins/feature/map/map.cpp @@ -37,6 +37,7 @@ MESSAGE_CLASS_DEFINITION(Map::MsgConfigureMap, Message) MESSAGE_CLASS_DEFINITION(Map::MsgFind, Message) +MESSAGE_CLASS_DEFINITION(Map::MsgSetDateTime, Message) const char* const Map::m_featureIdURI = "sdrangel.feature.map"; const char* const Map::m_featureId = "Map"; @@ -221,14 +222,17 @@ int Map::webapiActionsPost( if (getMessageQueueToGUI()) { getMessageQueueToGUI()->push(MsgFind::create(id)); } - - return 202; } - else + if (featureActionsKeys.contains("setDateTime")) { - errorMessage = "Unknown action"; - return 400; + QString dateTimeString = *swgMapActions->getSetDateTime(); + QDateTime dateTime = QDateTime::fromString(dateTimeString, Qt::ISODateWithMs); + + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(MsgSetDateTime::create(dateTime)); + } } + return 202; } else { diff --git a/plugins/feature/map/map.h b/plugins/feature/map/map.h index 0379cef7c..2e356d140 100644 --- a/plugins/feature/map/map.h +++ b/plugins/feature/map/map.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "feature/feature.h" #include "util/message.h" @@ -82,6 +83,25 @@ public: {} }; + class MsgSetDateTime : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QDateTime getDateTime() const { return m_dateTime; } + + static MsgSetDateTime* create(const QDateTime& dateTime) { + return new MsgSetDateTime(dateTime); + } + + private: + QDateTime m_dateTime; + + MsgSetDateTime(const QDateTime& dateTime) : + Message(), + m_dateTime(dateTime) + {} + }; + Map(WebAPIAdapterInterface *webAPIAdapterInterface); virtual ~Map(); virtual void destroy() { delete this; } diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc index c71cd4ae7..f57f85053 100644 --- a/plugins/feature/map/map.qrc +++ b/plugins/feature/map/map.qrc @@ -7,5 +7,9 @@ map/antennadab.png map/antennafm.png map/antennaam.png + map/map3d.html + + + Cesium/Cesium.js diff --git a/plugins/feature/map/map/map.qml b/plugins/feature/map/map/map.qml index 5fa290553..d6d4da49b 100644 --- a/plugins/feature/map/map/map.qml +++ b/plugins/feature/map/map/map.qml @@ -174,6 +174,7 @@ Item { id: text anchors.centerIn: parent text: mapText + textFormat: TextEdit.RichText } MouseArea { anchors.fill: parent @@ -215,6 +216,10 @@ Item { text: "Move to back" onTriggered: mapModel.moveToBack(index) } + MenuItem { + text: "Track on 3D map" + onTriggered: mapModel.track3D(index) + } } } } diff --git a/plugins/feature/map/map/map3d.html b/plugins/feature/map/map/map3d.html new file mode 100644 index 000000000..a136fd5b8 --- /dev/null +++ b/plugins/feature/map/map/map3d.html @@ -0,0 +1,403 @@ + + + + + + + + + +
+ + + + diff --git a/plugins/feature/map/map/map_5_12.qml b/plugins/feature/map/map/map_5_12.qml index 39a3be447..df6f94175 100644 --- a/plugins/feature/map/map/map_5_12.qml +++ b/plugins/feature/map/map/map_5_12.qml @@ -174,6 +174,7 @@ Item { id: text anchors.centerIn: parent text: mapText + textFormat: TextEdit.RichText } MouseArea { anchors.fill: parent @@ -215,6 +216,10 @@ Item { text: "Move to back" onTriggered: mapModel.moveToBack(index) } + MenuItem { + text: "Track on 3D map" + onTriggered: mapModel.track3D(index) + } } } } diff --git a/plugins/feature/map/mapbeacondialog.cpp b/plugins/feature/map/mapbeacondialog.cpp index bfe3e5a86..454552bf0 100644 --- a/plugins/feature/map/mapbeacondialog.cpp +++ b/plugins/feature/map/mapbeacondialog.cpp @@ -27,8 +27,7 @@ MapBeaconDialog::MapBeaconDialog(MapGUI *gui, QWidget* parent) : QDialog(parent), m_gui(gui), - ui(new Ui::MapBeaconDialog), - m_progressDialog(nullptr) + ui(new Ui::MapBeaconDialog) { ui->setupUi(this); connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &MapBeaconDialog::downloadFinished); @@ -79,47 +78,6 @@ void MapBeaconDialog::updateTable() ui->beacons->resizeColumnsToContents(); } -qint64 MapBeaconDialog::fileAgeInDays(QString filename) -{ - QFile file(filename); - if (file.exists()) - { - QDateTime modified = file.fileTime(QFileDevice::FileModificationTime); - if (modified.isValid()) - return modified.daysTo(QDateTime::currentDateTime()); - else - return -1; - } - return -1; -} - -bool MapBeaconDialog::confirmDownload(QString filename) -{ - qint64 age = fileAgeInDays(filename); - if ((age == -1) || (age > 100)) - return true; - else - { - QMessageBox::StandardButton reply; - if (age == 0) - reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded today. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); - else if (age == 1) - reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded yesterday. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); - else - reply = QMessageBox::question(this, "Confirm download", QString("This file was last downloaded %1 days ago. Are you sure you wish to redownload this file?").arg(age), QMessageBox::Yes|QMessageBox::No); - return reply == QMessageBox::Yes; - } -} - -void MapBeaconDialog::updateDownloadProgress(qint64 bytesRead, qint64 totalBytes) -{ - if (m_progressDialog) - { - m_progressDialog->setMaximum(totalBytes); - m_progressDialog->setValue(bytesRead); - } -} - void MapBeaconDialog::accept() { QDialog::accept(); @@ -127,32 +85,28 @@ void MapBeaconDialog::accept() void MapBeaconDialog::on_downloadIARU_clicked() { - if (m_progressDialog == nullptr) + if (!m_dlm.downloading()) { QString beaconFile = MapGUI::getBeaconFilename(); - if (confirmDownload(beaconFile)) + if (HttpDownloadManagerGUI::confirmDownload(beaconFile, this)) { // Download IARU beacons database to disk QUrl dbURL(QString(IARU_BEACONS_URL)); - m_progressDialog = new QProgressDialog(this); - m_progressDialog->setCancelButton(nullptr); - m_progressDialog->setMinimumDuration(500); - m_progressDialog->setLabelText(QString("Downloading %1.").arg(IARU_BEACONS_URL)); - QNetworkReply *reply = m_dlm.download(dbURL, beaconFile); - connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64))); + m_dlm.download(dbURL, beaconFile, this); } } } -void MapBeaconDialog::downloadFinished(const QString& filename, bool success) +void MapBeaconDialog::downloadFinished(const QString& filename, bool success, const QString &url, const QString &errorMessage) { if (success) { if (filename == MapGUI::getBeaconFilename()) { QList *beacons = Beacon::readIARUCSV(filename); - if (beacons != nullptr) + if (beacons != nullptr) { m_gui->setBeacons(beacons); + } } else { @@ -161,14 +115,7 @@ void MapBeaconDialog::downloadFinished(const QString& filename, bool success) } else { - qDebug() << "MapBeaconDialog::downloadFinished: Failed: " << filename; - QMessageBox::warning(this, "Download failed", QString("Failed to download %1").arg(filename)); - } - if (m_progressDialog) - { - m_progressDialog->close(); - delete m_progressDialog; - m_progressDialog = nullptr; + QMessageBox::warning(this, "Download failed", QString("Failed to download %1 to %2\n%3").arg(url).arg(filename).arg(errorMessage)); } } diff --git a/plugins/feature/map/mapbeacondialog.h b/plugins/feature/map/mapbeacondialog.h index 94dc137d3..eeffffc9d 100644 --- a/plugins/feature/map/mapbeacondialog.h +++ b/plugins/feature/map/mapbeacondialog.h @@ -20,9 +20,7 @@ #include "ui_mapbeacondialog.h" -#include - -#include "util/httpdownloadmanager.h" +#include "gui/httpdownloadmanagergui.h" #include "beacon.h" class MapGUI; @@ -36,22 +34,18 @@ public: void updateTable(); private: - qint64 fileAgeInDays(QString filename); - bool confirmDownload(QString filename); - void downloadFinished(const QString& filename, bool success); + void downloadFinished(const QString& filename, bool success, const QString &url, const QString &errorMessage); private slots: void accept(); void on_downloadIARU_clicked(); - void updateDownloadProgress(qint64 bytesRead, qint64 totalBytes); void on_beacons_cellDoubleClicked(int row, int column); void on_filter_currentIndexChanged(int index); private: MapGUI *m_gui; Ui::MapBeaconDialog* ui; - HttpDownloadManager m_dlm; - QProgressDialog *m_progressDialog; + HttpDownloadManagerGUI m_dlm; enum BeaconCol { BEACON_COL_CALLSIGN, diff --git a/plugins/feature/map/mapcolordialog.cpp b/plugins/feature/map/mapcolordialog.cpp new file mode 100644 index 000000000..5dd1bb36d --- /dev/null +++ b/plugins/feature/map/mapcolordialog.cpp @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 +#include +#include +#include + +#include "mapcolordialog.h" + +MapColorDialog::MapColorDialog(const QColor &initial, QWidget *parent) : + QDialog(parent) +{ + m_colorDialog = new QColorDialog(initial); + m_colorDialog->setWindowFlags(Qt::Widget); + m_colorDialog->setOptions(QColorDialog::ShowAlphaChannel | QColorDialog::NoButtons | QColorDialog::DontUseNativeDialog); + QVBoxLayout *v = new QVBoxLayout(this); + v->addWidget(m_colorDialog); + QHBoxLayout *h = new QHBoxLayout(); + m_noColorButton = new QPushButton("No Color"); + m_cancelButton = new QPushButton("Cancel"); + m_okButton = new QPushButton("OK"); + h->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); + h->addWidget(m_noColorButton); + h->addWidget(m_cancelButton); + h->addWidget(m_okButton); + v->addLayout(h); + + connect(m_noColorButton, &QPushButton::clicked, this, &MapColorDialog::noColorClicked); + connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject); + connect(m_okButton, &QPushButton::clicked, this, &QDialog::accept); + + m_noColorSelected = false; +} + +QColor MapColorDialog::selectedColor() const +{ + return m_colorDialog->selectedColor(); +} + +bool MapColorDialog::noColorSelected() const +{ + return m_noColorSelected; +} + +void MapColorDialog::accept() +{ + m_colorDialog->accept(); + QDialog::accept(); +} + +void MapColorDialog::noColorClicked() +{ + m_noColorSelected = true; + accept(); +} diff --git a/plugins/feature/map/mapcolordialog.h b/plugins/feature/map/mapcolordialog.h new file mode 100644 index 000000000..dddde2a7a --- /dev/null +++ b/plugins/feature/map/mapcolordialog.h @@ -0,0 +1,45 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_FEATURE_MAPCOLORDIALOG_H +#define INCLUDE_FEATURE_MAPCOLORDIALOG_H + +#include + +class MapColorDialog : public QDialog { + Q_OBJECT + +public: + explicit MapColorDialog(const QColor &initial, QWidget *parent = nullptr); + QColor selectedColor() const; + bool noColorSelected() const; + +public slots: + virtual void accept() override; + void noColorClicked(); + +private: + + QColorDialog *m_colorDialog; + QPushButton *m_noColorButton; + QPushButton *m_cancelButton; + QPushButton *m_okButton; + + bool m_noColorSelected; +}; + +#endif // INCLUDE_FEATURE_MAPCOLORDIALOG_H diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index f90d27ec0..211a3eefb 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -17,20 +17,19 @@ /////////////////////////////////////////////////////////////////////////////////// #include -#include -#include #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" @@ -46,539 +45,6 @@ #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); @@ -643,11 +109,34 @@ bool MapGUI::handleMessage(const Message& message) find(msgFind.getTarget()); return true; } + else if (Map::MsgSetDateTime::match(message)) + { + Map::MsgSetDateTime& msgSetDateTime = (Map::MsgSetDateTime&) message; + if (m_cesium) { + m_cesium->setDateTime(msgSetDateTime.getDateTime()); + } + 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); + + // TODO: Could have this in SWGMapItem so plugins can create additional groups + QString group; + for (int i = 0; i < m_availablePipes.size(); i++) + { + if (m_availablePipes[i].m_source == msgMapItem.getPipeSource()) + { + for (int j = 0; j < MapSettings::m_pipeTypes.size(); j++) + { + if (m_availablePipes[i].m_id == MapSettings::m_pipeTypes[j]) { + group = m_availablePipes[i].m_id; + } + } + } + } + update(msgMapItem.getPipeSource(), swgMapItem, group); return true; } @@ -685,15 +174,18 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur m_beacons(nullptr), m_beaconDialog(this), m_ibpBeaconDialog(this), - m_radioTimeDialog(this) + m_radioTimeDialog(this), + m_cesium(nullptr) { 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); + m_osmPort = 0; + m_templateServer = new OSMTemplateServer(thunderforestAPIKey(), maptilerAPIKey(), m_osmPort); + + // Web server to serve dynamic files from QResources + m_webPort = 0; + m_webServer = new WebServer(m_webPort); ui->map->rootContext()->setContextProperty("mapModel", &m_mapModel); // 5.12 doesn't display map items when fully zoomed out @@ -703,6 +195,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map.qml"))); #endif + m_settings.m_modelURL = QString("http://127.0.0.1:%1/3d/").arg(m_webPort); + m_webServer->addPathSubstitution("3d", m_settings.m_modelDir); + setAttribute(Qt::WA_DeleteOnClose, true); setChannelWidget(false); connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); @@ -715,8 +210,9 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); - displaySettings(); - applySettings(true); + QWebEngineSettings *settings = ui->web->settings(); + settings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true); + connect(ui->web->page(), &QWebEnginePage::fullScreenRequested, this, &MapGUI::fullScreenRequested); // Get station position float stationLatitude = MainCore::instance()->getSettings().getLatitude(); @@ -743,30 +239,76 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur 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); + antennaMapItem.setModel(new QString("antenna.glb")); + antennaMapItem.setFixedPosition(true); + antennaMapItem.setOrientation(0); + antennaMapItem.setLabel(new QString(MainCore::instance()->getSettings().getStationName())); + antennaMapItem.setLabelAltitudeOffset(4.5); + antennaMapItem.setAltitudeReference(1); + update(m_map, &antennaMapItem, "Station"); // Read beacons, if they exist QList *beacons = Beacon::readIARUCSV(MapGUI::getBeaconFilename()); - if (beacons != nullptr) + if (beacons != nullptr) { setBeacons(beacons); + } addIBPBeacons(); addRadioTimeTransmitters(); addRadar(); + + displaySettings(); + applySettings(true); + + ui->map->installEventFilter(this); } MapGUI::~MapGUI() { + //m_cesium->deleteLater(); + delete m_cesium; if (m_templateServer) { m_templateServer->close(); delete m_templateServer; } + if (m_webServer) + { + m_webServer->close(); + delete m_webServer; + } delete ui; } +// Update a map item or image +void MapGUI::update(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group) +{ + if (swgMapItem->getType() == 0) + { + m_mapModel.update(source, swgMapItem, group); + } + else if (m_cesium) + { + QString name = *swgMapItem->getName(); + QString image = *swgMapItem->getImage(); + if (!image.isEmpty()) + { + m_cesium->updateImage(name, + swgMapItem->getImageTileEast(), + swgMapItem->getImageTileWest(), + swgMapItem->getImageTileNorth(), + swgMapItem->getImageTileSouth(), + swgMapItem->getAltitude(), + image); + } + else + { + m_cesium->removeImage(name); + } + } +} + void MapGUI::setBeacons(QList *beacons) { delete m_beacons; @@ -786,9 +328,16 @@ void MapGUI::setBeacons(QList *beacons) 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); + beaconMapItem.setModel(new QString("antenna.glb")); + beaconMapItem.setFixedPosition(true); + beaconMapItem.setOrientation(0); + // Just use callsign for label, so we don't have multiple labels on top of each other on 3D map + // as it makes them unreadable + beaconMapItem.setLabel(new QString(beacon->m_callsign)); + beaconMapItem.setLabelAltitudeOffset(4.5); + beaconMapItem.setAltitudeReference(1); + update(m_map, &beaconMapItem, "Beacons"); } } @@ -804,9 +353,14 @@ void MapGUI::addIBPBeacons() 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); + beaconMapItem.setModel(new QString("antenna.glb")); + beaconMapItem.setFixedPosition(true); + beaconMapItem.setOrientation(0); + beaconMapItem.setLabel(new QString(beacon.m_callsign)); + beaconMapItem.setLabelAltitudeOffset(4.5); + beaconMapItem.setAltitudeReference(1); + update(m_map, &beaconMapItem, "Beacons"); } } @@ -831,13 +385,18 @@ void MapGUI::addRadioTimeTransmitters() 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); + timeMapItem.setModel(new QString("antenna.glb")); + timeMapItem.setFixedPosition(true); + timeMapItem.setOrientation(0); + timeMapItem.setLabel(new QString(name)); + timeMapItem.setLabelAltitudeOffset(4.5); + timeMapItem.setAltitudeReference(1); + update(m_map, &timeMapItem, "Radio Time Transmitters"); } } @@ -850,12 +409,17 @@ void MapGUI::addRadar() 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); + radarMapItem.setModel(new QString("antenna.glb")); + radarMapItem.setFixedPosition(true); + radarMapItem.setOrientation(0); + radarMapItem.setLabel(new QString("GRAVES")); + radarMapItem.setLabelAltitudeOffset(4.5); + radarMapItem.setAltitudeReference(1); + update(m_map, &radarMapItem, "Radar"); } static QString arrayToString(QJsonArray array) @@ -907,13 +471,18 @@ void MapGUI::addDAB() mapItem.setLongitude(lon); mapItem.setAltitude(alt); mapItem.setImageRotation(0); - mapItem.setImageMinZoom(8); + mapItem.setModel(new QString("antenna.glb")); + mapItem.setFixedPosition(true); + mapItem.setOrientation(0); + mapItem.setLabelAltitudeOffset(4.5); + mapItem.setAltitudeReference(1); 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")); + mapItem.setLabel(new QString(name)); // 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) @@ -928,7 +497,7 @@ void MapGUI::addDAB() .arg(tx.value("ensembleLabel").toString()) ; mapItem.setText(new QString(text)); - m_mapModel.update(m_map, &mapItem, MapSettings::SOURCE_DAB); + update(m_map, &mapItem, "DAB"); } else if (band == "FM") { @@ -936,6 +505,7 @@ void MapGUI::addDAB() QString name = QString("%1").arg(tx.value("tsId").toString()); mapItem.setName(new QString(name)); mapItem.setImage(new QString("antennafm.png")); + mapItem.setLabel(new QString(name)); QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 %4\nPower: %5 %6\nLanguage(s): %7\nType: %8") .arg(band) .arg(stationName) @@ -947,7 +517,7 @@ void MapGUI::addDAB() .arg(format) ; mapItem.setText(new QString(text)); - m_mapModel.update(m_map, &mapItem, MapSettings::SOURCE_FM); + update(m_map, &mapItem, "FM"); } else if (band == "AM") { @@ -955,6 +525,7 @@ void MapGUI::addDAB() QString name = QString("%1").arg(tx.value("tsId").toString()); mapItem.setName(new QString(name)); mapItem.setImage(new QString("antennaam.png")); + mapItem.setLabel(new QString(name)); QString text = QString("%1 Transmitter\nStation: %2\nFrequency: %3 %4\nPower: %5 %6\nLanguage(s): %7\nType: %8") .arg(band) .arg(stationName) @@ -966,7 +537,7 @@ void MapGUI::addDAB() .arg(format) ; mapItem.setText(new QString(text)); - m_mapModel.update(m_map, &mapItem, MapSettings::SOURCE_AM); + update(m_map, &mapItem, "AM"); } } } @@ -1014,90 +585,149 @@ void MapGUI::clearOSMCache() } } -void MapGUI::applyMapSettings() +void MapGUI::applyMap2DSettings(bool reloadMap) { - float stationLatitude = MainCore::instance()->getSettings().getLatitude(); - float stationLongitude = MainCore::instance()->getSettings().getLongitude(); - float stationAltitude = MainCore::instance()->getSettings().getAltitude(); + ui->map->setVisible(m_settings.m_map2DEnabled); - QQuickItem *item = ui->map->rootObject(); + if (m_settings.m_map2DEnabled && reloadMap) + { + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + float stationAltitude = MainCore::instance()->getSettings().getAltitude(); - 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; - } + QQuickItem *item = ui->map->rootObject(); - // 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()) + QObject *object = item->findChild("map"); + QGeoCoordinate coords; + double zoom; + if (object != nullptr) { - newMap->setProperty("zoomLevel", QVariant::fromValue(zoom)); - newMap->setProperty("center", QVariant::fromValue(coords)); + // 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 == "maplibre") + { + parameters["maplibre.access_token"] = m_settings.m_mapBoxAPIKey; + if (!m_settings.m_mapBoxStyles.isEmpty()) + parameters["maplibre.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::applyMap2DSettings - 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::applyMap2DSettings - createMap returned a nullptr"; + } + + supportedMapsChanged(); + } +} + +void MapGUI::redrawMap() +{ + // An awful workaround for https://bugreports.qt.io/browse/QTBUG-100333 + // Also used in ADS-B demod + QQuickItem *item = ui->map->rootObject(); + if (item) + { + QObject *object = item->findChild("map"); + if (object) + { + double zoom = object->property("zoomLevel").value(); + object->setProperty("zoomLevel", QVariant::fromValue(zoom+1)); + object->setProperty("zoomLevel", QVariant::fromValue(zoom)); } } - else - { - qCritical() << "MapGUI::applyMapSettings - createMap returned a nullptr"; - } +} - supportedMapsChanged(); +void MapGUI::showEvent(QShowEvent *event) +{ + if (!event->spontaneous()) + { + // Workaround for https://bugreports.qt.io/browse/QTBUG-100333 + // MapQuickItems can be in wrong position when window is first displayed + QTimer::singleShot(500, [this] { + redrawMap(); + }); + } +} + +bool MapGUI::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == ui->map) + { + if (event->type() == QEvent::Resize) + { + // Workaround for https://bugreports.qt.io/browse/QTBUG-100333 + // MapQuickItems can be in wrong position after vertical resize + QResizeEvent *resizeEvent = static_cast(event); + QSize oldSize = resizeEvent->oldSize(); + QSize size = resizeEvent->size(); + if (oldSize.height() != size.height()) { + redrawMap(); + } + } + } + return false; } void MapGUI::supportedMapsChanged() @@ -1106,6 +736,7 @@ void MapGUI::supportedMapsChanged() QObject *object = item->findChild("map"); // Get list of map types + ui->mapTypes->blockSignals(true); ui->mapTypes->clear(); if (object != nullptr) { @@ -1124,12 +755,85 @@ void MapGUI::supportedMapsChanged() } } } + ui->mapTypes->blockSignals(false); + + // Try to select desired map, if available + if (!m_settings.m_mapType.isEmpty()) + { + int index = ui->mapTypes->findText(m_settings.m_mapType); + if (index != -1) { + ui->mapTypes->setCurrentIndex(index); + } + } } void MapGUI::on_mapTypes_currentIndexChanged(int index) { - QVariant mapType = index; - QMetaObject::invokeMethod(ui->map->rootObject(), "setMapType", Q_ARG(QVariant, mapType)); + if (index >= 0) + { + QVariant mapType = index; + QMetaObject::invokeMethod(ui->map->rootObject(), "setMapType", Q_ARG(QVariant, mapType)); + QString currentMap = ui->mapTypes->currentText(); + if (!currentMap.isEmpty()) + { + m_settings.m_mapType = currentMap; + applySettings(); + } + } +} + +void MapGUI::applyMap3DSettings(bool reloadMap) +{ + if (m_settings.m_map3DEnabled && ((m_cesium == nullptr) || reloadMap)) + { + if (m_cesium == nullptr) { + m_cesium = new CesiumInterface(&m_settings); + connect(m_cesium, &CesiumInterface::connected, this, &MapGUI::init3DMap); + connect(m_cesium, &CesiumInterface::received, this, &MapGUI::receivedCesiumEvent); + } + m_webServer->addSubstitution("/map/map/map3d.html", "$WS_PORT$", QString::number(m_cesium->serverPort())); + m_webServer->addSubstitution("/map/map/map3d.html", "$CESIUM_ION_API_KEY$", cesiumIonAPIKey()); + //ui->web->page()->profile()->clearHttpCache(); + ui->web->load(QUrl(QString("http://127.0.0.1:%1/map/map/map3d.html").arg(m_webPort))); + //ui->web->load(QUrl("chrome://gpu/")); + ui->web->show(); + } + else if (!m_settings.m_map3DEnabled && (m_cesium != nullptr)) + { + ui->web->setHtml(""); + m_cesium->deleteLater(); + m_cesium = nullptr; + } + ui->web->setVisible(m_settings.m_map3DEnabled); + if (m_cesium && m_cesium->isConnected()) + { + m_cesium->setTerrain(m_settings.m_terrain, maptilerAPIKey()); + m_cesium->setBuildings(m_settings.m_buildings); + m_cesium->setSunLight(m_settings.m_sunLightEnabled); + m_cesium->setCameraReferenceFrame(m_settings.m_eciCamera); + m_cesium->setAntiAliasing(m_settings.m_antiAliasing); + } +} + +void MapGUI::init3DMap() +{ + qDebug() << "MapGUI::init3DMap"; + + m_cesium->initCZML(); + + m_cesium->setTerrain(m_settings.m_terrain, maptilerAPIKey()); + m_cesium->setBuildings(m_settings.m_buildings); + m_cesium->setSunLight(m_settings.m_sunLightEnabled); + m_cesium->setCameraReferenceFrame(m_settings.m_eciCamera); + m_cesium->setAntiAliasing(m_settings.m_antiAliasing); + + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + + // Set 3D view after loading initial objects + m_cesium->setHomeView(stationLatitude, stationLongitude); + + m_mapModel.allUpdated(); } void MapGUI::displaySettings() @@ -1143,10 +847,9 @@ void MapGUI::displaySettings() 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(); + m_mapModel.updateItemSettings(m_settings.m_itemSettings); + applyMap2DSettings(true); + applyMap3DSettings(true); restoreState(m_rollupState); blockApplySettings(false); } @@ -1242,7 +945,11 @@ void MapGUI::geoReply() if (qGeoLocs.size() == 1) { // Only one result, so centre map on that - map->setProperty("center", QVariant::fromValue(qGeoLocs.at(0).coordinate())); + QGeoCoordinate coord = qGeoLocs.at(0).coordinate(); + map->setProperty("center", QVariant::fromValue(coord)); + if (m_cesium) { + m_cesium->setView(coord.latitude(), coord.longitude()); + } } else if (qGeoLocs.size() == 0) { @@ -1254,14 +961,41 @@ void MapGUI::geoReply() // 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())); + { + QGeoCoordinate coord = dialog.m_selectedLocation.coordinate(); + map->setProperty("center", QVariant::fromValue(coord)); + if (m_cesium) { + m_cesium->setView(coord.latitude(), coord.longitude()); + } + } } } else + { qWarning() << "MapGUI::geoReply: GeoCode error: " << pQGeoCode->error(); + } pQGeoCode->deleteLater(); } +// Free keys, so no point in stealing them :) + +QString MapGUI::thunderforestAPIKey() const +{ + return m_settings.m_thunderforestAPIKey.isEmpty() ? "3e1f614f78a345459931ba3c898e975e" : m_settings.m_thunderforestAPIKey; +} + +QString MapGUI::maptilerAPIKey() const +{ + return m_settings.m_maptilerAPIKey.isEmpty() ? "q2RVNAe3eFKCH4XsrE3r" : m_settings.m_maptilerAPIKey; +} + +QString MapGUI::cesiumIonAPIKey() const +{ + return m_settings.m_cesiumIonAPIKey.isEmpty() + ? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyNTcxMDA2OC0yNTIzLTQxMGYtYTNiMS1iM2I3MDFhNWVlMDYiLCJpZCI6ODEyMDUsImlhdCI6MTY0MzY2OTIzOX0.A7NchU4LzaNsuAUpsrA9ZwekOJfMoNcja-8XeRdRoIg" + : m_settings.m_cesiumIonAPIKey; +} + void MapGUI::find(const QString& target) { if (!target.isEmpty()) @@ -1279,16 +1013,27 @@ void MapGUI::find(const QString& target) if (Units::stringToLatitudeAndLongitude(target, latitude, longitude)) { map->setProperty("center", QVariant::fromValue(QGeoCoordinate(latitude, longitude))); + if (m_cesium) { + m_cesium->setView(latitude, longitude); + } } else if (Maidenhead::fromMaidenhead(target, latitude, longitude)) { map->setProperty("center", QVariant::fromValue(QGeoCoordinate(latitude, longitude))); + if (m_cesium) { + m_cesium->setView(latitude, longitude); + } } else { MapItem *mapItem = m_mapModel.findMapItem(target); if (mapItem != nullptr) + { map->setProperty("center", QVariant::fromValue(mapItem->getCoordinates())); + if (m_cesium) { + m_cesium->track(target); + } + } else { QGeoServiceProvider* geoSrv = new QGeoServiceProvider("osm"); @@ -1297,22 +1042,37 @@ void MapGUI::find(const QString& target) QLocale qLocaleC(QLocale::C, QLocale::AnyCountry); geoSrv->setLocale(qLocaleC); QGeoCodeReply *pQGeoCode = geoSrv->geocodingManager()->geocode(target); - if (pQGeoCode) + if (pQGeoCode) { QObject::connect(pQGeoCode, &QGeoCodeReply::finished, this, &MapGUI::geoReply); - else + } else { qDebug() << "MapGUI::find: GeoCoding failed"; + } } else + { qDebug() << "MapGUI::find: osm not available"; + } } } } } } +void MapGUI::track3D(const QString& target) +{ + if (m_cesium) { + m_cesium->track(target); + } +} + void MapGUI::on_deleteAll_clicked() { m_mapModel.removeAll(); + if (m_cesium) + { + m_cesium->removeAllCZMLEntities(); + m_cesium->removeAllImages(); + } } void MapGUI::on_displaySettings_clicked() @@ -1323,15 +1083,10 @@ void MapGUI::on_displaySettings_clicked() if (dialog.m_osmURLChanged) { clearOSMCache(); } - if (dialog.m_mapSettingsChanged) { - applyMapSettings(); - } + applyMap2DSettings(dialog.m_map2DSettingsChanged); + applyMap3DSettings(dialog.m_map3DSettingsChanged); 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); + m_mapModel.allUpdated(); } } @@ -1351,22 +1106,6 @@ void MapGUI::on_radiotime_clicked() 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) @@ -1383,3 +1122,45 @@ QString MapGUI::getBeaconFilename() QQuickItem *MapGUI::getMapItem() { return ui->map->rootObject(); } + +void MapGUI::receivedCesiumEvent(const QJsonObject &obj) +{ + if (obj.contains("event")) + { + QString event = obj.value("event").toString(); + if (event == "selected") + { + if (obj.contains("id")) { + m_mapModel.setSelected3D(obj.value("id").toString()); + } else { + m_mapModel.setSelected3D(""); + } + } + else if (event == "tracking") + { + if (obj.contains("id")) { + //m_mapModel.setTarget(obj.value("id").toString()); + } else { + //m_mapModel.setTarget(""); + } + } + } + else + { + qDebug() << "MapGUI::receivedCesiumEvent - Unexpected event: " << obj; + } +} + +void MapGUI::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest) +{ + fullScreenRequest.accept(); + if (fullScreenRequest.toggleOn()) + { + ui->web->setParent(nullptr); + ui->web->showFullScreen(); + } + else + { + ui->splitter->addWidget(ui->web); + } +} diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index 375109eb2..1fc55ce14 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -20,9 +20,12 @@ #define INCLUDE_FEATURE_MAPGUI_H_ #include -#include -#include -#include +#include +#include +#include + +#include +#include #include "feature/featuregui.h" #include "util/messagequeue.h" @@ -36,7 +39,10 @@ #include "mapbeacondialog.h" #include "mapibpbeacondialog.h" #include "mapradiotimedialog.h" +#include "cesiuminterface.h" #include "osmtemplateserver.h" +#include "webserver.h" +#include "mapmodel.h" class PluginAPI; class FeatureUISet; @@ -47,8 +53,6 @@ namespace Ui { } class MapGUI; -class MapModel; -class QQuickItem; struct Beacon; struct RadioTimeTransmitter { @@ -59,415 +63,6 @@ struct RadioTimeTransmitter { int m_power; // In kW }; -// Information required about each item displayed on the map -class MapItem { - -public: - MapItem(const PipeEndPoint *sourcePipe, quint32 sourceMask, SWGSDRangel::SWGMapItem *mapItem) - { - m_sourcePipe = sourcePipe; - m_sourceMask = sourceMask; - m_name = *mapItem->getName(); - m_latitude = mapItem->getLatitude(); - m_longitude = mapItem->getLongitude(); - m_altitude = mapItem->getAltitude(); - m_image = *mapItem->getImage(); - m_imageRotation = mapItem->getImageRotation(); - m_imageMinZoom = mapItem->getImageMinZoom(); - QString *text = mapItem->getText(); - if (text != nullptr) - m_text = *text; - findFrequency(); - updateTrack(mapItem->getTrack()); - updatePredictedTrack(mapItem->getPredictedTrack()); - } - - void update(SWGSDRangel::SWGMapItem *mapItem) - { - m_latitude = mapItem->getLatitude(); - m_longitude = mapItem->getLongitude(); - m_altitude = mapItem->getAltitude(); - m_image = *mapItem->getImage(); - m_imageRotation = mapItem->getImageRotation(); - m_imageMinZoom = mapItem->getImageMinZoom(); - QString *text = mapItem->getText(); - if (text != nullptr) - m_text = *text; - findFrequency(); - updateTrack(mapItem->getTrack()); - updatePredictedTrack(mapItem->getPredictedTrack()); - } - - QGeoCoordinate getCoordinates() - { - QGeoCoordinate coords; - coords.setLatitude(m_latitude); - coords.setLongitude(m_longitude); - return coords; - } - -private: - - void findFrequency(); - - void updateTrack(QList *track) - { - if (track != nullptr) - { - qDeleteAll(m_takenTrackCoords); - m_takenTrackCoords.clear(); - m_takenTrack.clear(); - m_takenTrack1.clear(); - m_takenTrack2.clear(); - for (int i = 0; i < track->size(); i++) - { - SWGSDRangel::SWGMapCoordinate* p = track->at(i); - QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); - m_takenTrackCoords.push_back(c); - m_takenTrack.push_back(QVariant::fromValue(*c)); - } - } - else - { - // Automatically create a track - if (m_takenTrackCoords.size() == 0) - { - QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); - m_takenTrackCoords.push_back(c); - m_takenTrack.push_back(QVariant::fromValue(*c)); - } - else - { - QGeoCoordinate *prev = m_takenTrackCoords.last(); - if ((prev->latitude() != m_latitude) || (prev->longitude() != m_longitude) || (prev->altitude() != m_altitude)) - { - QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); - m_takenTrackCoords.push_back(c); - m_takenTrack.push_back(QVariant::fromValue(*c)); - } - } - } - } - - void updatePredictedTrack(QList *track) - { - if (track != nullptr) - { - qDeleteAll(m_predictedTrackCoords); - m_predictedTrackCoords.clear(); - m_predictedTrack.clear(); - m_predictedTrack1.clear(); - m_predictedTrack2.clear(); - for (int i = 0; i < track->size(); i++) - { - SWGSDRangel::SWGMapCoordinate* p = track->at(i); - QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); - m_predictedTrackCoords.push_back(c); - m_predictedTrack.push_back(QVariant::fromValue(*c)); - } - } - } - - friend MapModel; - const PipeEndPoint *m_sourcePipe; // Channel/feature that created the item - quint32 m_sourceMask; // Source bitmask as per MapSettings::SOURCE_* constants - QString m_name; - float m_latitude; - float m_longitude; - float m_altitude; // In metres - QString m_image; - int m_imageRotation; - int m_imageMinZoom; - QString m_text; - double m_frequency; // Frequency to set - QString m_frequencyString; - QList m_predictedTrackCoords; - QVariantList m_predictedTrack; // Line showing where the object is going - QVariantList m_predictedTrack1; - QVariantList m_predictedTrack2; - QGeoCoordinate m_predictedStart1; - QGeoCoordinate m_predictedStart2; - QGeoCoordinate m_predictedEnd1; - QGeoCoordinate m_predictedEnd2; - QList m_takenTrackCoords; - QVariantList m_takenTrack; // Line showing where the object has been - QVariantList m_takenTrack1; - QVariantList m_takenTrack2; - QGeoCoordinate m_takenStart1; - QGeoCoordinate m_takenStart2; - QGeoCoordinate m_takenEnd1; - QGeoCoordinate m_takenEnd2; -}; - -// Model used for each item on the map -class MapModel : public QAbstractListModel { - Q_OBJECT - -public: - using QAbstractListModel::QAbstractListModel; - enum MarkerRoles { - positionRole = Qt::UserRole + 1, - mapTextRole = Qt::UserRole + 2, - mapTextVisibleRole = Qt::UserRole + 3, - mapImageVisibleRole = Qt::UserRole + 4, - mapImageRole = Qt::UserRole + 5, - mapImageRotationRole = Qt::UserRole + 6, - mapImageMinZoomRole = Qt::UserRole + 7, - bubbleColourRole = Qt::UserRole + 8, - selectedRole = Qt::UserRole + 9, - targetRole = Qt::UserRole + 10, - frequencyRole = Qt::UserRole + 11, - frequencyStringRole = Qt::UserRole + 12, - predictedGroundTrack1Role = Qt::UserRole + 13, - predictedGroundTrack2Role = Qt::UserRole + 14, - groundTrack1Role = Qt::UserRole + 15, - groundTrack2Role = Qt::UserRole + 16, - groundTrackColorRole = Qt::UserRole + 17, - predictedGroundTrackColorRole = Qt::UserRole + 18 - }; - - MapModel(MapGUI *gui) : - m_gui(gui), - m_target(-1), - m_sources(-1) - { - setGroundTrackColor(0); - setPredictedGroundTrackColor(0); - } - - Q_INVOKABLE void add(MapItem *item) - { - beginInsertRows(QModelIndex(), rowCount(), rowCount()); - m_items.append(item); - m_selected.append(false); - endInsertRows(); - } - - void update(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *swgMapItem, quint32 sourceMask=0); - - void updateTarget(); - - void update(MapItem *item) - { - int row = m_items.indexOf(item); - if (row >= 0) - { - QModelIndex idx = index(row); - emit dataChanged(idx, idx); - if (row == m_target) - updateTarget(); - } - } - - void remove(MapItem *item) - { - int row = m_items.indexOf(item); - if (row >= 0) - { - beginRemoveRows(QModelIndex(), row, row); - m_items.removeAt(row); - m_selected.removeAt(row); - if (row == m_target) - m_target = -1; - endRemoveRows(); - } - } - - Q_INVOKABLE void moveToFront(int oldRow) - { - // Last item in list is drawn on top, so remove than add to end of list - if (oldRow < m_items.size() - 1) - { - bool wasTarget = m_target == oldRow; - MapItem *item = m_items[oldRow]; - bool wasSelected = m_selected[oldRow]; - remove(item); - add(item); - int newRow = m_items.size() - 1; - if (wasTarget) - m_target = newRow; - m_selected[newRow] = wasSelected; - QModelIndex idx = index(newRow); - emit dataChanged(idx, idx); - } - } - - Q_INVOKABLE void moveToBack(int oldRow) - { - // First item in list is drawn first, so remove item then add to front of list - if ((oldRow < m_items.size()) && (oldRow > 0)) - { - bool wasTarget = m_target == oldRow; - int newRow = 0; - // See: https://forum.qt.io/topic/122991/changing-the-order-mapquickitems-are-drawn-on-a-map - //QModelIndex parent; - //beginMoveRows(parent, oldRow, oldRow, parent, newRow); - beginResetModel(); - m_items.move(oldRow, newRow); - m_selected.move(oldRow, newRow); - if (wasTarget) - m_target = newRow; - //endMoveRows(); - endResetModel(); - //emit dataChanged(index(oldRow), index(newRow)); - } - } - - MapItem *findMapItem(const PipeEndPoint *source, const QString& name) - { - // FIXME: Should consider adding a QHash for this - QListIterator i(m_items); - while (i.hasNext()) - { - MapItem *item = i.next(); - if ((item->m_name == name) && (item->m_sourcePipe == source)) - return item; - } - return nullptr; - } - - MapItem *findMapItem(const QString& name) - { - QListIterator i(m_items); - while (i.hasNext()) - { - MapItem *item = i.next(); - if (item->m_name == name) - return item; - } - return nullptr; - } - - int rowCount(const QModelIndex &parent = QModelIndex()) const override - { - Q_UNUSED(parent) - return m_items.count(); - } - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override; - - Qt::ItemFlags flags(const QModelIndex &index) const override - { - (void) index; - return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; - } - - void allUpdated() - { - for (int i = 0; i < m_items.count(); i++) - { - QModelIndex idx = index(i); - emit dataChanged(idx, idx); - } - } - - void removeAll() - { - if (m_items.count() > 0) - { - beginRemoveRows(QModelIndex(), 0, m_items.count()); - m_items.clear(); - m_selected.clear(); - endRemoveRows(); - } - } - - void setDisplayNames(bool displayNames) - { - m_displayNames = displayNames; - allUpdated(); - } - - void setDisplaySelectedGroundTracks(bool displayGroundTracks) - { - m_displaySelectedGroundTracks = displayGroundTracks; - allUpdated(); - } - - void setDisplayAllGroundTracks(bool displayGroundTracks) - { - m_displayAllGroundTracks = displayGroundTracks; - allUpdated(); - } - - void setGroundTrackColor(quint32 color) - { - m_groundTrackColor = QVariant::fromValue(QColor::fromRgb(color)); - } - - void setPredictedGroundTrackColor(quint32 color) - { - m_predictedGroundTrackColor = QVariant::fromValue(QColor::fromRgb(color)); - } - - Q_INVOKABLE void setFrequency(double frequency); - - void interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); - void interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); - void interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen); - - void splitTracks(MapItem *item); - void splitTrack(const QList& coords, const QVariantList& track, - QVariantList& track1, QVariantList& track2, - QGeoCoordinate& start1, QGeoCoordinate& start2, - QGeoCoordinate& end1, QGeoCoordinate& end2); - Q_INVOKABLE void viewChanged(double bottomLeftLongitude, double bottomRightLongitude); - - QHash roleNames() const - { - QHash roles; - roles[positionRole] = "position"; - roles[mapTextRole] = "mapText"; - roles[mapTextVisibleRole] = "mapTextVisible"; - roles[mapImageVisibleRole] = "mapImageVisible"; - roles[mapImageRole] = "mapImage"; - roles[mapImageRotationRole] = "mapImageRotation"; - roles[mapImageMinZoomRole] = "mapImageMinZoom"; - roles[bubbleColourRole] = "bubbleColour"; - roles[selectedRole] = "selected"; - roles[targetRole] = "target"; - roles[frequencyRole] = "frequency"; - roles[frequencyStringRole] = "frequencyString"; - roles[predictedGroundTrack1Role] = "predictedGroundTrack1"; - roles[predictedGroundTrack2Role] = "predictedGroundTrack2"; - roles[groundTrack1Role] = "groundTrack1"; - roles[groundTrack2Role] = "groundTrack2"; - roles[groundTrackColorRole] = "groundTrackColor"; - roles[predictedGroundTrackColorRole] = "predictedGroundTrackColor"; - return roles; - } - - // Set the sources of data we should display - void setSources(quint32 sources) - { - m_sources = sources; - allUpdated(); - } - - // Linear interpolation - double interpolate(double x0, double y0, double x1, double y1, double x) - { - return (y0*(x1-x) + y1*(x-x0)) / (x1-x0); - } - -private: - MapGUI *m_gui; - QList m_items; - QList m_selected; - int m_target; // Row number of current target, or -1 for none - bool m_displayNames; - bool m_displaySelectedGroundTracks; - bool m_displayAllGroundTracks; - quint32 m_sources; - QVariant m_groundTrackColor; - QVariant m_predictedGroundTrackColor; - - double m_bottomLeftLongitude; - double m_bottomRightLongitude; -}; - class MapGUI : public FeatureGUI { Q_OBJECT public: @@ -481,7 +76,6 @@ public: AzEl *getAzEl() { return &m_azEl; } Map *getMap() { return m_map; } QQuickItem *getMapItem(); - quint32 getSourceMask(const PipeEndPoint *sourcePipe); static QString getBeaconFilename(); QList *getBeacons() { return m_beacons; } void setBeacons(QList *beacons); @@ -491,7 +85,10 @@ public: void addRadar(); void addDAB(); void find(const QString& target); + void track3D(const QString& target); Q_INVOKABLE void supportedMapsChanged(); + MapSettings::MapItemSettings *getItemSettings(const QString &group) { return m_settings.m_itemSettings[group]; } + CesiumInterface *cesium() { return m_cesium; } private: Ui::MapGUI* ui; @@ -513,17 +110,27 @@ private: quint16 m_osmPort; OSMTemplateServer *m_templateServer; + CesiumInterface *m_cesium; + WebServer *m_webServer; + quint16 m_webPort; + explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); virtual ~MapGUI(); + void update(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group); void blockApplySettings(bool block); void applySettings(bool force = false); - void applyMapSettings(); + void applyMap2DSettings(bool reloadMap); + void applyMap3DSettings(bool reloadMap); QString osmCachePath(); void clearOSMCache(); void displaySettings(); bool handleMessage(const Message& message); void geoReply(); + QString thunderforestAPIKey() const; + QString maptilerAPIKey() const; + QString cesiumIonAPIKey() const; + void redrawMap(); void leaveEvent(QEvent*); void enterEvent(QEvent*); @@ -532,6 +139,7 @@ private: static const QList m_radioTimeTransmitters; private slots: + void init3DMap(); void onMenuDialogCalled(const QPoint &p); void onWidgetRolled(QWidget* widget, bool rollDown); void handleInputMessages(); @@ -546,7 +154,11 @@ private slots: void on_beacons_clicked(); void on_ibpBeacons_clicked(); void on_radiotime_clicked(); + void receivedCesiumEvent(const QJsonObject &obj); + virtual void showEvent(QShowEvent *event); + virtual bool eventFilter(QObject *obj, QEvent *event); + void fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest); + }; - #endif // INCLUDE_FEATURE_MAPGUI_H_ diff --git a/plugins/feature/map/mapgui.ui b/plugins/feature/map/mapgui.ui index 36b6256cd..8a87d2234 100644 --- a/plugins/feature/map/mapgui.ui +++ b/plugins/feature/map/mapgui.ui @@ -7,7 +7,7 @@ 0 0 481 - 750 + 507 @@ -277,8 +277,8 @@ 0 60 - 471 - 681 + 483 + 223 @@ -290,47 +290,72 @@ Map - + - 2 + 3 - 3 + 0 - 3 + 0 - 3 + 0 - 3 + 0 - + 0 0 - - - 100 - 590 - - - - Map - - - QQuickWidget::SizeRootObjectToView - - - - - + + Qt::Vertical + + + + 0 + 0 + + + + + 100 + 100 + + + + Map + + + QQuickWidget::SizeRootObjectToView + + + + + + + + + + + 0 + 0 + + + + + 100 + 100 + + + @@ -353,6 +378,12 @@ QToolButton
gui/buttonswitch.h
+ + QWebEngineView + QWidget +
QWebEngineView.h
+ 1 +
find diff --git a/plugins/feature/map/mapmodel.cpp b/plugins/feature/map/mapmodel.cpp new file mode 100644 index 000000000..d793bee86 --- /dev/null +++ b/plugins/feature/map/mapmodel.cpp @@ -0,0 +1,997 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 + +#include "channel/channelwebapiutils.h" +#include "pipes/messagepipes.h" +#include "maincore.h" + +#include "mapmodel.h" +#include "mapgui.h" +#include "map.h" + +#include "SWGTargetAzimuthElevation.h" + +MapItem::MapItem(const PipeEndPoint *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) : + m_altitude(0.0) +{ + m_sourcePipe = sourcePipe; + m_group = group; + m_itemSettings = itemSettings; + m_name = *mapItem->getName(); + update(mapItem); +} + +void MapItem::update(SWGSDRangel::SWGMapItem *mapItem) +{ + if (mapItem->getLabel()) { + m_label = *mapItem->getLabel(); + } else { + m_label = ""; + } + m_latitude = mapItem->getLatitude(); + m_longitude = mapItem->getLongitude(); + m_altitude = mapItem->getAltitude(); + if (mapItem->getPositionDateTime()) { + m_positionDateTime = QDateTime::fromString(*mapItem->getPositionDateTime(), Qt::ISODateWithMs); + } else { + m_positionDateTime = QDateTime(); + } + m_useHeadingPitchRoll = mapItem->getOrientation() == 1; + m_heading = mapItem->getHeading(); + m_pitch = mapItem->getPitch(); + m_roll = mapItem->getRoll(); + if (mapItem->getOrientationDateTime()) { + m_orientationDateTime = QDateTime::fromString(*mapItem->getOrientationDateTime(), Qt::ISODateWithMs); + } else { + m_orientationDateTime = QDateTime(); + } + m_image = *mapItem->getImage(); + m_imageRotation = mapItem->getImageRotation(); + QString *text = mapItem->getText(); + if (text != nullptr) { + m_text = text->replace("\n", "
"); // Convert to HTML + } else { + m_text = ""; + } + if (mapItem->getModel()) { + m_model = *mapItem->getModel(); + } else { + m_model = ""; + } + m_labelAltitudeOffset = mapItem->getLabelAltitudeOffset(); + m_modelAltitudeOffset = mapItem->getModelAltitudeOffset(); + m_altitudeReference = mapItem->getAltitudeReference(); + m_fixedPosition = mapItem->getFixedPosition(); + QList *animations = mapItem->getAnimations(); + if (animations) + { + for (auto animation : *animations) { + m_animations.append(new CesiumInterface::Animation(animation)); + } + } + findFrequency(); + updateTrack(mapItem->getTrack()); + updatePredictedTrack(mapItem->getPredictedTrack()); +} + +QGeoCoordinate MapItem::getCoordinates() +{ + QGeoCoordinate coords; + coords.setLatitude(m_latitude); + coords.setLongitude(m_longitude); + return coords; +} + +void MapItem::findFrequency() +{ + // Look for a frequency in the text for this object + QRegExp re("(([0-9]+(\\.[0-9]+)?) *([kMG])?Hz)"); + if (re.indexIn(m_text) != -1) + { + QStringList capture = re.capturedTexts(); + m_frequency = capture[2].toDouble(); + if (capture.length() == 5) + { + QChar unit = capture[4][0]; + if (unit == 'k') + m_frequency *= 1000.0; + else if (unit == 'M') + m_frequency *= 1000000.0; + else if (unit == 'G') + m_frequency *= 1000000000.0; + } + m_frequencyString = capture[0]; + } + else + { + m_frequency = 0.0; + } +} + +void MapItem::updateTrack(QList *track) +{ + if (track != nullptr) + { + qDeleteAll(m_takenTrackCoords); + m_takenTrackCoords.clear(); + qDeleteAll(m_takenTrackDateTimes); + m_takenTrackDateTimes.clear(); + m_takenTrack.clear(); + m_takenTrack1.clear(); + m_takenTrack2.clear(); + for (int i = 0; i < track->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = track->at(i); + QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); + QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate)); + m_takenTrackCoords.push_back(c); + m_takenTrackDateTimes.push_back(d); + m_takenTrack.push_back(QVariant::fromValue(*c)); + } + } + else + { + // Automatically create a track + if (m_takenTrackCoords.size() == 0) + { + QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); + m_takenTrackCoords.push_back(c); + if (m_positionDateTime.isValid()) { + m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime)); + } else { + m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime())); + } + m_takenTrack.push_back(QVariant::fromValue(*c)); + } + else + { + QGeoCoordinate *prev = m_takenTrackCoords.last(); + QDateTime *prevDateTime = m_takenTrackDateTimes.last(); + if ((prev->latitude() != m_latitude) || (prev->longitude() != m_longitude) + || (prev->altitude() != m_altitude) || (*prevDateTime != m_positionDateTime)) + { + QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude); + m_takenTrackCoords.push_back(c); + if (m_positionDateTime.isValid()) { + m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime)); + } else { + m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime())); + } + m_takenTrack.push_back(QVariant::fromValue(*c)); + } + } + } +} + +void MapItem::updatePredictedTrack(QList *track) +{ + if (track != nullptr) + { + qDeleteAll(m_predictedTrackCoords); + m_predictedTrackCoords.clear(); + qDeleteAll(m_predictedTrackDateTimes); + m_predictedTrackDateTimes.clear(); + m_predictedTrack.clear(); + m_predictedTrack1.clear(); + m_predictedTrack2.clear(); + for (int i = 0; i < track->size(); i++) + { + SWGSDRangel::SWGMapCoordinate* p = track->at(i); + QGeoCoordinate *c = new QGeoCoordinate(p->getLatitude(), p->getLongitude(), p->getAltitude()); + QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate)); + m_predictedTrackCoords.push_back(c); + m_predictedTrackDateTimes.push_back(d); + m_predictedTrack.push_back(QVariant::fromValue(*c)); + } + } +} + +MapModel::MapModel(MapGUI *gui) : + m_gui(gui), + m_target(-1) +{ + connect(this, &MapModel::dataChanged, this, &MapModel::update3DMap); +} + +Q_INVOKABLE void MapModel::add(MapItem *item) +{ + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_items.append(item); + m_selected.append(false); + endInsertRows(); +} + +void MapModel::update(const PipeEndPoint *sourcePipe, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group) +{ + 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); + // Need to call update, for it to be removed in 3D map + // Item is set to not be available from this point in time + // It will still be avialable if time is set in the past + item->update(swgMapItem); + } + 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()) + { + // Add new item + item = new MapItem(sourcePipe, group, m_gui->getItemSettings(group), swgMapItem); + add(item); + // Add to 3D Map (we don't appear to get a dataChanged signal when adding) + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) { + cesium->update(item, isTarget(item), isSelected3D(item)); + } + playAnimations(item); + } + } +} + +// Slot called on dataChanged signal, to update 3D map +void MapModel::update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) + { + for (int row = topLeft.row(); row <= bottomRight.row(); row++) + { + cesium->update(m_items[row], isTarget(m_items[row]), isSelected3D(m_items[row])); + playAnimations(m_items[row]); + } + } +} + +void MapModel::playAnimations(MapItem *item) +{ + CesiumInterface *cesium = m_gui->cesium(); + if (cesium) + { + for (auto animation : item->m_animations) { + m_gui->cesium()->playAnimation(item->m_name, animation); + } + } + qDeleteAll(item->m_animations); + item->m_animations.clear(); +} + +void MapModel::update(MapItem *item) +{ + int row = m_items.indexOf(item); + if (row >= 0) + { + QModelIndex idx = index(row); + emit dataChanged(idx, idx); + if (row == m_target) { + updateTarget(); + } + } +} + +void MapModel::remove(MapItem *item) +{ + int row = m_items.indexOf(item); + if (row >= 0) + { + beginRemoveRows(QModelIndex(), row, row); + m_items.removeAt(row); + m_selected.removeAt(row); + if (row == m_target) { + m_target = -1; + } else if (row < m_target) { + m_target--; + } + endRemoveRows(); + } +} + +void MapModel::allUpdated() +{ + for (int i = 0; i < m_items.count(); i++) + { + // Updates both 2D and 3D Map + QModelIndex idx = index(i); + emit dataChanged(idx, idx); + } +} + +void MapModel::removeAll() +{ + if (m_items.count() > 0) + { + beginRemoveRows(QModelIndex(), 0, m_items.count()); + m_items.clear(); + m_selected.clear(); + endRemoveRows(); + } +} + +// After new settings are deserialised, we need to update +// pointers to item settings for all existing items +void MapModel::updateItemSettings(QHash m_itemSettings) +{ + for (auto item : m_items) { + item->m_itemSettings = m_itemSettings[item->m_group]; + } +} + +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::setTarget(const QString& name) +{ + if (name.isEmpty()) + { + QModelIndex idx = index(-1); + setData(idx, QVariant(-1), MapModel::targetRole); + } + else + { + QModelIndex idx = findMapItemIndex(name); + setData(idx, QVariant(idx.row()), MapModel::targetRole); + } +} + +bool MapModel::isTarget(const MapItem *mapItem) const +{ + if (m_target < 0) { + return false; + } else { + return m_items[m_target] == mapItem; + } +} + +// FIXME: This should use Z order - rather than adding/removing +// but I couldn't quite get it to work +Q_INVOKABLE void MapModel::moveToFront(int oldRow) +{ + // Last item in list is drawn on top, so remove than add to end of list + if (oldRow < m_items.size() - 1) + { + bool wasTarget = m_target == oldRow; + MapItem *item = m_items[oldRow]; + bool wasSelected = m_selected[oldRow]; + remove(item); + add(item); + int newRow = m_items.size() - 1; + if (wasTarget) { + m_target = newRow; + } + m_selected[newRow] = wasSelected; + QModelIndex idx = index(newRow); + emit dataChanged(idx, idx); + } +} + +Q_INVOKABLE void MapModel::moveToBack(int oldRow) +{ + // First item in list is drawn first, so remove item then add to front of list + if ((oldRow < m_items.size()) && (oldRow > 0)) + { + bool wasTarget = m_target == oldRow; + int newRow = 0; + // See: https://forum.qt.io/topic/122991/changing-the-order-mapquickitems-are-drawn-on-a-map + //QModelIndex parent; + //beginMoveRows(parent, oldRow, oldRow, parent, newRow); + beginResetModel(); + m_items.move(oldRow, newRow); + m_selected.move(oldRow, newRow); + if (wasTarget) { + m_target = newRow; + } else if (m_target >= 0) { + m_target++; + } + //endMoveRows(); + endResetModel(); + //emit dataChanged(index(oldRow), index(newRow)); + } +} + +MapItem *MapModel::findMapItem(const PipeEndPoint *source, const QString& name) +{ + // FIXME: Should consider adding a QHash for this + QListIterator i(m_items); + while (i.hasNext()) + { + MapItem *item = i.next(); + if ((item->m_name == name) && (item->m_sourcePipe == source)) + return item; + } + return nullptr; +} + +MapItem *MapModel::findMapItem(const QString& name) +{ + QListIterator i(m_items); + while (i.hasNext()) + { + MapItem *item = i.next(); + if (item->m_name == name) + return item; + } + return nullptr; +} + +QModelIndex MapModel::findMapItemIndex(const QString& name) +{ + int idx = 0; + QListIterator i(m_items); + while (i.hasNext()) + { + MapItem *item = i.next(); + if (item->m_name == name) { + return index(idx); + } + idx++; + } + return index(-1); +} + +int MapModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_items.count(); +} + +QVariant MapModel::data(const QModelIndex &index, int role) const +{ + 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%5 El: %3%5 Dist: %4 km") + .arg(m_selected[row] ? m_items[row]->m_text : m_items[row]->m_name) + .arg(std::round(azEl->getAzimuth())) + .arg(std::round(azEl->getElevation())) + .arg(std::round(azEl->getDistance() / 1000.0)) + .arg(QChar(0xb0)); + 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_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DLabel); + } + else if (role == MapModel::mapImageVisibleRole) + { + return QVariant::fromValue(m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DIcon); + } + else if (role == MapModel::mapImageRole) + { + // Set an image to use + return QVariant::fromValue(m_items[row]->m_image); + } + else if (role == MapModel::mapImageRotationRole) + { + // Angle to rotate image by + return QVariant::fromValue(m_items[row]->m_imageRotation); + } + else if (role == MapModel::mapImageMinZoomRole) + { + // Minimum zoom level + //return QVariant::fromValue(m_items[row]->m_imageMinZoom); + return QVariant::fromValue(m_items[row]->m_itemSettings->m_2DMinZoom); + } + else if (role == MapModel::bubbleColourRole) + { + // 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_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { + return m_items[row]->m_predictedTrack1; + } else { + return QVariantList(); + } + } + else if (role == MapModel::predictedGroundTrack2Role) + { + if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) + && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { + return m_items[row]->m_predictedTrack2; + } else { + return QVariantList(); + } + } + else if (role == MapModel::groundTrack1Role) + { + if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) + && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { + return m_items[row]->m_takenTrack1; + } else { + return QVariantList(); + } + } + else if (role == MapModel::groundTrack2Role) + { + if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row])) + && m_items[row]->m_itemSettings->m_enabled && m_items[row]->m_itemSettings->m_display2DTrack) { + return m_items[row]->m_takenTrack2; + } else { + return QVariantList(); + } + } + else if (role == groundTrackColorRole) + { + return QVariant::fromValue(QColor::fromRgb(m_items[row]->m_itemSettings->m_2DTrackColor)); + } + else if (role == predictedGroundTrackColorRole) + { + return QVariant::fromValue(QColor::fromRgb(m_items[row]->m_itemSettings->m_2DTrackColor).lighter()); + } + 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; +} + +Qt::ItemFlags MapModel::flags(const QModelIndex &index) const +{ + (void) index; + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; +} + +void MapModel::setDisplayNames(bool displayNames) +{ + m_displayNames = displayNames; + allUpdated(); +} + +void MapModel::setDisplaySelectedGroundTracks(bool displayGroundTracks) +{ + m_displaySelectedGroundTracks = displayGroundTracks; + allUpdated(); +} + +void MapModel::setDisplayAllGroundTracks(bool displayGroundTracks) +{ + m_displayAllGroundTracks = displayGroundTracks; + allUpdated(); +} + +void MapModel::setFrequency(double frequency) +{ + // Set as centre frequency + ChannelWebAPIUtils::setCenterFrequency(0, frequency); +} + +void MapModel::track3D(int index) +{ + if (index < m_items.count()) + { + MapItem *item = m_items[index]; + m_gui->track3D(item->m_name); + } +} + +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); + } + } + } +} + diff --git a/plugins/feature/map/mapmodel.h b/plugins/feature/map/mapmodel.h new file mode 100644 index 000000000..2b63dc740 --- /dev/null +++ b/plugins/feature/map/mapmodel.h @@ -0,0 +1,229 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_FEATURE_MAPMODEL_H_ +#define INCLUDE_FEATURE_MAPMODEL_H_ + +#include +#include +#include + +#include "util/azel.h" +#include "pipes/pipeendpoint.h" +#include "mapsettings.h" +#include "cesiuminterface.h" + +#include "SWGMapItem.h" + +class MapModel; +class MapGUI; +class CZML; + +// Information required about each item displayed on the map +class MapItem { + +public: + MapItem(const PipeEndPoint *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem); + void update(SWGSDRangel::SWGMapItem *mapItem); + QGeoCoordinate getCoordinates(); + +private: + void findFrequency(); + void updateTrack(QList *track); + void updatePredictedTrack(QList *track); + + friend MapModel; + friend CZML; + QString m_group; + MapSettings::MapItemSettings *m_itemSettings; + const PipeEndPoint *m_sourcePipe; // Channel/feature that created the item + QString m_name; + QString m_label; + float m_latitude; + float m_longitude; + float m_altitude; // In metres + QDateTime m_positionDateTime; + bool m_useHeadingPitchRoll; + float m_heading; + float m_pitch; + float m_roll; + QDateTime m_orientationDateTime; + QString m_image; + int m_imageRotation; + QString m_text; + double m_frequency; // Frequency to set + QString m_frequencyString; + QList m_predictedTrackCoords; + QList m_predictedTrackDateTimes; + QVariantList m_predictedTrack; // Line showing where the object is going + QVariantList m_predictedTrack1; + QVariantList m_predictedTrack2; + QGeoCoordinate m_predictedStart1; + QGeoCoordinate m_predictedStart2; + QGeoCoordinate m_predictedEnd1; + QGeoCoordinate m_predictedEnd2; + QList m_takenTrackCoords; + QList m_takenTrackDateTimes; + QVariantList m_takenTrack; // Line showing where the object has been + QVariantList m_takenTrack1; + QVariantList m_takenTrack2; + QGeoCoordinate m_takenStart1; + QGeoCoordinate m_takenStart2; + QGeoCoordinate m_takenEnd1; + QGeoCoordinate m_takenEnd2; + + // For 3D map + QString m_model; + int m_altitudeReference; + float m_labelAltitudeOffset; + float m_modelAltitudeOffset; + bool m_fixedPosition; + QList m_animations; +}; + +// Model used for each item on the map +class MapModel : public QAbstractListModel { + Q_OBJECT + +public: + using QAbstractListModel::QAbstractListModel; + enum MarkerRoles { + positionRole = Qt::UserRole + 1, + mapTextRole = Qt::UserRole + 2, + mapTextVisibleRole = Qt::UserRole + 3, + mapImageVisibleRole = Qt::UserRole + 4, + mapImageRole = Qt::UserRole + 5, + mapImageRotationRole = Qt::UserRole + 6, + mapImageMinZoomRole = Qt::UserRole + 7, + bubbleColourRole = Qt::UserRole + 8, + selectedRole = Qt::UserRole + 9, + targetRole = Qt::UserRole + 10, + frequencyRole = Qt::UserRole + 11, + frequencyStringRole = Qt::UserRole + 12, + predictedGroundTrack1Role = Qt::UserRole + 13, + predictedGroundTrack2Role = Qt::UserRole + 14, + groundTrack1Role = Qt::UserRole + 15, + groundTrack2Role = Qt::UserRole + 16, + groundTrackColorRole = Qt::UserRole + 17, + predictedGroundTrackColorRole = Qt::UserRole + 18 + }; + + MapModel(MapGUI *gui); + + void playAnimations(MapItem *item); + + Q_INVOKABLE void add(MapItem *item); + void update(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group=""); + void update(MapItem *item); + void remove(MapItem *item); + void allUpdated(); + void removeAll(); + void updateItemSettings(QHash m_itemSettings); + + void updateTarget(); + void setTarget(const QString& name); + bool isTarget(const MapItem *mapItem) const; + + Q_INVOKABLE void moveToFront(int oldRow); + Q_INVOKABLE void moveToBack(int oldRow); + + MapItem *findMapItem(const PipeEndPoint *source, const QString& name); + MapItem *findMapItem(const QString& name); + QModelIndex findMapItemIndex(const QString& name); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + void setDisplayNames(bool displayNames); + void setDisplaySelectedGroundTracks(bool displayGroundTracks); + void setDisplayAllGroundTracks(bool displayGroundTracks); + Q_INVOKABLE void setFrequency(double frequency); + Q_INVOKABLE void track3D(int index); + + void interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); + void interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen); + void interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen); + + void splitTracks(MapItem *item); + void splitTrack(const QList& coords, const QVariantList& track, + QVariantList& track1, QVariantList& track2, + QGeoCoordinate& start1, QGeoCoordinate& start2, + QGeoCoordinate& end1, QGeoCoordinate& end2); + Q_INVOKABLE void viewChanged(double bottomLeftLongitude, double bottomRightLongitude); + + QHash roleNames() const + { + QHash roles; + roles[positionRole] = "position"; + roles[mapTextRole] = "mapText"; + roles[mapTextVisibleRole] = "mapTextVisible"; + roles[mapImageVisibleRole] = "mapImageVisible"; + roles[mapImageRole] = "mapImage"; + roles[mapImageRotationRole] = "mapImageRotation"; + roles[mapImageMinZoomRole] = "mapImageMinZoom"; + roles[bubbleColourRole] = "bubbleColour"; + roles[selectedRole] = "selected"; + roles[targetRole] = "target"; + roles[frequencyRole] = "frequency"; + roles[frequencyStringRole] = "frequencyString"; + roles[predictedGroundTrack1Role] = "predictedGroundTrack1"; + roles[predictedGroundTrack2Role] = "predictedGroundTrack2"; + roles[groundTrack1Role] = "groundTrack1"; + roles[groundTrack2Role] = "groundTrack2"; + roles[groundTrackColorRole] = "groundTrackColor"; + roles[predictedGroundTrackColorRole] = "predictedGroundTrackColor"; + return roles; + } + + // Linear interpolation + double interpolate(double x0, double y0, double x1, double y1, double x) + { + return (y0*(x1-x) + y1*(x-x0)) / (x1-x0); + } + + bool isSelected3D(const MapItem *item) const + { + return m_selected3D == item->m_name; + } + + void setSelected3D(const QString &selected) + { + m_selected3D = selected; + } + +public slots: + void update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()); + +private: + MapGUI *m_gui; + QList m_items; + QList m_selected; + int m_target; // Row number of current target, or -1 for none + bool m_displayNames; + bool m_displaySelectedGroundTracks; + bool m_displayAllGroundTracks; + + double m_bottomLeftLongitude; + double m_bottomRightLongitude; + + QString m_selected3D; // Name of item selected on 3D map - only supports 1 item, unlike 2D map +}; + + +#endif // INCLUDE_FEATURE_MAPMODEL_H_ diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index be7149f01..e12997a18 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -17,8 +17,10 @@ /////////////////////////////////////////////////////////////////////////////////// #include +#include #include "util/simpleserializer.h" +#include "util/httpdownloadmanager.h" #include "settings/serializable.h" #include "mapsettings.h" @@ -27,6 +29,7 @@ const QStringList MapSettings::m_pipeTypes = { QStringLiteral("ADSBDemod"), QStringLiteral("AIS"), QStringLiteral("APRS"), + QStringLiteral("APTDemod"), QStringLiteral("StarTracker"), QStringLiteral("SatelliteTracker") }; @@ -35,6 +38,7 @@ const QStringList MapSettings::m_pipeURIs = { QStringLiteral("sdrangel.channel.adsbdemod"), QStringLiteral("sdrangel.feature.ais"), QStringLiteral("sdrangel.feature.aprs"), + QStringLiteral("sdrangel.channel.aptdemod"), QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.satellitetracker") }; @@ -44,15 +48,33 @@ const QStringList MapSettings::m_mapProviders = { QStringLiteral("osm"), QStringLiteral("esri"), QStringLiteral("mapbox"), - QStringLiteral("mapboxgl") + QStringLiteral("mapboxgl"), + QStringLiteral("maplibre") }; MapSettings::MapSettings() : m_rollupState(nullptr) { + // Source names should match m_pipeTypes + // Colors currently match color of rollup widget for that plugin + int modelMinPixelSize = 50; + m_itemSettings.insert("ADSBDemod", new MapItemSettings("ADSBDemod", QColor(244, 151, 57), false, 11, modelMinPixelSize)); + m_itemSettings.insert("AIS", new MapItemSettings("AIS", QColor(102, 0, 0), false, 11, modelMinPixelSize)); + m_itemSettings.insert("APRS", new MapItemSettings("APRS", QColor(255, 255, 0), false, 11)); + m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", QColor(230, 230, 230), true, 3)); + m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", QColor(0, 0, 255), false, 0, modelMinPixelSize)); + m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", QColor(255, 0, 0), true, 8)); + m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", QColor(255, 0, 0), true, 8)); + m_itemSettings.insert("Radar", new MapItemSettings("Radar", QColor(255, 0, 0), true, 8)); + m_itemSettings.insert("Station", new MapItemSettings("Station", QColor(255, 0, 0), true, 11)); resetToDefaults(); } +MapSettings::~MapSettings() +{ + //qDeleteAll(m_itemSettings); +} + void MapSettings::resetToDefaults() { m_displayNames = true; @@ -62,11 +84,8 @@ void MapSettings::resetToDefaults() m_mapBoxAPIKey = ""; m_osmURL = ""; m_mapBoxStyles = ""; - m_sources = -1; m_displaySelectedGroundTracks = true; m_displayAllGroundTracks = true; - m_groundTrackColor = QColor(150, 0, 20).rgb(); - m_predictedGroundTrackColor = QColor(225, 0, 50).rgb(); m_title = "Map"; m_rgbColor = QColor(225, 25, 99).rgb(); m_useReverseAPI = false; @@ -74,6 +93,14 @@ void MapSettings::resetToDefaults() m_reverseAPIPort = 8888; m_reverseAPIFeatureSetIndex = 0; m_reverseAPIFeatureIndex = 0; + m_map2DEnabled = true; + m_map3DEnabled = true; + m_terrain = "Cesium World Terrain"; + m_buildings = "None"; + m_sunLightEnabled = true; + m_eciCamera = false; + m_modelDir = HttpDownloadManager::downloadDir() + "/3d"; + m_antiAliasing = "None"; } QByteArray MapSettings::serialize() const @@ -84,9 +111,6 @@ QByteArray MapSettings::serialize() const s.writeString(2, m_mapProvider); s.writeString(3, m_mapBoxAPIKey); s.writeString(4, m_mapBoxStyles); - s.writeU32(5, m_sources); - s.writeU32(6, m_groundTrackColor); - s.writeU32(7, m_predictedGroundTrackColor); s.writeString(8, m_title); s.writeU32(9, m_rgbColor); s.writeBool(10, m_useReverseAPI); @@ -104,6 +128,17 @@ QByteArray MapSettings::serialize() const } s.writeString(20, m_osmURL); + s.writeString(21, m_mapType); + s.writeBool(22, m_map2DEnabled); + s.writeBool(23, m_map3DEnabled); + s.writeString(24, m_terrain); + s.writeString(25, m_buildings); + s.writeBlob(27, serializeItemSettings(m_itemSettings)); + s.writeString(28, m_modelDir); + s.writeBool(29, m_sunLightEnabled); + s.writeBool(30, m_eciCamera); + s.writeString(31, m_cesiumIonAPIKey); + s.writeString(32, m_antiAliasing); return s.final(); } @@ -123,14 +158,12 @@ bool MapSettings::deserialize(const QByteArray& data) QByteArray bytetmp; uint32_t utmp; QString strtmp; + QByteArray blob; d.readBool(1, &m_displayNames, true); d.readString(2, &m_mapProvider, "osm"); 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()); - d.readU32(7, &m_predictedGroundTrackColor, QColor(225, 0, 50).rgb()); d.readString(8, &m_title, "Map"); d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb()); d.readBool(10, &m_useReverseAPI, false); @@ -159,6 +192,19 @@ bool MapSettings::deserialize(const QByteArray& data) } d.readString(20, &m_osmURL, ""); + d.readString(21, &m_mapType, ""); + + d.readBool(22, &m_map2DEnabled, true); + d.readBool(23, &m_map3DEnabled, true); + d.readString(24, &m_terrain, "Cesium World Terrain"); + d.readString(25, &m_buildings, "None"); + d.readBlob(27, &blob); + deserializeItemSettings(blob, m_itemSettings); + d.readString(28, &m_modelDir, HttpDownloadManager::downloadDir() + "/3d"); + d.readBool(29, &m_sunLightEnabled, true); + d.readBool(30, &m_eciCamera, false); + d.readString(31, &m_cesiumIonAPIKey, ""); + d.readString(32, &m_antiAliasing, "None"); return true; } @@ -168,3 +214,147 @@ bool MapSettings::deserialize(const QByteArray& data) return false; } } + +MapSettings::MapItemSettings::MapItemSettings(const QString& group, + const QColor color, + bool display3DPoint, + int minZoom, + int modelMinPixelSize) +{ + m_group = group; + resetToDefaults(); + m_3DPointColor = color.rgb(); + m_2DTrackColor = color.darker().rgb(); + m_3DTrackColor = color.darker().rgb(); + m_display3DPoint = display3DPoint; + m_2DMinZoom = minZoom; + m_3DModelMinPixelSize = modelMinPixelSize; +} + +MapSettings::MapItemSettings::MapItemSettings(const QByteArray& data) +{ + deserialize(data); +} + +void MapSettings::MapItemSettings::resetToDefaults() +{ + m_enabled = true; + m_display2DIcon = true; + m_display2DLabel = true; + m_display2DTrack = true; + m_2DTrackColor = QColor(150, 0, 20).rgb(); + m_2DMinZoom = 1; + m_display3DModel = true; + m_display3DPoint = false; + m_3DPointColor = QColor(225, 0, 0).rgb(); + m_display3DLabel = true; + m_display3DTrack = true; + m_3DTrackColor = QColor(150, 0, 20).rgb(); + m_3DModelMinPixelSize = 0; +} + +QByteArray MapSettings::MapItemSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_group); + s.writeBool(2, m_enabled); + s.writeBool(3, m_display2DIcon); + s.writeBool(4, m_display2DLabel); + s.writeBool(5, m_display2DTrack); + s.writeU32(6, m_2DTrackColor); + s.writeS32(7, m_2DMinZoom); + s.writeBool(8, m_display3DModel); + s.writeBool(9, m_display3DLabel); + s.writeBool(10, m_display3DPoint); + s.writeU32(11, m_3DPointColor); + s.writeBool(12, m_display3DTrack); + s.writeU32(13, m_3DTrackColor); + s.writeS32(14, m_3DModelMinPixelSize); + + return s.final(); +} + +bool MapSettings::MapItemSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + d.readString(1, &m_group, ""); + d.readBool(2, &m_enabled, true); + d.readBool(3, &m_display2DIcon, true); + d.readBool(4, &m_display2DLabel, true); + d.readBool(5, &m_display2DTrack, true); + d.readU32(6, &m_2DTrackColor, QColor(150, 0, 0).rgb()); + d.readS32(7, &m_2DMinZoom, 1); + d.readBool(8, &m_display3DModel, true); + d.readBool(9, &m_display3DLabel, true); + d.readBool(10, &m_display3DPoint, true); + d.readU32(11, &m_3DPointColor, QColor(255, 0, 0).rgb()); + d.readBool(12, &m_display3DTrack, true); + d.readU32(13, &m_3DTrackColor, QColor(150, 0, 20).rgb()); + d.readS32(14, &m_3DModelMinPixelSize, 0); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +QByteArray MapSettings::serializeItemSettings(QHash itemSettings) const +{ + SimpleSerializer s(1); + + int idx = 1; + QHashIterator i(itemSettings); + while (i.hasNext()) + { + i.next(); + + s.writeString(idx+1, i.key()); + s.writeBlob(idx+2, i.value()->serialize()); + + idx += 2; + } + + return s.final(); +} + +void MapSettings::deserializeItemSettings(const QByteArray& data, QHash& itemSettings) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return; + } + + int idx = 1; + bool done = false; + do + { + QString key; + QByteArray blob; + + if (!d.readString(idx+1, &key)) + { + done = true; + } + else + { + d.readBlob(idx+2, &blob); + MapItemSettings *settings = new MapItemSettings(blob); + itemSettings.insert(key, settings); + } + + idx += 2; + } while(!done); +} diff --git a/plugins/feature/map/mapsettings.h b/plugins/feature/map/mapsettings.h index 3ac505854..8f4f81f16 100644 --- a/plugins/feature/map/mapsettings.h +++ b/plugins/feature/map/mapsettings.h @@ -21,14 +21,35 @@ #include #include - -#include "util/message.h" +#include class Serializable; -class PipeEndPoint; struct MapSettings { + struct MapItemSettings { + QString m_group; // Name of the group the settings apply to + bool m_enabled; // Whether enabled at all on 2D or 3D map + bool m_display2DIcon; // Display image 2D map + bool m_display2DLabel; // Display label on 2D map + bool m_display2DTrack; // Display tracks on 2D map + quint32 m_2DTrackColor; + int m_2DMinZoom; + bool m_display3DModel; // Draw 3D model for item + bool m_display3DLabel; // Display a label next to this item on the 3D map + bool m_display3DPoint; // Draw a point for this item on the 3D map + quint32 m_3DPointColor; + bool m_display3DTrack; // Display a ground track for this item on the 3D map + quint32 m_3DTrackColor; + int m_3DModelMinPixelSize; + + MapItemSettings(const QString& group, const QColor color, bool display3DPoint=true, int minZoom=11, int modelMinPixelSize=0); + MapItemSettings(const QByteArray& data); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + }; + bool m_displayNames; QString m_mapProvider; QString m_thunderforestAPIKey; @@ -36,11 +57,8 @@ struct MapSettings QString m_mapBoxAPIKey; QString m_osmURL; QString m_mapBoxStyles; - quint32 m_sources; // Bitmask of SOURCE_* bool m_displayAllGroundTracks; bool m_displaySelectedGroundTracks; - quint32 m_groundTrackColor; - quint32 m_predictedGroundTrackColor; QString m_title; quint32 m_rgbColor; bool m_useReverseAPI; @@ -49,31 +67,36 @@ struct MapSettings uint16_t m_reverseAPIFeatureSetIndex; uint16_t m_reverseAPIFeatureIndex; Serializable *m_rollupState; + bool m_map2DEnabled; + QString m_mapType; // "Street Map", "Satellite Map", etc.. as selected in combobox + + // 3D Map settings + bool m_map3DEnabled; + QString m_terrain; // "Ellipsoid" or "Cesium World Terrain" + QString m_buildings; // "None" or "Cesium OSM Buildings" + QString m_modelURL; // Base URL for 3D models (Not user settable, as depends on web server port) + QString m_modelDir; // Directory to store 3D models (not customizable for now, as ADS-B plugin needs to know) + bool m_sunLightEnabled; // Light globe from direction of Sun + bool m_eciCamera; // Use ECI instead of ECEF for camera + QString m_cesiumIonAPIKey; + QString m_antiAliasing; + + // Per source settings + QHash m_itemSettings; MapSettings(); + ~MapSettings(); void resetToDefaults(); QByteArray serialize() const; bool deserialize(const QByteArray& data); void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + QByteArray serializeItemSettings(QHash itemSettings) const; + void deserializeItemSettings(const QByteArray& data, QHash& itemSettings); static const QStringList m_pipeTypes; static const QStringList m_pipeURIs; static const QStringList m_mapProviders; - - // The first few should match the order in m_pipeTypes for MapGUI::getSourceMask to work - static const quint32 SOURCE_ADSB = 0x1; - static const quint32 SOURCE_AIS = 0x2; - static const quint32 SOURCE_APRS = 0x4; - static const quint32 SOURCE_STAR_TRACKER = 0x8; - static const quint32 SOURCE_SATELLITE_TRACKER = 0x10; - static const quint32 SOURCE_BEACONS = 0x20; - static const quint32 SOURCE_RADIO_TIME = 0x40; - static const quint32 SOURCE_RADAR = 0x80; - static const quint32 SOURCE_AM = 0x100; - static const quint32 SOURCE_FM = 0x200; - static const quint32 SOURCE_DAB = 0x400; - 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 599458ddd..74a7b3c01 100644 --- a/plugins/feature/map/mapsettingsdialog.cpp +++ b/plugins/feature/map/mapsettingsdialog.cpp @@ -18,26 +18,92 @@ #include #include #include +#include +#include + +#include #include "util/units.h" #include "mapsettingsdialog.h" #include "maplocationdialog.h" +#include "mapcolordialog.h" static QString rgbToColor(quint32 rgb) { - QColor color = QColor::fromRgb(rgb); + QColor color = QColor::fromRgba(rgb); return QString("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue()); } static QString backgroundCSS(quint32 rgb) { - return QString("QToolButton { background:rgb(%1); }").arg(rgbToColor(rgb)); + // Must specify a border, otherwise we end up with a gradient instead of solid background + return QString("QToolButton { background-color: rgb(%1); border: none; }").arg(rgbToColor(rgb)); +} + +static QString noColorCSS() +{ + return "QToolButton { background-color: black; border: none; }"; +} + +MapColorGUI::MapColorGUI(QTableWidget *table, int row, int col, bool noColor, quint32 color) : + m_noColor(noColor), + m_color(color) +{ + m_colorButton = new QToolButton(table); + m_colorButton->setFixedSize(22, 22); + if (!m_noColor) + { + m_colorButton->setStyleSheet(backgroundCSS(m_color)); + } + else + { + m_colorButton->setStyleSheet(noColorCSS()); + m_colorButton->setText("-"); + } + table->setCellWidget(row, col, m_colorButton); + connect(m_colorButton, &QToolButton::clicked, this, &MapColorGUI::on_color_clicked); +} + +void MapColorGUI::on_color_clicked() +{ + MapColorDialog dialog(QColor::fromRgba(m_color), m_colorButton); + if (dialog.exec() == QDialog::Accepted) + { + m_noColor = dialog.noColorSelected(); + if (!m_noColor) + { + m_colorButton->setText(""); + m_color = dialog.selectedColor().rgba(); + m_colorButton->setStyleSheet(backgroundCSS(m_color)); + } + else + { + m_colorButton->setText("-"); + m_colorButton->setStyleSheet(noColorCSS()); + } + } +} + +MapItemSettingsGUI::MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings) : + m_track2D(table, row, MapSettingsDialog::COL_2D_TRACK, !settings->m_display2DTrack, settings->m_2DTrackColor), + m_point3D(table, row, MapSettingsDialog::COL_3D_POINT, !settings->m_display3DPoint, settings->m_3DPointColor), + m_track3D(table, row, MapSettingsDialog::COL_3D_TRACK, !settings->m_display3DTrack, settings->m_3DTrackColor) +{ + m_minZoom = new QSpinBox(table); + m_minZoom->setRange(0, 15); + m_minZoom->setValue(settings->m_2DMinZoom); + m_minPixels = new QSpinBox(table); + m_minPixels->setRange(0, 200); + m_minPixels->setValue(settings->m_3DModelMinPixelSize); + table->setCellWidget(row, MapSettingsDialog::COL_2D_MIN_ZOOM, m_minZoom); + table->setCellWidget(row, MapSettingsDialog::COL_3D_MIN_PIXELS, m_minPixels); } MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : QDialog(parent), m_settings(settings), + m_downloadDialog(this), ui(new Ui::MapSettingsDialog) { ui->setupUi(this); @@ -45,28 +111,74 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) : ui->thunderforestAPIKey->setText(settings->m_thunderforestAPIKey); ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey); ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey); + ui->cesiumIonAPIKey->setText(settings->m_cesiumIonAPIKey); ui->osmURL->setText(settings->m_osmURL); ui->mapBoxStyles->setText(settings->m_mapBoxStyles); - 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->map2DEnabled->setChecked(m_settings->m_map2DEnabled); + ui->map3DEnabled->setChecked(m_settings->m_map3DEnabled); + ui->terrain->setCurrentIndex(ui->terrain->findText(m_settings->m_terrain)); + ui->buildings->setCurrentIndex(ui->buildings->findText(m_settings->m_buildings)); + ui->sunLightEnabled->setCurrentIndex((int)m_settings->m_sunLightEnabled); + ui->eciCamera->setCurrentIndex((int)m_settings->m_eciCamera); + ui->antiAliasing->setCurrentIndex(ui->antiAliasing->findText(m_settings->m_antiAliasing)); + + QHashIterator itr(m_settings->m_itemSettings); + while (itr.hasNext()) + { + itr.next(); + MapSettings::MapItemSettings *itemSettings = itr.value(); + + // Add row to table with header + int row = ui->mapItemSettings->rowCount(); + ui->mapItemSettings->setRowCount(row + 1); + QTableWidgetItem *header = new QTableWidgetItem(itemSettings->m_group); + ui->mapItemSettings->setVerticalHeaderItem(row, header); + + QTableWidgetItem *item; + item = new QTableWidgetItem(); + item->setCheckState(itemSettings->m_enabled ? Qt::Checked : Qt::Unchecked); + ui->mapItemSettings->setItem(row, COL_ENABLED, item); + + item = new QTableWidgetItem(); + item->setCheckState(itemSettings->m_display2DIcon ? Qt::Checked : Qt::Unchecked); + ui->mapItemSettings->setItem(row, COL_2D_ICON, item); + item = new QTableWidgetItem(); + item->setCheckState(itemSettings->m_display2DLabel ? Qt::Checked : Qt::Unchecked); + ui->mapItemSettings->setItem(row, COL_2D_LABEL, item); + + item = new QTableWidgetItem(); + item->setCheckState(itemSettings->m_display3DModel ? Qt::Checked : Qt::Unchecked); + ui->mapItemSettings->setItem(row, COL_3D_MODEL, item); + item = new QTableWidgetItem(); + item->setCheckState(itemSettings->m_display3DLabel ? Qt::Checked : Qt::Unchecked); + ui->mapItemSettings->setItem(row, COL_3D_LABEL, item); + + MapItemSettingsGUI *gui = new MapItemSettingsGUI(ui->mapItemSettings, row, itemSettings); + m_mapItemSettingsGUIs.append(gui); } - ui->groundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_groundTrackColor)); - ui->predictedGroundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_predictedGroundTrackColor)); + ui->mapItemSettings->resizeColumnsToContents(); + + on_map2DEnabled_clicked(m_settings->m_map2DEnabled); + on_map3DEnabled_clicked(m_settings->m_map3DEnabled); + + connect(&m_dlm, &HttpDownloadManagerGUI::downloadComplete, this, &MapSettingsDialog::downloadComplete); } MapSettingsDialog::~MapSettingsDialog() { delete ui; + qDeleteAll(m_mapItemSettingsGUIs); } void MapSettingsDialog::accept() { QString mapProvider = MapSettings::m_mapProviders[ui->mapProvider->currentIndex()]; - QString mapBoxAPIKey = ui->mapBoxAPIKey->text(); QString osmURL = ui->osmURL->text(); QString mapBoxStyles = ui->mapBoxStyles->text(); + QString mapBoxAPIKey = ui->mapBoxAPIKey->text(); QString thunderforestAPIKey = ui->thunderforestAPIKey->text(); QString maptilerAPIKey = ui->maptilerAPIKey->text(); + QString cesiumIonAPIKey = ui->cesiumIonAPIKey->text(); m_osmURLChanged = osmURL != m_settings->m_osmURL; if ((mapProvider != m_settings->m_mapProvider) || (thunderforestAPIKey != m_settings->m_thunderforestAPIKey) @@ -81,38 +193,210 @@ void MapSettingsDialog::accept() m_settings->m_mapBoxAPIKey = mapBoxAPIKey; m_settings->m_osmURL = osmURL; m_settings->m_mapBoxStyles = mapBoxStyles; - m_mapSettingsChanged = true; + m_settings->m_cesiumIonAPIKey = cesiumIonAPIKey; + m_map2DSettingsChanged = true; } else { - m_mapSettingsChanged = false; + m_map2DSettingsChanged = false; } - m_settings->m_sources = 0; - quint32 sources = MapSettings::SOURCE_STATION; - for (int i = 0; i < ui->sourceList->count(); i++) { - sources |= (ui->sourceList->item(i)->checkState() == Qt::Checked) << i; + if (cesiumIonAPIKey != m_settings->m_cesiumIonAPIKey) + { + m_settings->m_cesiumIonAPIKey = cesiumIonAPIKey; + m_map3DSettingsChanged = true; } - m_sourcesChanged = sources != m_settings->m_sources; - m_settings->m_sources = sources; + else + { + m_map3DSettingsChanged = false; + } + + m_settings->m_map2DEnabled = ui->map2DEnabled->isChecked(); + m_settings->m_map3DEnabled = ui->map3DEnabled->isChecked(); + m_settings->m_terrain = ui->terrain->currentText(); + m_settings->m_buildings = ui->buildings->currentText(); + m_settings->m_sunLightEnabled = ui->sunLightEnabled->currentIndex() == 1; + m_settings->m_eciCamera = ui->eciCamera->currentIndex() == 1; + m_settings->m_antiAliasing = ui->antiAliasing->currentText(); + + for (int row = 0; row < ui->mapItemSettings->rowCount(); row++) + { + MapSettings::MapItemSettings *itemSettings = m_settings->m_itemSettings[ui->mapItemSettings->verticalHeaderItem(row)->text()]; + MapItemSettingsGUI *gui = m_mapItemSettingsGUIs[row]; + + itemSettings->m_enabled = ui->mapItemSettings->item(row, COL_ENABLED)->checkState() == Qt::Checked; + itemSettings->m_display2DIcon = ui->mapItemSettings->item(row, COL_2D_ICON)->checkState() == Qt::Checked; + itemSettings->m_display2DLabel = ui->mapItemSettings->item(row, COL_2D_LABEL)->checkState() == Qt::Checked; + itemSettings->m_display2DTrack = !gui->m_track2D.m_noColor; + itemSettings->m_2DTrackColor = gui->m_track2D.m_color; + itemSettings->m_2DMinZoom = gui->m_minZoom->value(); + itemSettings->m_display3DModel = ui->mapItemSettings->item(row, COL_3D_MODEL)->checkState() == Qt::Checked; + itemSettings->m_display3DLabel = ui->mapItemSettings->item(row, COL_3D_LABEL)->checkState() == Qt::Checked; + itemSettings->m_display3DPoint = !gui->m_point3D.m_noColor; + itemSettings->m_3DPointColor = gui->m_point3D.m_color; + itemSettings->m_display3DTrack = !gui->m_track3D.m_noColor; + itemSettings->m_3DTrackColor = gui->m_track3D.m_color; + itemSettings->m_3DModelMinPixelSize = gui->m_minPixels->value(); + } + QDialog::accept(); } -void MapSettingsDialog::on_groundTrackColor_clicked() +void MapSettingsDialog::on_map2DEnabled_clicked(bool checked) { - QColorDialog dialog(QColor::fromRgb(m_settings->m_groundTrackColor), this); - if (dialog.exec() == QDialog::Accepted) + if (checked) { - m_settings->m_groundTrackColor = dialog.selectedColor().rgb(); - ui->groundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_groundTrackColor)); + ui->mapItemSettings->showColumn(COL_2D_ICON); + ui->mapItemSettings->showColumn(COL_2D_LABEL); + ui->mapItemSettings->showColumn(COL_2D_MIN_ZOOM); + ui->mapItemSettings->showColumn(COL_2D_TRACK); + } + else + { + ui->mapItemSettings->hideColumn(COL_2D_ICON); + ui->mapItemSettings->hideColumn(COL_2D_LABEL); + ui->mapItemSettings->hideColumn(COL_2D_MIN_ZOOM); + ui->mapItemSettings->hideColumn(COL_2D_TRACK); + } + ui->mapProvider->setEnabled(checked); + ui->osmURL->setEnabled(checked); + ui->mapBoxStyles->setEnabled(checked); +} + +void MapSettingsDialog::on_map3DEnabled_clicked(bool checked) +{ + if (checked) + { + ui->mapItemSettings->showColumn(COL_3D_MODEL); + ui->mapItemSettings->showColumn(COL_3D_MIN_PIXELS); + ui->mapItemSettings->showColumn(COL_3D_LABEL); + ui->mapItemSettings->showColumn(COL_3D_POINT); + ui->mapItemSettings->showColumn(COL_3D_TRACK); + } + else + { + ui->mapItemSettings->hideColumn(COL_3D_MODEL); + ui->mapItemSettings->hideColumn(COL_3D_MIN_PIXELS); + ui->mapItemSettings->hideColumn(COL_3D_LABEL); + ui->mapItemSettings->hideColumn(COL_3D_POINT); + ui->mapItemSettings->hideColumn(COL_3D_TRACK); + } + ui->terrain->setEnabled(checked); + ui->buildings->setEnabled(checked); + ui->sunLightEnabled->setEnabled(checked); + ui->eciCamera->setEnabled(checked); +} + +// Models have individual licensing. See LICENSE on github +#define SDRANGEL_3D_MODELS "https://github.com/srcejon/sdrangel-3d-models/releases/latest/download/sdrangel3dmodels.zip" +// Textures from Bluebell CSL - https://github.com/oktal3700/bluebell +// These are Copyrighted by their authors and shouldn't be uploaded to any other sites +#define BB_AIRBUS_PNG "https://drive.google.com/uc?export=download&id=10fFhflgWXCu7hmd8wqNdXw1qHJ6ecz9Z" +#define BB_BOEING_PNG "https://drive.google.com/uc?export=download&id=1OA3pmAp5jqrjP7kRS1z_zNNyi_iLu9z_" +#define BB_GA_PNG "https://drive.google.com/uc?export=download&id=1TZsvlLqT5x3KLkiqtN8LzAzoLxeYTA-1" +#define BB_HELI_PNG "https://drive.google.com/uc?export=download&id=1qB2xDVHdooLeLKCPyVnVDDHRlhPVpUYs" +#define BB_JETS_PNG "https://drive.google.com/uc?export=download&id=1v1fzTpyjjfcXyoT7vHjnyvuwqrSQzPrg" +#define BB_MIL_PNG "https://drive.google.com/uc?export=download&id=1lI-2bAVVxhKvel7_suGVdkky4BQDQE9n" +#define BB_PROPS_PNG "https://drive.google.com/uc?export=download&id=1fD8YxKsa9P_z2gL1aM97ZEN-HoI28SLE" + +static QStringList urls = { + SDRANGEL_3D_MODELS, + BB_AIRBUS_PNG, + BB_BOEING_PNG, + BB_GA_PNG, + BB_HELI_PNG, + BB_JETS_PNG, + BB_MIL_PNG, + BB_PROPS_PNG +}; + +static QStringList files = { + "sdrangel3dmodels.zip", + "bb_airbus_png.zip", + "bb_boeing_png.zip", + "bb_ga_png.zip", + "bb_heli_png.zip", + "bb_jets_png.zip", + "bb_mil_png.zip", + "bb_props_png.zip" +}; + +void MapSettingsDialog::unzip(const QString &filename) +{ + QZipReader reader(filename); + if (!reader.extractAll(m_settings->m_modelDir)) { + qWarning() << "MapSettingsDialog::unzip - Failed to extract files from " << filename << " to " << m_settings->m_modelDir; + } else { + qDebug() << "MapSettingsDialog::unzip - Unzipped " << filename << " to " << m_settings->m_modelDir; } } -void MapSettingsDialog::on_predictedGroundTrackColor_clicked() +void MapSettingsDialog::on_downloadModels_clicked() { - QColorDialog dialog(QColor::fromRgb(m_settings->m_predictedGroundTrackColor), this); - if (dialog.exec() == QDialog::Accepted) + m_downloadDialog.setText("Downloading 3D models"); + m_downloadDialog.setStandardButtons(QMessageBox::NoButton); + Qt::WindowFlags flags = m_downloadDialog.windowFlags(); + flags |= Qt::CustomizeWindowHint; + flags &= ~Qt::WindowCloseButtonHint; + m_downloadDialog.setWindowFlags(flags); + m_downloadDialog.open(); + m_fileIdx = 0; + + QUrl url(urls[m_fileIdx]); + QString filename = HttpDownloadManager::downloadDir() + "/" + files[m_fileIdx]; + + m_dlm.download(url, filename, this); +} + +void MapSettingsDialog::downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage) +{ + if (success) { - m_settings->m_predictedGroundTrackColor = dialog.selectedColor().rgb(); - ui->predictedGroundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_predictedGroundTrackColor)); + // Unzip + if (filename.endsWith(".zip")) + { + unzip(filename); + + if (filename.endsWith("bb_boeing_png.zip")) + { + // Copy missing textures + // These are wrong, but prevents cesium from stopping rendering + // Waiting on: https://github.com/oktal3700/bluebell/issues/63 + if (!QFile::copy(m_settings->m_modelDir + "/BB_Boeing_png/B77L/B77L_LIT.png", m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png")) { + qDebug() << "Failed to copy " + m_settings->m_modelDir + "/BB_Boeing_png/B77L/B77L_LIT.png" + " to " + m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png"; + } + if (!QFile::copy(m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png", m_settings->m_modelDir + "/BB_Boeing_png/B77W/B773_LIT.png")) { + qDebug() << "Failed to copy " + m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png" + " to " + m_settings->m_modelDir + "/BB_Boeing_png/B77W/B773_LIT.png"; + } + if (!QFile::copy(m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png", m_settings->m_modelDir + "/BB_Boeing_png/B773/B773_LIT.png")) { + qDebug() << "Failed to copy " + m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png" + " to " + m_settings->m_modelDir + "/BB_Boeing_png/B773/B773_LIT.png"; + } + if (!QFile::copy(m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png", m_settings->m_modelDir + "/BB_Boeing_png/B788/B788_LIT.png")) { + qDebug() << "Failed to copy " + m_settings->m_modelDir + "/BB_Boeing_png/B772/B772_LIT.png" + " to " + m_settings->m_modelDir + "/BB_Boeing_png/B788/B788_LIT.png"; + } + if (!QFile::copy(m_settings->m_modelDir + "/BB_Boeing_png/B752/B75F_LIT.png", m_settings->m_modelDir + "/BB_Boeing_png/B752/B752_LIT.png")) { + qDebug() << "Failed to copy " + m_settings->m_modelDir + "/BB_Boeing_png/B752/B75F_LIT.png" + " to " + m_settings->m_modelDir + "/BB_Boeing_png/B752/B752_LIT.png"; + } + } + } + + m_fileIdx++; + + // Download next file + if (m_fileIdx < urls.size()) + { + QUrl url(urls[m_fileIdx]); + QString filename = HttpDownloadManager::downloadDir() + "/" + files[m_fileIdx]; + + m_dlm.download(url, filename, this); + } + else + { + m_downloadDialog.reject(); + } + } + else + { + m_downloadDialog.reject(); + QMessageBox::warning(this, "Download failed", QString("Failed to download %1 to %2\n%3").arg(url).arg(filename).arg(errorMessage)); } } diff --git a/plugins/feature/map/mapsettingsdialog.h b/plugins/feature/map/mapsettingsdialog.h index 34d1de67b..ee82acd5c 100644 --- a/plugins/feature/map/mapsettingsdialog.h +++ b/plugins/feature/map/mapsettingsdialog.h @@ -18,9 +18,46 @@ #ifndef INCLUDE_FEATURE_MAPSETTINGSDIALOG_H #define INCLUDE_FEATURE_MAPSETTINGSDIALOG_H +#include +#include + +#include "gui/httpdownloadmanagergui.h" + #include "ui_mapsettingsdialog.h" #include "mapsettings.h" +class MapColorGUI : public QObject { + Q_OBJECT +public: + + MapColorGUI(QTableWidget *table, int row, int col, bool noColor, quint32 color); + +public slots: + void on_color_clicked(); + +private: + QToolButton *m_colorButton; + +public: + // Have copies of settings, so we don't change unless main dialog is accepted + bool m_noColor; + quint32 m_color; + +}; + +class MapItemSettingsGUI : public QObject { + Q_OBJECT +public: + + MapItemSettingsGUI(QTableWidget *table, int row, MapSettings::MapItemSettings *settings); + + MapColorGUI m_track2D; + MapColorGUI m_point3D; + MapColorGUI m_track3D; + QSpinBox *m_minZoom; + QSpinBox *m_minPixels; +}; + class MapSettingsDialog : public QDialog { Q_OBJECT @@ -28,18 +65,43 @@ public: explicit MapSettingsDialog(MapSettings *settings, QWidget* parent = 0); ~MapSettingsDialog(); - MapSettings *m_settings; - bool m_mapSettingsChanged; + enum Columns { + COL_ENABLED, + COL_2D_ICON, + COL_2D_LABEL, + COL_2D_MIN_ZOOM, + COL_2D_TRACK, + COL_3D_MODEL, + COL_3D_MIN_PIXELS, + COL_3D_LABEL, + COL_3D_POINT, + COL_3D_TRACK + }; + +public: + bool m_map2DSettingsChanged; // 2D map needs to be reloaded + bool m_map3DSettingsChanged; // 3D map needs to be reloaded bool m_osmURLChanged; - bool m_sourcesChanged; + +private: + MapSettings *m_settings; + QList m_mapItemSettingsGUIs; + HttpDownloadManagerGUI m_dlm; + int m_fileIdx; + QMessageBox m_downloadDialog; + + void unzip(const QString &filename); private slots: void accept(); - void on_groundTrackColor_clicked(); - void on_predictedGroundTrackColor_clicked(); + void on_map2DEnabled_clicked(bool checked=false); + void on_map3DEnabled_clicked(bool checked=false); + void on_downloadModels_clicked(); + void downloadComplete(const QString &filename, bool success, const QString &url, const QString &errorMessage); private: Ui::MapSettingsDialog* ui; + }; #endif // INCLUDE_FEATURE_MAPSETTINGSDIALOG_H diff --git a/plugins/feature/map/mapsettingsdialog.ui b/plugins/feature/map/mapsettingsdialog.ui index b07e64e74..d99536276 100644 --- a/plugins/feature/map/mapsettingsdialog.ui +++ b/plugins/feature/map/mapsettingsdialog.ui @@ -6,13 +6,12 @@ 0 0 - 436 - 520 + 946 + 800
- Liberation Sans 9 @@ -29,141 +28,101 @@ - Select data to display: + Select how to display items on the maps: - + - QAbstractItemView::MultiSelection + QAbstractItemView::NoSelection - - false - - + - ADS-B + Enabled - - Checked - - - + + - AIS + 2D Icon - - Checked - - - + + - APRS + 2D Label - - Checked - - - + + - Star Tracker + 2D Min Zoom - - Checked - - - + + - Satellite Tracker + 2D Track - - Checked - - - + + - Beacons + 3D Model - - Checked - - - + + - Radio Time Transmitters + 3D Min Pixels - - Checked - - - + + - Radar + 3D Label - - Checked + + + + 3D Point - + + + + 3D Track + + - + - Colours + 2D Map Settings - - - - - Ground tracks (predicted) - - - - - - - Ground tracks (taken) - - - + - - - Select color for predicted ground tracks + + + + 140 + 0 + + + Enabled + + + + + - - - Select color for taken ground tracks - - - - - - - - - - - - - Map Provider Settings - - - Map provider - + Select map provider @@ -188,44 +147,224 @@ MapboxGL + + + MapLibre + + + + + + + + OSM Custom URL + + + + + + + URL of custom map for use with OpenStreetMap provider + - - - Mapbox API Key - - - - - - - Enter a Mapbox API key in order to use Mapbox maps: https://www.mapbox.com/ - - - - MapboxGL Styles - + Comma separated list of MapBox styles + + + + + + + 3D Map Settings + + + + + + + 140 + 0 + + + + Enabled + + + + + + + + + + + + + Terrain + + + + + + + + Cesium World Terrain + + + + + Ellipsoid + + + + + Maptiler + + + + + ArcGIS + + + + + + + + Buildings + + + + + + + + None + + + + + Cesium OSM Buildings + + + + + + + + Lighting + + + + + + + Whether lighting is from the Sun or Camera + + + + Camera + + + + + Sun + + + + + + + + Camera reference frame + + + + + + + Selects camera reference frame. For ECEF the camera rotates with the Earth. For ECI, the camera position is fixed relative to the stars and the Earth's rotation will be visible. + + + + ECEF + + + + + ECI + + + + + + + + Anti-aliasing + + + + + + + Set anti-aliasing to use. This can remove jagged pixels on the edge of 3D models. + + + + None + + + + + FXAA + + + + + + + + + + + API Keys + + + + + + 140 + 0 + + Thunderforest API Key - + + + + Enter a Thunderforest API key in order to use non-watermarked Thunderforest maps: https://www.thunderforest.com/ + + + + Maptiler API Key @@ -233,36 +372,70 @@ - - - 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 + Mapbox API Key - - + + - URL of custom map for use with OpenStreetMap provider + Enter a Mapbox API key in order to use Mapbox maps: https://www.mapbox.com/ + + + + + + + Cesium Ion API Key + + + + + + + Enter a Cesium Ion Access Token + + + + + + Download 3D models. It is recommended to restart SDRangel after download. + + + Download 3D Models (1.6GB) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -278,6 +451,23 @@ + + mapItemSettings + map2DEnabled + mapProvider + osmURL + mapBoxStyles + map3DEnabled + terrain + buildings + sunLightEnabled + eciCamera + thunderforestAPIKey + maptilerAPIKey + mapBoxAPIKey + cesiumIonAPIKey + downloadModels + diff --git a/plugins/feature/map/mapwebsocketserver.cpp b/plugins/feature/map/mapwebsocketserver.cpp new file mode 100644 index 000000000..fedc787c9 --- /dev/null +++ b/plugins/feature/map/mapwebsocketserver.cpp @@ -0,0 +1,100 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 + +#include "mapwebsocketserver.h" + +MapWebSocketServer::MapWebSocketServer(QObject *parent) : + QObject(parent), + m_socket("", QWebSocketServer::NonSecureMode, this), + m_client(nullptr) +{ + connect(&m_socket, &QWebSocketServer::newConnection, this, &MapWebSocketServer::onNewConnection); + int port = 0; + if (!m_socket.listen(QHostAddress::Any, port)) { + qCritical() << "MapWebSocketServer - Unable to listen on port " << port; + } +} + +quint16 MapWebSocketServer::serverPort() +{ + return m_socket.serverPort(); +} + +void MapWebSocketServer::onNewConnection() +{ + QWebSocket *socket = m_socket.nextPendingConnection(); + + connect(socket, &QWebSocket::textMessageReceived, this, &MapWebSocketServer::processTextMessage); + connect(socket, &QWebSocket::binaryMessageReceived, this, &MapWebSocketServer::processBinaryMessage); + connect(socket, &QWebSocket::disconnected, this, &MapWebSocketServer::socketDisconnected); + + m_client = socket; + + emit connected(); +} + +void MapWebSocketServer::processTextMessage(QString message) +{ + QWebSocket *client = qobject_cast(sender()); + //qDebug() << "MapWebSocketServer::processTextMessage - Received text " << message; + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &error); + if (!doc.isNull() && doc.isObject()) { + emit received(doc.object()); + } else { + qDebug() << "MapWebSocketServer::processTextMessage: " << error.errorString(); + } +} + +void MapWebSocketServer::processBinaryMessage(QByteArray message) +{ + QWebSocket *client = qobject_cast(sender()); + // Shouldn't receive any binary messages for now + qDebug() << "MapWebSocketServer::processBinaryMessage - Received binary " << message; +} + +void MapWebSocketServer::socketDisconnected() +{ + QWebSocket *client = qobject_cast(sender()); + if (client) { + client->deleteLater(); + m_client = nullptr; + } +} + +bool MapWebSocketServer::isConnected() +{ + return m_client != nullptr; +} + +void MapWebSocketServer::send(const QJsonObject &obj) +{ + if (m_client) + { + QJsonDocument doc(obj); + QByteArray bytes = doc.toJson(); + qint64 bytesSent = m_client->sendTextMessage(bytes); + m_client->flush(); // Try to reduce latency + if (bytesSent != bytes.size()) { + qDebug() << "MapWebSocketServer::update - Sent only " << bytesSent << " bytes out of " << bytes.size(); + } + } +} + diff --git a/plugins/feature/map/mapwebsocketserver.h b/plugins/feature/map/mapwebsocketserver.h new file mode 100644 index 000000000..9a8744c33 --- /dev/null +++ b/plugins/feature/map/mapwebsocketserver.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_FEATURE_MAPWEBSOCKERSERVER_H_ +#define INCLUDE_FEATURE_MAPWEBSOCKERSERVER_H_ + +#include +#include +#include +#include + +class MapWebSocketServer : public QObject +{ + Q_OBJECT + +private: + + QWebSocketServer m_socket; + QWebSocket *m_client; + +public: + + MapWebSocketServer(QObject *parent = nullptr); + quint16 serverPort(); + + bool isConnected(); + void send(const QJsonObject &obj); + +signals: + void connected(); + void received(const QJsonObject &obj); + +public slots: + + void onNewConnection(); + void processTextMessage(QString message); + void processBinaryMessage(QByteArray message); + void socketDisconnected(); + +}; + +#endif // INCLUDE_FEATURE_MAPWEBSOCKERSERVER_H_ diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index b41006662..bee8ec8c9 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -2,13 +2,14 @@

Introduction

-The Map Feature plugin displays a world map. It can display street maps, satellite imagery as well as custom map types. +The Map Feature plugin displays a world map in 2D and 3D. It can display street maps, satellite imagery as well as custom map types. On top of this, it can plot data from other plugins, such as: * APRS symbols from the APRS Feature, * Aircraft from the ADS-B Demodulator, * Ships from the AIS Demodulator, * Satellites from the Satellite Tracker, +* Weather imagery from APT Demodulator, * The Sun, Moon and Stars from the Star Tracker, * Beacons based on the IARU Region 1 beacon database and International Beacon Project, * Radio time transmitters, @@ -16,7 +17,11 @@ On top of this, it can plot data from other plugins, such as: It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites. -![Map feature](../../../doc/img/Map_plugin_beacons.png) +![2D Map feature](../../../doc/img/Map_plugin_beacons.png) + +![3D Map feature](../../../doc/img/Map_plugin_apt.png) + +3D Models are not included with SDRangel. They must be downloaded by pressing the Download 3D Models button in the Display Settings dialog (11).

Interface

@@ -33,7 +38,7 @@ To centre the map on an object or location, enter:

2: Map Type

-Allows you to select a map type. The available types will depend upon the Map provider +Allows you to select a 2D map type. The available types will depend upon the Map provider selected under Display Settings (7).

3: Maidenhead locator conversion

@@ -90,11 +95,33 @@ When clicked, all items will be deleted from the map.

11: Display settings

-When clicked, opens the Map Display Settings dialog, which allows setting: +When clicked, opens the Map Display Settings dialog: + +![Map Display Settings Dialog](../../../doc/img/Map_plugin_display_settings.png) + +The top half of the dialog allows customization of how objects from different SDRangel +plugins are dispayed on the 2D and 3D maps. This includes: + +* Whether images are displayed on the 2D map and whether 3D models are displayed on the 2D map. +* Whether labels are displayed giving the name of the object. +* Whether taken and predicted tracks are displayed and in which colour. +* How the image or 3D model is scaled as the zoom level changes. + +For the 2D map, the settings include: + +* Whether the 2D map is displayed. +* Which Map provider will be used to source the map images. +* 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/ +* When MapboxGL is used as the provider, custom styles can be specified. + +For the 3D map, the settings include: + +* The terrain provider, which provides elevation data. For a "flat" globe, terrain can be set to Ellipsoid for the WGS-84 ellipsoid. +* The buildings provider, which provides 3D building models. This can be set to None if no buildings are desired. +* Whether the globe and models are lit from the direction of the Sun or the camera. +* The camera reference frame. For ECEF (Earth Centered Earth Fixed), the camera rotates with the globe. +For ECI (Earth Centred Inertial) the camera is fixed in space and the globe will rotate under it. -* Which data the Map will display. -* The colour of the taken and predicted tracks. -* Which Map provider will be used to source the map image. * API keys, required to access maps from different providers. Free API keys are available by signing up for an accounts with: @@ -102,31 +129,45 @@ Free API keys are available by signing up for an accounts with: * [Thunderforest](https://www.thunderforest.com/) * [Maptiler](https://www.maptiler.com/) * [Mapbox](https://www.mapbox.com/) +* [Cesium ion](https://cesium.com/ion/signup) 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/ +The "Download 3D Models" button will download the 3D models of aircraft, ships and satellites that are required for the 3D map. +These are not included with the SDRangel distribution, so must be downloaded.

Map

-The map displays objects reported by other SDRangel channels and features, as well as beacon locations. +The map feature displays a 2D and a 3D map overlaid with objects reported by other SDRangel channels and features, as well as beacon locations. -* The "Home" antenna location is placed according to My Position set under the Preferences > My Position menu. The position is only updated when the Map plugin is first opened. +* The "Home Station" antenna location is placed according to My Position set under the Preferences > My Position menu. The position is only updated when the Map plugin is first opened. * To pan around the map, click the left mouse button and drag. To zoom in or out, use the mouse scroll wheel. * Single clicking on an object in the map will display a text bubble with additional information about the object. -* Right clicking on a object will open a context menu, which allows: +* Right clicking on a object on the 2D map will open a context menu, which allows: * To set an object as the target. The target object will have its azimuth and elevation displayed in the text bubble and sent to the Rotator Controller feature. * Setting the Device center frequency to the first frequency found in the text bubble for the object. * Changing the order in which the objects are drawn, which can help to cycle through multiple objects that are at the same location on the map. + * Setting the object as the tracking target on the 3D map. + +The 2D map will only display the last reported positions for objects. +The 3D map, however, has a timeline that allows replaying how objects have moved over time. +To the right of the timeline is the fullscreen toggle button, which allows the 3D map to be displayed fullscreen.

Attribution

IARU Region 1 beacon list used with permission from: https://iaru-r1-c5-beacons.org/ To add or update a beacon, see: https://iaru-r1-c5-beacons.org/index.php/beacon-update/ -Mapping and geolocation services are by Open Street Map: https://www.openstreetmap.org/ esri: https://www.esri.com/ and Mapbox: https://www.mapbox.com/ +Mapping and geolocation services are by Open Street Map: https://www.openstreetmap.org/ esri: https://www.esri.com/ +Mapbox: https://www.mapbox.com/ Cesium: https://www.cesium.com Bing: https://www.bing.com/maps/ Icons made by Google from Flaticon https://www.flaticon.com +3D models are by various artists under a variety of liceneses. See: https://github.com/srcejon/sdrangel-3d-models + +

Creating 3D Models

+ +If you wish to contribute a 3D model, see the https://github.com/srcejon/sdrangel-3d-models project. +

API

Full details of the API can be found in the Swagger documentation. Here is a quick example of how to centre the map on an object from the command line: diff --git a/plugins/feature/map/webserver.cpp b/plugins/feature/map/webserver.cpp new file mode 100644 index 000000000..354ca61b5 --- /dev/null +++ b/plugins/feature/map/webserver.cpp @@ -0,0 +1,199 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 +#include +#include + +#include "webserver.h" + +// port - port to listen on / is listening on. Use 0 for any free port. +WebServer::WebServer(quint16 &port, QObject* parent) : + QTcpServer(parent), + m_defaultMimeType("application/octet-stream") +{ + listen(QHostAddress::Any, port); + port = serverPort(); + qDebug() << "WebServer on port " << port; + + m_mimeTypes.insert(".html", new MimeType("text/html; charset=\"utf-8\"", false)); + m_mimeTypes.insert(".png", new MimeType("image/png")); + m_mimeTypes.insert(".glb", new MimeType("model/gltf-binary")); + m_mimeTypes.insert(".glbe", new MimeType("model/gltf-binary")); + m_mimeTypes.insert(".js", new MimeType("text/javascript")); + m_mimeTypes.insert(".css", new MimeType("text/css")); + m_mimeTypes.insert(".json", new MimeType("application/json")); +} + +void WebServer::incomingConnection(qintptr socket) +{ + QTcpSocket* s = new QTcpSocket(this); + connect(s, SIGNAL(readyRead()), this, SLOT(readClient())); + connect(s, SIGNAL(disconnected()), this, SLOT(discardClient())); + s->setSocketDescriptor(socket); + //addPendingConnection(socket); +} + +// Don't include leading or trailing / in from +void WebServer::addPathSubstitution(const QString &from, const QString &to) +{ + qDebug() << "Mapping " << from << " to " << to; + m_pathSubstitutions.insert(from, to); +} + +void WebServer::addSubstitution(QString path, QString from, QString to) +{ + Substitution *s = new Substitution(from, to); + if (m_substitutions.contains(path)) + { + QList *list = m_substitutions.value(path); + QMutableListIterator i(*list); + while (i.hasNext()) { + Substitution *sub = i.next(); + if (sub->m_from == from) { + i.remove(); + delete sub; + } + } + list->append(s); + } + else + { + QList *list = new QList(); + list->append(s); + m_substitutions.insert(path, list); + } +} + +QString WebServer::substitute(QString path, QString html) +{ + QList *list = m_substitutions.value(path); + for (const auto s : *list) { + html = html.replace(s->m_from, s->m_to); + } + return html; +} + +void WebServer::sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path) +{ + QString header = QString("HTTP/1.0 200 Ok\r\nContent-Type: %1\r\n\r\n").arg(mimeType->m_type); + if (mimeType->m_binary) + { + // Send file as binary + QByteArray headerUtf8 = header.toUtf8(); + socket->write(headerUtf8); + socket->write(data); + } + else + { + // Send file as text + QString html = QString(data); + // Make any substitutions in the content of the file + if (m_substitutions.contains(path)) { + html = substitute(path, html); + } + QTextStream os(socket); + os.setAutoDetectUnicode(true); + os << header << html; + } +} + +void WebServer::readClient() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + if (socket->canReadLine()) + { + QString line = socket->readLine(); + //qDebug() << "WebServer HTTP Request: " << line; + + QStringList tokens = QString(line).split(QRegExp("[ \r\n][ \r\n]*")); + if (tokens[0] == "GET") + { + // Get file type from extension + QString path = tokens[1]; + MimeType *mimeType = &m_defaultMimeType; + int extensionIdx = path.lastIndexOf("."); + if (extensionIdx != -1) { + QString extension = path.mid(extensionIdx); + if (m_mimeTypes.contains(extension)) { + mimeType = m_mimeTypes[extension]; + } + } + + // Try mapping path + QStringList dirs = path.split('/'); + if ((dirs.length() >= 2) && m_pathSubstitutions.contains(dirs[1])) + { + dirs[1] = m_pathSubstitutions.value(dirs[1]); + dirs.removeFirst(); + QString newPath = dirs.join('/'); + //qDebug() << "Mapping " << path << " to " << newPath; + path = newPath; + } + + // See if we can find the file in our resources + QResource res(path); + if (res.isValid() && (res.uncompressedSize() > 0)) + { + QByteArray data = res.uncompressedData(); + sendFile(socket, data, mimeType, path); + } + else + { + // See if we can find a file + QFile file(path); + if (file.open(QIODevice::ReadOnly)) + { + QByteArray data = file.readAll(); + if (path.endsWith(".glbe")) { + for (int i = 0; i < data.size(); i++) { + data[i] = data[i] ^ 0x55; + } + } + sendFile(socket, data, mimeType, path); + } + else + { + qDebug() << "WebServer " << path << " not found"; + // File not found + QTextStream os(socket); + os.setAutoDetectUnicode(true); + os << "HTTP/1.0 404 Not Found\r\n" + "Content-Type: text/html; charset=\"utf-8\"\r\n" + "\r\n" + "\n" + "\n" + "

404 Not Found

\n" + "\n" + "\n"; + } + } + + socket->close(); + + if (socket->state() == QTcpSocket::UnconnectedState) { + delete socket; + } + } + } +} + +void WebServer::discardClient() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + socket->deleteLater(); +} diff --git a/plugins/feature/map/webserver.h b/plugins/feature/map/webserver.h new file mode 100644 index 000000000..d69ca58cf --- /dev/null +++ b/plugins/feature/map/webserver.h @@ -0,0 +1,76 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_WEB_SERVER_H_ +#define INCLUDE_WEB_SERVER_H_ + +#include +#include + +// WebServer for making simple dynamic html pages and serving binaries from +// resources or local disk +class WebServer : public QTcpServer +{ + Q_OBJECT + + struct Substitution { + QString m_from; + QString m_to; + Substitution(const QString& from, const QString& to) : + m_from(from), + m_to(to) + { + } + }; + + struct MimeType { + QString m_type; + bool m_binary; + MimeType(const QString& type, bool binary=true) : + m_type(type), + m_binary(binary) + { + } + }; + +private: + + // Hash of a list of paths to substitude + QHash m_pathSubstitutions; + + // Hash of path to a list of substitutions to make in the file + QHash*> m_substitutions; + + // Hash of filename extension to MIME type information + QHash m_mimeTypes; + MimeType m_defaultMimeType; + +public: + WebServer(quint16 &port, QObject* parent = 0); + void incomingConnection(qintptr socket) override; + void addPathSubstitution(const QString &from, const QString &to); + void addSubstitution(QString path, QString from, QString to); + QString substitute(QString path, QString html); + void sendFile(QTcpSocket* socket, const QByteArray &data, MimeType *mimeType, const QString &path); + +private slots: + void readClient(); + void discardClient(); + +}; + +#endif