1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2025-08-24 16:32:26 -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;
@ -318,14 +364,49 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
}
}
else
{
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 (!fixedPosition && mapItem->m_positionDateTime.isValid()) {
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);
coords.push_back(mapItem->m_altitude);
}
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
}
}
}
}
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,8 +484,257 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{"forwardExtrapolationType", "NONE"}
};
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)
{
if (mapItem->m_useHeadingPitchRoll) {
obj.insert("orientation", orientation);
} else {
obj.insert("orientation", orientationPosition);
}
}
// Point
QColor pointColor = QColor::fromRgba(mapItem->m_itemSettings->m_3DPointColor);
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()
};
@ -409,68 +744,32 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
QJsonObject point {
{"pixelSize", 8},
{"color", pointColorObj},
{"heightReference", heightReferences[mapItem->m_altitudeReference]},
{"show", mapItem->m_itemSettings->m_enabled && mapItem->m_itemSettings->m_display3DPoint}
{"heightReference", heightReferences[pointAltitudeReference]},
{"show", pointShow}
};
obj.insert("point", point);
// 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);
state.m_pointColorInt = pointColorInt;
state.m_pointAltitudeReference = pointAltitudeReference;
state.m_pointShow = pointShow;
}
// 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
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();
@ -499,7 +798,7 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{"cartesian2", labelPixelOffsetArray}
};
QJsonArray labelEyeOffsetArray {
0, mapItem->m_labelAltitudeOffset, 0 // Position above the object, dependent on the height of the model
0, labelAltitudeOffset, 0 // Position above the object, dependent on the height of the model
};
QJsonObject labelEyeOffset {
{"cartesian", labelEyeOffsetArray}
@ -513,23 +812,101 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
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},
//{"text", labelText},
{"show", labelShow},
{"scale", labelScale},
{"pixelOffset", labelPixelOffset},
{"pixelOffsetScaleByDistance", labelPixelOffsetScaleObject},
{"eyeOffset", labelEyeOffset},
{"verticalOrigin", "BASELINE"},
{"horizontalOrigin", "LEFT"},
{"heightReference", heightReferences[mapItem->m_altitudeReference]},
{"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://")) {
@ -541,27 +918,64 @@ QJsonObject CZML::update(ObjectMapItem *mapItem, bool isTarget, bool isSelected)
{"verticalOrigin", "BOTTOM"} // To stop it being cut in half when zoomed out
};
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) {
// 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

View File

@ -5,6 +5,7 @@
<script src="/Cesium/Cesium.js"></script>
<style>
@import url(/Cesium/Widgets/widgets.css);
html,
body,
#cesiumContainer {
@ -15,14 +16,13 @@
overflow: hidden;
}
</style>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<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 src="grid.js"></script>
<script>
// See: https://community.cesium.com/t/how-to-run-an-animation-for-an-entity-model/16932
function getActiveAnimations(viewer, entity) {
@ -48,7 +48,7 @@
startOffset: command.startOffset,
reverse: command.reverse,
loop: command.loop ? Cesium.ModelAnimationLoop.REPEAT : Cesium.ModelAnimationLoop.NONE,
multiplier: command.multiplier,
multiplier: 0.2 // command.multiplier,
};
options.startTime = Cesium.JulianDate.fromIso8601(command.startDateTime);
// https://github.com/CesiumGS/cesium/issues/10048
@ -60,7 +60,7 @@
if (command.duration != 0) {
options.stopTime = Cesium.JulianDate.addSeconds(options.startTime, command.duration, new Cesium.JulianDate());
}
animations.add(options);
const anim = 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)) {
@ -127,7 +127,7 @@
loop: anim.loop,
multiplier: anim.multiplier,
startTime: anim.startTime,
stopTime: Cesium.JulianDate.fromIso8601(command.startDateTime)
stopTime: Cesium.JulianDate.fromIso8601(command.startDateTime) // FIXME: Should this be stopDateTime?
});
}
}
@ -148,8 +148,7 @@
}
// Polygons (such as for airspaces) should be prioritized behind other entities
function pickEntityPrioritized(e)
{
function pickEntityPrioritized(e) {
var picked = viewer.scene.drillPick(e.position);
if (Cesium.defined(picked)) {
var firstPolygon = null;
@ -171,6 +170,24 @@
viewer.selectedEntity = pickEntityPrioritized(e);
}
function pickAndTrack(e) {
const entity = pickEntityPrioritized(e);
if (Cesium.defined(entity)) {
if (viewFirstPerson) {
setFirstPersonView(entity);
} else {
if (Cesium.Property.getValueOrUndefined(entity.position, viewer.clock.currentTime)) {
setThirdPersonView(entity);
} else {
viewer.zoomTo(entity);
}
}
} else if (Cesium.defined(viewer.trackedEntity)) {
//viewer.trackedEntity = undefined;
setThirdPersonView(undefined);
}
}
function showCoords(e) {
if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
var cartesian = viewer.camera.pickEllipsoid(e.position);
@ -189,8 +206,9 @@
const ray = viewer.camera.getPickRay(e.position);
const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [cartographic]);
Cesium.when(promise, function(updatedPositions) {
Promise.resolve(
Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [cartographic]),
).then((updatedPositions) => {
longitudeString = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6);
latitudeString = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6);
heightString = updatedPositions[0].height.toFixed(1);
@ -201,8 +219,6 @@
`Lon: ${` ${longitudeString}`}\u00B0` +
`\nLat: ${` ${latitudeString}`}\u00B0` +
`\nAlt: ${` ${heightString}`}m`;
}, function() {
console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
});
}
}
@ -213,9 +229,14 @@
}
Cesium.Ion.defaultAccessToken = '$CESIUM_ION_API_KEY$';
if ('$ARCGIS_API_KEY$' != '') {
Cesium.ArcGisMapService.defaultAccessToken = '$ARCGIS_API_KEY$';
}
// Start time is set via CZML::init()
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain(),
baseLayer: false,
terrainProvider: Cesium.createWorldTerrainAsync(),
animation: true,
shouldAnimate: true,
timeline: true,
@ -225,19 +246,32 @@
navigationInstructionsInitiallyVisible: false,
terrainProviderViewModels: [] // User should adjust terrain via dialog, so depthTestAgainstTerrain doesn't get set
});
//viewer.scene.debugShowFramesPerSecond = true; // FIXME: Embedded Chrome only runs at 60fps
viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain (this prevents pickPosition from working)
viewer.scene.globe.tileCacheSize = 5000; // FIXME: Embedded Chrome is slower at loading from cache
viewer.scene.moon.onlySunLighting = false; // Moon can be just a black dot if default of true
viewer.screenSpaceEventHandler.setInputAction(pickEntity, Cesium.ScreenSpaceEventType.LEFT_CLICK);
viewer.screenSpaceEventHandler.setInputAction(pickAndTrack, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
viewer.screenSpaceEventHandler.setInputAction(showCoords, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK, Cesium.KeyboardEventModifier.SHIFT);
viewer.screenSpaceEventHandler.setInputAction(hideCoords, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
viewer.useBrowserRecommendedResolution = false; // Improves label quality when false, as drawn at higher res
viewer.infoBox.frame.setAttribute('sandbox', 'allow-same-origin allow-popups allow-forms allow-scripts allow-top-navigation');
viewer.infoBox.frame.src = "about:blank"; // Force reload so new attributes are applied
viewer.infoBox.viewModel.cameraClicked.removeEventListener(Cesium.Viewer.prototype._onInfoBoxCameraClicked, viewer); // Override info box camera button being clicked
viewer.infoBox.viewModel.cameraClicked.addEventListener(infoBoxCameraClicked, viewer);
var pfdTimer = undefined;
var pfdRadioAltTimer = undefined;
var buildings = undefined;
const images = new Map();
var mufGeoJSONStream = null;
var foF2GeoJSONStream = null;
var wmmGeoJSONStream = null;
const positionMarker = viewer.entities.add({
id: 'Position marker',
@ -284,6 +318,18 @@
return html;
}
// Generate HTML for WMM contour info box from properties in GeoJSON
function describeWMM(properties, nameProperty) {
let html = "";
if (properties.hasOwnProperty("Contour")) {
const value = properties["Contour"];
if (Cesium.defined(value)) {
html = `<p>Magnetic declination: ${value} degrees`;
}
}
return html;
}
// Use CZML to stream data from Map plugin to Cesium
var czmlStream = new Cesium.CzmlDataSource();
@ -293,6 +339,72 @@
viewer.scene.light.direction = Cesium.Cartesian3.clone(scene.camera.directionWC, viewer.scene.light.direction);
}
var velocityVectorProperty = undefined;
const velocityVector = new Cesium.Cartesian3();
var viewFirstPerson = false;
var firstPersonEntity;
var firstPersonOffset = 0.0;
var cameraSavedPositionValid = false;
var cameraSavedPosition;
var cameraSavedHeading;
var cameraSavedPitch;
var cameraSavedRoll;
var cameraSavedTransform;
var cameraInitPos = false;
// First person camera
function cameraFirstPerson(scene, time) {
const entity = firstPersonEntity;
if (!Cesium.defined(entity)) {
return;
}
const position = entity.position.getValue(time);
if (!Cesium.defined(position)) {
return;
}
let transform;
if (!Cesium.defined(entity.orientation)) {
transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
} else {
const orientation = entity.orientation.getValue(time);
if (!Cesium.defined(orientation)) {
return;
}
transform = Cesium.Matrix4.fromRotationTranslation(
Cesium.Matrix3.fromQuaternion(orientation),
position,
);
}
const camera = viewer.camera;
if (cameraInitPos) {
camera.position = new Cesium.Cartesian3(firstPersonOffset, 0.0, 0.0);
camera.direction = new Cesium.Cartesian3(1.0, 0.0, 0.0);
camera.up = new Cesium.Cartesian3(0.0, 0.0, 1.0);
camera.right = new Cesium.Cartesian3(0.0, -1.0, 0.0);
cameraInitPos = false;
}
// Save camera state
const offset = Cesium.Cartesian3.clone(camera.position);
const direction = Cesium.Cartesian3.clone(camera.direction);
const up = Cesium.Cartesian3.clone(camera.up);
// Set camera to be in model's reference frame.
camera.lookAtTransform(transform);
// Reset the camera state to the saved state so it appears fixed in the model's frame.
Cesium.Cartesian3.clone(offset, camera.position);
Cesium.Cartesian3.clone(direction, camera.direction);
Cesium.Cartesian3.clone(up, camera.up);
Cesium.Cartesian3.cross(direction, up, camera.right);
}
// Image overlays
function dataCallback(interval, index) {
@ -321,6 +433,7 @@
// See https://wiki.earthdata.nasa.gov/display/GIBS/GIBS+API+for+Developers#GIBSAPIforDevelopers-OGCWebMapService(WMS)
var gibsProvider = new Cesium.WebMapTileServiceImageryProvider({
url: "https://gibs.earthdata.nasa.gov/wmts/epsg4326/best/MODIS_Terra_CorrectedReflectance_TrueColor/default/{Time}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.jpg",
layer: '', // FIXME
style: "default",
tileMatrixSetID: "250m",
format: "image/jpeg",
@ -339,6 +452,11 @@
var cloudProvider = new Cesium.UrlTemplateImageryProvider({
url: "https://tilecache.rainviewer.com/v2/satellite/0000000000/256/{z}/{x}/{y}/0/0_0.png"
});
var auroraProvider = new Cesium.SingleTileImageryProvider({
url: "aurora.png",
tileWidth: 360,
tileHeight: 181
});
var gibsLayer = new Cesium.ImageryLayer(gibsProvider);
gibsLayer.show = false;
@ -355,13 +473,17 @@
const railwaysLayer = new Cesium.ImageryLayer(railwaysProvider);
railwaysLayer.show = false;
viewer.imageryLayers.add(railwaysLayer);
var auroraLayer = new Cesium.ImageryLayer(auroraProvider);
auroraLayer.show = false;
viewer.imageryLayers.add(auroraLayer);
const layers = new Map([
["nasaGlobalImagery", gibsLayer],
["clouds", cloudLayer],
["rain", rainLayer],
["seaMarks", seaMarksLayer],
["railways", railwaysLayer]
["railways", railwaysLayer],
["aurora", auroraLayer]
]);
function downloadBlob(filename, blob) {
@ -440,13 +562,22 @@
heading: 0,
},
});
} else if (command.command == "setViewFirstPerson") {
if (command.firstPerson) {
// Use first person view from entity
viewFirstPerson = true;
setFirstPersonView(viewer.trackedEntity);
} else {
viewFirstPerson = false;
setThirdPersonView(firstPersonEntity);
}
} 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}`);
//console.log(`playing animation ${command.animation} for ${command.id} command` + JSON.stringify(command));
playAnimation(viewer, command, 30);
}
} else if (command.command == "setDateTime") {
@ -465,15 +596,35 @@
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
}
} else if (command.provider == "Cesium World Terrain") {
viewer.terrainProvider = Cesium.createWorldTerrain();
viewer.scene.setTerrain(
Cesium.Terrain.fromWorldTerrain({
requestWaterMask: command.water,
requestVertexNormals: command.terrainLighting
})
);
} else if (command.provider == "CesiumTerrainProvider") {
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
url: command.url
});
viewer.scene.setTerrain(
new Cesium.Terrain(
Cesium.CesiumTerrainProvider.fromUrl(
command.url,
{
requestWaterMask: command.water,
requestVertexNormals: command.terrainLighting
}
)
)
);
} else if (command.provider == "ArcGISTiledElevationTerrainProvider") {
viewer.terrainProvider = new Cesium.ArcGISTiledElevationTerrainProvider({
url: command.url
});
viewer.scene.setTerrain(
new Cesium.Terrain(
Cesium.ArcGISTiledElevationTerrainProvider.fromUrl(
command.url,
{
requestWaterMask: command.water,
requestVertexNormals: command.terrainLighting
})
)
);
} else {
console.log(`Unknown terrain ${command.terrain}`);
}
@ -486,16 +637,21 @@
}
} else {
if (buildings === undefined) {
buildings = viewer.scene.primitives.add(Cesium.createOsmBuildings());
Promise.resolve(
Cesium.createOsmBuildingsAsync()
).then((osmBuildingsTileset) => {
buildings = viewer.scene.primitives.add(osmBuildingsTileset);
});
}
}
} else if (command.command == "setSunLight") {
// Enable illumination of the globe from the direction of the Sun or camera
} else if (command.command == "setLighting") {
viewer.scene.globe.enableLighting = command.useSunLight;
viewer.scene.globe.nightFadeOutDistance = 0.0;
//viewer.scene.globe.nightFadeOutDistance = 0.0; // FIXME: Can't be nearly 0. Causes terrain above horizon to be blacked out in 1.129
// Currently Cesium only supports a single light source, either Sun or Directional
if (!command.useSunLight) {
viewer.scene.light = new Cesium.DirectionalLight({
direction: new Cesium.Cartesian3(1, 0, 0)
direction: new Cesium.Cartesian3(1, 0, 0),
intensity: command.cameraLightIntensity
});
viewer.scene.preRender.addEventListener(cameraLight);
} else {
@ -509,10 +665,40 @@
viewer.scene.postUpdate.removeEventListener(icrf);
}
} else if (command.command == "setAntiAliasing") {
if (command.antiAliasing == "FXAA") {
viewer.scene.postProcessStages.fxaa.enabled = true;
viewer.scene.postProcessStages.fxaa.enabled = command.fxaa;
viewer.scene.msaaSamples = command.msaa;
} else if (command.command == "setHDR") {
if (command.hdr) {
viewer.scene.highDynamicRange = true;
} else {
viewer.scene.postProcessStages.fxaa.enabled = false;
viewer.scene.highDynamicRange = false;
}
} else if (command.command == "setFog") {
if (command.fog) {
viewer.scene.fog.enabled = true;
} else {
viewer.scene.fog.enabled = false;
}
} else if (command.command == "showFPS") {
if (command.show) {
viewer.scene.debugShowFramesPerSecond = true;
} else {
viewer.scene.debugShowFramesPerSecond = false;
}
} else if (command.command == "showPFD") {
const pfdCanvas = document.getElementById("pfdCanvas");
if (command.show == true) {
if (pfdTimer === undefined) {
pfdTimer = setInterval(updatePFD, 10);
pfdRadioAltTimer = setInterval(updateRadioAlt, 250);
}
canvas.removeAttribute("hidden");
} else {
canvas.setAttribute("hidden", "hidden");
clearInterval(pfdTimer);
clearInterval(pfdRadioAltTimer);
pfdTimer = undefined;
pfdRadioAltTimer = undefined;
}
} else if (command.command == "showMUF") {
if (command.show == true) {
@ -550,6 +736,42 @@
viewer.dataSources.remove(foF2GeoJSONStream, true);
foF2GeoJSONStream = null;
}
} else if (command.command == "showMagneticDeclination") {
if (command.show == true) {
viewer.dataSources.add(
Cesium.GeoJsonDataSource.load(
"/map/data/wmm.geojson",
{ describe: describeWMM }
)
).then(function (dataSource) {
if (wmmGeoJSONStream != null) {
viewer.dataSources.remove(wmmGeoJSONStream, true);
wmmGeoJSONStream = null;
}
wmmGeoJSONStream = dataSource;
});
} else {
viewer.dataSources.remove(wmmGeoJSONStream, true);
wmmGeoJSONStream = null;
}
} else if (command.command == "showMaidenheadGrid") {
showGrid(command.show);
} else if (command.command == "setDefaultImagery") {
// For indexes, see pacakges/widgets/Source/BaseLayerPicker/createDefaultImageryProviderViewModels.js
if (command.imagery == "Bing Maps Aerial") {
viewer.baseLayerPicker.viewModel.selectedImagery = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[0];
} else if (command.imagery == "ArcGIS world imagery") {
viewer.baseLayerPicker.viewModel.selectedImagery = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[3];
} else if (command.imagery == "Ersi world ocean") {
viewer.baseLayerPicker.viewModel.selectedImagery = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[5];
} else if (command.imagery == "Sentinel-2") {
viewer.baseLayerPicker.viewModel.selectedImagery = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[11];
} else if (command.imagery == "Earth at night") {
viewer.baseLayerPicker.viewModel.selectedImagery = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[13];
} else {
console.log(`Unknown imagery ${command.imagery}`);
}
} else if (command.command == "showLayer") {
layers.get(command.layer).show = command.show;
} else if (command.command == "setLayerSettings") {
@ -585,6 +807,7 @@
gibsProvider = new Cesium.WebMapTileServiceImageryProvider({
url: command.url,
layer: '', // FIXME
style: "default",
tileMatrixSetID: command.tileMatrixSet,
format: command.format,
@ -619,13 +842,24 @@
rainLayer.show = command.show;
viewer.imageryLayers.add(rainLayer);
layers.set(command.layer, rainLayer);
} else if (command.layer == "aurora") {
viewer.imageryLayers.remove(auroraLayer, true);
auroraProvider = new Cesium.SingleTileImageryProvider({
url: "aurora.png",
tileWidth: 360,
tileHeight: 181
});
auroraLayer = new Cesium.ImageryLayer(auroraProvider);
auroraLayer.show = command.show;
viewer.imageryLayers.add(auroraLayer);
layers.set(command.layer, auroraLayer);
} else {
console.log("Unknown layer: " + command.layer);
}
} 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
// so we use a primitive instead of an entity - FIXME: No longer working
// 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);
@ -652,8 +886,12 @@
}));
images.set(command.name, image);
if (oldImage !== undefined) {
image.readyPromise.then(function(prim) {
const removeListener = viewer.scene.postRender.addEventListener(() => {
if (!image.ready) {
return;
}
viewer.scene.primitives.remove(oldImage);
removeListener();
});
}
} else if (command.command == "removeImage") {
@ -670,7 +908,7 @@
} 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
// Implement CLIP_TO_GROUND, to work around https://github.com/CesiumGS/cesium/issues/4049 - Now fixed, so this may be obsolete
if (command.hasOwnProperty('altitudeReference') && command.hasOwnProperty('position') && command.position.hasOwnProperty('cartographicDegrees')) {
var size = command.position.cartographicDegrees.length;
if ((size == 3) || (size == 4)) {
@ -694,8 +932,9 @@
}
czmlStream.process(command);
} else {
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]);
Cesium.when(promise, function (updatedPositions) {
Promise.resolve(
Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]),
).then((updatedPositions) => {
if (height < updatedPositions[0].height) {
if (size == 3) {
command.position.cartographicDegrees[2] = updatedPositions[0].height;
@ -704,13 +943,10 @@
}
}
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`);
console.log(`Can't currently use altitudeReference when more than one position`, command.position);
czmlStream.process(command);
}
} else if ((command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference'))
@ -739,8 +975,9 @@
for (let i = 0; i < positionCount; i++) {
positions[i] = Cesium.Cartographic.fromDegrees(prim.positions.cartographicDegrees[i * 3 + 0], prim.positions.cartographicDegrees[i * 3 + 1]);
}
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions);
Cesium.when(promise, function (updatedPositions) {
Promise.resolve(
Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions),
).then((updatedPositions) => {
if (clampToGround) {
for (let i = 0; i < positionCount; i++) {
prim.positions.cartographicDegrees[i * 3 + 2] = updatedPositions[i].height;
@ -753,9 +990,6 @@
}
}
czmlStream.process(command);
}, function () {
console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
czmlStream.process(command);
});
}
} else {
@ -784,9 +1018,97 @@
}
};
function setFirstPersonView(entity) {
if (!Cesium.defined(entity)) {
viewer.scene.postUpdate.removeEventListener(cameraFirstPerson);
return;
}
const camera = viewer.camera;
// Save current camera position/orientation so we can restore it, when going back to third person
/*
cameraSavedPosition = camera.positionWC.clone(cameraSavedPosition);
cameraSavedHeading = camera.heading;
cameraSavedPitch = camera.pitch;
cameraSavedRoll = camera.roll;
cameraSavedTransform = camera.transform.clone(cameraSavedTransform);
cameraSavedPositionValid = true;
console.log("******* SAVED POSITION", cameraSavedPosition, cameraSavedHeading, cameraSavedPitch, cameraSavedRoll, cameraSavedTransform);*/
viewer.trackedEntity = entity; // So infobox camera icon indicates we're trackingd
viewer.cesiumWidget._needTrackedEntityUpdate = false; // Prevent camera from zooming to it a bit later, and overwriting the position we set
firstPersonEntity = entity;
// Get size of model, so we can position camera at front of it, rather than in the middle
if (Cesium.defined(firstPersonEntity) && Cesium.defined(firstPersonEntity.id)) {
var primitives = viewer.scene.primitives;
var length = primitives.length;
for (var i = 0; i < length; i++) {
var primitive = primitives.get(i);
if (primitive.id === firstPersonEntity && primitive instanceof Cesium.Model && primitive.ready) {
firstPersonOffset = primitive.boundingSphere.radius;
}
}
}
cameraInitPos = true;
viewer.scene.postUpdate.addEventListener(cameraFirstPerson);
}
function setThirdPersonView(entity) {
viewer.trackedEntity = undefined; // If we're switching from first to third, ensure trackedEntity changes, so camera switches to it
viewer.trackedEntity = entity;
viewer.scene.postUpdate.removeEventListener(cameraFirstPerson);
firstPersonEntity = undefined;
/*if (cameraSavedPositionValid && !Cesium.defined(entity)) {
console.log("******* SAVED POSITION RESTORED", cameraSavedPosition, cameraSavedHeading, cameraSavedPitch, cameraSavedRoll, cameraSavedTransform);
const camera = viewer.camera;
camera.setView({
destination: cameraSavedPosition,
orientation: {
heading: cameraSavedHeading,
pitch: cameraSavedPitch,
roll: cameraSavedRoll
},
endTransform: cameraSavedTransform
});
//camera.transform = Cesium.Matrix4.clone(cameraSavedTransform, camera.transform);
cameraSavedPositionValid = false;
} else {
console.log("******* SAVED POSITION NOT RESTORED");
}*/
}
function infoBoxCameraClicked(infoBoxViewModel) {
if (infoBoxViewModel.isCameraTracking && viewer.trackedEntity === viewer.selectedEntity) {
if (viewFirstPerson === true) {
setThirdPersonView(undefined);
} else {
viewer.trackedEntity = undefined;
}
} else {
const selectedEntity = viewer.selectedEntity;
if (viewFirstPerson === true) {
setFirstPersonView(selectedEntity);
} else {
const position = selectedEntity.position;
if (Cesium.defined(position)) {
setThirdPersonView(selectedEntity);
} else {
viewer.zoomTo(viewer.selectedEntity);
}
}
}
}
viewer.selectedEntityChanged.addEventListener(function (selectedEntity) {
if (Cesium.defined(selectedEntity) && Cesium.defined(selectedEntity.id)) {
socket.send(JSON.stringify({ event: "selected", id: selectedEntity.id }));
// Calculate it's velocity for PFD
velocityVectorProperty = new Cesium.VelocityVectorProperty(selectedEntity.position, false);
} else {
socket.send(JSON.stringify({ event: "selected" }));
}
@ -822,6 +1144,24 @@
return false;
}
// Use WASD keys to move camera
document.addEventListener('keydown', function (event) {
var amount = 0.5;
if (event.key == 'w') {
viewer.camera.moveUp(amount);
} else if (event.key == 's') {
viewer.camera.moveDown(amount);
} else if (event.key == 'a') {
viewer.camera.moveLeft(amount);
} else if (event.key == 'd') {
viewer.camera.moveRight(amount);
} else if (event.key == 'q') {
viewer.camera.moveForward(amount);
} else if (event.key == 'e') {
viewer.camera.moveBackward(amount);
}
});
Cesium.knockout.getObservable(viewer.clockViewModel, 'shouldAnimate').subscribe(function (isAnimating) {
reportClock();
});
@ -843,6 +1183,166 @@
</script>
</div>
<canvas id="pfdCanvas" width="1000" height="1000" style="border:1px solid #000000;" hidden>
Browser does not support canvas.
</canvas>
<style>
#pfdCanvas {
position: absolute;
width: 500px;
height: 500px;
}
</style>
<script src="cockpit.js"></script>
<script>
// Position PFD in centre at bottom
const pfdCanvas = document.getElementById("pfdCanvas");
pfdCanvas.style.left = ((window.innerWidth / 2) - 250).toString() + "px";
pfdCanvas.style.top = (window.innerHeight - 500 - 30).toString() + "px";
if (pfdCanvas.style.width > window.innerWidth) {
pfdCanvas.style.width = Math.max(window.innerWidth, 250);
}
if (pfdCanvas.style.height > window.innerHeight) {
pfdCanvas.style.height = Math.max(window.innerHeight, 250);
}
function getPropertyValue(entity, propertyName) {
const property = entity.properties[propertyName];
if (Cesium.defined(property)) {
return property.getValue(viewer.clock.currentTime);
} else {
return undefined;
}
}
// Only valid for TimeIntervalCollection properties
function getTimerIntervalPropertyValueAt(entity, propertyName, time) {
var value = undefined;
const property = entity.properties[propertyName];
if (Cesium.defined(property)) {
value = property.getValue(time);
if (!Cesium.defined(value)) {
value = property.intervals.get(0).data; // Get first available value
}
}
return value;
}
// Only valid for SampledProperty properties
function getSampledPropertyValueAt(entity, propertyName, time) {
var value = undefined;
const property = entity.properties[propertyName];
if (Cesium.defined(property)) {
value = property.getValue(time);
if (!Cesium.defined(value)) {
value = property.getValue(property.getSample(0)); // Get first available value
}
}
return value;
}
var pfdEntity;
var pfd60SecsAgo = new Cesium.JulianDate();
var pfdPrevClock;
var pfdRadioAltitude;
var pfdRadioAltitudeEntity;
var pfdRadioAltPosition;
function updatePFD() {
// Display PFD for last selected aircraft
const entity = viewer.selectedEntity;
if (Cesium.defined(entity) && Cesium.defined(entity.properties) && ((entity.properties.hasProperty("pfdAltitude") || entity.properties.hasProperty("pfdOnSurface")))) {
if (entity !== pfdEntity) {
pfdRadioAltitude = undefined;
}
pfdEntity = entity;
}
if (Cesium.defined(pfdEntity)) {
var callsign;
var aircraftType;
if (pfdEntity.properties.hasProperty("pfdCallsign")) {
callsign = pfdEntity.properties["pfdCallsign"].getValue();
} else {
callsign = "";
}
if (pfdEntity.properties.hasProperty("pfdAircraftType")) {
aircraftType = pfdEntity.properties["pfdAircraftType"].getValue();
} else {
aircraftType = "";
}
pfd60SecsAgo = Cesium.JulianDate.addSeconds(viewer.clock.currentTime, -60, pfd60SecsAgo);
const onSurface = pfdEntity.properties["pfdOnSurface"].getValue(viewer.clock.currentTime);
const wasOnSurface60SecsAgo = getTimerIntervalPropertyValueAt(pfdEntity, "pfdOnSurface", pfd60SecsAgo);
const indicatedAirspeed = getPropertyValue(pfdEntity, "pfdIndicatedAirspeed");
const trueAirspeed = getPropertyValue(pfdEntity, "pfdTrueAirspeed");
const groundspeed = getPropertyValue(pfdEntity, "pfdGroundspeed");
const mach = getPropertyValue(pfdEntity, "pfdMach");
const altitude = getPropertyValue(pfdEntity, "pfdAltitude");
var runwayAltitudeEstimate = undefined;
if ((onSurface === 0) && (wasOnSurface60SecsAgo > 0)) {
runwayAltitudeEstimate = getSampledPropertyValueAt(pfdEntity, "pfdAltitude", pfd60SecsAgo);
}
const qnh = getPropertyValue(pfdEntity, "pfdQNH");
const verticalSpeed = getPropertyValue(pfdEntity, "pfdVerticalSpeed");
const heading = getPropertyValue(pfdEntity, "pfdHeading");
const track = getPropertyValue(pfdEntity, "pfdTrack");
const roll = getPropertyValue(pfdEntity, "pfdRoll");
const selectedAltitude = getPropertyValue(pfdEntity, "pfdSelectedAltitude");
const selectedHeading = getPropertyValue(pfdEntity, "pfdSelectedHeading");
const autopilot = getPropertyValue(pfdEntity, "pfdAutopilot");
const verticalMode = getPropertyValue(pfdEntity, "pfdVerticalMode");
const lateralMode = getPropertyValue(pfdEntity, "pfdLateralMode");
const tcasMode = getPropertyValue(pfdEntity, "pfdTCASMode");
const windSpeed = getPropertyValue(pfdEntity, "pfdWindSpeed");
const windDirection = getPropertyValue(pfdEntity, "pfdWindDirection");
const staticAirTemperature = getPropertyValue(pfdEntity, "pfdStaticAirTemperature");
velocityVectorProperty.getValue(viewer.clock.currentTime, velocityVector);
const modelSpeedMps = Cesium.Cartesian3.magnitude(velocityVector);
const modelSpeedKnots = Math.round(modelSpeedMps * 1.944);
// Is the clock moving forwards
const forward = pfdPrevClock === undefined ? true : Cesium.JulianDate.compare(viewer.clock.currentTime, pfdPrevClock) > 0;
setPFDData(forward, pfdEntity.id, callsign, aircraftType, onSurface, wasOnSurface60SecsAgo, runwayAltitudeEstimate,
modelSpeedKnots, indicatedAirspeed, trueAirspeed, groundspeed, mach, altitude, pfdRadioAltitude, qnh, verticalSpeed, heading, track, roll,
selectedAltitude, selectedHeading, autopilot, verticalMode, lateralMode, tcasMode,
windSpeed, windDirection, staticAirTemperature
);
}
drawPFD();
pfdPrevClock = Cesium.JulianDate.clone(viewer.clock.currentTime, pfdPrevClock);
}
function convertToNearest10Foot(metres) {
return Math.round((metres * 3.28084) / 10.0) * 10.0;
}
function updateRadioAlt() {
if (Cesium.defined(pfdEntity)) {
pfdRadioAltitudeEntity = pfdEntity;
pfdRadioAltPosition = pfdRadioAltitudeEntity.position.getValue(viewer.clock.currentTime, pfdRadioAltPosition);
if (Cesium.defined(pfdRadioAltPosition)) {
if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
pfdRadioAltitude = convertToNearest10Foot(Cesium.Cartographic.fromCartesian(pfdRadioAltPosition).height);
} else {
Promise.resolve(
Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [pfdRadioAltPosition]),
).then((updatedPositions) => {
if (pfdRadioAltitudeEntity === pfdEntity) {
pfdRadioAltitude = convertToNearest10Foot(Cesium.Cartographic.fromCartesian(updatedPositions[0]).height);
}
});
}
}
}
}
</script>
</body>
</html>

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,10 +1969,13 @@ void MapGUI::applyMap3DSettings(bool reloadMap)
m_polylineMapModel.allUpdated();
}
MapSettings::MapItemSettings *ionosondeItemSettings = getItemSettings("Ionosonde Stations");
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)
if (m_giro) {
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (m_cesium && !m_settings.m_displayMUF) {
}
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;
if (m_giro) {
m_giro->getIndexPeriodically((m_settings.m_displayMUF || m_settings.m_displayfoF2) ? 15 : 0);
if (m_cesium && !m_settings.m_displayfoF2) {
}
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>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>