mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-11-22 16:08:39 -05:00
Add 3D Map to Map feature
This commit is contained in:
parent
70c99d54c7
commit
97f9835a71
@ -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()
|
||||
|
219
plugins/feature/map/cesiuminterface.cpp
Normal file
219
plugins/feature/map/cesiuminterface.cpp
Normal 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);
|
||||
}
|
80
plugins/feature/map/cesiuminterface.h
Normal file
80
plugins/feature/map/cesiuminterface.h
Normal 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_
|
531
plugins/feature/map/czml.cpp
Normal file
531
plugins/feature/map/czml.cpp
Normal 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;
|
||||
}
|
57
plugins/feature/map/czml.h
Normal file
57
plugins/feature/map/czml.h
Normal 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_
|
@ -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
|
||||
{
|
||||
|
@ -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; }
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
403
plugins/feature/map/map/map3d.html
Normal file
403
plugins/feature/map/map/map3d.html
Normal 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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
70
plugins/feature/map/mapcolordialog.cpp
Normal file
70
plugins/feature/map/mapcolordialog.cpp
Normal 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();
|
||||
}
|
45
plugins/feature/map/mapcolordialog.h
Normal file
45
plugins/feature/map/mapcolordialog.h
Normal 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
@ -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_
|
||||
|
@ -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>
|
||||
|
997
plugins/feature/map/mapmodel.cpp
Normal file
997
plugins/feature/map/mapmodel.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
229
plugins/feature/map/mapmodel.h
Normal file
229
plugins/feature/map/mapmodel.h
Normal 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_
|
@ -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);
|
||||
}
|
||||
|
@ -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_
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
100
plugins/feature/map/mapwebsocketserver.cpp
Normal file
100
plugins/feature/map/mapwebsocketserver.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
56
plugins/feature/map/mapwebsocketserver.h
Normal file
56
plugins/feature/map/mapwebsocketserver.h
Normal 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_
|
@ -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:
|
||||
|
199
plugins/feature/map/webserver.cpp
Normal file
199
plugins/feature/map/webserver.cpp
Normal 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();
|
||||
}
|
76
plugins/feature/map/webserver.h
Normal file
76
plugins/feature/map/webserver.h
Normal 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
|
Loading…
Reference in New Issue
Block a user