1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2025-08-22 07:22:27 -04:00

Map: Add PFD, first person view and path smoothing. Only send changes via CZML.

This commit is contained in:
srcejon 2025-06-09 10:44:17 +01:00
parent 29f7d534e5
commit ff3b3f4ef5
21 changed files with 4249 additions and 1019 deletions

View File

@ -50,6 +50,15 @@ void CesiumInterface::setView(float latitude, float longitude, float altitude)
send(obj);
}
void CesiumInterface::setViewFirstPerson(bool firstPerson)
{
QJsonObject obj {
{"command", "setViewFirstPerson"},
{"firstPerson", firstPerson}
};
send(obj);
}
// Play glTF model animation for the map item with the specified name
void CesiumInterface::playAnimation(const QString &name, Animation *animation)
{

View File

@ -56,6 +56,7 @@ public:
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 setViewFirstPerson(bool firstPerson);
void playAnimation(const QString &name, Animation *animation);
void setDateTime(QDateTime dateTime);
void getDateTime();
@ -69,6 +70,7 @@ public:
void setHDR(bool enabled);
void setFog(bool enabled);
void showFPS(bool show);
void showPFD(bool show);
void showMUF(bool show);
void showfoF2(bool show);
void showMagDec(bool show);

View File

@ -23,6 +23,8 @@
#include "util/coordinates.h"
// FIXME: Cesium now has some additional options: CLAMP_TO_TERRAIN, RELATIVE_TO_TERRAIN, CLAMP_TO_3D_TILE, RELATIVE_TO_3D_TILE
// CLIP_TO_GROUND is our own addition
const QStringList CZML::m_heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "CLIP_TO_GROUND"};
CZML::CZML(const MapSettings *settings) :
@ -48,7 +50,8 @@ bool CZML::filter(const MapItem *mapItem) const
QJsonObject CZML::init()
{
QString start = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
// Start a few seconds in the past, to allow for data to be received
QString start = QDateTime::currentDateTimeUtc().addSecs(-4).toString(Qt::ISODate);
QString stop = QDateTime::currentDateTimeUtc().addSecs(60*60).toString(Qt::ISODate);
QString interval = QString("%1/%2").arg(start).arg(stop);
@ -241,6 +244,48 @@ QJsonObject CZML::update(PolylineMapItem *mapItem)
return obj;
}
static void insertConstantProperty(QJsonObject& properties, const QString& name, const QString& value)
{
properties.insert(name, value);
}
static void insertProperty(QJsonObject& properties, const QString& name, const QString& dateTime, float value)
{
if (!std::isnan(value))
{
QJsonArray ar;
ar.push_back(dateTime);
ar.push_back(value);
QJsonObject obj {
{"number", ar},
{"backwardExtrapolationType", "HOLD"},
{"backwardExtrapolationDuration", 30},
{"forwardExtrapolationType", "HOLD"},
{"forwardExtrapolationDuration", 30},
};
properties.insert(name, obj);
}
}
// SampledProperties are interpolated
// Need to use intervals to avoid interpolation
// See: https://sandcastle.cesium.com/?src=CZML%20Custom%20Properties.html&label=All
static void insertProperty0(QJsonObject& properties, const QString& name, const QString& dateTime, float value)
{
if (!std::isnan(value))
{
QJsonObject obj {
{"interval", dateTime + "/3000"}, // Year 3000
{"number", value}
};
QJsonArray array {
obj
};
properties.insert(name, array);
}
}
QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{
(void) isTarget;
@ -274,8 +319,9 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
// Keep a hash of the time we first saw each item
bool existingId = m_ids.contains(id);
State& state = m_ids[id];
if (!existingId) {
m_ids.insert(id, dt);
state.m_firstSeenDateTime = dt;
}
bool removeObj = false;
@ -319,13 +365,48 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
}
else
{
// Only send latest position, to reduce processing
if (!fixedPosition && mapItem->m_positionDateTime.isValid()) {
coords.push_back(mapItem->m_positionDateTime.toString(Qt::ISODateWithMs));
bool useDateTime = !fixedPosition && mapItem->m_positionDateTime.isValid();
// Update positions that have been recalculated with interpolation
if (!mapItem->m_interpolatedCoords.isEmpty())
{
for (int i = 0; i < mapItem->m_interpolatedCoords.size(); i++)
{
if (useDateTime) {
coords.push_back(mapItem->m_interpolatedDateTimes[i]->toString(Qt::ISODateWithMs));
}
coords.push_back(mapItem->m_interpolatedCoords[i]->longitude());
coords.push_back(mapItem->m_interpolatedCoords[i]->latitude());
coords.push_back(mapItem->m_interpolatedCoords[i]->altitude());
}
mapItem->m_interpolatedCoords.clear();
mapItem->m_interpolatedDateTimes.clear();
}
else
{
// Only send latest position, to reduce processing
if (!mapItem->m_takenTrackPositionExtrapolated.isEmpty() && mapItem->m_takenTrackPositionExtrapolated.back())
{
if (useDateTime) {
coords.push_back(mapItem->m_takenTrackDateTimes.back()->toString(Qt::ISODateWithMs));
}
coords.push_back(mapItem->m_takenTrackCoords.back()->longitude());
coords.push_back(mapItem->m_takenTrackCoords.back()->latitude());
}
else
{
if (useDateTime) {
coords.push_back(mapItem->m_positionDateTime.toString(Qt::ISODateWithMs));
}
coords.push_back(mapItem->m_longitude);
coords.push_back(mapItem->m_latitude);
}
if (!mapItem->m_takenTrackAltitudeExtrapolated.isEmpty() && mapItem->m_takenTrackAltitudeExtrapolated.back()) {
coords.push_back(mapItem->m_takenTrackCoords.back()->altitude());
} else {
coords.push_back(mapItem->m_altitude + mapItem->m_modelAltitudeOffset); // See nodeTransformations comment below, as to why we use this here
}
}
coords.push_back(mapItem->m_longitude);
coords.push_back(mapItem->m_latitude);
coords.push_back(mapItem->m_altitude);
}
}
else
@ -343,6 +424,7 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{
// 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;
@ -351,12 +433,16 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
if (hasMoved && (mapItem->m_itemSettings->m_extrapolate > 0))
{
position.insert("forwardExtrapolationType", "EXTRAPOLATE");
//position.insert("forwardExtrapolationType", "LINEAR_EXTRAPOLATE"); // LAGRANGE is poor for extrapolation
//position.insert("forwardExtrapolationType", "HOLD"); // Keeps at last position
position.insert("forwardExtrapolationDuration", mapItem->m_itemSettings->m_extrapolate);
// Use linear interpolation for now - other two can go crazy with aircraft on the ground
// To calc acceleration, we need to use non-linear interpolation.
position.insert("interpolationAlgorithm", "LINEAR");
position.insert("interpolationDegree", 2);
//position.insert("interpolationAlgorithm", "HERMITE");
//position.insert("interpolationDegree", "2");
//position.insert("interpolationDegree", 2);
//position.insert("interpolationAlgorithm", "LAGRANGE");
//position.insert("interpolationDegree", "5");
//position.insert("interpolationDegree", 5); // crazy interpolation for LAGRANGE
}
else
{
@ -365,11 +451,11 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
}
else
{
// Interpolation goes wrong at end points
// Interpolation goes wrong at end points. FIXME: Check if still true
//position.insert("interpolationAlgorithm", "LAGRANGE");
//position.insert("interpolationDegree", "5");
//position.insert("interpolationDegree", 5);
//position.insert("interpolationAlgorithm", "HERMITE");
//position.insert("interpolationDegree", "2");
//position.insert("interpolationDegree", 2);
}
}
@ -398,151 +484,235 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{"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}
};
// 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
// Prevent labels from being too cluttered when zoomed out
// FIXME: These values should come from mapItem or mapItemSettings
float displayDistanceMax = std::numeric_limits<float>::max();
if ((mapItem->m_group == "Beacons")
|| (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")
|| (mapItem->m_group == "NavAid")
|| (mapItem->m_group == "Waypoints")
) {
displayDistanceMax = 1000000;
} else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) {
displayDistanceMax = 10000000;
} else if (mapItem->m_group == "Ionosonde Stations") {
displayDistanceMax = 30000000;
}
QJsonArray labelPixelOffsetScaleArray {
1000000, 20, 10000000, 5
};
QJsonObject labelPixelOffsetScaleObject {
{"nearFarScalar", labelPixelOffsetScaleArray}
};
QJsonArray labelPixelOffsetArray {
1, 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}
};
QString labelText = mapItem->m_label;
labelText.replace("<br>", "\n");
QJsonObject label {
{"text", labelText},
{"show", m_settings->m_displayNames && mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DLabel},
{"scale", mapItem->m_itemSettings->m_3DLabelScale},
{"pixelOffset", labelPixelOffset},
{"pixelOffsetScaleByDistance", labelPixelOffsetScaleObject},
{"eyeOffset", labelEyeOffset},
{"verticalOrigin", "BASELINE"},
{"horizontalOrigin", "LEFT"},
{"heightReference", heightReferences[mapItem->m_altitudeReference]},
};
if (displayDistanceMax != std::numeric_limits<float>::max()) {
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 (!removeObj)
{
if (mapItem->m_aircraftState)
{
QJsonObject properties;
QDateTime aircraftStateDateTime = mapItem->m_positionDateTime;
QString dateTime = aircraftStateDateTime.toString(Qt::ISODateWithMs);
bool aircraftStateDateTimeChanged = aircraftStateDateTime != state.m_aircraftStateDateTime;
QString aircraftCallsign = mapItem->m_aircraftState->m_callsign;
QString aircraftType = mapItem->m_aircraftState->m_aircraftType;
QString aircraftIndicatedAirspeedDateTime = mapItem->m_aircraftState->m_indicatedAirspeedDateTime;
float aircraftIndicatedAirspeed = mapItem->m_aircraftState->m_indicatedAirspeed;
float aircraftTrueAirspeed = mapItem->m_aircraftState->m_trueAirspeed;
float aircraftGroundspeed = mapItem->m_aircraftState->m_groundspeed;
QString aircraftAltitudeDateTime = mapItem->m_aircraftState->m_altitudeDateTime;
float aircraftAltitude = mapItem->m_aircraftState->m_altitude;
int aircraftOnSurface = mapItem->m_aircraftState->m_onSurface;
float aircraftMach = mapItem->m_aircraftState->m_mach;
float aircraftQNH = mapItem->m_aircraftState->m_qnh;
float aircraftVerticalSpeed = mapItem->m_aircraftState->m_verticalSpeed;
float aircraftHeading = mapItem->m_aircraftState->m_heading;
float aircraftTrack = mapItem->m_aircraftState->m_track;
float aircraftRoll = mapItem->m_roll;
float aircraftSelectedAltitude = mapItem->m_aircraftState->m_selectedAltitude;
float aircraftSelectedHeading = mapItem->m_aircraftState->m_selectedHeading;
int aircraftAutopilot = mapItem->m_aircraftState->m_autopilot;
MapAircraftState::VerticalMode aircraftVerticalMode = mapItem->m_aircraftState->m_verticalMode;
MapAircraftState::LateralMode aircraftLateralMode = mapItem->m_aircraftState->m_lateralMode;
MapAircraftState::TCASMode aircraftTCASMode = mapItem->m_aircraftState->m_tcasMode;
float aircraftWindSpeed = mapItem->m_aircraftState->m_windSpeed;
float aircraftWindDirection = mapItem->m_aircraftState->m_windDirection;
float aircraftStaticAirTemperature = mapItem->m_aircraftState->m_staticAirTemperature;
if ( (!existingId && !aircraftCallsign.isEmpty())
|| (aircraftCallsign != state.m_aircraftState.m_callsign)
)
{
insertConstantProperty(properties, "pfdCallsign", aircraftCallsign);
state.m_aircraftState.m_callsign = aircraftCallsign;
}
if ( (!existingId && !aircraftType.isEmpty())
|| (aircraftType != state.m_aircraftState.m_aircraftType)
)
{
insertConstantProperty(properties, "pfdAircraftType", aircraftType);
state.m_aircraftState.m_aircraftType = aircraftType;
}
if ( (!existingId && !aircraftIndicatedAirspeedDateTime.isEmpty())
|| (aircraftIndicatedAirspeedDateTime != state.m_aircraftState.m_indicatedAirspeedDateTime)
|| (aircraftIndicatedAirspeed != state.m_aircraftState.m_indicatedAirspeed)
)
{
insertProperty(properties, "pfdIndicatedAirspeed", aircraftIndicatedAirspeedDateTime, aircraftIndicatedAirspeed);
state.m_aircraftState.m_indicatedAirspeedDateTime = aircraftIndicatedAirspeedDateTime;
state.m_aircraftState.m_indicatedAirspeed = aircraftIndicatedAirspeed;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftTrueAirspeed != state.m_aircraftState.m_trueAirspeed)
)
{
insertProperty(properties, "pfdTrueAirspeed", dateTime, aircraftTrueAirspeed);
state.m_aircraftState.m_trueAirspeed = aircraftTrueAirspeed;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftGroundspeed != state.m_aircraftState.m_groundspeed)
)
{
insertProperty(properties, "pfdGroundspeed", dateTime, aircraftGroundspeed);
state.m_aircraftState.m_groundspeed = aircraftGroundspeed;
}
if ( (!existingId && !aircraftAltitudeDateTime.isEmpty())
|| (aircraftAltitudeDateTime != state.m_aircraftState.m_altitudeDateTime)
|| (aircraftAltitude != state.m_aircraftState.m_altitude)
)
{
insertProperty(properties, "pfdAltitude", aircraftAltitudeDateTime, aircraftAltitude);
state.m_aircraftState.m_altitudeDateTime = aircraftAltitudeDateTime;
state.m_aircraftState.m_altitude = aircraftAltitude;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftOnSurface != state.m_aircraftState.m_onSurface)
)
{
insertProperty0(properties, "pfdOnSurface", dateTime, aircraftOnSurface);
state.m_aircraftState.m_onSurface = aircraftOnSurface;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftMach != state.m_aircraftState.m_mach)
)
{
insertProperty(properties, "pfdMach", dateTime, aircraftMach);
state.m_aircraftState.m_mach = aircraftMach;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftQNH != state.m_aircraftState.m_qnh)
)
{
insertProperty0(properties, "pfdQNH", dateTime, aircraftQNH);
state.m_aircraftState.m_qnh = aircraftQNH;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftVerticalSpeed != state.m_aircraftState.m_verticalSpeed)
)
{
insertProperty(properties, "pfdVerticalSpeed", dateTime, aircraftVerticalSpeed);
state.m_aircraftState.m_verticalSpeed = aircraftVerticalSpeed;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftHeading != state.m_aircraftState.m_heading)
)
{
insertProperty(properties, "pfdHeading", dateTime, aircraftHeading);
state.m_aircraftState.m_heading = aircraftHeading;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftTrack != state.m_aircraftState.m_track)
)
{
insertProperty(properties, "pfdTrack", dateTime, aircraftTrack);
state.m_aircraftState.m_track = aircraftTrack;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftRoll != state.m_aircraftRoll)
)
{
insertProperty(properties, "pfdRoll", dateTime, aircraftRoll);
state.m_aircraftRoll = aircraftRoll;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftSelectedAltitude != state.m_aircraftState.m_selectedAltitude)
)
{
insertProperty0(properties, "pfdSelectedAltitude", dateTime, aircraftSelectedAltitude);
state.m_aircraftState.m_selectedAltitude = aircraftSelectedAltitude;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftSelectedHeading != state.m_aircraftState.m_selectedHeading)
)
{
insertProperty0(properties, "pfdSelectedHeading", dateTime, aircraftSelectedHeading);
state.m_aircraftState.m_selectedHeading = aircraftSelectedHeading;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftAutopilot != state.m_aircraftState.m_autopilot)
)
{
insertProperty0(properties, "pfdAutopilot", dateTime, aircraftAutopilot);
state.m_aircraftState.m_autopilot = aircraftAutopilot;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftVerticalMode != state.m_aircraftState.m_verticalMode)
)
{
insertProperty0(properties, "pfdVerticalMode", dateTime, aircraftVerticalMode);
state.m_aircraftState.m_verticalMode = aircraftVerticalMode;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftLateralMode != state.m_aircraftState.m_lateralMode)
)
{
insertProperty0(properties, "pfdLateralMode", dateTime, aircraftLateralMode);
state.m_aircraftState.m_lateralMode = aircraftLateralMode;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftTCASMode != state.m_aircraftState.m_tcasMode)
)
{
insertProperty0(properties, "pfdTCASMode", dateTime, aircraftTCASMode);
state.m_aircraftState.m_tcasMode = aircraftTCASMode;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftWindSpeed != state.m_aircraftState.m_windSpeed)
)
{
insertProperty0(properties, "pfdWindSpeed", dateTime, aircraftWindSpeed);
state.m_aircraftState.m_windSpeed = aircraftWindSpeed;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftWindDirection != state.m_aircraftState.m_windDirection)
)
{
insertProperty0(properties, "pfdWindDirection", dateTime, aircraftWindDirection);
state.m_aircraftState.m_windDirection = aircraftWindDirection;
}
if ( !existingId
|| aircraftStateDateTimeChanged
|| (aircraftStaticAirTemperature != state.m_aircraftState.m_staticAirTemperature)
)
{
insertProperty0(properties, "pfdStaticAirTemperature", dateTime, aircraftStaticAirTemperature);
state.m_aircraftState.m_staticAirTemperature = aircraftStaticAirTemperature;
}
//QJsonObject speedProperty {
// {"velocityReference", "#position"},
//};
//properties.insert("pfdSpeed", speedProperty);
if ( !existingId
|| aircraftStateDateTimeChanged
)
{
state.m_aircraftStateDateTime = aircraftStateDateTime;
}
if (properties.size() > 0) {
obj.insert("properties", properties);
}
}
obj.insert("position", position);
if (!fixedPosition)
{
@ -552,16 +722,260 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
obj.insert("orientation", orientationPosition);
}
}
obj.insert("point", point);
if (!mapItem->m_model.isEmpty()) {
obj.insert("model", model);
} else {
// Point
quint32 pointColorInt = mapItem->m_itemSettings->m_3DPointColor;
int pointAltitudeReference = mapItem->m_altitudeReference;
bool pointShow = mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DPoint;
if ( !existingId
|| (pointColorInt != state.m_pointColorInt)
|| (pointAltitudeReference != state.m_pointAltitudeReference)
|| (pointShow != state.m_pointShow)
)
{
QColor pointColor = QColor::fromRgba(pointColorInt);
QJsonArray pointRGBA {
pointColor.red(), pointColor.green(), pointColor.blue(), pointColor.alpha()
};
QJsonObject pointColorObj {
{"rgba", pointRGBA}
};
QJsonObject point {
{"pixelSize", 8},
{"color", pointColorObj},
{"heightReference", heightReferences[pointAltitudeReference]},
{"show", pointShow}
};
obj.insert("point", point);
state.m_pointColorInt = pointColorInt;
state.m_pointAltitudeReference = pointAltitudeReference;
state.m_pointShow = pointShow;
}
// Label
float labelAltitudeOffset = mapItem->m_labelAltitudeOffset;
QString labelText = mapItem->m_label;
bool labelShow = m_settings->m_displayNames && mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DLabel;
float labelScale = mapItem->m_itemSettings->m_3DLabelScale;
int labelAltitudeReference = mapItem->m_altitudeReference;
if ( !existingId
|| (labelAltitudeOffset != state.m_lableAltitudeOffset)
|| (labelText != state.m_labelText)
|| (labelShow != state.m_labelShow)
|| (labelScale != state.m_labelScale)
|| (labelAltitudeReference != state.m_labelAltitudeReference)
)
{
// Prevent labels from being too cluttered when zoomed out
// FIXME: These values should come from mapItem or mapItemSettings
float displayDistanceMax = std::numeric_limits<float>::max();
if ((mapItem->m_group == "Beacons")
|| (mapItem->m_group == "AM") || (mapItem->m_group == "FM") || (mapItem->m_group == "DAB")
|| (mapItem->m_group == "NavAid")
|| (mapItem->m_group == "Waypoints")
) {
displayDistanceMax = 1000000;
} else if ((mapItem->m_group == "Station") || (mapItem->m_group == "Radar") || (mapItem->m_group == "Radio Time Transmitters")) {
displayDistanceMax = 10000000;
} else if (mapItem->m_group == "Ionosonde Stations") {
displayDistanceMax = 30000000;
}
QJsonArray labelPixelOffsetScaleArray {
1000000, 20, 10000000, 5
};
QJsonObject labelPixelOffsetScaleObject {
{"nearFarScalar", labelPixelOffsetScaleArray}
};
QJsonArray labelPixelOffsetArray {
1, 0
};
QJsonObject labelPixelOffset {
{"cartesian2", labelPixelOffsetArray}
};
QJsonArray labelEyeOffsetArray {
0, 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}
};
labelText.replace("<br>", "\n");
QJsonObject label {
//{"text", labelText},
{"show", labelShow},
{"scale", labelScale},
{"pixelOffset", labelPixelOffset},
{"pixelOffsetScaleByDistance", labelPixelOffsetScaleObject},
{"eyeOffset", labelEyeOffset},
{"verticalOrigin", "BASELINE"},
{"horizontalOrigin", "LEFT"},
{"heightReference", heightReferences[labelAltitudeReference]},
{"style", "FILL_AND_OUTLINE"},
};
if (!mapItem->m_labelDateTime.isValid())
{
label.insert("text", labelText);
}
else
{
QString interval = mapItem->m_labelDateTime.toString(Qt::ISODateWithMs) + "/2999-12-31";
QJsonObject labelInterval {
{"interval", interval},
{"string", labelText}
};
QJsonArray labelIntervalArray {
labelInterval
};
label.insert("text", labelIntervalArray);
}
if (displayDistanceMax != std::numeric_limits<float>::max()) {
label.insert("distanceDisplayCondition", labelDistanceDisplayCondition);
}
obj.insert("label", label);
state.m_lableAltitudeOffset = labelAltitudeOffset;
state.m_labelText = labelText;
state.m_labelShow = labelShow;
state.m_labelScale = labelScale;
state.m_labelAltitudeReference = labelAltitudeReference;
}
if (!mapItem->m_model.isEmpty())
{
// Model
QString modelGLTF = m_settings->m_modelURL + mapItem->m_model;
int modelAltitudeReference = mapItem->m_altitudeReference;
bool modelShow = mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DModel;
int modelMinimumPixelSize = mapItem->m_itemSettings->m_3DModelMinPixelSize;
if ( !existingId
|| (modelGLTF != state.m_modelGLTF)
|| (modelAltitudeReference != state.m_modelAltitudeReference)
|| (modelShow != state.m_modelShow)
|| (modelMinimumPixelSize != state.m_modelMinimumPixelSize)
)
{
QJsonObject model {
{"gltf", modelGLTF},
//{"incrementallyLoadTextures", false}, // Aircraft will flash as they appear without textures if this is the default of true
{"heightReference", heightReferences[modelAltitudeReference]},
{"runAnimations", false},
{"show", modelShow},
{"minimumPixelSize", modelMinimumPixelSize},
{"maximumScale", 20000} // Stop it getting too big when zoomed really far out
};
// Using nodeTransformations stops animations from running.
// See: https://github.com/CesiumGS/cesium/issues/11566
/*QJsonArray node0Cartesian {
{0.0, mapItem->m_modelAltitudeOffset, 0.0}
};
QJsonObject node0Translation {
{"cartesian", node0Cartesian}
};
QJsonObject node0Transform {
{"translation", node0Translation}
};
QJsonObject nodeTransforms {
{"node0", node0Transform},
};
if (mapItem->m_modelAltitudeOffset != 0.0) {
model.insert("nodeTransformations", nodeTransforms);
}*/
obj.insert("model", model);
state.m_modelGLTF = modelGLTF;
state.m_modelAltitudeReference = modelAltitudeReference;
state.m_modelShow = modelShow;
state.m_modelMinimumPixelSize = modelMinimumPixelSize;
}
}
else
{
// 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
};
obj.insert("billboard", billboard);
}
obj.insert("label", label);
obj.insert("description", mapItem->m_text);
if (!fixedPosition) {
obj.insert("path", path);
// Description
QString description = mapItem->m_text;
if ( !existingId
|| (description != state.m_description)
)
{
obj.insert("description", description);
state.m_description = description;
}
// Path
if (!fixedPosition)
{
quint32 pathColorInt = mapItem->m_itemSettings->m_3DTrackColor;
bool pathShow = mapItem->m_itemSettings->m_enabled
&& mapItem->m_itemSettings->m_display3DTrack
&& ( m_settings->m_displayAllGroundTracks
|| (m_settings->m_displaySelectedGroundTracks && isSelected));
if ( !existingId
|| (pathColorInt != state.m_pathColorInt)
|| (pathShow != state.m_pathShow)
)
{
QColor pathColor = QColor::fromRgba(pathColorInt);
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}
};
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", pathShow},
{"resolution", 1}
};
obj.insert("path", path);
state.m_pathColorInt = pathColorInt;
state.m_pathShow = pathShow;
}
}
if (!fixedPosition)
@ -577,7 +991,7 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{
if (mapItem->m_availableUntil.isValid())
{
QString period = QString("%1/%2").arg(m_ids[id]).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs));
QString period = QString("%1/%2").arg(state.m_firstSeenDateTime).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs));
obj.insert("availability", period);
}
}
@ -586,7 +1000,7 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{
if (mapItem->m_availableUntil.isValid())
{
QString period = QString("%1/%2").arg(m_ids[id]).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs));
QString period = QString("%1/%2").arg(state.m_firstSeenDateTime).arg(mapItem->m_availableUntil.toString(Qt::ISODateWithMs));
obj.insert("availability", period);
}
}
@ -599,13 +1013,24 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
}
// 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");
}
// https://github.com/CesiumGS/cesium/issues/4049 - Has now been fixed
//if (mapItem->m_altitudeReference == 3) {
// obj.insert("altitudeReference", "CLIP_TO_GROUND");
//}
//qDebug() << obj;
/*if (id == "400b00")
{
QJsonDocument doc(obj);
if (!m_file.isOpen())
{
m_file.setFileName(QString("%1.czml").arg(id));
m_file.open(QIODeviceBase::WriteOnly);
}
m_file.write(doc.toJson());
m_file.write(",\n");
}*/
return obj;
}

View File

@ -24,10 +24,13 @@
#define INCLUDE_FEATURE_CZML_H_
#include <QHash>
#include <QFile>
#include <QJsonArray>
#include <QJsonObject>
#include <QGeoCoordinate>
#include "mapaircraftstate.h"
struct MapSettings;
class MapItem;
class ObjectMapItem;
@ -36,13 +39,59 @@ class PolylineMapItem;
class CZML
{
// Record previous state of object, so we only send changes in state
struct State {
QString m_firstSeenDateTime;
QString m_modelGLTF;
int m_modelAltitudeReference;
bool m_modelShow;
int m_modelMinimumPixelSize;
quint32 m_pointColorInt;
int m_pointAltitudeReference;
bool m_pointShow;
float m_lableAltitudeOffset;
QString m_labelText;
bool m_labelShow;
float m_labelScale;
int m_labelAltitudeReference;
quint32 m_pathColorInt;
bool m_pathShow;
QString m_description;
QDateTime m_aircraftStateDateTime;
MapAircraftState m_aircraftState;
float m_aircraftRoll;
State() :
m_modelAltitudeReference(0),
m_modelShow(false),
m_modelMinimumPixelSize(0),
m_pointColorInt(0),
m_pointAltitudeReference(0),
m_pointShow(false),
m_lableAltitudeOffset(0.0),
m_labelShow(false),
m_labelScale(0.0),
m_labelAltitudeReference(0),
m_pathColorInt(0),
m_pathShow(false)
{ }
};
private:
const MapSettings *m_settings;
QHash<QString, QString> m_ids;
QHash<QString, State> m_ids;
QHash<QString, QJsonArray> m_lastPosition;
QHash<QString, bool> m_hasMoved;
QGeoCoordinate m_position;
static const QStringList m_heightReferences;
QFile m_file;
QFile m_csvFile;
public:
CZML(const MapSettings *settings);

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2025 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// 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_MAPAIRCRAFTSTATE_H_
#define INCLUDE_FEATURE_MAPAIRCRAFTSTATE_H_
struct MapAircraftState {
QString m_callsign;
QString m_aircraftType;
int m_onSurface; // -1 for unknown
float m_indicatedAirspeed; // NaN for unknown
QString m_indicatedAirspeedDateTime;
float m_trueAirspeed;
float m_groundspeed;
float m_mach;
float m_altitude;
QString m_altitudeDateTime;
float m_qnh;
float m_verticalSpeed;
float m_heading;
float m_track;
float m_selectedAltitude;
float m_selectedHeading;
int m_autopilot; // -1 for unknown
enum VerticalMode {
UNKNOWN_VERTICAL_MODE,
VNAV,
ALT_HOLD,
GS
} m_verticalMode;
enum LateralMode {
UNKNOWN_LATERAL_MODE,
LNAV,
LOC
} m_lateralMode;
enum TCASMode {
UNKNOWN_TCAS_MODE,
TCAS_OFF,
TA,
TA_RA
} m_tcasMode;
float m_windSpeed;
float m_windDirection;
float m_staticAirTemperature;
};
#endif // INCLUDE_FEATURE_MAPAIRCRAFTSTATE_H_

View File

@ -1929,7 +1929,6 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
ui->web->load(QUrl(QString("http://127.0.0.1:%1/map/map/map3d.html").arg(m_webPort)));
//ui->web->load(QUrl(QString("http://webglreport.com/")));
//ui->web->load(QUrl(QString("https://sandcastle.cesium.com/")));
//ui->web->load(QUrl("chrome://gpu/"));
ui->web->show();
}
else if (!m_settings.m_map3DEnabled && (m_cesium != nullptr))
@ -1948,9 +1947,11 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
m_cesium->setCameraReferenceFrame(m_settings.m_eciCamera);
m_cesium->setAntiAliasing(m_settings.m_fxaa, m_settings.m_msaa);
m_cesium->getDateTime();
m_cesium->setViewFirstPerson(m_settings.m_viewFirstPerson);
m_cesium->setHDR(m_settings.m_hdr);
m_cesium->setFog(m_settings.m_fog);
m_cesium->showFPS(m_settings.m_fps);
m_cesium->showPFD(m_settings.m_displayPFD);
m_cesium->showMUF(m_settings.m_displayMUF);
m_cesium->showfoF2(m_settings.m_displayfoF2);
m_cesium->showMagDec(m_settings.m_displayMagDec);
@ -1968,9 +1969,12 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
m_polylineMapModel.allUpdated();
}
MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations");
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (ionosondeItemSettings) {
m_giro->getDataPeriodically(ionosondeItemSettings->m_enabled ? 2 : 0);
if (m_giro)
{
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (ionosondeItemSettings) {
m_giro->getDataPeriodically(ionosondeItemSettings->m_enabled ? 2 : 0);
}
}
if (m_aurora) {
m_aurora->getDataPeriodically(m_settings.m_displayAurora ? 30 : 0);
@ -2084,6 +2088,8 @@ void MapGUI::displaySettings()
setTitle(m_settings.m_title);
blockApplySettings(true);
ui->displayNames->setChecked(m_settings.m_displayNames);
ui->viewFirstPerson->setChecked(m_settings.m_viewFirstPerson);
ui->displayPFD->setChecked(m_settings.m_displayPFD);
ui->displaySelectedGroundTracks->setChecked(m_settings.m_displaySelectedGroundTracks);
ui->displayAllGroundTracks->setChecked(m_settings.m_displayAllGroundTracks);
ui->displayRain->setChecked(m_settings.m_displayRain);
@ -2202,6 +2208,24 @@ void MapGUI::on_maidenhead_clicked()
dialog.exec();
}
void MapGUI::on_viewFirstPerson_clicked(bool checked)
{
m_settings.m_viewFirstPerson = checked;
if (m_cesium) {
m_cesium->setViewFirstPerson(checked);
}
applySetting("viewFirstPerson");
}
void MapGUI::on_displayPFD_clicked(bool checked)
{
m_settings.m_displayPFD = checked;
if (m_cesium) {
m_cesium->showPFD(checked);
}
applySetting("viewFirstPerson");
}
void MapGUI::on_displayNames_clicked(bool checked)
{
m_settings.m_displayNames = checked;
@ -2415,10 +2439,10 @@ void MapGUI::on_displayMUF_clicked(bool checked)
m_displayMUF->setChecked(checked);
}
m_settings.m_displayMUF = checked;
// Only call show if disabling, so we don't get two updates
// (as getMUFPeriodically results in a call to showMUF when the data is available)
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (m_cesium && !m_settings.m_displayMUF) {
if (m_giro) {
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
}
if (m_cesium) {
m_cesium->showMUF(m_settings.m_displayMUF);
}
applySetting("displayMUF");
@ -2433,8 +2457,10 @@ void MapGUI::on_displayfoF2_clicked(bool checked)
m_displayfoF2->setChecked(checked);
}
m_settings.m_displayfoF2 = checked;
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (m_cesium && !m_settings.m_displayfoF2) {
if (m_giro) {
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
}
if (m_cesium) {
m_cesium->showfoF2(m_settings.m_displayfoF2);
}
applySetting("displayfoF2");
@ -3009,6 +3035,8 @@ void MapGUI::preferenceChanged(int elementType)
void MapGUI::makeUIConnections()
{
QObject::connect(ui->displayNames, &ButtonSwitch::clicked, this, &MapGUI::on_displayNames_clicked);
QObject::connect(ui->viewFirstPerson, &ButtonSwitch::clicked, this, &MapGUI::on_viewFirstPerson_clicked);
QObject::connect(ui->displayPFD, &ButtonSwitch::clicked, this, &MapGUI::on_displayPFD_clicked);
QObject::connect(ui->displayAllGroundTracks, &ButtonSwitch::clicked, this, &MapGUI::on_displayAllGroundTracks_clicked);
QObject::connect(ui->displaySelectedGroundTracks, &ButtonSwitch::clicked, this, &MapGUI::on_displaySelectedGroundTracks_clicked);
QObject::connect(ui->displayRain, &ButtonSwitch::clicked, this, &MapGUI::on_displayRain_clicked);

View File

@ -326,6 +326,8 @@ private slots:
void onMenuDialogCalled(const QPoint &p);
void onWidgetRolled(QWidget* widget, bool rollDown);
void handleInputMessages();
void on_viewFirstPerson_clicked(bool checked=false);
void on_displayPFD_clicked(bool checked=false);
void on_displayNames_clicked(bool checked=false);
void on_displayAllGroundTracks_clicked(bool checked=false);
void on_displaySelectedGroundTracks_clicked(bool checked=false);

View File

@ -29,7 +29,7 @@
</font>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="windowTitle">
<string>Map</string>
@ -39,7 +39,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1221</width>
<width>1251</width>
<height>41</height>
</rect>
</property>
@ -166,7 +166,7 @@
<string>IBP</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/ibp.png</normaloff>:/map/icons/ibp.png</iconset>
</property>
</widget>
@ -180,7 +180,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/clock.png</normaloff>:/map/icons/clock.png</iconset>
</property>
</widget>
@ -191,11 +191,11 @@
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/layers.png</normaloff>:/map/icons/layers.png</iconset>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
<enum>QToolButton::ToolButtonPopupMode::InstantPopup</enum>
</property>
</widget>
</item>
@ -205,10 +205,10 @@
<string>Display satellite infra-red (clouds)</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/cloud.png</normaloff>:/map/icons/cloud.png</iconset>
</property>
<property name="checkable">
@ -225,10 +225,10 @@
<string>Display weather radar (rain/snow)</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/precipitation.png</normaloff>:/map/icons/precipitation.png</iconset>
</property>
<property name="checkable">
@ -245,10 +245,10 @@
<string>Display sea marks</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/anchor.png</normaloff>:/map/icons/anchor.png</iconset>
</property>
<property name="checkable">
@ -265,10 +265,10 @@
<string>Display railways</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/railway.png</normaloff>:/map/icons/railway.png</iconset>
</property>
<property name="checkable">
@ -285,10 +285,10 @@
<string>Display MUF (Maximum Usable Frequency) contours (3D only)</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/muf.png</normaloff>:/map/icons/muf.png</iconset>
</property>
<property name="checkable">
@ -305,10 +305,10 @@
<string>Display foF2 (F2 layer critical frequency) contours (3D only)</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/fof2.png</normaloff>:/map/icons/fof2.png</iconset>
</property>
<property name="checkable">
@ -365,10 +365,10 @@
<string>Display NASA GIBS data</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/earthsat.png</normaloff>:/map/icons/earthsat.png</iconset>
</property>
<property name="checkable">
@ -430,7 +430,7 @@
<string>Display names</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
@ -444,13 +444,54 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="viewFirstPerson">
<property name="toolTip">
<string>First person / third person view on 3D map</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/thirdperson.png</normaloff>
<normalon>:/map/icons/firstperson.png</normalon>:/map/icons/thirdperson.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displayPFD">
<property name="toolTip">
<string>Display aircraft PFD (Primary Flight Display) on 3D map</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/pfd.png</normaloff>:/map/icons/pfd.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="displaySelectedGroundTracks">
<property name="toolTip">
<string>Display ground tracks for selected item</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
@ -470,10 +511,10 @@
<string>Display all ground tracks</string>
</property>
<property name="text">
<string>^</string>
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/groundtracks.png</normaloff>:/map/icons/groundtracks.png</iconset>
</property>
<property name="checkable">
@ -573,7 +614,7 @@
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<widget class="QQuickWidget" name="map">
<property name="sizePolicy">
@ -592,7 +633,7 @@
<string>Map</string>
</property>
<property name="resizeMode">
<enum>QQuickWidget::SizeRootObjectToView</enum>
<enum>QQuickWidget::ResizeMode::SizeRootObjectToView</enum>
</property>
<property name="source">
<url>
@ -655,7 +696,7 @@
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
<include location="icons.qrc"/>
<include location="mapicons.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -15,6 +15,7 @@
<file>icons/waypoints.png</file>
<file>icons/earthsat.png</file>
<file>icons/aurora.png</file>
<file>icons/pfd.png</file>
<file>icons/compass.png</file>
<file>icons/grid.png</file>
<file>icons/thirdperson.png</file>

View File

@ -34,6 +34,11 @@ void MapItem::update(SWGSDRangel::SWGMapItem *mapItem)
} else {
m_label = "";
}
if (mapItem->getLabelDateTime()) {
m_labelDateTime = QDateTime::fromString(*mapItem->getLabelDateTime(), Qt::ISODateWithMs);
} else {
m_labelDateTime = QDateTime();
}
m_latitude = mapItem->getLatitude();
m_longitude = mapItem->getLongitude();
m_altitude = mapItem->getAltitude();
@ -47,6 +52,8 @@ QGeoCoordinate MapItem::getCoordinates()
return coords;
}
WhittakerEilers ObjectMapItem::m_filter;
void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
{
MapItem::update(mapItem);
@ -55,6 +62,11 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
} else {
m_positionDateTime = QDateTime();
}
if (mapItem->getAltitudeDateTime()) {
m_altitudeDateTime = QDateTime::fromString(*mapItem->getAltitudeDateTime(), Qt::ISODateWithMs);
} else {
m_altitudeDateTime = QDateTime();
}
m_useHeadingPitchRoll = mapItem->getOrientation() == 1;
m_heading = mapItem->getHeading();
m_pitch = mapItem->getPitch();
@ -79,6 +91,14 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
}
m_labelAltitudeOffset = mapItem->getLabelAltitudeOffset();
m_modelAltitudeOffset = mapItem->getModelAltitudeOffset();
// FIXME: See nodeTransformations comment in czml.cpp
// We can't use nodeTransformations, so adjust altitude instead
if (m_modelAltitudeOffset != 0)
{
m_labelAltitudeOffset -= m_modelAltitudeOffset;
m_altitude += m_modelAltitudeOffset;
m_modelAltitudeOffset = 0;
}
m_altitudeReference = mapItem->getAltitudeReference();
m_fixedPosition = mapItem->getFixedPosition();
QList<SWGSDRangel::SWGMapAnimation *> *animations = mapItem->getAnimations();
@ -91,7 +111,7 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
findFrequencies();
if (!m_fixedPosition)
{
updateTrack(mapItem->getTrack());
updateTrack(mapItem->getTrack(), m_itemSettings);
updatePredictedTrack(mapItem->getPredictedTrack());
}
if (mapItem->getAvailableFrom()) {
@ -104,6 +124,47 @@ void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
} else {
m_availableUntil = QDateTime();
}
if (mapItem->getAircraftState()) {
if (!m_aircraftState) {
m_aircraftState = new MapAircraftState();
}
SWGSDRangel::SWGMapAircraftState *as = mapItem->getAircraftState();
if (as->getCallsign()) {
m_aircraftState->m_callsign = *as->getCallsign();
}
if (as->getAircraftType()) {
m_aircraftState->m_aircraftType = *as->getAircraftType();
}
m_aircraftState->m_onSurface = as->getOnSurface();
m_aircraftState->m_indicatedAirspeed = as->getAirspeed();
if (as->getAirspeedDateTime()) {
m_aircraftState->m_indicatedAirspeedDateTime = *as->getAirspeedDateTime();
} else {
m_aircraftState->m_indicatedAirspeedDateTime = QString();
}
m_aircraftState->m_trueAirspeed = as->getTrueAirspeed();
m_aircraftState->m_groundspeed = as->getGroundspeed();
m_aircraftState->m_mach = as->getMach();
m_aircraftState->m_altitude = as->getAltitude();
if (as->getAltitudeDateTime()) {
m_aircraftState->m_altitudeDateTime = *as->getAltitudeDateTime();
} else {
m_aircraftState->m_altitudeDateTime = QString();
}
m_aircraftState->m_qnh = as->getQnh();
m_aircraftState->m_verticalSpeed = as->getVerticalSpeed();
m_aircraftState->m_heading = as->getHeading();
m_aircraftState->m_track = as->getTrack();
m_aircraftState->m_selectedAltitude = as->getSelectedAltitude();
m_aircraftState->m_selectedHeading = as->getSelectedHeading();
m_aircraftState->m_autopilot = as->getAutopilot();
m_aircraftState->m_verticalMode = (MapAircraftState::VerticalMode) as->getVerticalMode();
m_aircraftState->m_lateralMode = (MapAircraftState::LateralMode) as->getLateralMode();
m_aircraftState->m_tcasMode = (MapAircraftState::TCASMode) as->getTcasMode();
m_aircraftState->m_windSpeed = as->getWindSpeed();
m_aircraftState->m_windDirection = as->getWindDirection();
m_aircraftState->m_staticAirTemperature = as->getStaticAirTemperature();
}
}
void ImageMapItem::update(SWGSDRangel::SWGMapItem *mapItem)
@ -225,7 +286,145 @@ void ObjectMapItem::findFrequencies()
}
}
void ObjectMapItem::updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track)
void ObjectMapItem::extrapolatePosition(QGeoCoordinate *c, const QDateTime& dateTime)
{
int p1;
int p2;
// Find last two non extrapolated position
for (p2 = m_takenTrackPositionExtrapolated.size() - 1 ; p2 >= 0; p2--)
{
if (!m_takenTrackPositionExtrapolated[p2]) {
break;
}
}
for (p1 = p2 - 1 ; p1 >= 0; p1--)
{
if (!m_takenTrackPositionExtrapolated[p1]) {
break;
}
}
if (p1 < 0) {
return;
}
qint64 t1 = m_takenTrackDateTimes[p1]->msecsTo(*m_takenTrackDateTimes[p2]);
qint64 t2 = m_takenTrackDateTimes[p2]->msecsTo(dateTime);
double latV = (m_takenTrackCoords[p2]->latitude() - m_takenTrackCoords[p1]->latitude()) / t1;
double lonV = (m_takenTrackCoords[p2]->longitude() - m_takenTrackCoords[p1]->longitude()) / t1;
double newLat = m_takenTrackCoords[p2]->latitude() + latV * t2;
double newLon = m_takenTrackCoords[p2]->longitude() + lonV * t2;
c->setLatitude(newLat);
c->setLongitude(newLon);
}
void ObjectMapItem::extrapolateAltitude(QGeoCoordinate *c, const QDateTime& dateTime)
{
int p1;
int p2;
// Find last two non extrapolated position
for (p2 = m_takenTrackPositionExtrapolated.size() - 1 ; p2 >= 0; p2--)
{
if (!m_takenTrackPositionExtrapolated[p2]) {
break;
}
}
for (p1 = p2 - 1 ; p1 >= 0; p1--)
{
if (!m_takenTrackPositionExtrapolated[p1]) {
break;
}
}
if (p1 < 0) {
return;
}
qint64 t1 = m_takenTrackDateTimes[p1]->msecsTo(*m_takenTrackDateTimes[p2]);
qint64 t2 = m_takenTrackDateTimes[p2]->msecsTo(dateTime);
double vertV = (m_takenTrackCoords[p2]->altitude() - m_takenTrackCoords[p1]->altitude()) / t1;
double newAlt = m_takenTrackCoords[p2]->latitude() + vertV * t2;
c->setAltitude(newAlt);
}
void ObjectMapItem::interpolatePosition(int p2, const float p3Latitude, const float p3Longitude, const QDateTime &p3DateTime)
{
// p1 last non extrapolated position
// p2 interpolated position
// p3 current non extrapolated position
// Find last non extrapolated position
int p1;
for (p1 = p2 - 1; p1 >= 0; p1--)
{
if (!m_takenTrackPositionExtrapolated[p1]) {
break;
}
}
if (p1 < 0) {
return;
}
qint64 t1 = m_takenTrackDateTimes[p1]->msecsTo(p3DateTime);
qint64 t2 = m_takenTrackDateTimes[p1]->msecsTo(*m_takenTrackDateTimes[p2]);
double latV = (p3Latitude - m_takenTrackCoords[p1]->latitude()) / t1;
double lonV = (p3Longitude - m_takenTrackCoords[p1]->longitude()) / t1;
double newLat = m_takenTrackCoords[p1]->latitude() + latV * t2;
double newLon = m_takenTrackCoords[p1]->longitude() + lonV * t2;
m_takenTrackCoords[p2]->setLatitude(newLat);
m_takenTrackCoords[p2]->setLongitude(newLon);
m_interpolatedCoords.append(m_takenTrackCoords[p2]);
m_interpolatedDateTimes.append(m_takenTrackDateTimes[p2]);
}
void ObjectMapItem::interpolateAltitude(int p2, const float p3Altitude, const QDateTime &p3DateTime)
{
// p1 last non extrapolated position
// p2 interpolated position
// p3 current non extrapolated position
// Find last non extrapolated position
int p1;
for (p1 = p2 - 1; p1 >= 0; p1--)
{
if (!m_takenTrackPositionExtrapolated[p1]) {
break;
}
}
if (p1 < 0) {
return;
}
qint64 t1 = m_takenTrackDateTimes[p1]->msecsTo(p3DateTime);
qint64 t2 = m_takenTrackDateTimes[p1]->msecsTo(*m_takenTrackDateTimes[p2]);
double vertV = (p3Altitude - m_takenTrackCoords[p1]->altitude()) / t1;
double newAlt = m_takenTrackCoords[p1]->altitude() + vertV * t2;
m_takenTrackCoords[p2]->setAltitude(newAlt);
m_interpolatedCoords.append(m_takenTrackCoords[p2]);
m_interpolatedDateTimes.append(m_takenTrackDateTimes[p2]);
}
void ObjectMapItem::updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track, MapSettings::MapItemSettings *itemSettings)
{
if (track != nullptr)
{
@ -233,6 +432,8 @@ void ObjectMapItem::updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track)
m_takenTrackCoords.clear();
qDeleteAll(m_takenTrackDateTimes);
m_takenTrackDateTimes.clear();
m_takenTrackPositionExtrapolated.clear();
m_takenTrackAltitudeExtrapolated.clear();
m_takenTrack.clear();
m_takenTrack1.clear();
m_takenTrack2.clear();
@ -243,6 +444,8 @@ void ObjectMapItem::updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track)
QDateTime *d = new QDateTime(QDateTime::fromString(*p->getDateTime(), Qt::ISODate));
m_takenTrackCoords.push_back(c);
m_takenTrackDateTimes.push_back(d);
m_takenTrackPositionExtrapolated.push_back(false);
m_takenTrackAltitudeExtrapolated.push_back(false);
m_takenTrack.push_back(QVariant::fromValue(*c));
}
}
@ -253,28 +456,188 @@ void ObjectMapItem::updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track)
{
QGeoCoordinate *c = new QGeoCoordinate(m_latitude, m_longitude, m_altitude);
m_takenTrackCoords.push_back(c);
if (m_positionDateTime.isValid()) {
if (m_altitudeDateTime.isValid()) {
m_takenTrackDateTimes.push_back(new QDateTime(m_altitudeDateTime));
} else if (m_positionDateTime.isValid()) {
m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime));
} else {
m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime()));
}
m_takenTrackPositionExtrapolated.push_back(false);
m_takenTrackAltitudeExtrapolated.push_back(false);
m_takenTrack.push_back(QVariant::fromValue(*c));
}
else
{
QGeoCoordinate *prev = m_takenTrackCoords.last();
// For Whittaker-Eilers filtering, we need to make sure we don't have 2 data with the same time
// so we just update the last item if the prev time is the same
// To reduce size of list for stationary items, we only store two items with same position
// We store two, rather than one, so that we have the times this position was arrived at and left
const bool interpolate = false;
QGeoCoordinate *prev1 = m_takenTrackCoords.last();
bool samePos1 = (prev1->latitude() == m_latitude) && (prev1->longitude() == m_longitude) && (prev1->altitude() == m_altitude);
QGeoCoordinate *prev2 = m_takenTrackCoords.size() > 1 ? m_takenTrackCoords[m_takenTrackCoords.size() - 2] : nullptr;
bool samePos2 = prev2 && samePos1 ? (prev2->latitude() == m_latitude) && (prev2->longitude() == m_longitude) && (prev2->altitude() == m_altitude) : false;
QDateTime *prevDateTime = m_takenTrackDateTimes.last();
if ((prev->latitude() != m_latitude) || (prev->longitude() != m_longitude)
|| (prev->altitude() != m_altitude) || (*prevDateTime != m_positionDateTime))
QGeoCoordinate c(m_latitude, m_longitude, m_altitude);
int prevSize = m_takenTrackPositionExtrapolated.size();
if (m_altitudeDateTime.isValid() && m_positionDateTime.isValid() && (m_altitudeDateTime > 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()));
if (interpolate)
{
for (int i = m_takenTrackAltitudeExtrapolated.size() - 1; (i >= 0) && m_takenTrackAltitudeExtrapolated[i]; i--)
{
interpolateAltitude(i, m_altitude, m_altitudeDateTime);
m_takenTrackAltitudeExtrapolated[i] = false;
}
}
if (samePos2)
{
*m_takenTrackDateTimes.last() = m_altitudeDateTime;
}
else
{
extrapolatePosition(&c, m_altitudeDateTime);
if (m_altitudeDateTime == *prevDateTime)
{
m_takenTrackPositionExtrapolated[m_takenTrackPositionExtrapolated.size() - 1] = true;
*m_takenTrackCoords.last() = c;
}
else
{
m_takenTrackDateTimes.push_back(new QDateTime(m_altitudeDateTime));
m_takenTrackPositionExtrapolated.push_back(true);
m_takenTrackAltitudeExtrapolated.push_back(false);
m_takenTrackCoords.push_back(new QGeoCoordinate(c));
}
}
}
else if (m_positionDateTime.isValid())
{
if (interpolate)
{
for (int i = m_takenTrackPositionExtrapolated.size() - 1; (i >= 0) && m_takenTrackPositionExtrapolated[i]; i--)
{
interpolatePosition(i, m_latitude, m_longitude, m_positionDateTime);
m_takenTrackPositionExtrapolated[i] = false;
}
}
if (m_positionDateTime > *m_takenTrackDateTimes.last())
{
if (samePos2)
{
*m_takenTrackDateTimes.last() = m_positionDateTime;
}
else
{
bool extrapolateAlt = m_altitudeDateTime.isValid() && (m_positionDateTime > m_altitudeDateTime);
if (extrapolateAlt) {
extrapolateAltitude(&c, m_positionDateTime);
}
if (m_positionDateTime == *prevDateTime)
{
m_takenTrackAltitudeExtrapolated[m_takenTrackPositionExtrapolated.size() - 1] = extrapolateAlt;
*m_takenTrackCoords.last() = c;
}
else
{
m_takenTrackDateTimes.push_back(new QDateTime(m_positionDateTime));
m_takenTrackPositionExtrapolated.push_back(false);
m_takenTrackAltitudeExtrapolated.push_back(extrapolateAlt);
m_takenTrackCoords.push_back(new QGeoCoordinate(c));
}
}
}
else
{
//qDebug() << "m_positionDateTime matches last datetime" << samePos1 << samePos2;
}
}
else
{
m_takenTrackDateTimes.push_back(new QDateTime(QDateTime::currentDateTime()));
m_takenTrackPositionExtrapolated.push_back(false);
m_takenTrackAltitudeExtrapolated.push_back(false);
//m_takenTrackCoords.push_back(c);
m_takenTrackCoords.push_back(new QGeoCoordinate(c));
}
//m_takenTrack.push_back(QVariant::fromValue(*c));
m_takenTrack.push_back(QVariant::fromValue(c));
if (m_takenTrackDateTimes.size() >= 2) {
if (*m_takenTrackDateTimes[m_takenTrackDateTimes.size() - 1] < *m_takenTrackDateTimes[m_takenTrackDateTimes.size() - 2]) {
qDebug() << "Out of order";
}
}
if ((m_takenTrackPositionExtrapolated.size() > 0) && (prevSize != m_takenTrackPositionExtrapolated.size()))
{
const int filterLen = itemSettings->m_smoothingWindow;
if ((filterLen > 0) && (m_takenTrackCoords.size() >= filterLen) && (m_takenTrackCoords.size() % (filterLen/2)) == 0)
{
// Filter last filterLen coords
QVector<double> x(filterLen);
QVector<double> y1(filterLen);
QVector<double> y2(filterLen);
QVector<double> y3(filterLen);
QVector<double> w1(filterLen);
QVector<double> w3(filterLen);
//qDebug() << "Filter from" << (m_takenTrackCoords.size() - (filterLen - 0)) << "to" << (m_takenTrackCoords.size() - (filterLen - (filterLen - 1)));
for (int i = 0; i < filterLen; i++)
{
int idx = m_takenTrackCoords.size() - (filterLen - i);
x[i] = (m_takenTrackDateTimes[idx]->toMSecsSinceEpoch() - m_takenTrackDateTimes[0]->toMSecsSinceEpoch()) / 1000.0;
y1[i] = m_takenTrackCoords[idx]->latitude();
y2[i] = m_takenTrackCoords[idx]->longitude();
y3[i] = m_takenTrackCoords[idx]->altitude();
if (i < (filterLen / 4))
{
w1[i] = 10.0; // Try to avoid discontinuities between windows
w3[i] = 10.0;
}
else if (i == filterLen - 1)
{
w1[i] = 1.0;
w3[i] = 1.0;
}
else
{
w1[i] = m_takenTrackPositionExtrapolated[idx] ? 0.0 : 1.0;
w3[i] = m_takenTrackAltitudeExtrapolated[idx] ? 0.0 : 1.0;
}
}
const double lambda = itemSettings->m_smoothingLambda;
m_filter.filter(x.data(), y1.data(), w1.data(), filterLen, lambda);
m_filter.filter(x.data(), y2.data(), w1.data(), filterLen, lambda);
m_filter.filter(x.data(), y3.data(), w3.data(), filterLen, lambda);
for (int i = 0; i < filterLen; i++)
{
int idx = m_takenTrackCoords.size() - (filterLen - i);
m_takenTrackCoords[idx]->setLatitude(y1[i]);
m_takenTrackCoords[idx]->setLongitude(y2[i]);
m_takenTrackCoords[idx]->setAltitude(y3[i]);
m_takenTrackPositionExtrapolated[idx] = false;
m_takenTrackAltitudeExtrapolated[idx] = false;
m_interpolatedCoords.append(m_takenTrackCoords[idx]);
m_interpolatedDateTimes.append(m_takenTrackDateTimes[idx]);
}
// Update current position
m_latitude = m_takenTrackCoords[filterLen-1]->latitude();
m_longitude = m_takenTrackCoords[filterLen-1]->longitude();
m_altitude = m_takenTrackCoords[filterLen-1]->altitude();
}
m_takenTrack.push_back(QVariant::fromValue(*c));
}
}
}

View File

@ -25,6 +25,8 @@
#include "mapsettings.h"
#include "cesiuminterface.h"
#include "util/whittakereilers.h"
#include "mapaircraftstate.h"
#include "SWGMapItem.h"
@ -58,6 +60,7 @@ protected:
QString m_name; // Unique id
QString m_label;
QDateTime m_labelDateTime; // Date & time from which this label is valid from (for 3D map). Invalid date/time is forever
float m_latitude; // Position for label
float m_longitude;
float m_altitude; // In metres
@ -65,13 +68,13 @@ protected:
QDateTime m_availableUntil; // Date & time this item is visible until (for 3D map). Invalid date/time is forever
};
// Information required about each item displayed on the map
class ObjectMapItem : public MapItem {
public:
ObjectMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem) :
MapItem(sourcePipe, group, itemSettings, mapItem)
MapItem(sourcePipe, group, itemSettings, mapItem),
m_aircraftState(nullptr)
{
update(mapItem);
}
@ -79,12 +82,17 @@ public:
protected:
void findFrequencies();
void updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track);
void updateTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track, MapSettings::MapItemSettings *itemSettings);
void updatePredictedTrack(QList<SWGSDRangel::SWGMapCoordinate *> *track);
void extrapolatePosition(QGeoCoordinate *c, const QDateTime& dateTime);
void extrapolateAltitude(QGeoCoordinate *c, const QDateTime& dateTime);
void interpolatePosition(int i, const float latitude, const float longitude, const QDateTime &dateTime);
void interpolateAltitude(int i, const float altitude, const QDateTime &dateTime);
friend ObjectMapModel;
friend CZML;
QDateTime m_positionDateTime;
QDateTime m_altitudeDateTime;
bool m_useHeadingPitchRoll;
float m_heading;
float m_pitch;
@ -107,6 +115,8 @@ protected:
QGeoCoordinate m_predictedEnd2;
QList<QGeoCoordinate *> m_takenTrackCoords;
QList<QDateTime *> m_takenTrackDateTimes;
QList<bool> m_takenTrackPositionExtrapolated;
QList<bool> m_takenTrackAltitudeExtrapolated;
QVariantList m_takenTrack; // Line showing where the object has been
QVariantList m_takenTrack1;
QVariantList m_takenTrack2;
@ -114,6 +124,8 @@ protected:
QGeoCoordinate m_takenStart2;
QGeoCoordinate m_takenEnd1;
QGeoCoordinate m_takenEnd2;
QList<QGeoCoordinate *> m_interpolatedCoords;
QList<QDateTime *> m_interpolatedDateTimes;
// For 3D map
QString m_model;
@ -121,6 +133,9 @@ protected:
float m_labelAltitudeOffset;
float m_modelAltitudeOffset;
QList<CesiumInterface::Animation *> m_animations;
MapAircraftState *m_aircraftState;
static WhittakerEilers m_filter; // For smoothing/interpolating position
};
class PolygonMapItem : public MapItem {

View File

@ -269,7 +269,6 @@ QByteArray MapSettings::serialize() const
s.writeBool(29, m_sunLightEnabled);
s.writeBool(30, m_eciCamera);
s.writeString(31, m_cesiumIonAPIKey);
s.writeString(32, m_antiAliasing);
s.writeS32(33, m_workspaceIndex);
s.writeBlob(34, m_geometryBytes);
@ -292,6 +291,9 @@ QByteArray MapSettings::serialize() const
s.writeBool(48, m_displayMaidenheadGrid);
s.writeString(49, m_defaultImagery);
s.writeString(50, m_arcGISAPIKey);
s.writeBool(51, m_displayPFD);
s.writeBool(52, m_viewFirstPerson);
s.writeBool(53, m_terrainLighting);
s.writeBool(54, m_water);
s.writeBool(55, m_hdr);
@ -299,6 +301,7 @@ QByteArray MapSettings::serialize() const
s.writeBool(57, m_fps);
s.writeBool(58, m_fxaa);
s.writeS32(59, m_msaa);
return s.final();
}
@ -369,7 +372,6 @@ bool MapSettings::deserialize(const QByteArray& data)
d.readBool(29, &m_sunLightEnabled, true);
d.readBool(30, &m_eciCamera, false);
d.readString(31, &m_cesiumIonAPIKey, "");
d.readString(32, &m_antiAliasing, "None");
d.readS32(33, &m_workspaceIndex, 0);
d.readBlob(34, &m_geometryBytes);
@ -391,6 +393,9 @@ bool MapSettings::deserialize(const QByteArray& data)
d.readBool(48, &m_displayMaidenheadGrid, false);
d.readString(49, &m_defaultImagery, "Sentinel-2");
d.readString(50, &m_arcGISAPIKey, "");
d.readBool(51, &m_displayPFD, false);
d.readBool(52, &m_viewFirstPerson, false);
d.readBool(53, &m_terrainLighting, true);
d.readBool(54, &m_water, false);
d.readBool(55, &m_hdr, true);
@ -398,6 +403,7 @@ bool MapSettings::deserialize(const QByteArray& data)
d.readBool(57, &m_fps, false);
d.readBool(58, &m_fxaa, false);
d.readS32(59, &m_msaa, 1);
return true;
}
else
@ -451,6 +457,8 @@ void MapSettings::MapItemSettings::resetToDefaults()
m_filterName = "";
m_filterDistance = 0;
m_extrapolate = 60;
m_smoothingWindow = 0;
m_smoothingLambda = 100;
}
QByteArray MapSettings::MapItemSettings::serialize() const
@ -475,6 +483,8 @@ QByteArray MapSettings::MapItemSettings::serialize() const
s.writeString(16, m_filterName);
s.writeS32(17, m_filterDistance);
s.writeS32(18, m_extrapolate);
s.writeS32(19, m_smoothingWindow);
s.writeFloat(20, m_smoothingLambda);
return s.final();
}
@ -511,6 +521,16 @@ bool MapSettings::MapItemSettings::deserialize(const QByteArray& data)
d.readS32(18, &m_extrapolate, 60);
m_filterNameRE.setPattern(m_filterName);
m_filterNameRE.optimize();
if (m_group == "ADSBDemod")
{
d.readS32(19, &m_smoothingWindow, 10);
d.readFloat(20, &m_smoothingLambda, 100);
}
else
{
d.readS32(19, &m_smoothingWindow, 0);
d.readFloat(20, &m_smoothingLambda, 100);
}
return true;
}
else
@ -703,6 +723,12 @@ void MapSettings::applySettings(const QStringList& settingsKeys, const MapSettin
if (settingsKeys.contains("arcGISAPIKey")) {
m_arcGISAPIKey = settings.m_arcGISAPIKey;
}
if (settingsKeys.contains("displayPFD")) {
m_displayPFD = settings.m_displayPFD;
}
if (settingsKeys.contains("viewFirstPerson")) {
m_viewFirstPerson = settings.m_viewFirstPerson;
}
if (settingsKeys.contains("workspaceIndex")) {
m_workspaceIndex = settings.m_workspaceIndex;
}
@ -847,6 +873,12 @@ QString MapSettings::getDebugString(const QStringList& settingsKeys, bool force)
if (settingsKeys.contains("arcGISAPIKey") || force) {
ostr << " m_arcGISAPIKey: " << m_arcGISAPIKey.toStdString();
}
if (settingsKeys.contains("displayPFD") || force) {
ostr << " m_displayPFD: " << m_displayPFD;
}
if (settingsKeys.contains("viewFirstPerson") || force) {
ostr << " m_viewFirstPerson: " << m_viewFirstPerson;
}
if (settingsKeys.contains("workspaceIndex") || force) {
ostr << " m_workspaceIndex: " << m_workspaceIndex;
}

View File

@ -52,6 +52,8 @@ struct MapSettings
QRegularExpression m_filterNameRE;
int m_filterDistance; // Filter items > this distance in metres away from My Position. <= 0 don't filter
int m_extrapolate; // Extrapolate duration in seconds on 3D map
int m_smoothingWindow; // Window size (numer of points) to smooth over. 0 for no smoothing
float m_smoothingLambda; // Lambda parameter for WhittakerEilers, controls how much smoothing to apply
MapItemSettings(const QString& group, bool enabled, const QColor color, bool display2DTrack=true, bool display3DPoint=true, int minZoom=11, int modelMinPixelSize=0);
MapItemSettings(const QByteArray& data);
@ -120,6 +122,8 @@ struct MapSettings
bool m_displayAurora;
bool m_displayMagDec;
bool m_displayMaidenheadGrid;
bool m_displayPFD;
bool m_viewFirstPerson;
QString m_defaultImagery;
QString m_checkWXAPIKey; //!< checkwxapi.com API key

View File

@ -53,10 +53,20 @@ MapItemSettingsGUI::MapItemSettingsGUI(QTableWidget *table, int row, MapSettings
m_filterDistance->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
m_filterDistance->setSpecialValueText(" ");
m_filterDistance->setCorrectionMode(QAbstractSpinBox::CorrectToNearestValue);
m_smoothingWindow = new QSpinBox(table);
m_smoothingWindow->setRange(0, 1000);
m_smoothingWindow->setValue(settings->m_smoothingWindow);
m_smoothingWindow->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
m_smoothingLambda = new QDoubleSpinBox(table);
m_smoothingLambda->setRange(0, 1e9);
m_smoothingLambda->setValue(settings->m_smoothingLambda);
m_smoothingLambda->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
table->setCellWidget(row, MapSettingsDialog::COL_2D_MIN_ZOOM, m_minZoom);
table->setCellWidget(row, MapSettingsDialog::COL_3D_MIN_PIXELS, m_minPixels);
table->setCellWidget(row, MapSettingsDialog::COL_3D_LABEL_SCALE, m_labelScale);
table->setCellWidget(row, MapSettingsDialog::COL_FILTER_DISTANCE, m_filterDistance);
table->setCellWidget(row, MapSettingsDialog::COL_SMOOTHING_WINDOW, m_smoothingWindow);
table->setCellWidget(row, MapSettingsDialog::COL_SMOOTHING_LAMBDA, m_smoothingLambda);
}
MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
@ -142,6 +152,12 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
item = new QTableWidgetItem(itemSettings->m_filterName);
ui->mapItemSettings->setItem(row, COL_FILTER_NAME, item);
item = new QTableWidgetItem();
ui->mapItemSettings->setItem(row, COL_SMOOTHING_WINDOW, item);
item = new QTableWidgetItem();
ui->mapItemSettings->setItem(row, COL_SMOOTHING_LAMBDA, item);
MapItemSettingsGUI *gui = new MapItemSettingsGUI(ui->mapItemSettings, row, itemSettings);
m_mapItemSettingsGUIs.append(gui);
}
@ -170,6 +186,8 @@ MapSettingsDialog::MapSettingsDialog(MapSettings *settings, QWidget* parent) :
ui->mapItemSettings->hideColumn(COL_3D_POINT);
ui->mapItemSettings->hideColumn(COL_3D_TRACK);
ui->mapItemSettings->hideColumn(COL_3D_LABEL_SCALE);
ui->mapItemSettings->hideColumn(COL_SMOOTHING_WINDOW);
ui->mapItemSettings->hideColumn(COL_SMOOTHING_LAMBDA);
#endif
}
@ -203,7 +221,6 @@ void MapSettingsDialog::accept()
m_settings->m_mapBoxAPIKey = mapBoxAPIKey;
m_settings->m_osmURL = osmURL;
m_settings->m_mapBoxStyles = mapBoxStyles;
m_settings->m_cesiumIonAPIKey = cesiumIonAPIKey;
m_map2DSettingsChanged = true;
}
else
@ -223,7 +240,6 @@ void MapSettingsDialog::accept()
{
m_map3DSettingsChanged = false;
}
if (m_settings->m_map2DEnabled != ui->map2DEnabled->isChecked())
{
m_settings->m_map2DEnabled = ui->map2DEnabled->isChecked();
@ -323,6 +339,8 @@ void MapSettingsDialog::accept()
itemSettings->m_filterNameRE.setPattern(itemSettings->m_filterName);
itemSettings->m_filterNameRE.optimize();
itemSettings->m_filterDistance = gui->m_filterDistance->value() * 1000;
itemSettings->m_smoothingWindow = gui->m_smoothingWindow->value();
itemSettings->m_smoothingLambda = gui->m_smoothingLambda->value();
}
QDialog::accept();
@ -359,6 +377,8 @@ void MapSettingsDialog::on_map3DEnabled_clicked(bool checked)
ui->mapItemSettings->showColumn(COL_3D_POINT);
ui->mapItemSettings->showColumn(COL_3D_TRACK);
ui->mapItemSettings->showColumn(COL_3D_LABEL_SCALE);
ui->mapItemSettings->showColumn(COL_SMOOTHING_WINDOW);
ui->mapItemSettings->showColumn(COL_SMOOTHING_LAMBDA);
}
else
{
@ -368,6 +388,8 @@ void MapSettingsDialog::on_map3DEnabled_clicked(bool checked)
ui->mapItemSettings->hideColumn(COL_3D_POINT);
ui->mapItemSettings->hideColumn(COL_3D_TRACK);
ui->mapItemSettings->hideColumn(COL_3D_LABEL_SCALE);
ui->mapItemSettings->hideColumn(COL_SMOOTHING_WINDOW);
ui->mapItemSettings->hideColumn(COL_SMOOTHING_LAMBDA);
}
ui->terrain->setEnabled(checked);
ui->buildings->setEnabled(checked);

View File

@ -48,6 +48,8 @@ public:
QSpinBox *m_minPixels;
QDoubleSpinBox *m_labelScale;
QSpinBox *m_filterDistance;
QSpinBox *m_smoothingWindow;
QDoubleSpinBox *m_smoothingLambda;
};
class MapSettingsDialog : public QDialog {
@ -70,7 +72,9 @@ public:
COL_3D_TRACK,
COL_3D_LABEL_SCALE,
COL_FILTER_NAME,
COL_FILTER_DISTANCE
COL_FILTER_DISTANCE,
COL_SMOOTHING_WINDOW,
COL_SMOOTHING_LAMBDA
};
public:

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1267</width>
<height>648</height>
<height>775</height>
</rect>
</property>
<property name="font">
@ -153,13 +153,52 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="defaultImageryLabel">
<property name="text">
<string>Default imagery</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="defaultImagery">
<property name="toolTip">
<string>Default imagery (Note there is a quota on Bing Maps Ariel usage)</string>
</property>
<item>
<property name="text">
<string>ArcGIS world imagery</string>
</property>
</item>
<item>
<property name="text">
<string>Bing Maps Aerial</string>
</property>
</item>
<item>
<property name="text">
<string>Sentinel-2</string>
</property>
</item>
<item>
<property name="text">
<string>Earth at night</string>
</property>
</item>
<item>
<property name="text">
<string>Ersi world ocean</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="terrainLabel">
<property name="text">
<string>Terrain</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QComboBox" name="terrain">
<item>
<property name="text">
@ -183,14 +222,48 @@
</item>
</widget>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="terrainLightingLabel">
<property name="text">
<string>Terrain lighting</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="terrainLighting">
<property name="toolTip">
<string>Enable terrain lighting</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="waterLabel">
<property name="text">
<string>Water effects</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="water">
<property name="toolTip">
<string>Enable water effects such as waves</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="buildingsLabel">
<property name="text">
<string>Buildings</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="5" column="1">
<widget class="QComboBox" name="buildings">
<item>
<property name="text">
@ -204,14 +277,14 @@
</item>
</widget>
</item>
<item row="3" column="0">
<item row="6" column="0">
<widget class="QLabel" name="sunLightEnabledLabel">
<property name="text">
<string>Lighting</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="6" column="1">
<widget class="QComboBox" name="sunLightEnabled">
<property name="toolTip">
<string>Whether lighting is from the Sun or Camera</string>
@ -228,14 +301,71 @@
</item>
</widget>
</item>
<item row="4" column="0">
<item row="7" column="0">
<widget class="QLabel" name="lightIntensityLabel">
<property name="text">
<string>Camera light Intensity</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QDoubleSpinBox" name="lightIntensity">
<property name="toolTip">
<string>Intensity of camera light</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="maximum">
<double>100.000000000000000</double>
</property>
<property name="value">
<double>3.000000000000000</double>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="hdrLabel">
<property name="text">
<string>HDR</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="hdr">
<property name="toolTip">
<string>High dynamic range</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="fogLabel">
<property name="text">
<string>Fog</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="fog">
<property name="toolTip">
<string>Enable fog effect</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="eciCameraLabel">
<property name="text">
<string>Camera reference frame</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="11" 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>
@ -252,28 +382,77 @@
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="antiAliasingLabel">
<item row="14" column="0">
<widget class="QLabel" name="fpsLabel">
<property name="text">
<string>Anti-aliasing</string>
<string>Display FPS</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="antiAliasing">
<item row="14" column="1">
<widget class="QCheckBox" name="fps">
<property name="toolTip">
<string>Set anti-aliasing to use. This can remove jagged pixels on the edge of 3D models.</string>
<string>Display frames per second (FPS)</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="msaaLabel">
<property name="text">
<string>MSAA</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QComboBox" name="msaa">
<property name="toolTip">
<string>Multisample Anti-Aliasing</string>
</property>
<item>
<property name="text">
<string>None</string>
<string>Off</string>
</property>
</item>
<item>
<property name="text">
<string>FXAA</string>
<string>2</string>
</property>
</item>
<item>
<property name="text">
<string>4</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
<item>
<property name="text">
<string>16</string>
</property>
</item>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="fxaaLabel">
<property name="text">
<string>FXAA</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QCheckBox" name="fxaa">
<property name="toolTip">
<string>Fast Approximate Anti-aliasing</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
@ -282,7 +461,7 @@
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -313,7 +492,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/controltower.png</normaloff>:/map/icons/controltower.png</iconset>
</property>
</widget>
@ -327,7 +506,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/vor.png</normaloff>:/map/icons/vor.png</iconset>
</property>
</widget>
@ -341,7 +520,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<iconset resource="mapicons.qrc">
<normaloff>:/map/icons/waypoints.png</normaloff>:/map/icons/waypoints.png</iconset>
</property>
</widget>
@ -349,7 +528,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -371,7 +550,7 @@
<item>
<widget class="QTableWidget" name="mapItemSettings">
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
<enum>QAbstractItemView::SelectionMode::NoSelection</enum>
</property>
<column>
<property name="text">
@ -468,6 +647,22 @@
<string>Filter objects further than this distance in km away from My Position</string>
</property>
</column>
<column>
<property name="text">
<string>Smoothing Window</string>
</property>
<property name="toolTip">
<string>How many coordinates to apply smoothing filter to. Set to 0 for no smoothing.</string>
</property>
</column>
<column>
<property name="text">
<string>Smoothing Lambda</string>
</property>
<property name="toolTip">
<string>Smoothing parameter. Higher values result in more smoothing.</string>
</property>
</column>
</widget>
</item>
</layout>
@ -563,20 +758,34 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="checkWXAPIKeyLabel">
<property name="text">
<string>CheckWX API key</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QLineEdit" name="checkWXAPIKey">
<property name="toolTip">
<string>checkwxapi.com API key for accessing airport weather (METARs)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="arcGISAPIKeyLabel">
<property name="text">
<string>ArcGIS API Key</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="arcGISAPIKey">
<property name="toolTip">
<string>Enter an ArcGIS API Key</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -590,10 +799,10 @@
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
@ -609,7 +818,7 @@
<tabstop>buildings</tabstop>
<tabstop>sunLightEnabled</tabstop>
<tabstop>eciCamera</tabstop>
<tabstop>antiAliasing</tabstop>
<tabstop>msaa</tabstop>
<tabstop>downloadModels</tabstop>
<tabstop>thunderforestAPIKey</tabstop>
<tabstop>maptilerAPIKey</tabstop>
@ -617,7 +826,7 @@
<tabstop>cesiumIonAPIKey</tabstop>
</tabstops>
<resources>
<include location="icons.qrc"/>
<include location="mapicons.qrc"/>
</resources>
<connections>
<connection>