Add 3D Map to Map feature

This commit is contained in:
Jon Beniston 2022-02-04 20:40:43 +00:00
parent 70c99d54c7
commit 97f9835a71
30 changed files with 4661 additions and 1387 deletions

View File

@ -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 $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QJsonObject>
#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);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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_

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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<float>::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<QGeoCoordinate *> i(mapItem->m_takenTrackCoords);
QListIterator<QDateTime *> 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<QGeoCoordinate *> k(mapItem->m_predictedTrackCoords);
QListIterator<QDateTime *> 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<float>::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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_CZML_H_
#define INCLUDE_FEATURE_CZML_H_
#include <QHash>
#include <QJsonArray>
#include <QMatrix3x3>
#include <QMatrix4x4>
#include <QQuaternion>
#include <QVector3D>
#include <QJsonObject>
struct MapSettings;
class MapItem;
class CZML
{
private:
const MapSettings *m_settings;
QHash<QString, QString> m_ids;
QHash<QString, QJsonArray> m_lastPosition;
QHash<QString, bool> 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_

View File

@ -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
{

View File

@ -23,6 +23,7 @@
#include <QHash>
#include <QNetworkRequest>
#include <QTimer>
#include <QDateTime>
#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; }

View File

@ -7,5 +7,9 @@
<file>map/antennadab.png</file>
<file>map/antennafm.png</file>
<file>map/antennaam.png</file>
<file>map/map3d.html</file>
</qresource>
<qresource prefix="/">
<file>Cesium/Cesium.js</file>
</qresource>
</RCC>

View File

@ -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)
}
}
}
}

View File

@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="/Cesium/Cesium.js"></script>
<style>
@import url(/Cesium/Widgets/widgets.css);
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
</head>
<body style="margin:0;padding:0">
<div id="cesiumContainer"></div>
<script>
// See: https://community.cesium.com/t/how-to-run-an-animation-for-an-entity-model/16932
function getActiveAnimations(viewer, entity) {
var primitives = viewer.scene.primitives;
var length = primitives.length;
for(var i = 0; i < length; i++) {
var primitive = primitives.get(i);
if (primitive.id === entity && primitive instanceof Cesium.Model && primitive.ready) {
return primitive.activeAnimations;
}
}
return undefined;
}
function playAnimation(viewer, command, retries) {
var entity = czmlStream.entities.getById(command.id);
if (entity !== undefined) {
var animations = getActiveAnimations(viewer, entity);
if (animations !== undefined) {
try {
let options = {
name: command.animation,
startOffset: command.startOffset,
reverse: command.reverse,
loop: command.loop ? Cesium.ModelAnimationLoop.REPEAT : Cesium.ModelAnimationLoop.NONE,
multiplier: command.multiplier,
};
options.startTime = Cesium.JulianDate.fromIso8601(command.startDateTime);
// https://github.com/CesiumGS/cesium/issues/10048
// Animations aren't moved to last frame if startTime in the past
// so just play now, in order to ensure gears are down, etc
if (Cesium.JulianDate.compare(options.startTime, viewer.clock.currentTime) < 0) {
options.startTime = viewer.clock.currentTime;
}
if (command.duration != 0) {
options.stopTime = Cesium.JulianDate.addSeconds(options.startTime, command.duration, new Cesium.JulianDate());
}
animations.add(options);
} catch (e) {
// Note we get TypeError instead of DeveloperError, if running minified version of Cesium
if ((e instanceof Cesium.DeveloperError) || (e instanceof TypeError)) {
// ADS-B plugin doesn't know which animations each aircraft has
// so we should expect a lot of these, as it tries to start slat animations
// on aircraft that do not have them
console.log(`Exception playing ${command.animation} for ${command.id}\n${e}`);
} else {
throw e;
}
}
} else {
// Give Entity time to create primitive
// No ready promise in entity API - https://github.com/CesiumGS/cesium/issues/4727
if (retries > 0) {
setTimeout(function() {
//console.log(`Retrying animation for entity ${command.id}`);
playAnimation(viewer, command, retries-1);
}, 1000);
} else {
console.log(`Gave up trying to play animation for entity ${command.id}`);
}
}
} else {
// It seems in some cases, entities aren't created immediately, so wait and retry
if (retries > 0) {
setTimeout(function() {
//console.log(`Retrying entity ${command.id}`);
playAnimation(viewer, command, retries-1);
}, 1000);
} else {
console.log(`Gave up trying to find entity ${command.id}`);
}
}
}
// There's no way to stop a looped animation that doesn't have a stopTime,
// only remove it
// So we need to remove it, then re-add it with a new stopTime, so that it
// plays again if the timeline is changed
function stopAnimation(viewer, command) {
var entity = czmlStream.entities.getById(command.id);
if (entity !== undefined) {
var animations = getActiveAnimations(viewer, entity);
if (animations !== undefined) {
var length = animations.length;
var anim = undefined;
// Find animation with lastet startTime
for (var i = 0; i < length; i++) {
var a = animations.get(i);
if (a.name == command.animation) {
if ((anim === undefined) || (Cesium.JulianDate.compare(a.startTime, anim.startTime) >= 0)) {
anim = a;
}
}
}
if (anim !== undefined) {
animations.remove(anim);
// Re add with new stopTime
animations.add({
name: anim.name,
startOffset: anim.startOffset,
reverse: anim.reverse,
loop: anim.loop,
multiplier: anim.multiplier,
startTime: anim.startTime,
stopTime: Cesium.JulianDate.fromIso8601(command.startDateTime)
});
}
}
}
}
function icrf(scene, time) {
if (scene.mode !== Cesium.SceneMode.SCENE3D) {
return;
}
var icrfToFixed = Cesium.Transforms.computeIcrfToFixedMatrix(time);
if (Cesium.defined(icrfToFixed)) {
var camera = viewer.camera;
var offset = Cesium.Cartesian3.clone(camera.position);
var transform = Cesium.Matrix4.fromRotationTranslation(icrfToFixed);
camera.lookAtTransform(transform, offset);
}
}
Cesium.Ion.defaultAccessToken = '$CESIUM_ION_API_KEY$';
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain(),
animation: true,
shouldAnimate: true,
timeline: true,
geocoder: false,
fullscreenButton: true,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false
});
var buildings = undefined;
const images = new Map();
// Use CZML to stream data from Map plugin to Cesium
var czmlStream = new Cesium.CzmlDataSource();
viewer.dataSources.add(czmlStream);
function cameraLight(scene, time) {
viewer.scene.light.direction = Cesium.Cartesian3.clone(scene.camera.directionWC, viewer.scene.light.direction);
}
// Use WebSockets for handling commands from MapPlugin
// (CZML doesn't support camera control, for example)
// and sending events back to it
let socket = new WebSocket("ws://127.0.0.1:$WS_PORT$");
socket.onmessage = function(event) {
try {
const command = JSON.parse(event.data);
if (command.command == "trackId") {
// Track an entity with the given ID
viewer.trackedEntity = czmlStream.entities.getById(command.id);
} else if (command.command == "setHomeView") {
// Set the viewing rectangle used when the home button is pressed
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(
command.longitude - command.angle,
command.latitude - command.angle,
command.longitude + command.angle,
command.latitude + command.angle
);
Cesium.Camera.DEFAULT_VIEW_FACTOR = 0.0;
viewer.camera.flyHome(0);
} else if (command.command == "setView") {
// Set the camera view
viewer.scene.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(command.longitude, command.latitude, command.altitude),
orientation: {
heading: 0,
},
});
} else if (command.command == "playAnimation") {
// Play model animation
if (command.stop) {
//console.log(`stopping animation ${command.animation} for ${command.id}`);
stopAnimation(viewer, command);
} else {
//console.log(`playing animation ${command.animation} for ${command.id}`);
playAnimation(viewer, command, 30);
}
} else if (command.command == "setDateTime") {
// Set current date and time of viewer
var dateTime = Cesium.JulianDate.fromIso8601(command.dateTime);
viewer.clock.currentTime = dateTime;
} else if (command.command == "getDateTime") {
// Get current date and time of viewer
socket.send(JSON.stringify({
command: "getDateTime",
dateTime: Cesium.JulianDate.toIso8601(viewer.clock.currentTime)
}));
} else if (command.command == "setTerrain") {
// Support using Ellipsoid terrain for performance and also
// because paths can't be clammped to ground, so AIS paths
// currently appear underground if terrain is used
if (command.provider == "Ellipsoid") {
if (!(viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider)) {
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
}
} else if (command.provider == "Cesium World Terrain") {
viewer.terrainProvider = Cesium.createWorldTerrain();
} else if (command.provider == "CesiumTerrainProvider") {
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
url: command.url
});
} else if (command.provider == "ArcGISTiledElevationTerrainProvider") {
viewer.terrainProvider = new Cesium.ArcGISTiledElevationTerrainProvider({
url: command.url
});
} else {
console.log(`Unknown terrain ${command.terrain}`);
}
} else if (command.command == "setBuildings") {
if (command.buildings == "None") {
if (buildings !== undefined) {
viewer.scene.primitives.remove(buildings);
buildings = undefined;
}
} else {
if (buildings === undefined) {
buildings = viewer.scene.primitives.add(Cesium.createOsmBuildings());
}
}
} else if (command.command == "setSunLight") {
// Enable illumination of the globe from the direction of the Sun or camera
viewer.scene.globe.enableLighting = command.useSunLight;
viewer.scene.globe.nightFadeOutDistance = 0.0;
if (!command.useSunLight) {
viewer.scene.light = new Cesium.DirectionalLight({
direction : new Cesium.Cartesian3(1, 0, 0)
});
viewer.scene.preRender.addEventListener(cameraLight);
} else {
viewer.scene.light = new Cesium.SunLight();
viewer.scene.preRender.removeEventListener(cameraLight);
}
} else if (command.command == "setCameraReferenceFrame") {
if (command.eci) {
viewer.scene.postUpdate.addEventListener(icrf);
} else {
viewer.scene.postUpdate.removeEventListener(icrf);
}
} else if (command.command == "setAntiAliasing") {
if (command.antiAliasing == "FXAA") {
viewer.scene.postProcessStages.fxaa.enabled = true;
} else {
viewer.scene.postProcessStages.fxaa.enabled = false;
}
} else if (command.command == "updateImage") {
// Textures on entities can flash white when changed: https://github.com/CesiumGS/cesium/issues/1640
// so we use a primitive instead of an entity
// Can't modify geometry of primitives, so need to create a new primitive each time
// Material needs to be set as translucent in order to allow camera to zoom through it
var oldImage = images.get(command.name);
var image = viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances : new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(command.west, command.south, command.east, command.north),
vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
height: command.altitude
})
}),
appearance : new Cesium.EllipsoidSurfaceAppearance({
aboveGround : false,
material: new Cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image: 'data:image/png;base64,' + command.data,
}
},
translucent: true
})
})
}));
images.set(command.name, image);
if (oldImage !== undefined) {
image.readyPromise.then(function(prim) {
viewer.scene.primitives.remove(oldImage);
});
}
} else if (command.command == "removeImage") {
var image = images.get(command.name);
if (image !== undefined) {
viewer.scene.primitives.remove(image);
} else {
console.log(`Can't find image ${command.name} to remove it`);
}
} else if (command.command == "removeAllImages") {
for (let [k,image] of images) {
viewer.scene.primitives.remove(image);
}
} else if (command.command == "removeAllCZMLEntities") {
czmlStream.entities.removeAll();
} else if (command.command == "czml") {
// Implement CLIP_TO_GROUND, to work around https://github.com/CesiumGS/cesium/issues/4049
if (command.hasOwnProperty('altitudeReference') && command.hasOwnProperty('position') && command.position.hasOwnProperty('cartographicDegrees')) {
var size = command.position.cartographicDegrees.length;
if ((size == 3) || (size == 4)) {
var position;
var height;
if (size == 3) {
position = Cesium.Cartographic.fromDegrees(command.position.cartographicDegrees[0], command.position.cartographicDegrees[1]);
height = command.position.cartographicDegrees[2];
} else if (size == 4) {
position = Cesium.Cartographic.fromDegrees(command.position.cartographicDegrees[1], command.position.cartographicDegrees[2]);
height = command.position.cartographicDegrees[3];
}
if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
// sampleTerrainMostDetailed will reject Ellipsoid.
if (height < 0) {
if (size == 3) {
command.position.cartographicDegrees[2] = 0;
} else if (size == 4) {
command.position.cartographicDegrees[3] = 0;
}
}
czmlStream.process(command);
} else {
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]);
Cesium.when(promise, function(updatedPositions) {
if (height < updatedPositions[0].height) {
if (size == 3) {
command.position.cartographicDegrees[2] = updatedPositions[0].height;
} else if (size == 4) {
command.position.cartographicDegrees[3] = updatedPositions[0].height;
}
}
czmlStream.process(command);
}, function() {
console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
czmlStream.process(command);
});
};
} else {
console.log(`Can't currently use altitudeReference when more than one position`);
czmlStream.process(command);
}
} else {
czmlStream.process(command);
}
} else {
console.log(`Unknown command ${command.command}`);
}
} catch(e) {
console.log(`Erroring processing received message:\n${e}\n${event.data}`);
}
};
viewer.selectedEntityChanged.addEventListener(function(selectedEntity) {
if (Cesium.defined(selectedEntity) && Cesium.defined(selectedEntity.id)) {
socket.send(JSON.stringify({event: "selected", id: selectedEntity.id}));
} else {
socket.send(JSON.stringify({event: "selected"}));
}
});
viewer.trackedEntityChanged.addEventListener(function(trackedEntity) {
if (Cesium.defined(trackedEntity) && Cesium.defined(trackedEntity.id)) {
socket.send(JSON.stringify({event: "tracking", id: trackedEntity.id}));
} else {
socket.send(JSON.stringify({event: "tracking"}));
}
});
</script>
</div>
</body>
</html>

View File

@ -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)
}
}
}
}

View File

@ -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<Beacon *> *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));
}
}

View File

@ -20,9 +20,7 @@
#include "ui_mapbeacondialog.h"
#include <QProgressDialog>
#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,

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QDebug>
#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();
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAPCOLORDIALOG_H
#define INCLUDE_FEATURE_MAPCOLORDIALOG_H
#include <QColorDialog>
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

File diff suppressed because it is too large Load Diff

View File

@ -20,9 +20,12 @@
#define INCLUDE_FEATURE_MAPGUI_H_
#include <QTimer>
#include <QAbstractListModel>
#include <QGeoCoordinate>
#include <QGeoRectangle>
#include <QQuickItem>
#include <QJsonObject>
#include <QWebEngineFullScreenRequest>
#include <math.h>
#include <limits>
#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<SWGSDRangel::SWGMapCoordinate *> *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<SWGSDRangel::SWGMapCoordinate *> *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<QGeoCoordinate *> 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<QGeoCoordinate *> 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<MapItem *> 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<MapItem *> 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<QGeoCoordinate *>& 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<int, QByteArray> roleNames() const
{
QHash<int, QByteArray> 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<MapItem *> m_items;
QList<bool> 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<Beacon *> *getBeacons() { return m_beacons; }
void setBeacons(QList<Beacon *> *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<RadioTimeTransmitter> 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_

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>481</width>
<height>750</height>
<height>507</height>
</rect>
</property>
<property name="sizePolicy">
@ -277,8 +277,8 @@
<rect>
<x>0</x>
<y>60</y>
<width>471</width>
<height>681</height>
<width>483</width>
<height>223</height>
</rect>
</property>
<property name="sizePolicy">
@ -290,47 +290,72 @@
<property name="windowTitle">
<string>Map</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutMap" stretch="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>2</number>
<number>3</number>
</property>
<property name="leftMargin">
<number>3</number>
<number>0</number>
</property>
<property name="topMargin">
<number>3</number>
<number>0</number>
</property>
<property name="rightMargin">
<number>3</number>
<number>0</number>
</property>
<property name="bottomMargin">
<number>3</number>
<number>0</number>
</property>
<item>
<widget class="QQuickWidget" name="map">
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>590</height>
</size>
</property>
<property name="toolTip">
<string>Map</string>
</property>
<property name="resizeMode">
<enum>QQuickWidget::SizeRootObjectToView</enum>
</property>
<property name="source">
<url>
<string/>
</url>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QQuickWidget" name="map">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
<property name="toolTip">
<string>Map</string>
</property>
<property name="resizeMode">
<enum>QQuickWidget::SizeRootObjectToView</enum>
</property>
<property name="source">
<url>
<string/>
</url>
</property>
</widget>
<widget class="QWebEngineView" name="web" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
</widget>
</widget>
</item>
</layout>
@ -353,6 +378,12 @@
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QWebEngineView.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>find</tabstop>

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QGeoRectangle>
#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", "<br>"); // 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<SWGSDRangel::SWGMapAnimation *> *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<SWGSDRangel::SWGMapCoordinate *> *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<SWGSDRangel::SWGMapCoordinate *> *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<int> &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<QString, MapSettings::MapItemSettings *> 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<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_gui->getMap(), "target");
if (mapMessageQueues)
{
QList<MessageQueue*>::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<MapItem *> 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<MapItem *> 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<MapItem *> 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 x1<x2 to x1>x2
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<QGeoCoordinate *>& 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<QGeoCoordinate>();
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<QGeoRectangle>(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<QVariantList *> tracks({&track1, &track2});
QList<QGeoCoordinate *> ends({&end1, &end2});
QList<QGeoCoordinate *> 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<QGeoCoordinate>();
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<QGeoCoordinate>();
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);
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAPMODEL_H_
#define INCLUDE_FEATURE_MAPMODEL_H_
#include <QAbstractListModel>
#include <QGeoCoordinate>
#include <QColor>
#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<SWGSDRangel::SWGMapCoordinate *> *track);
void updatePredictedTrack(QList<SWGSDRangel::SWGMapCoordinate *> *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<QGeoCoordinate *> m_predictedTrackCoords;
QList<QDateTime *> 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<QGeoCoordinate *> m_takenTrackCoords;
QList<QDateTime *> 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<CesiumInterface::Animation *> 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<QString, MapSettings::MapItemSettings *> 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<QGeoCoordinate *>& 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<int, QByteArray> roleNames() const
{
QHash<int, QByteArray> 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<int> &roles = QVector<int>());
private:
MapGUI *m_gui;
QList<MapItem *> m_items;
QList<bool> 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_

View File

@ -17,8 +17,10 @@
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#include <QDebug>
#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<QString, MapItemSettings *> itemSettings) const
{
SimpleSerializer s(1);
int idx = 1;
QHashIterator<QString, MapItemSettings *> 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<QString, MapItemSettings *>& 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);
}

View File

@ -21,14 +21,35 @@
#include <QByteArray>
#include <QString>
#include "util/message.h"
#include <QHash>
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<QString, MapItemSettings *> m_itemSettings;
MapSettings();
~MapSettings();
void resetToDefaults();
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
QByteArray serializeItemSettings(QHash<QString, MapItemSettings *> itemSettings) const;
void deserializeItemSettings(const QByteArray& data, QHash<QString, MapItemSettings *>& 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_

View File

@ -18,26 +18,92 @@
#include <QDebug>
#include <QColorDialog>
#include <QColor>
#include <QToolButton>
#include <QFileDialog>
#include <QtGui/private/qzipreader_p.h>
#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<QString, MapSettings::MapItemSettings *> 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));
}
}

View File

@ -18,9 +18,46 @@
#ifndef INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
#define INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
#include <QSpinBox>
#include <QMessageBox>
#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<MapItemSettingsGUI *> 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

View File

@ -6,13 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>436</width>
<height>520</height>
<width>946</width>
<height>800</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
@ -29,141 +28,101 @@
<item>
<widget class="QLabel" name="locationsLabel">
<property name="text">
<string>Select data to display:</string>
<string>Select how to display items on the maps:</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="sourceList">
<widget class="QTableWidget" name="mapItemSettings">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="sortingEnabled">
<bool>false</bool>
</property>
<item>
<column>
<property name="text">
<string>ADS-B</string>
<string>Enabled</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>AIS</string>
<string>2D Icon</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>APRS</string>
<string>2D Label</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>Star Tracker</string>
<string>2D Min Zoom</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>Satellite Tracker</string>
<string>2D Track</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>Beacons</string>
<string>3D Model</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>Radio Time Transmitters</string>
<string>3D Min Pixels</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</property>
</item>
<item>
</column>
<column>
<property name="text">
<string>Radar</string>
<string>3D Label</string>
</property>
<property name="checkState">
<enum>Checked</enum>
</column>
<column>
<property name="text">
<string>3D Point</string>
</property>
</item>
</column>
<column>
<property name="text">
<string>3D Track</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QGroupBox" name="colorsGroupBox">
<widget class="QGroupBox" name="map2DSettings">
<property name="title">
<string>Colours</string>
<string>2D Map Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QLabel" name="predictedGroundTrackColorLabel">
<property name="text">
<string>Ground tracks (predicted)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="groundTrackColorLabel">
<property name="text">
<string>Ground tracks (taken)</string>
</property>
</widget>
</item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QToolButton" name="predictedGroundTrackColor">
<property name="toolTip">
<string>Select color for predicted ground tracks</string>
<widget class="QLabel" name="map2DEnabledLabel">
<property name="minimumSize">
<size>
<width>140</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="map2DEnabled">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QToolButton" name="groundTrackColor">
<property name="toolTip">
<string>Select color for taken ground tracks</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="mapProvidersGroupBox">
<property name="title">
<string>Map Provider Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="mapProviderLabel">
<property name="text">
<string>Map provider</string>
</property>
</widget>
</item>
<item row="0" column="1">
<item row="1" column="1">
<widget class="QComboBox" name="mapProvider">
<property name="toolTip">
<string>Select map provider</string>
@ -188,44 +147,224 @@
<string>MapboxGL</string>
</property>
</item>
<item>
<property name="text">
<string>MapLibre</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="osmURLLabel">
<property name="text">
<string>OSM Custom URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="osmURL">
<property name="toolTip">
<string>URL of custom map for use with OpenStreetMap provider</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="mapBoxAPIKeyLabel">
<property name="text">
<string>Mapbox API Key</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="mapBoxAPIKey">
<property name="toolTip">
<string>Enter a Mapbox API key in order to use Mapbox maps: https://www.mapbox.com/</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="mapBoxStylesLabel">
<property name="text">
<string>MapboxGL Styles</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="3" column="1">
<widget class="QLineEdit" name="mapBoxStyles">
<property name="toolTip">
<string>Comma separated list of MapBox styles</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="map3DSettings">
<property name="title">
<string>3D Map Settings</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="map3DEnabledLabel">
<property name="minimumSize">
<size>
<width>140</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="map3DEnabled">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="terrainLabel">
<property name="text">
<string>Terrain</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="terrain">
<item>
<property name="text">
<string>Cesium World Terrain</string>
</property>
</item>
<item>
<property name="text">
<string>Ellipsoid</string>
</property>
</item>
<item>
<property name="text">
<string>Maptiler</string>
</property>
</item>
<item>
<property name="text">
<string>ArcGIS</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="buildingsLabel">
<property name="text">
<string>Buildings</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="buildings">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Cesium OSM Buildings</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="sunLightEnabledLabel">
<property name="text">
<string>Lighting</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="sunLightEnabled">
<property name="toolTip">
<string>Whether lighting is from the Sun or Camera</string>
</property>
<item>
<property name="text">
<string>Camera</string>
</property>
</item>
<item>
<property name="text">
<string>Sun</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="eciCameraLabel">
<property name="text">
<string>Camera reference frame</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="eciCamera">
<property name="toolTip">
<string>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.</string>
</property>
<item>
<property name="text">
<string>ECEF</string>
</property>
</item>
<item>
<property name="text">
<string>ECI</string>
</property>
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="antiAliasingLabel">
<property name="text">
<string>Anti-aliasing</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="antiAliasing">
<property name="toolTip">
<string>Set anti-aliasing to use. This can remove jagged pixels on the edge of 3D models.</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>FXAA</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="apiKeys">
<property name="title">
<string>API Keys</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="thunderforestAPIKeyLabel">
<property name="minimumSize">
<size>
<width>140</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Thunderforest API Key</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="0" column="1">
<widget class="QLineEdit" name="thunderforestAPIKey">
<property name="toolTip">
<string>Enter a Thunderforest API key in order to use non-watermarked Thunderforest maps: https://www.thunderforest.com/</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="maptilerAPIKeyLabel">
<property name="text">
<string>Maptiler API Key</string>
@ -233,36 +372,70 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="thunderforestAPIKey">
<property name="toolTip">
<string>Enter a Thunderforest API key in order to use non-watermarked Thunderforest maps: https://www.thunderforest.com/</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="maptilerAPIKey">
<property name="toolTip">
<string>Enter a Maptiler API key in order to use Maptiler maps: https://www.maptiler.com/</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="osmURLLabel">
<item row="2" column="0">
<widget class="QLabel" name="mapBoxAPIKeyLabel">
<property name="text">
<string>OSM Custom URL</string>
<string>Mapbox API Key</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="osmURL">
<item row="2" column="1">
<widget class="QLineEdit" name="mapBoxAPIKey">
<property name="toolTip">
<string>URL of custom map for use with OpenStreetMap provider</string>
<string>Enter a Mapbox API key in order to use Mapbox maps: https://www.mapbox.com/</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="cesiumIonAPIKeyLabel">
<property name="text">
<string>Cesium Ion API Key</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="cesiumIonAPIKey">
<property name="toolTip">
<string>Enter a Cesium Ion Access Token</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="downloadModels">
<property name="toolTip">
<string>Download 3D models. It is recommended to restart SDRangel after download.</string>
</property>
<property name="text">
<string>Download 3D Models (1.6GB)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
@ -278,6 +451,23 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>mapItemSettings</tabstop>
<tabstop>map2DEnabled</tabstop>
<tabstop>mapProvider</tabstop>
<tabstop>osmURL</tabstop>
<tabstop>mapBoxStyles</tabstop>
<tabstop>map3DEnabled</tabstop>
<tabstop>terrain</tabstop>
<tabstop>buildings</tabstop>
<tabstop>sunLightEnabled</tabstop>
<tabstop>eciCamera</tabstop>
<tabstop>thunderforestAPIKey</tabstop>
<tabstop>maptilerAPIKey</tabstop>
<tabstop>mapBoxAPIKey</tabstop>
<tabstop>cesiumIonAPIKey</tabstop>
<tabstop>downloadModels</tabstop>
</tabstops>
<resources/>
<connections>
<connection>

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QJsonDocument>
#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<QWebSocket *>(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<QWebSocket *>(sender());
// Shouldn't receive any binary messages for now
qDebug() << "MapWebSocketServer::processBinaryMessage - Received binary " << message;
}
void MapWebSocketServer::socketDisconnected()
{
QWebSocket *client = qobject_cast<QWebSocket *>(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();
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAPWEBSOCKERSERVER_H_
#define INCLUDE_FEATURE_MAPWEBSOCKERSERVER_H_
#include <QObject>
#include <QWebSocketServer>
#include <QWebSocket>
#include <QJsonObject>
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_

View File

@ -2,13 +2,14 @@
<h2>Introduction</h2>
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).
<h2>Interface</h2>
@ -33,7 +38,7 @@ To centre the map on an object or location, enter:
<h3>2: Map Type</h3>
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).
<h3>3: Maidenhead locator conversion</h3>
@ -90,11 +95,33 @@ When clicked, all items will be deleted from the map.
<h3>11: Display settings</h3>
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.
<h3>Map</h3>
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.
<h2>Attribution</h2>
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
<h2>Creating 3D Models</h2>
If you wish to contribute a 3D model, see the https://github.com/srcejon/sdrangel-3d-models project.
<h2>API</h2>
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:

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QResource>
#include <QFile>
#include <QDebug>
#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<Substitution *> *list = m_substitutions.value(path);
QMutableListIterator<Substitution *> i(*list);
while (i.hasNext()) {
Substitution *sub = i.next();
if (sub->m_from == from) {
i.remove();
delete sub;
}
}
list->append(s);
}
else
{
QList<Substitution *> *list = new QList<Substitution *>();
list->append(s);
m_substitutions.insert(path, list);
}
}
QString WebServer::substitute(QString path, QString html)
{
QList<Substitution *> *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"
"<html>\n"
"<body>\n"
"<h1>404 Not Found</h1>\n"
"</body>\n"
"</html>\n";
}
}
socket->close();
if (socket->state() == QTcpSocket::UnconnectedState) {
delete socket;
}
}
}
}
void WebServer::discardClient()
{
QTcpSocket* socket = (QTcpSocket*)sender();
socket->deleteLater();
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_WEB_SERVER_H_
#define INCLUDE_WEB_SERVER_H_
#include <QTcpServer>
#include <QTcpSocket>
// 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<QString, QString> m_pathSubstitutions;
// Hash of path to a list of substitutions to make in the file
QHash<QString, QList<Substitution *>*> m_substitutions;
// Hash of filename extension to MIME type information
QHash<QString, MimeType *> 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