mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-09-28 15:56:33 -04: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
|
mapwebapiadapter.cpp
|
||||||
osmtemplateserver.cpp
|
osmtemplateserver.cpp
|
||||||
ibpbeacon.cpp
|
ibpbeacon.cpp
|
||||||
|
webserver.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(map_HEADERS
|
set(map_HEADERS
|
||||||
@ -18,10 +19,12 @@ set(map_HEADERS
|
|||||||
osmtemplateserver.h
|
osmtemplateserver.h
|
||||||
beacon.h
|
beacon.h
|
||||||
ibpbeacon.h
|
ibpbeacon.h
|
||||||
|
webserver.h
|
||||||
)
|
)
|
||||||
|
|
||||||
include_directories(
|
include_directories(
|
||||||
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
|
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
|
||||||
|
${Qt5Gui_PRIVATE_INCLUDE_DIRS}
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT SERVER_MODE)
|
if(NOT SERVER_MODE)
|
||||||
@ -41,8 +44,14 @@ if(NOT SERVER_MODE)
|
|||||||
mapibpbeacondialog.ui
|
mapibpbeacondialog.ui
|
||||||
mapradiotimedialog.cpp
|
mapradiotimedialog.cpp
|
||||||
mapradiotimedialog.ui
|
mapradiotimedialog.ui
|
||||||
|
mapcolordialog.cpp
|
||||||
|
mapmodel.cpp
|
||||||
|
mapwebsocketserver.cpp
|
||||||
|
cesiuminterface.cpp
|
||||||
|
czml.cpp
|
||||||
map.qrc
|
map.qrc
|
||||||
icons.qrc
|
icons.qrc
|
||||||
|
cesium.qrc
|
||||||
)
|
)
|
||||||
set(map_HEADERS
|
set(map_HEADERS
|
||||||
${map_HEADERS}
|
${map_HEADERS}
|
||||||
@ -53,10 +62,15 @@ if(NOT SERVER_MODE)
|
|||||||
mapbeacondialog.h
|
mapbeacondialog.h
|
||||||
mapibpbeacon.h
|
mapibpbeacon.h
|
||||||
mapradiotimedialog.h
|
mapradiotimedialog.h
|
||||||
|
mapcolordialog.h
|
||||||
|
mapmodel.h
|
||||||
|
mapwebsocketserver.h
|
||||||
|
cesiuminterface.h
|
||||||
|
czml.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(TARGET_NAME map)
|
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(TARGET_LIB_GUI "sdrgui")
|
||||||
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
|
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
|
||||||
else()
|
else()
|
||||||
@ -84,3 +98,8 @@ if(WIN32)
|
|||||||
include(DeployQt)
|
include(DeployQt)
|
||||||
windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/map)
|
windeployqt(${TARGET_NAME} ${SDRANGEL_BINARY_BIN_DIR} ${PROJECT_SOURCE_DIR}/map)
|
||||||
endif()
|
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::MsgConfigureMap, Message)
|
||||||
MESSAGE_CLASS_DEFINITION(Map::MsgFind, 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_featureIdURI = "sdrangel.feature.map";
|
||||||
const char* const Map::m_featureId = "Map";
|
const char* const Map::m_featureId = "Map";
|
||||||
@ -221,14 +222,17 @@ int Map::webapiActionsPost(
|
|||||||
if (getMessageQueueToGUI()) {
|
if (getMessageQueueToGUI()) {
|
||||||
getMessageQueueToGUI()->push(MsgFind::create(id));
|
getMessageQueueToGUI()->push(MsgFind::create(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return 202;
|
|
||||||
}
|
}
|
||||||
else
|
if (featureActionsKeys.contains("setDateTime"))
|
||||||
{
|
{
|
||||||
errorMessage = "Unknown action";
|
QString dateTimeString = *swgMapActions->getSetDateTime();
|
||||||
return 400;
|
QDateTime dateTime = QDateTime::fromString(dateTimeString, Qt::ISODateWithMs);
|
||||||
|
|
||||||
|
if (getMessageQueueToGUI()) {
|
||||||
|
getMessageQueueToGUI()->push(MsgSetDateTime::create(dateTime));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return 202;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
#include "feature/feature.h"
|
#include "feature/feature.h"
|
||||||
#include "util/message.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);
|
Map(WebAPIAdapterInterface *webAPIAdapterInterface);
|
||||||
virtual ~Map();
|
virtual ~Map();
|
||||||
virtual void destroy() { delete this; }
|
virtual void destroy() { delete this; }
|
||||||
|
@ -7,5 +7,9 @@
|
|||||||
<file>map/antennadab.png</file>
|
<file>map/antennadab.png</file>
|
||||||
<file>map/antennafm.png</file>
|
<file>map/antennafm.png</file>
|
||||||
<file>map/antennaam.png</file>
|
<file>map/antennaam.png</file>
|
||||||
|
<file>map/map3d.html</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="/">
|
||||||
|
<file>Cesium/Cesium.js</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
@ -174,6 +174,7 @@ Item {
|
|||||||
id: text
|
id: text
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: mapText
|
text: mapText
|
||||||
|
textFormat: TextEdit.RichText
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -215,6 +216,10 @@ Item {
|
|||||||
text: "Move to back"
|
text: "Move to back"
|
||||||
onTriggered: mapModel.moveToBack(index)
|
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
|
id: text
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: mapText
|
text: mapText
|
||||||
|
textFormat: TextEdit.RichText
|
||||||
}
|
}
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -215,6 +216,10 @@ Item {
|
|||||||
text: "Move to back"
|
text: "Move to back"
|
||||||
onTriggered: mapModel.moveToBack(index)
|
onTriggered: mapModel.moveToBack(index)
|
||||||
}
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: "Track on 3D map"
|
||||||
|
onTriggered: mapModel.track3D(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,7 @@
|
|||||||
MapBeaconDialog::MapBeaconDialog(MapGUI *gui, QWidget* parent) :
|
MapBeaconDialog::MapBeaconDialog(MapGUI *gui, QWidget* parent) :
|
||||||
QDialog(parent),
|
QDialog(parent),
|
||||||
m_gui(gui),
|
m_gui(gui),
|
||||||
ui(new Ui::MapBeaconDialog),
|
ui(new Ui::MapBeaconDialog)
|
||||||
m_progressDialog(nullptr)
|
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &MapBeaconDialog::downloadFinished);
|
connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &MapBeaconDialog::downloadFinished);
|
||||||
@ -79,47 +78,6 @@ void MapBeaconDialog::updateTable()
|
|||||||
ui->beacons->resizeColumnsToContents();
|
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()
|
void MapBeaconDialog::accept()
|
||||||
{
|
{
|
||||||
QDialog::accept();
|
QDialog::accept();
|
||||||
@ -127,32 +85,28 @@ void MapBeaconDialog::accept()
|
|||||||
|
|
||||||
void MapBeaconDialog::on_downloadIARU_clicked()
|
void MapBeaconDialog::on_downloadIARU_clicked()
|
||||||
{
|
{
|
||||||
if (m_progressDialog == nullptr)
|
if (!m_dlm.downloading())
|
||||||
{
|
{
|
||||||
QString beaconFile = MapGUI::getBeaconFilename();
|
QString beaconFile = MapGUI::getBeaconFilename();
|
||||||
if (confirmDownload(beaconFile))
|
if (HttpDownloadManagerGUI::confirmDownload(beaconFile, this))
|
||||||
{
|
{
|
||||||
// Download IARU beacons database to disk
|
// Download IARU beacons database to disk
|
||||||
QUrl dbURL(QString(IARU_BEACONS_URL));
|
QUrl dbURL(QString(IARU_BEACONS_URL));
|
||||||
m_progressDialog = new QProgressDialog(this);
|
m_dlm.download(dbURL, beaconFile, 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)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (success)
|
||||||
{
|
{
|
||||||
if (filename == MapGUI::getBeaconFilename())
|
if (filename == MapGUI::getBeaconFilename())
|
||||||
{
|
{
|
||||||
QList<Beacon *> *beacons = Beacon::readIARUCSV(filename);
|
QList<Beacon *> *beacons = Beacon::readIARUCSV(filename);
|
||||||
if (beacons != nullptr)
|
if (beacons != nullptr) {
|
||||||
m_gui->setBeacons(beacons);
|
m_gui->setBeacons(beacons);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -161,14 +115,7 @@ void MapBeaconDialog::downloadFinished(const QString& filename, bool success)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
qDebug() << "MapBeaconDialog::downloadFinished: Failed: " << filename;
|
QMessageBox::warning(this, "Download failed", QString("Failed to download %1 to %2\n%3").arg(url).arg(filename).arg(errorMessage));
|
||||||
QMessageBox::warning(this, "Download failed", QString("Failed to download %1").arg(filename));
|
|
||||||
}
|
|
||||||
if (m_progressDialog)
|
|
||||||
{
|
|
||||||
m_progressDialog->close();
|
|
||||||
delete m_progressDialog;
|
|
||||||
m_progressDialog = nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@
|
|||||||
|
|
||||||
#include "ui_mapbeacondialog.h"
|
#include "ui_mapbeacondialog.h"
|
||||||
|
|
||||||
#include <QProgressDialog>
|
#include "gui/httpdownloadmanagergui.h"
|
||||||
|
|
||||||
#include "util/httpdownloadmanager.h"
|
|
||||||
#include "beacon.h"
|
#include "beacon.h"
|
||||||
|
|
||||||
class MapGUI;
|
class MapGUI;
|
||||||
@ -36,22 +34,18 @@ public:
|
|||||||
void updateTable();
|
void updateTable();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
qint64 fileAgeInDays(QString filename);
|
void downloadFinished(const QString& filename, bool success, const QString &url, const QString &errorMessage);
|
||||||
bool confirmDownload(QString filename);
|
|
||||||
void downloadFinished(const QString& filename, bool success);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void accept();
|
void accept();
|
||||||
void on_downloadIARU_clicked();
|
void on_downloadIARU_clicked();
|
||||||
void updateDownloadProgress(qint64 bytesRead, qint64 totalBytes);
|
|
||||||
void on_beacons_cellDoubleClicked(int row, int column);
|
void on_beacons_cellDoubleClicked(int row, int column);
|
||||||
void on_filter_currentIndexChanged(int index);
|
void on_filter_currentIndexChanged(int index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MapGUI *m_gui;
|
MapGUI *m_gui;
|
||||||
Ui::MapBeaconDialog* ui;
|
Ui::MapBeaconDialog* ui;
|
||||||
HttpDownloadManager m_dlm;
|
HttpDownloadManagerGUI m_dlm;
|
||||||
QProgressDialog *m_progressDialog;
|
|
||||||
|
|
||||||
enum BeaconCol {
|
enum BeaconCol {
|
||||||
BEACON_COL_CALLSIGN,
|
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_
|
#define INCLUDE_FEATURE_MAPGUI_H_
|
||||||
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QAbstractListModel>
|
#include <QQuickItem>
|
||||||
#include <QGeoCoordinate>
|
#include <QJsonObject>
|
||||||
#include <QGeoRectangle>
|
#include <QWebEngineFullScreenRequest>
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "feature/featuregui.h"
|
#include "feature/featuregui.h"
|
||||||
#include "util/messagequeue.h"
|
#include "util/messagequeue.h"
|
||||||
@ -36,7 +39,10 @@
|
|||||||
#include "mapbeacondialog.h"
|
#include "mapbeacondialog.h"
|
||||||
#include "mapibpbeacondialog.h"
|
#include "mapibpbeacondialog.h"
|
||||||
#include "mapradiotimedialog.h"
|
#include "mapradiotimedialog.h"
|
||||||
|
#include "cesiuminterface.h"
|
||||||
#include "osmtemplateserver.h"
|
#include "osmtemplateserver.h"
|
||||||
|
#include "webserver.h"
|
||||||
|
#include "mapmodel.h"
|
||||||
|
|
||||||
class PluginAPI;
|
class PluginAPI;
|
||||||
class FeatureUISet;
|
class FeatureUISet;
|
||||||
@ -47,8 +53,6 @@ namespace Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MapGUI;
|
class MapGUI;
|
||||||
class MapModel;
|
|
||||||
class QQuickItem;
|
|
||||||
struct Beacon;
|
struct Beacon;
|
||||||
|
|
||||||
struct RadioTimeTransmitter {
|
struct RadioTimeTransmitter {
|
||||||
@ -59,415 +63,6 @@ struct RadioTimeTransmitter {
|
|||||||
int m_power; // In kW
|
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 {
|
class MapGUI : public FeatureGUI {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
@ -481,7 +76,6 @@ public:
|
|||||||
AzEl *getAzEl() { return &m_azEl; }
|
AzEl *getAzEl() { return &m_azEl; }
|
||||||
Map *getMap() { return m_map; }
|
Map *getMap() { return m_map; }
|
||||||
QQuickItem *getMapItem();
|
QQuickItem *getMapItem();
|
||||||
quint32 getSourceMask(const PipeEndPoint *sourcePipe);
|
|
||||||
static QString getBeaconFilename();
|
static QString getBeaconFilename();
|
||||||
QList<Beacon *> *getBeacons() { return m_beacons; }
|
QList<Beacon *> *getBeacons() { return m_beacons; }
|
||||||
void setBeacons(QList<Beacon *> *beacons);
|
void setBeacons(QList<Beacon *> *beacons);
|
||||||
@ -491,7 +85,10 @@ public:
|
|||||||
void addRadar();
|
void addRadar();
|
||||||
void addDAB();
|
void addDAB();
|
||||||
void find(const QString& target);
|
void find(const QString& target);
|
||||||
|
void track3D(const QString& target);
|
||||||
Q_INVOKABLE void supportedMapsChanged();
|
Q_INVOKABLE void supportedMapsChanged();
|
||||||
|
MapSettings::MapItemSettings *getItemSettings(const QString &group) { return m_settings.m_itemSettings[group]; }
|
||||||
|
CesiumInterface *cesium() { return m_cesium; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::MapGUI* ui;
|
Ui::MapGUI* ui;
|
||||||
@ -513,17 +110,27 @@ private:
|
|||||||
quint16 m_osmPort;
|
quint16 m_osmPort;
|
||||||
OSMTemplateServer *m_templateServer;
|
OSMTemplateServer *m_templateServer;
|
||||||
|
|
||||||
|
CesiumInterface *m_cesium;
|
||||||
|
WebServer *m_webServer;
|
||||||
|
quint16 m_webPort;
|
||||||
|
|
||||||
explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
|
explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
|
||||||
virtual ~MapGUI();
|
virtual ~MapGUI();
|
||||||
|
|
||||||
|
void update(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group);
|
||||||
void blockApplySettings(bool block);
|
void blockApplySettings(bool block);
|
||||||
void applySettings(bool force = false);
|
void applySettings(bool force = false);
|
||||||
void applyMapSettings();
|
void applyMap2DSettings(bool reloadMap);
|
||||||
|
void applyMap3DSettings(bool reloadMap);
|
||||||
QString osmCachePath();
|
QString osmCachePath();
|
||||||
void clearOSMCache();
|
void clearOSMCache();
|
||||||
void displaySettings();
|
void displaySettings();
|
||||||
bool handleMessage(const Message& message);
|
bool handleMessage(const Message& message);
|
||||||
void geoReply();
|
void geoReply();
|
||||||
|
QString thunderforestAPIKey() const;
|
||||||
|
QString maptilerAPIKey() const;
|
||||||
|
QString cesiumIonAPIKey() const;
|
||||||
|
void redrawMap();
|
||||||
|
|
||||||
void leaveEvent(QEvent*);
|
void leaveEvent(QEvent*);
|
||||||
void enterEvent(QEvent*);
|
void enterEvent(QEvent*);
|
||||||
@ -532,6 +139,7 @@ private:
|
|||||||
static const QList<RadioTimeTransmitter> m_radioTimeTransmitters;
|
static const QList<RadioTimeTransmitter> m_radioTimeTransmitters;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void init3DMap();
|
||||||
void onMenuDialogCalled(const QPoint &p);
|
void onMenuDialogCalled(const QPoint &p);
|
||||||
void onWidgetRolled(QWidget* widget, bool rollDown);
|
void onWidgetRolled(QWidget* widget, bool rollDown);
|
||||||
void handleInputMessages();
|
void handleInputMessages();
|
||||||
@ -546,7 +154,11 @@ private slots:
|
|||||||
void on_beacons_clicked();
|
void on_beacons_clicked();
|
||||||
void on_ibpBeacons_clicked();
|
void on_ibpBeacons_clicked();
|
||||||
void on_radiotime_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_
|
#endif // INCLUDE_FEATURE_MAPGUI_H_
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>481</width>
|
<width>481</width>
|
||||||
<height>750</height>
|
<height>507</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -277,8 +277,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>60</y>
|
<y>60</y>
|
||||||
<width>471</width>
|
<width>483</width>
|
||||||
<height>681</height>
|
<height>223</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -290,47 +290,72 @@
|
|||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Map</string>
|
<string>Map</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayoutMap" stretch="0">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>2</number>
|
<number>3</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>3</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="topMargin">
|
<property name="topMargin">
|
||||||
<number>3</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="rightMargin">
|
<property name="rightMargin">
|
||||||
<number>3</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>3</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QQuickWidget" name="map">
|
<widget class="QSplitter" name="splitter">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="orientation">
|
||||||
<size>
|
<enum>Qt::Vertical</enum>
|
||||||
<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>
|
</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>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
@ -353,6 +378,12 @@
|
|||||||
<extends>QToolButton</extends>
|
<extends>QToolButton</extends>
|
||||||
<header>gui/buttonswitch.h</header>
|
<header>gui/buttonswitch.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>QWebEngineView</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header location="global">QWebEngineView.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>find</tabstop>
|
<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 <QColor>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
#include "util/simpleserializer.h"
|
#include "util/simpleserializer.h"
|
||||||
|
#include "util/httpdownloadmanager.h"
|
||||||
#include "settings/serializable.h"
|
#include "settings/serializable.h"
|
||||||
|
|
||||||
#include "mapsettings.h"
|
#include "mapsettings.h"
|
||||||
@ -27,6 +29,7 @@ const QStringList MapSettings::m_pipeTypes = {
|
|||||||
QStringLiteral("ADSBDemod"),
|
QStringLiteral("ADSBDemod"),
|
||||||
QStringLiteral("AIS"),
|
QStringLiteral("AIS"),
|
||||||
QStringLiteral("APRS"),
|
QStringLiteral("APRS"),
|
||||||
|
QStringLiteral("APTDemod"),
|
||||||
QStringLiteral("StarTracker"),
|
QStringLiteral("StarTracker"),
|
||||||
QStringLiteral("SatelliteTracker")
|
QStringLiteral("SatelliteTracker")
|
||||||
};
|
};
|
||||||
@ -35,6 +38,7 @@ const QStringList MapSettings::m_pipeURIs = {
|
|||||||
QStringLiteral("sdrangel.channel.adsbdemod"),
|
QStringLiteral("sdrangel.channel.adsbdemod"),
|
||||||
QStringLiteral("sdrangel.feature.ais"),
|
QStringLiteral("sdrangel.feature.ais"),
|
||||||
QStringLiteral("sdrangel.feature.aprs"),
|
QStringLiteral("sdrangel.feature.aprs"),
|
||||||
|
QStringLiteral("sdrangel.channel.aptdemod"),
|
||||||
QStringLiteral("sdrangel.feature.startracker"),
|
QStringLiteral("sdrangel.feature.startracker"),
|
||||||
QStringLiteral("sdrangel.feature.satellitetracker")
|
QStringLiteral("sdrangel.feature.satellitetracker")
|
||||||
};
|
};
|
||||||
@ -44,15 +48,33 @@ const QStringList MapSettings::m_mapProviders = {
|
|||||||
QStringLiteral("osm"),
|
QStringLiteral("osm"),
|
||||||
QStringLiteral("esri"),
|
QStringLiteral("esri"),
|
||||||
QStringLiteral("mapbox"),
|
QStringLiteral("mapbox"),
|
||||||
QStringLiteral("mapboxgl")
|
QStringLiteral("mapboxgl"),
|
||||||
|
QStringLiteral("maplibre")
|
||||||
};
|
};
|
||||||
|
|
||||||
MapSettings::MapSettings() :
|
MapSettings::MapSettings() :
|
||||||
m_rollupState(nullptr)
|
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();
|
resetToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MapSettings::~MapSettings()
|
||||||
|
{
|
||||||
|
//qDeleteAll(m_itemSettings);
|
||||||
|
}
|
||||||
|
|
||||||
void MapSettings::resetToDefaults()
|
void MapSettings::resetToDefaults()
|
||||||
{
|
{
|
||||||
m_displayNames = true;
|
m_displayNames = true;
|
||||||
@ -62,11 +84,8 @@ void MapSettings::resetToDefaults()
|
|||||||
m_mapBoxAPIKey = "";
|
m_mapBoxAPIKey = "";
|
||||||
m_osmURL = "";
|
m_osmURL = "";
|
||||||
m_mapBoxStyles = "";
|
m_mapBoxStyles = "";
|
||||||
m_sources = -1;
|
|
||||||
m_displaySelectedGroundTracks = true;
|
m_displaySelectedGroundTracks = true;
|
||||||
m_displayAllGroundTracks = true;
|
m_displayAllGroundTracks = true;
|
||||||
m_groundTrackColor = QColor(150, 0, 20).rgb();
|
|
||||||
m_predictedGroundTrackColor = QColor(225, 0, 50).rgb();
|
|
||||||
m_title = "Map";
|
m_title = "Map";
|
||||||
m_rgbColor = QColor(225, 25, 99).rgb();
|
m_rgbColor = QColor(225, 25, 99).rgb();
|
||||||
m_useReverseAPI = false;
|
m_useReverseAPI = false;
|
||||||
@ -74,6 +93,14 @@ void MapSettings::resetToDefaults()
|
|||||||
m_reverseAPIPort = 8888;
|
m_reverseAPIPort = 8888;
|
||||||
m_reverseAPIFeatureSetIndex = 0;
|
m_reverseAPIFeatureSetIndex = 0;
|
||||||
m_reverseAPIFeatureIndex = 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
|
QByteArray MapSettings::serialize() const
|
||||||
@ -84,9 +111,6 @@ QByteArray MapSettings::serialize() const
|
|||||||
s.writeString(2, m_mapProvider);
|
s.writeString(2, m_mapProvider);
|
||||||
s.writeString(3, m_mapBoxAPIKey);
|
s.writeString(3, m_mapBoxAPIKey);
|
||||||
s.writeString(4, m_mapBoxStyles);
|
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.writeString(8, m_title);
|
||||||
s.writeU32(9, m_rgbColor);
|
s.writeU32(9, m_rgbColor);
|
||||||
s.writeBool(10, m_useReverseAPI);
|
s.writeBool(10, m_useReverseAPI);
|
||||||
@ -104,6 +128,17 @@ QByteArray MapSettings::serialize() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.writeString(20, m_osmURL);
|
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();
|
return s.final();
|
||||||
}
|
}
|
||||||
@ -123,14 +158,12 @@ bool MapSettings::deserialize(const QByteArray& data)
|
|||||||
QByteArray bytetmp;
|
QByteArray bytetmp;
|
||||||
uint32_t utmp;
|
uint32_t utmp;
|
||||||
QString strtmp;
|
QString strtmp;
|
||||||
|
QByteArray blob;
|
||||||
|
|
||||||
d.readBool(1, &m_displayNames, true);
|
d.readBool(1, &m_displayNames, true);
|
||||||
d.readString(2, &m_mapProvider, "osm");
|
d.readString(2, &m_mapProvider, "osm");
|
||||||
d.readString(3, &m_mapBoxAPIKey, "");
|
d.readString(3, &m_mapBoxAPIKey, "");
|
||||||
d.readString(4, &m_mapBoxStyles, "");
|
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.readString(8, &m_title, "Map");
|
||||||
d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb());
|
d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb());
|
||||||
d.readBool(10, &m_useReverseAPI, false);
|
d.readBool(10, &m_useReverseAPI, false);
|
||||||
@ -159,6 +192,19 @@ bool MapSettings::deserialize(const QByteArray& data)
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.readString(20, &m_osmURL, "");
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@ -168,3 +214,147 @@ bool MapSettings::deserialize(const QByteArray& data)
|
|||||||
return false;
|
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 <QByteArray>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QHash>
|
||||||
#include "util/message.h"
|
|
||||||
|
|
||||||
class Serializable;
|
class Serializable;
|
||||||
class PipeEndPoint;
|
|
||||||
|
|
||||||
struct MapSettings
|
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;
|
bool m_displayNames;
|
||||||
QString m_mapProvider;
|
QString m_mapProvider;
|
||||||
QString m_thunderforestAPIKey;
|
QString m_thunderforestAPIKey;
|
||||||
@ -36,11 +57,8 @@ struct MapSettings
|
|||||||
QString m_mapBoxAPIKey;
|
QString m_mapBoxAPIKey;
|
||||||
QString m_osmURL;
|
QString m_osmURL;
|
||||||
QString m_mapBoxStyles;
|
QString m_mapBoxStyles;
|
||||||
quint32 m_sources; // Bitmask of SOURCE_*
|
|
||||||
bool m_displayAllGroundTracks;
|
bool m_displayAllGroundTracks;
|
||||||
bool m_displaySelectedGroundTracks;
|
bool m_displaySelectedGroundTracks;
|
||||||
quint32 m_groundTrackColor;
|
|
||||||
quint32 m_predictedGroundTrackColor;
|
|
||||||
QString m_title;
|
QString m_title;
|
||||||
quint32 m_rgbColor;
|
quint32 m_rgbColor;
|
||||||
bool m_useReverseAPI;
|
bool m_useReverseAPI;
|
||||||
@ -49,31 +67,36 @@ struct MapSettings
|
|||||||
uint16_t m_reverseAPIFeatureSetIndex;
|
uint16_t m_reverseAPIFeatureSetIndex;
|
||||||
uint16_t m_reverseAPIFeatureIndex;
|
uint16_t m_reverseAPIFeatureIndex;
|
||||||
Serializable *m_rollupState;
|
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();
|
||||||
|
~MapSettings();
|
||||||
void resetToDefaults();
|
void resetToDefaults();
|
||||||
QByteArray serialize() const;
|
QByteArray serialize() const;
|
||||||
bool deserialize(const QByteArray& data);
|
bool deserialize(const QByteArray& data);
|
||||||
void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; }
|
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_pipeTypes;
|
||||||
static const QStringList m_pipeURIs;
|
static const QStringList m_pipeURIs;
|
||||||
|
|
||||||
static const QStringList m_mapProviders;
|
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_
|
#endif // INCLUDE_FEATURE_MAPSETTINGS_H_
|
||||||
|
@ -18,26 +18,92 @@
|
|||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QFileDialog>
|
||||||
|
|
||||||
|
#include <QtGui/private/qzipreader_p.h>
|
||||||
|
|
||||||
#include "util/units.h"
|
#include "util/units.h"
|
||||||
|
|
||||||
#include "mapsettingsdialog.h"
|
#include "mapsettingsdialog.h"
|
||||||
#include "maplocationdialog.h"
|
#include "maplocationdialog.h"
|
||||||
|
#include "mapcolordialog.h"
|
||||||
|
|
||||||
static QString rgbToColor(quint32 rgb)
|
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());
|
return QString("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue());
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString backgroundCSS(quint32 rgb)
|
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) :
|
MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
||||||
QDialog(parent),
|
QDialog(parent),
|
||||||
m_settings(settings),
|
m_settings(settings),
|
||||||
|
m_downloadDialog(this),
|
||||||
ui(new Ui::MapSettingsDialog)
|
ui(new Ui::MapSettingsDialog)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
@ -45,28 +111,74 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
|
|||||||
ui->thunderforestAPIKey->setText(settings->m_thunderforestAPIKey);
|
ui->thunderforestAPIKey->setText(settings->m_thunderforestAPIKey);
|
||||||
ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey);
|
ui->maptilerAPIKey->setText(settings->m_maptilerAPIKey);
|
||||||
ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey);
|
ui->mapBoxAPIKey->setText(settings->m_mapBoxAPIKey);
|
||||||
|
ui->cesiumIonAPIKey->setText(settings->m_cesiumIonAPIKey);
|
||||||
ui->osmURL->setText(settings->m_osmURL);
|
ui->osmURL->setText(settings->m_osmURL);
|
||||||
ui->mapBoxStyles->setText(settings->m_mapBoxStyles);
|
ui->mapBoxStyles->setText(settings->m_mapBoxStyles);
|
||||||
for (int i = 0; i < ui->sourceList->count(); i++) {
|
ui->map2DEnabled->setChecked(m_settings->m_map2DEnabled);
|
||||||
ui->sourceList->item(i)->setCheckState((m_settings->m_sources & (1 << i)) ? Qt::Checked : Qt::Unchecked);
|
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->mapItemSettings->resizeColumnsToContents();
|
||||||
ui->predictedGroundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_predictedGroundTrackColor));
|
|
||||||
|
on_map2DEnabled_clicked(m_settings->m_map2DEnabled);
|
||||||
|
on_map3DEnabled_clicked(m_settings->m_map3DEnabled);
|
||||||
|
|
||||||
|
connect(&m_dlm, &HttpDownloadManagerGUI::downloadComplete, this, &MapSettingsDialog::downloadComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
MapSettingsDialog::~MapSettingsDialog()
|
MapSettingsDialog::~MapSettingsDialog()
|
||||||
{
|
{
|
||||||
delete ui;
|
delete ui;
|
||||||
|
qDeleteAll(m_mapItemSettingsGUIs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MapSettingsDialog::accept()
|
void MapSettingsDialog::accept()
|
||||||
{
|
{
|
||||||
QString mapProvider = MapSettings::m_mapProviders[ui->mapProvider->currentIndex()];
|
QString mapProvider = MapSettings::m_mapProviders[ui->mapProvider->currentIndex()];
|
||||||
QString mapBoxAPIKey = ui->mapBoxAPIKey->text();
|
|
||||||
QString osmURL = ui->osmURL->text();
|
QString osmURL = ui->osmURL->text();
|
||||||
QString mapBoxStyles = ui->mapBoxStyles->text();
|
QString mapBoxStyles = ui->mapBoxStyles->text();
|
||||||
|
QString mapBoxAPIKey = ui->mapBoxAPIKey->text();
|
||||||
QString thunderforestAPIKey = ui->thunderforestAPIKey->text();
|
QString thunderforestAPIKey = ui->thunderforestAPIKey->text();
|
||||||
QString maptilerAPIKey = ui->maptilerAPIKey->text();
|
QString maptilerAPIKey = ui->maptilerAPIKey->text();
|
||||||
|
QString cesiumIonAPIKey = ui->cesiumIonAPIKey->text();
|
||||||
m_osmURLChanged = osmURL != m_settings->m_osmURL;
|
m_osmURLChanged = osmURL != m_settings->m_osmURL;
|
||||||
if ((mapProvider != m_settings->m_mapProvider)
|
if ((mapProvider != m_settings->m_mapProvider)
|
||||||
|| (thunderforestAPIKey != m_settings->m_thunderforestAPIKey)
|
|| (thunderforestAPIKey != m_settings->m_thunderforestAPIKey)
|
||||||
@ -81,38 +193,210 @@ void MapSettingsDialog::accept()
|
|||||||
m_settings->m_mapBoxAPIKey = mapBoxAPIKey;
|
m_settings->m_mapBoxAPIKey = mapBoxAPIKey;
|
||||||
m_settings->m_osmURL = osmURL;
|
m_settings->m_osmURL = osmURL;
|
||||||
m_settings->m_mapBoxStyles = mapBoxStyles;
|
m_settings->m_mapBoxStyles = mapBoxStyles;
|
||||||
m_mapSettingsChanged = true;
|
m_settings->m_cesiumIonAPIKey = cesiumIonAPIKey;
|
||||||
|
m_map2DSettingsChanged = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_mapSettingsChanged = false;
|
m_map2DSettingsChanged = false;
|
||||||
}
|
}
|
||||||
m_settings->m_sources = 0;
|
if (cesiumIonAPIKey != m_settings->m_cesiumIonAPIKey)
|
||||||
quint32 sources = MapSettings::SOURCE_STATION;
|
{
|
||||||
for (int i = 0; i < ui->sourceList->count(); i++) {
|
m_settings->m_cesiumIonAPIKey = cesiumIonAPIKey;
|
||||||
sources |= (ui->sourceList->item(i)->checkState() == Qt::Checked) << i;
|
m_map3DSettingsChanged = true;
|
||||||
}
|
}
|
||||||
m_sourcesChanged = sources != m_settings->m_sources;
|
else
|
||||||
m_settings->m_sources = sources;
|
{
|
||||||
|
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();
|
QDialog::accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MapSettingsDialog::on_groundTrackColor_clicked()
|
void MapSettingsDialog::on_map2DEnabled_clicked(bool checked)
|
||||||
{
|
{
|
||||||
QColorDialog dialog(QColor::fromRgb(m_settings->m_groundTrackColor), this);
|
if (checked)
|
||||||
if (dialog.exec() == QDialog::Accepted)
|
|
||||||
{
|
{
|
||||||
m_settings->m_groundTrackColor = dialog.selectedColor().rgb();
|
ui->mapItemSettings->showColumn(COL_2D_ICON);
|
||||||
ui->groundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_groundTrackColor));
|
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);
|
m_downloadDialog.setText("Downloading 3D models");
|
||||||
if (dialog.exec() == QDialog::Accepted)
|
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();
|
// Unzip
|
||||||
ui->predictedGroundTrackColor->setStyleSheet(backgroundCSS(m_settings->m_predictedGroundTrackColor));
|
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
|
#ifndef INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
|
||||||
#define INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
|
#define INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
|
||||||
|
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
#include "gui/httpdownloadmanagergui.h"
|
||||||
|
|
||||||
#include "ui_mapsettingsdialog.h"
|
#include "ui_mapsettingsdialog.h"
|
||||||
#include "mapsettings.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 {
|
class MapSettingsDialog : public QDialog {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@ -28,18 +65,43 @@ public:
|
|||||||
explicit MapSettingsDialog(MapSettings *settings, QWidget* parent = 0);
|
explicit MapSettingsDialog(MapSettings *settings, QWidget* parent = 0);
|
||||||
~MapSettingsDialog();
|
~MapSettingsDialog();
|
||||||
|
|
||||||
MapSettings *m_settings;
|
enum Columns {
|
||||||
bool m_mapSettingsChanged;
|
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_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:
|
private slots:
|
||||||
void accept();
|
void accept();
|
||||||
void on_groundTrackColor_clicked();
|
void on_map2DEnabled_clicked(bool checked=false);
|
||||||
void on_predictedGroundTrackColor_clicked();
|
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:
|
private:
|
||||||
Ui::MapSettingsDialog* ui;
|
Ui::MapSettingsDialog* ui;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
|
#endif // INCLUDE_FEATURE_MAPSETTINGSDIALOG_H
|
||||||
|
@ -6,13 +6,12 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>436</width>
|
<width>946</width>
|
||||||
<height>520</height>
|
<height>800</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<family>Liberation Sans</family>
|
|
||||||
<pointsize>9</pointsize>
|
<pointsize>9</pointsize>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
@ -29,141 +28,101 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="locationsLabel">
|
<widget class="QLabel" name="locationsLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Select data to display:</string>
|
<string>Select how to display items on the maps:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QListWidget" name="sourceList">
|
<widget class="QTableWidget" name="mapItemSettings">
|
||||||
<property name="selectionMode">
|
<property name="selectionMode">
|
||||||
<enum>QAbstractItemView::MultiSelection</enum>
|
<enum>QAbstractItemView::NoSelection</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sortingEnabled">
|
<column>
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>ADS-B</string>
|
<string>Enabled</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>AIS</string>
|
<string>2D Icon</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>APRS</string>
|
<string>2D Label</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Star Tracker</string>
|
<string>2D Min Zoom</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Satellite Tracker</string>
|
<string>2D Track</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Beacons</string>
|
<string>3D Model</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Radio Time Transmitters</string>
|
<string>3D Min Pixels</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Radar</string>
|
<string>3D Label</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checkState">
|
</column>
|
||||||
<enum>Checked</enum>
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>3D Point</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>3D Track</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="colorsGroupBox">
|
<widget class="QGroupBox" name="map2DSettings">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Colours</string>
|
<string>2D Map Settings</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QFormLayout" name="formLayout_2">
|
||||||
<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>
|
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QToolButton" name="predictedGroundTrackColor">
|
<widget class="QLabel" name="map2DEnabledLabel">
|
||||||
<property name="toolTip">
|
<property name="minimumSize">
|
||||||
<string>Select color for predicted ground tracks</string>
|
<size>
|
||||||
|
<width>140</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Enabled</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QCheckBox" name="map2DEnabled">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<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">
|
<widget class="QLabel" name="mapProviderLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Map provider</string>
|
<string>Map provider</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QComboBox" name="mapProvider">
|
<widget class="QComboBox" name="mapProvider">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Select map provider</string>
|
<string>Select map provider</string>
|
||||||
@ -188,44 +147,224 @@
|
|||||||
<string>MapboxGL</string>
|
<string>MapboxGL</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</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>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<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">
|
<widget class="QLabel" name="mapBoxStylesLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>MapboxGL Styles</string>
|
<string>MapboxGL Styles</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QLineEdit" name="mapBoxStyles">
|
<widget class="QLineEdit" name="mapBoxStyles">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Comma separated list of MapBox styles</string>
|
<string>Comma separated list of MapBox styles</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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">
|
<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">
|
<widget class="QLabel" name="thunderforestAPIKeyLabel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>140</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Thunderforest API Key</string>
|
<string>Thunderforest API Key</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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">
|
<widget class="QLabel" name="maptilerAPIKeyLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Maptiler API Key</string>
|
<string>Maptiler API Key</string>
|
||||||
@ -233,36 +372,70 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<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">
|
<widget class="QLineEdit" name="maptilerAPIKey">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Enter a Maptiler API key in order to use Maptiler maps: https://www.maptiler.com/</string>
|
<string>Enter a Maptiler API key in order to use Maptiler maps: https://www.maptiler.com/</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="osmURLLabel">
|
<widget class="QLabel" name="mapBoxAPIKeyLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>OSM Custom URL</string>
|
<string>Mapbox API Key</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QLineEdit" name="osmURL">
|
<widget class="QLineEdit" name="mapBoxAPIKey">
|
||||||
<property name="toolTip">
|
<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>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -278,6 +451,23 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</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/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<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>
|
<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:
|
On top of this, it can plot data from other plugins, such as:
|
||||||
|
|
||||||
* APRS symbols from the APRS Feature,
|
* APRS symbols from the APRS Feature,
|
||||||
* Aircraft from the ADS-B Demodulator,
|
* Aircraft from the ADS-B Demodulator,
|
||||||
* Ships from the AIS Demodulator,
|
* Ships from the AIS Demodulator,
|
||||||
* Satellites from the Satellite Tracker,
|
* Satellites from the Satellite Tracker,
|
||||||
|
* Weather imagery from APT Demodulator,
|
||||||
* The Sun, Moon and Stars from the Star Tracker,
|
* The Sun, Moon and Stars from the Star Tracker,
|
||||||
* Beacons based on the IARU Region 1 beacon database and International Beacon Project,
|
* Beacons based on the IARU Region 1 beacon database and International Beacon Project,
|
||||||
* Radio time transmitters,
|
* 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.
|
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>
|
<h2>Interface</h2>
|
||||||
|
|
||||||
@ -33,7 +38,7 @@ To centre the map on an object or location, enter:
|
|||||||
|
|
||||||
<h3>2: Map Type</h3>
|
<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).
|
selected under Display Settings (7).
|
||||||
|
|
||||||
<h3>3: Maidenhead locator conversion</h3>
|
<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>
|
<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.
|
* API keys, required to access maps from different providers.
|
||||||
|
|
||||||
Free API keys are available by signing up for an accounts with:
|
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/)
|
* [Thunderforest](https://www.thunderforest.com/)
|
||||||
* [Maptiler](https://www.maptiler.com/)
|
* [Maptiler](https://www.maptiler.com/)
|
||||||
* [Mapbox](https://www.mapbox.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.
|
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>
|
<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.
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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>
|
<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/
|
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
|
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>
|
<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:
|
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