diff --git a/modules/exoplanets/CMakeLists.txt b/modules/exoplanets/CMakeLists.txt index 169beb4215..4f5a98e8d5 100644 --- a/modules/exoplanets/CMakeLists.txt +++ b/modules/exoplanets/CMakeLists.txt @@ -25,6 +25,7 @@ include(${PROJECT_SOURCE_DIR}/support/cmake/module_definition.cmake) set(HEADER_FILES + datastructure.h exoplanetshelper.h exoplanetsmodule.h rendering/renderableorbitdisc.h @@ -33,6 +34,7 @@ set(HEADER_FILES source_group("Header Files" FILES ${HEADER_FILES}) set(SOURCE_FILES + datastructure.cpp exoplanetshelper.cpp exoplanetsmodule.cpp exoplanetsmodule_lua.inl diff --git a/modules/exoplanets/datastructure.cpp b/modules/exoplanets/datastructure.cpp new file mode 100644 index 0000000000..f752c037dc --- /dev/null +++ b/modules/exoplanets/datastructure.cpp @@ -0,0 +1,337 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr std::string_view _loggerCat = "ExoplanetsModule"; + + // This is the documentation for the data object that is returned from the + // `openspace.exoplanets.systemData` function in the Scripting API. + // + // The data object provides the information needed to create the scene graph nodes + // for an exoplanet system. However, depending on what data is available for the + // specific planets and star, some values may be undefined. + struct [[codegen::Dictionary(ExoplanetSystemData)]] Parameters { + // The identifier of the system, which equals the identifier of the host star + // created based on the star name. + std::string systemId; + + // The position of the host star, expressed in meters. + glm::dvec3 position; + + // The number of known planets in the system that have enough data to be + // visualized. + int numPlanets; + + // The name of the star. + std::string starName; + + // The radius of the star, in meters, if data exists. + std::optional starRadius; + + // An RGB color for the host star, computed from the star's B-V value, if data + // exists. + std::optional starColor [[codegen::color()]]; + + // The effective temperature of the star, in Kelvin, if data exists. + std::optional starTeff; + + // The luminosity for the star, in units of solar luminosities [log10(Solar)], if + // data exists. + std::optional starLuminosity; + + struct Planet { + // An identifier to use for the planet, created based on the planet name. + std::string id; + + // The name of the exoplanet. + std::string name; + + // The radius of the planet, in meters, if data exists. + std::optional starRadius; + + // The semi-major axis of the planet's orbit, in meters. That is, the orbit + // size. This is required to be able to be visualize the planetary orbit. + double semiMajorAxis; + + // The uncertainty of the semi-major axis, given as an asymmetric uncertainty + // with two values: the lower and upper uncertainty for the orbit size. The + // values are given as values relative to the size of the `SemiMajorAxis`. For + // example, the lower (first) value is computed as the lower uncertainty + // value divided by the value for the semi-major axis. + // + // If no value is given, there is no uncertainty and the semi-major axis value + // is exact. + std::optional semiMajorAxisUncertainty; + + // The eccentricity of the planet's orbit. If it does not exist in the data, + // it is given a default value of 0. + double eccentricity; + + // The inclination of the planet's orbit, given as an angle in degrees in the + // range 0-360. If it does not exist in the data, it is given a default value + // of 90 degrees. + double inclination; + + // The longitude of ascending node, that is, the angle for the direction of + // the point where the orbit passes through a reference plane. Also + // known as the right ascension of the ascending node. The angle is given as + // degrees in the range 0-360. If it does not exist in the data, it is given + // a default value of 180 degrees. + double ascendingNode; + + // The argument of periapsis of the orbit, given as an angle in degrees in + // the range 0-360. It is the angle between the planet's periapsis (the point + // where it is closest to its star) and its ascending node, that is, where it + // passes through its reference plane. If it does not exist in the data, it + // is given a default value of 90 degrees. + double argumentOfPeriapsis; + + // The epoch to use when computing the planet's initial position in its orbit, + // given as an ISO 8601-like date string of the format YYYY-MM-DDTHH:MN:SS. + // + // Computed based on the \"Time of Conjunction (Transit Midpoint)\" value in + // the data from the exoplanet archive. + std::string epoch; + + // Time the planet takes to make a complete orbit around the host star or + // system, in days. + double period; + + // A rotation matrix that represents the rotation of the plane of the orbit. + // It is computed based on the inclination, ascending node, and argument of + // periapsis values. Hence, it may be derived from default values. + glm::dmat3 orbitPlaneRotationMatrix; + + // True, if default values have been used for any of the orbit parameters, due + // to missing data. + bool hasUsedDefaultValues; + }; + + // A list of data tables for the planets in the system (that have enough data to be + // visualized), with data about the planet and its orbit. + std::vector planets; + + // A rotation matrix to use for the entire system's rotation. Broadly speaking, it + // rotates the system so that the reference plane is perpendicular to the + // line-of-sight from Earth (and so that a 90 degree orbit inclination leads to + // an orbit that passes in front of its star, relative to the line-of-sight). + glm::dmat3 systemRotation; + + // A rotation matrix that represents the average of all the orbit planes in the + // system. Computed using the planets' average inclinations, ascending nodes, and + // arguments of periapsis. + glm::dmat3 meanOrbitRotation; + + // The distance from this system to our solar system, in light years. + double distance; + }; +#include "datastructure_codegen.cpp" +} // namespace + +namespace openspace::exoplanets { + +ghoul::Dictionary ExoplanetSystem::toDataDictionary() const { + ghoul_assert( + planetNames.size() == planetsData.size(), + "The length of the planet names list must match the planet data list" + ); + + ghoul::Dictionary res; + + res.setValue("SystemId", makeIdentifier(starName)); + + const glm::dvec3 starPosInParsec = static_cast(starData.position); + + if (!isValidPosition(starPosInParsec)) { + LERROR(std::format( + "Insufficient data available for exoplanet system '{}'. Could not " + "determine star position", starName + )); + return ghoul::Dictionary(); + } + + const glm::dvec3 starPos = starPosInParsec * distanceconstants::Parsec; + res.setValue("Position", starPos); + + res.setValue("NumPlanets", static_cast(planetNames.size())); + + std::string sanitizedStarName = starName; + sanitizeNameString(sanitizedStarName); + res.setValue("StarName", sanitizedStarName); + + if (!std::isnan(starData.radius)) { + double radiusInMeter = distanceconstants::SolarRadius * starData.radius; + res.setValue("StarRadius", radiusInMeter); + } + + if (!std::isnan(starData.bv)) { + res.setValue("StarColor", glm::dvec3(computeStarColor(starData.bv))); + } + + if (!std::isnan(starData.teff)) { + res.setValue("StarTeff", static_cast(starData.teff)); + } + + if (!std::isnan(starData.luminosity)) { + res.setValue("StarLuminosity", static_cast(starData.luminosity)); + } + + ghoul::Dictionary planets; + std::vector inclinations; + inclinations.reserve(planetNames.size()); + + for (size_t i = 0; i < planetNames.size(); i++) { + const ExoplanetDataEntry& data = planetsData[i]; + const std::string& name = planetNames[i]; + const std::string id = makeIdentifier(name); + + ghoul::Dictionary planet; + + planet.setValue("Id", id); + planet.setValue("Name", name); + + bool hasUsedDefaultValues = false; + + if (!std::isnan(data.r)) { + double planetRadius = data.r * distanceconstants::JupiterRadius; + planet.setValue("Radius", planetRadius); + } + + // Orbit data + { + planet.setValue( + "SemiMajorAxis", + static_cast(data.a) * distanceconstants::AstronomicalUnit + ); + + bool hasUpperAUncertainty = !std::isnan(data.aUpper); + bool hasLowerAUncertainty = !std::isnan(data.aLower); + + if (hasUpperAUncertainty && hasLowerAUncertainty) { + const float lowerOffset = static_cast(data.aLower / data.a); + const float upperOffset = static_cast(data.aUpper / data.a); + planet.setValue( + "SemiMajorAxisUncertainty", + glm::dvec2(lowerOffset, upperOffset) + ); + } + + if (!std::isnan(data.ecc)) { + planet.setValue("Eccentricity", static_cast(data.ecc)); + } + else { + planet.setValue("Eccentricity", 0.0); + hasUsedDefaultValues = true; + } + + // KeplerTranslation requires angles in range [0, 360] + auto validAngle = [&hasUsedDefaultValues](float angle, float defaultValue) { + if (std::isnan(angle)) { + hasUsedDefaultValues = true; + return defaultValue; + } + while (angle < 0.f) { + angle += 360.f; + } + while (angle > 360.f) { + angle = -360.f; + } + return angle; + }; + + float inclination = validAngle(data.i, 90.f); + float bigOmega = validAngle(data.bigOmega, 180.f); + float omega = validAngle(data.omega, 90.f); + + inclinations.push_back(inclination); + + planet.setValue("Inclination", static_cast(inclination)); + planet.setValue("AscendingNode", static_cast(bigOmega)); + planet.setValue("ArgumentOfPeriapsis", static_cast(omega)); + + std::string sEpoch; + if (!std::isnan(data.tt)) { + Time epoch; + epoch.setTime("JD " + std::to_string(data.tt)); + sEpoch = std::string(epoch.ISO8601()); + } + else { + hasUsedDefaultValues = true; + sEpoch = "2009-05-19T07:11:34.080"; + } + planet.setValue("Epoch", sEpoch); + planet.setValue("Period", data.per); + + const glm::dmat4 rotation = computeOrbitPlaneRotationMatrix( + inclination, + bigOmega, + omega + ); + const glm::dmat3 rotationMat3 = static_cast(rotation); + planet.setValue("OrbitPlaneRotationMatrix", rotationMat3); + + planet.setValue("HasUsedDefaultValues", hasUsedDefaultValues); + } + + planets.setValue(id, planet); + } + + res.setValue("Planets", planets); + + // Extra stuff that is useful for rendering + const glm::dmat3 exoplanetSystemRotation = computeSystemRotation(starPos); + res.setValue("SystemRotation", exoplanetSystemRotation); + + float meanInclination = 0.f; + for (const float& i : inclinations) { + meanInclination += i / static_cast(inclinations.size()); + } + const glm::dmat4 rotation = computeOrbitPlaneRotationMatrix(meanInclination); + const glm::dmat3 meanOrbitPlaneRotationMatrix = static_cast(rotation); + + res.setValue("MeanOrbitRotation", meanOrbitPlaneRotationMatrix); + + double distanceToOurSystem = glm::length(starPosInParsec) * + distanceconstants::Parsec / distanceconstants::LightYear; + + res.setValue("Distance", distanceToOurSystem); + + return res; +} + +documentation::Documentation ExoplanetSystem::Documentation() { + return codegen::doc("exoplanets_exoplanet_system_data"); +} + +} // namespace openspace::exoplanets diff --git a/modules/exoplanets/datastructure.h b/modules/exoplanets/datastructure.h new file mode 100644 index 0000000000..be3e6c2ea1 --- /dev/null +++ b/modules/exoplanets/datastructure.h @@ -0,0 +1,155 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_EXOPLANETS___DATASTRUCTURE___H__ +#define __OPENSPACE_MODULE_EXOPLANETS___DATASTRUCTURE___H__ + +#include +#include +#include +#include +#include +#include + +namespace openspace::exoplanets { + +struct ExoplanetDataEntry { + /// Orbital semi-major axis in AU + float a = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of orbital semi-major axis + double aUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of orbital semi-major axis + double aLower = std::numeric_limits::quiet_NaN(); + + /// Longitude of ascending node in degrees + float bigOmega = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of longitude of ascending node + float bigOmegaUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of longitude of ascending node + float bigOmegaLower = std::numeric_limits::quiet_NaN(); + + /// Star known to be binary? + bool binary = false; + + /// Star B − V color + float bmv = std::numeric_limits::quiet_NaN(); + + /// Orbital eccentricity + float ecc = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of orbital eccentricity + float eccUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of orbital eccentricity + float eccLower = std::numeric_limits::quiet_NaN(); + + /// Orbital inclination in degrees (for transiting systems only) + float i = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of orbital inclination + float iUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of orbital inclination + float iLower = std::numeric_limits::quiet_NaN(); + + /// Number of known planets in the planetary system + int nPlanets = std::numeric_limits::quiet_NaN(); + + /// Number of stars in the planetary system + int nStars = std::numeric_limits::quiet_NaN(); + + /// Argument of periastron in degrees + float omega = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of argument of periastron + float omegaUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of argument of periastron + float omegaLower = std::numeric_limits::quiet_NaN(); + + /// Orbital period in days + double per = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of period + float perUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of period + float perLower = std::numeric_limits::quiet_NaN(); + + /// Radius of the planet in Jupiter radii + double r = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of radius of the planet + double rUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of radius of the planet + double rLower = std::numeric_limits::quiet_NaN(); + + /// Estimated radius of the star in solar radii + float rStar = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of estimated star radius + float rStarUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of estimated star radius + float rStarLower = std::numeric_limits::quiet_NaN(); + + /// Star luminosity, in units of solar luminosities + float luminosity = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of star luminosity + float luminosityUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of star luminosity + float luminosityLower = std::numeric_limits::quiet_NaN(); + + /// Star's effective temperature in Kelvin + float teff = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of effective temperature + float teffUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of effective temperature + float teffLower = std::numeric_limits::quiet_NaN(); + + /// Epoch of transit center in HJD-2440000 + double tt = std::numeric_limits::quiet_NaN(); + /// Upper uncertainty of epoch of transit center + float ttUpper = std::numeric_limits::quiet_NaN(); + /// Lower uncertainty of epoch of transit center + float ttLower = std::numeric_limits::quiet_NaN(); + + /// Star position's X-coordinate in parsec + float positionX = std::numeric_limits::quiet_NaN(); + /// Star position's Y-coordinate in parsec + float positionY = std::numeric_limits::quiet_NaN(); + /// Star position's Z-coordinate in parsec + float positionZ = std::numeric_limits::quiet_NaN(); +}; + +struct StarData { + glm::vec3 position = glm::vec3(std::numeric_limits::quiet_NaN()); // In parsec + float radius = std::numeric_limits::quiet_NaN(); // In solar radii + float bv = std::numeric_limits::quiet_NaN(); + float teff = std::numeric_limits::quiet_NaN(); // In Kelvin + float luminosity = std::numeric_limits::quiet_NaN(); // In solar luminosities +}; + +struct ExoplanetSystem { + std::string starName; + StarData starData; + std::vector planetNames; + std::vector planetsData; + + ghoul::Dictionary toDataDictionary() const; + static documentation::Documentation Documentation(); +}; + +} // namespace openspace::exoplanets + +#endif // __OPENSPACE_MODULE_EXOPLANETS___DATASTRUCTURE___H__ diff --git a/modules/exoplanets/exoplanetshelper.cpp b/modules/exoplanets/exoplanetshelper.cpp index ff8c5287bc..0504aa700d 100644 --- a/modules/exoplanets/exoplanetshelper.cpp +++ b/modules/exoplanets/exoplanetshelper.cpp @@ -24,12 +24,13 @@ #include +#include #include #include #include +#include #include #include -#include #include #include #include diff --git a/modules/exoplanets/exoplanetshelper.h b/modules/exoplanets/exoplanetshelper.h index 9af3a0c0d4..b1f99e2210 100644 --- a/modules/exoplanets/exoplanetshelper.h +++ b/modules/exoplanets/exoplanetshelper.h @@ -26,124 +26,13 @@ #define __OPENSPACE_MODULE_EXOPLANETS___EXOPLANETSHELPER___H__ #include -#include +#include #include -#include namespace openspace::exoplanets { -struct ExoplanetDataEntry { - /// Orbital semi-major axis in AU - float a = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of orbital semi-major axis - double aUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of orbital semi-major axis - double aLower = std::numeric_limits::quiet_NaN(); - - /// Longitude of ascending node in degrees - float bigOmega = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of longitude of ascending node - float bigOmegaUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of longitude of ascending node - float bigOmegaLower = std::numeric_limits::quiet_NaN(); - - /// Star known to be binary? - bool binary = false; - - /// Star B − V color - float bmv = std::numeric_limits::quiet_NaN(); - - /// Orbital eccentricity - float ecc = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of orbital eccentricity - float eccUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of orbital eccentricity - float eccLower = std::numeric_limits::quiet_NaN(); - - /// Orbital inclination in degrees (for transiting systems only) - float i = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of orbital inclination - float iUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of orbital inclination - float iLower = std::numeric_limits::quiet_NaN(); - - /// Number of known planets in the planetary system - int nPlanets = std::numeric_limits::quiet_NaN(); - - /// Number of stars in the planetary system - int nStars = std::numeric_limits::quiet_NaN(); - - /// Argument of periastron in degrees - float omega = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of argument of periastron - float omegaUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of argument of periastron - float omegaLower = std::numeric_limits::quiet_NaN(); - - /// Orbital period in days - double per = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of period - float perUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of period - float perLower = std::numeric_limits::quiet_NaN(); - - /// Radius of the planet in Jupiter radii - double r = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of radius of the planet - double rUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of radius of the planet - double rLower = std::numeric_limits::quiet_NaN(); - - /// Estimated radius of the star in solar radii - float rStar = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of estimated star radius - float rStarUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of estimated star radius - float rStarLower = std::numeric_limits::quiet_NaN(); - - /// Star luminosity, in units of solar luminosities - float luminosity = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of star luminosity - float luminosityUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of star luminosity - float luminosityLower = std::numeric_limits::quiet_NaN(); - - /// Star's effective temperature in Kelvin - float teff = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of effective temperature - float teffUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of effective temperature - float teffLower = std::numeric_limits::quiet_NaN(); - - /// Epoch of transit center in HJD-2440000 - double tt = std::numeric_limits::quiet_NaN(); - /// Upper uncertainty of epoch of transit center - float ttUpper = std::numeric_limits::quiet_NaN(); - /// Lower uncertainty of epoch of transit center - float ttLower = std::numeric_limits::quiet_NaN(); - - /// Star position's X-coordinate in parsec - float positionX = std::numeric_limits::quiet_NaN(); - /// Star position's Y-coordinate in parsec - float positionY = std::numeric_limits::quiet_NaN(); - /// Star position's Z-coordinate in parsec - float positionZ = std::numeric_limits::quiet_NaN(); -}; - -struct StarData { - glm::vec3 position = glm::vec3(std::numeric_limits::quiet_NaN()); // In parsec - float radius = std::numeric_limits::quiet_NaN(); // In solar radii - float bv = std::numeric_limits::quiet_NaN(); - float teff = std::numeric_limits::quiet_NaN(); // In Kelvin - float luminosity = std::numeric_limits::quiet_NaN(); // In solar luminosities -}; - -struct ExoplanetSystem { - std::string starName; - StarData starData; - std::vector planetNames; - std::vector planetsData; -}; +struct ExoplanetDataEntry; +struct StarData; bool isValidPosition(const glm::vec3& pos); diff --git a/modules/exoplanets/exoplanetsmodule.cpp b/modules/exoplanets/exoplanetsmodule.cpp index f27abf7ccc..ad56c50dd0 100644 --- a/modules/exoplanets/exoplanetsmodule.cpp +++ b/modules/exoplanets/exoplanetsmodule.cpp @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -411,20 +412,24 @@ void ExoplanetsModule::internalInitialize(const ghoul::Dictionary& dict) { std::vector ExoplanetsModule::documentations() const { return { ExoplanetsDataPreparationTask::documentation(), - RenderableOrbitDisc::Documentation() + RenderableOrbitDisc::Documentation(), + ExoplanetSystem::Documentation() }; } scripting::LuaLibrary ExoplanetsModule::luaLibrary() const { return { - "exoplanets", - { - codegen::lua::AddExoplanetSystem, + .name = "exoplanets", + .functions = { codegen::lua::RemoveExoplanetSystem, + codegen::lua::SystemData, codegen::lua::ListOfExoplanets, codegen::lua::ListOfExoplanetsDeprecated, codegen::lua::ListAvailableExoplanetSystems, - codegen::lua::LoadExoplanetsFromCsv + codegen::lua::LoadSystemDataFromCsv + }, + .scripts = { + absPath("${MODULE_EXOPLANETS}/scripts/systemcreation.lua") } }; } diff --git a/modules/exoplanets/exoplanetsmodule_lua.inl b/modules/exoplanets/exoplanetsmodule_lua.inl index 29e555d34c..8338b2d70e 100644 --- a/modules/exoplanets/exoplanetsmodule_lua.inl +++ b/modules/exoplanets/exoplanetsmodule_lua.inl @@ -22,6 +22,9 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ +#include +#include +#include #include #include #include @@ -34,18 +37,7 @@ namespace { constexpr std::string_view _loggerCat = "ExoplanetsModule"; -constexpr std::string_view ExoplanetsGuiPath = "/Milky Way/Exoplanets/Exoplanet Systems/"; - -// Lua cannot handle backslashes, so replace these with forward slashes -std::string formatPathToLua(const std::filesystem::path& path) { - std::string resPath = path.string(); - std::replace(resPath.begin(), resPath.end(), '\\', '/'); - return resPath; -} - -openspace::exoplanets::ExoplanetSystem findExoplanetSystemInData( - std::string_view starName) -{ +openspace::exoplanets::ExoplanetSystem findSystemInData(std::string_view starName) { using namespace openspace; using namespace exoplanets; @@ -54,15 +46,17 @@ openspace::exoplanets::ExoplanetSystem findExoplanetSystemInData( const std::filesystem::path binPath = module->exoplanetsDataPath(); std::ifstream data(absPath(binPath), std::ios::in | std::ios::binary); if (!data.good()) { - LERROR(std::format("Failed to open exoplanets data file '{}'", binPath)); - return ExoplanetSystem(); + throw ghoul::lua::LuaError(std::format( + "Failed to open exoplanets data file '{}'", binPath + )); } const std::filesystem::path lutPath = module->lookUpTablePath(); std::ifstream lut(absPath(lutPath)); if (!lut.good()) { - LERROR(std::format("Failed to open exoplanets look-up table '{}'", lutPath)); - return ExoplanetSystem(); + throw ghoul::lua::LuaError(std::format( + "Failed to open exoplanets look-up table '{}'", lutPath + )); } ExoplanetSystem system; @@ -105,596 +99,6 @@ openspace::exoplanets::ExoplanetSystem findExoplanetSystemInData( return system; } -void queueAddSceneGraphNodeScript(const std::string& sgnTableAsString) { - using namespace openspace; - // No sync or send because this will already be called inside a Lua script, - // therefor it has already been synced and sent to the connected nodes and peers - global::scriptEngine->queueScript({ - .code = std::format("openspace.addSceneGraphNode({})", sgnTableAsString), - .synchronized = scripting::ScriptEngine::Script::ShouldBeSynchronized::No, - .sendToRemote = scripting::ScriptEngine::Script::ShouldSendToRemote::No - }); -} - -void createExoplanetSystem(const std::string& starName, - openspace::exoplanets::ExoplanetSystem system) -{ - using namespace openspace; - using namespace exoplanets; - - const std::string starIdentifier = makeIdentifier(starName); - - std::string sanitizedStarName = starName; - sanitizeNameString(sanitizedStarName); - - const std::string guiPath = std::format("{}{}", ExoplanetsGuiPath, sanitizedStarName); - - SceneGraphNode* existingStarNode = sceneGraphNode(starIdentifier); - if (existingStarNode) { - LERROR(std::format( - "Adding of exoplanet system '{}' failed. The system has already been added", - starName - )); - return; - } - - const glm::vec3 starPosInParsec = system.starData.position; - if (!isValidPosition(starPosInParsec)) { - LERROR(std::format( - "Insufficient data available for exoplanet system '{}'. Could not determine " - "star position", starName - )); - return; - } - - const ExoplanetsModule* module = global::moduleEngine->module(); - - const glm::dvec3 starPos = - static_cast(starPosInParsec) * distanceconstants::Parsec; - const glm::dmat3 exoplanetSystemRotation = computeSystemRotation(starPos); - - // Star - double radiusInMeter = distanceconstants::SolarRadius; - if (!std::isnan(system.starData.radius)) { - radiusInMeter *= system.starData.radius; - } - - std::string colorLayers; - std::optional starColor = std::nullopt; - const float bv = system.starData.bv; - - if (!std::isnan(bv)) { - starColor = computeStarColor(bv); - const std::filesystem::path starTexture = module->starTexturePath(); - - if (!starTexture.empty() && !std::filesystem::is_regular_file(starTexture)) { - LWARNING(std::format( - "Could not find specified star texture set in {} module: '{}'", - module->guiName(), starTexture - )); - } - - colorLayers = - "{" - "Identifier = 'StarColor'," - "Type = 'SolidColor'," - "Color = " + ghoul::to_string(*starColor) + "," - "BlendMode = 'Normal'," - "Enabled = true" - "}," - "{" - "Identifier = 'StarTexture'," - "FilePath = openspace.absPath('" + formatPathToLua(starTexture) + "')," - "BlendMode = 'Color'," - "Enabled = true" - "}"; - } - else { - const std::filesystem::path noDataTexture = module->noDataTexturePath(); - colorLayers = - "{" - "Identifier = 'NoDataStarTexture'," - "FilePath = openspace.absPath('" + formatPathToLua(noDataTexture) + "')," - "BlendMode = 'Color'," - "Enabled = true" - "}"; - } - - // @TODO (2024-09-16, emmbr) Compose a more extensive summary of the system, - // based on more data in the archive (like number of stars) and the planets. - // Also include more data on the star itself (like spectral type) - float distanceToOurSystem = glm::length(starPosInParsec) * - distanceconstants::Parsec / distanceconstants::LightYear; - - size_t nPlanets = system.planetNames.size(); - const std::string starDescription = std::format( - "The star {} is the host star of an exoplanet system with {} {} that {} " - "enough data to be visualized. It has a size of {:.2f} solar radii and an " - "effective temperature of {:.0f} Kelvin. The system is located at a " - "distance of {:.0f} light-years from Earth.", - sanitizedStarName, nPlanets, - nPlanets > 1 ? "planets" : "planet", - nPlanets > 1 ? "have" : "has", - radiusInMeter / distanceconstants::SolarRadius, - system.starData.teff, distanceToOurSystem - ); - - const std::string starGlobeRenderableString = "Renderable = {" - "Type = 'RenderableGlobe'," - "Radii = " + std::to_string(radiusInMeter) + "," - "PerformShading = false," - "Layers = {" - "ColorLayers = { " + colorLayers + "}" - "}" - "},"; - - const std::string starParent = "{" - "Identifier = '" + starIdentifier + "'," - "Parent = 'SolarSystemBarycenter'," - "" + starGlobeRenderableString + "" - "Transform = {" - "Rotation = {" - "Type = 'StaticRotation'," - "Rotation = " + ghoul::to_string(exoplanetSystemRotation) + "" - "}," - "Translation = {" - "Type = 'StaticTranslation'," - "Position = " + ghoul::to_string(starPos) + "" - "}" - "}," - "Tag = {'exoplanet_system'}," - "GUI = {" - "Name = '" + sanitizedStarName + " (Star)'," - "Path = '" + guiPath + "'," - "Description = \"" + starDescription + "\"" - "}" - "}"; - - queueAddSceneGraphNodeScript(starParent); - - // Add a label for the star. - // The fade values are set based on the values for the Sun label - const std::string starLabel = "{" - "Identifier = '" + starIdentifier + "_Label'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderableLabel'," - "Enabled = false," - "Text = '" + sanitizedStarName + "'," - "FontSize = 70.0," - "Size = 14.17," - "MinMaxSize = { 1, 50 }," - "EnableFading = true," - "FadeUnit = 'pc'," - "FadeDistances = { 1.33, 15.0 }," - "FadeWidths = {1.0, 20.0}" - "}," - "Tag = {'exoplanet_system_labels'}," - "GUI = {" - "Name = '" + sanitizedStarName + " Label'," - "Path = '" + guiPath + "'" - "}" - "}"; - - queueAddSceneGraphNodeScript(starLabel); - - // Planets - - const std::filesystem::path planetTexture = module->planetDefaultTexturePath(); - if (!planetTexture.empty() && !std::filesystem::is_regular_file(planetTexture)) { - LWARNING(std::format( - "Could not find specified planet default texture set in {} module: '{}'", - module->guiName(), planetTexture - )); - } - - for (size_t i = 0; i < system.planetNames.size(); i++) { - // Note that we are here overriding some invalid parameters in the planet data. - // Use a reference, so that it is changed down the line - ExoplanetDataEntry& planet = system.planetsData[i]; - const std::string planetName = system.planetNames[i]; - - if (std::isnan(planet.ecc)) { - planet.ecc = 0.f; - } - - // KeplerTranslation requires angles in range [0, 360] - auto validAngle = [](float angle, float defaultValue) { - if (std::isnan(angle)) { return defaultValue; } - if (angle < 0.f) { return angle + 360.f; } - if (angle > 360.f) { return angle - 360.f; } - return angle; - }; - - planet.i = validAngle(planet.i, 90.f); - planet.bigOmega = validAngle(planet.bigOmega, 180.f); - planet.omega = validAngle(planet.omega, 90.f); - - Time epoch; - std::string sEpoch; - if (!std::isnan(planet.tt)) { - epoch.setTime("JD " + std::to_string(planet.tt)); - sEpoch = std::string(epoch.ISO8601()); - } - else { - sEpoch = "2009-05-19T07:11:34.080"; - } - - double planetRadius; - std::string enabled; - if (std::isnan(planet.r)) { - if (std::isnan(planet.rStar)) { - planetRadius = planet.a * 0.001 * distanceconstants::AstronomicalUnit; - } - else { - planetRadius = planet.rStar * 0.1 * distanceconstants::SolarRadius; - } - enabled = "false"; - } - else { - planetRadius = planet.r * distanceconstants::JupiterRadius; - enabled = "true"; - } - - float periodInSeconds = static_cast(planet.per * SecondsPerDay); - double semiMajorAxisInMeter = planet.a * distanceconstants::AstronomicalUnit; - double semiMajorAxisInKm = semiMajorAxisInMeter * 0.001; - - const std::string planetIdentifier = makeIdentifier(planetName); - - const std::string planetKeplerTranslation = "{" - "Type = 'KeplerTranslation'," - "Eccentricity = " + std::to_string(planet.ecc) + "," - "SemiMajorAxis = " + std::to_string(semiMajorAxisInKm) + "," - "Inclination = " + std::to_string(planet.i) + "," - "AscendingNode = " + std::to_string(planet.bigOmega) + "," - "ArgumentOfPeriapsis = " + std::to_string(planet.omega) + "," - "MeanAnomaly = 0.0," - "Epoch = '" + sEpoch + "'," //TT. JD to YYYY MM DD hh:mm:ss - "Period = " + std::to_string(periodInSeconds) + "" - "}"; - - std::string planetLayers; - std::string planetTypeDesc; - - // Constant for different categories of sizes of planets (in Earth radii) - // Source: https://www.nasa.gov/image-article/sizes-of-known-exoplanets/ - constexpr float TerrestrialMaxR = 1.25f; - constexpr float SuperEarthMaxR = 2.f; - constexpr float NeptuneLikeMaxR = 6.f; - - const std::string TerrestrialDesc = std::format( - "Terrestrial planets (R < {} Earth radii)", - TerrestrialMaxR - ); - - const std::string SuperEarthDesc = std::format( - "Super-Earths ({} < R < {} Earth radii)", - TerrestrialMaxR, SuperEarthMaxR - ); - - const std::string NeptuneLikeDesc = std::format( - "Neptune-like planets ({} < R < {} Earth radii)", - SuperEarthMaxR, NeptuneLikeMaxR - ); - - const std::string GasGiantDesc = std::format( - "Gas giants or larger planets (R > {} Earth radii)", - NeptuneLikeMaxR - ); - - // Add a color layer with a fixed single color that represent the planet size, - // that is, approximately what type of planet it is. - // @TODO (2024-09-10, emmbr) Ideally the user should be able to edit this - if (!std::isnan(planet.r)) { - float rInMeter = static_cast(planetRadius); - glm::vec3 colorFromSize = glm::vec3(0.f); - - if (rInMeter < TerrestrialMaxR * distanceconstants::EarthRadius) { - // Terrestrial - colorFromSize = glm::vec3(0.32f, 0.2f, 0.1f); // Brown - planetTypeDesc = TerrestrialDesc; - } - else if (rInMeter < SuperEarthMaxR * distanceconstants::EarthRadius) { - // Super-Earths - colorFromSize = glm::vec3(1.f, 0.76f, 0.65f); // Beige - planetTypeDesc = SuperEarthDesc; - } - else if (rInMeter < NeptuneLikeMaxR * distanceconstants::EarthRadius) { - // Neptune-like - colorFromSize = glm::vec3(0.22f, 0.49f, 0.50f); // Blue - planetTypeDesc = NeptuneLikeDesc; - } - else { - // Gas Giants (Saturn and Jupiter size, and much larger!) - colorFromSize = glm::vec3(0.55f, 0.34f, 0.39f); // Wine red - planetTypeDesc = GasGiantDesc; - } - - const std::string description = std::format( - "This layer gives a fixed color to the planet surface based on the " - "planet radius. The planets are split into four categories based on " - "their radius (in Earth radii). 1) {} are Brown, 2) {} are Beige, 3) " - "{} are Blue, and 4) {} are Wine red.", - TerrestrialDesc, SuperEarthDesc, NeptuneLikeDesc, GasGiantDesc - ); - - planetLayers += "{" - "Identifier = 'ColorFromSize'," - "Name = 'Color From Size'," - "Type = 'SolidColor'," - "Color = " + ghoul::to_string(colorFromSize) + "," - "Enabled = true," - "Description = \"" + description + "\"" - "}"; - } - - if (!planetTexture.empty()) { - planetLayers += ",{" - "Identifier = 'PlanetTexture'," - "Name = 'Planet Texture'," - "FilePath = openspace.absPath('" + formatPathToLua(planetTexture) + "')," - "Enabled = true" - "}"; - } - - const std::string planetDescription = std::format( - "The exoplanet {} falls into the category of {}. Some key facts: " - "Radius: {:.2f} Earth radii, {:.2f} Jupiter radii. " - "Orbit Period: {:.1f} (Earth) days. " - "Orbit Semi-major axis: {:.2f} (AU). " - "Orbit Eccentricity: {:.2f}.", - planetName, planetTypeDesc, - planetRadius / distanceconstants::EarthRadius, - planetRadius / distanceconstants::JupiterRadius, - planet.per, planet.a, planet.ecc - ); - - // Use a higher ambient intensity when only the color from size is used, so that - // the color is more clearly visible from any direction - float ambientIntensity = planetTexture.empty() ? 0.5f : 0.15f; - - const std::string planetNode = "{" - "Identifier = '" + planetIdentifier + "'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderableGlobe'," - "Enabled = " + enabled + "," - "Radii = " + std::to_string(planetRadius) + "," // in meters - "PerformShading = true," - "Layers = {" - "ColorLayers = {" + planetLayers + "}" - "}," - "LightSourceNode = '" + starIdentifier + "'," - "AmbientIntensity = " + std::to_string(ambientIntensity) + - "}," - "Tag = { 'exoplanet_planet' }, " - "Transform = { " - "Translation = " + planetKeplerTranslation + "" - "}," - "GUI = {" - "Name = '" + planetName + "'," - "Path = '" + guiPath + "'," - "Description = [[" + planetDescription + "]]" - "}" - "}"; - - int trailResolution = 1000; - - // Increase the resolution for highly eccentric orbits - constexpr float EccentricityThreshold = 0.85f; - if (planet.ecc > EccentricityThreshold) { - trailResolution *= 2; - } - - const std::string planetTrailNode = "{" - "Identifier = '" + planetIdentifier + "_Trail'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderableTrailOrbit'," - "Period = " + std::to_string(planet.per) + "," - "Resolution = " + std::to_string(trailResolution) + "," - "Translation = " + planetKeplerTranslation + "," - "Color = { 1, 1, 1 }" - "}," - "GUI = {" - "Name = '" + planetName + " Trail'," - "Path = '" + guiPath + "'" - "}" - "}"; - - queueAddSceneGraphNodeScript(planetTrailNode); - queueAddSceneGraphNodeScript(planetNode); - - bool hasUpperAUncertainty = !std::isnan(planet.aUpper); - bool hasLowerAUncertainty = !std::isnan(planet.aLower); - - if (hasUpperAUncertainty && hasLowerAUncertainty) { - const glm::dmat4 rotation = computeOrbitPlaneRotationMatrix( - planet.i, - planet.bigOmega, - planet.omega - ); - const glm::dmat3 rotationMat3 = static_cast(rotation); - - const float lowerOffset = static_cast(planet.aLower / planet.a); - const float upperOffset = static_cast(planet.aUpper / planet.a); - - const std::filesystem::path discTexture = module->orbitDiscTexturePath(); - - bool isDiscEnabled = module->showOrbitUncertainty(); - - const std::string discNode = "{" - "Identifier = '" + planetIdentifier + "_Disc'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderableOrbitDisc'," - "Enabled = " + (isDiscEnabled ? "true" : "false") + "," - "Texture = openspace.absPath('" + - formatPathToLua(discTexture) + - "')," - "Size = " + std::to_string(semiMajorAxisInMeter) + "," - "Eccentricity = " + std::to_string(planet.ecc) + "," - "Offset = { " + - std::to_string(lowerOffset) + ", " + - std::to_string(upperOffset) + - "}," //min / max extend - "Opacity = 0.25" - "}," - "Transform = {" - "Rotation = {" - "Type = 'StaticRotation'," - "Rotation = " + ghoul::to_string(rotationMat3) + "" - "}" - "}," - "Tag = {'exoplanet_uncertainty_disc'}," - "GUI = {" - "Name = '" + planetName + " Disc'," - "Path = '" + guiPath + "'," - "Description = \"The width of this disc around the planet's orbit " - "marks the uncertainty of the orbit (based on the uncertainty of " - "the semi-major axis and eccentricity measures). The wider the " - "disc, the more uncertain the orbit is.\"" - "}" - "}"; - - queueAddSceneGraphNodeScript(discNode); - } - } - - float meanInclination = 0.f; - for (const ExoplanetDataEntry& p : system.planetsData) { - meanInclination += p.i; - } - meanInclination /= static_cast(system.planetsData.size()); - const glm::dmat4 rotation = computeOrbitPlaneRotationMatrix(meanInclination); - const glm::dmat3 meanOrbitPlaneRotationMatrix = static_cast(rotation); - - bool isCircleEnabled = module->showComparisonCircle(); - glm::vec3 circleColor = module->comparisonCircleColor(); - - // 1 AU Size Comparison Circle - const std::string circle = "{" - "Identifier = '" + starIdentifier + "_1AU_Circle'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderableRadialGrid'," - "Enabled = " + (isCircleEnabled ? "true" : "false") + "," - "Radii = { 0.0, 1.0 }," - "Color = " + ghoul::to_string(circleColor) + "," - "CircleSegments = 64," - "LineWidth = 2.0," - "}," - "Transform = {" - "Rotation = {" - "Type = 'StaticRotation'," - "Rotation = " + ghoul::to_string(meanOrbitPlaneRotationMatrix) + "" - "}," - "Scale = {" - "Type = 'StaticScale'," - "Scale = " + std::to_string(distanceconstants::AstronomicalUnit) + "" - "}" - "}," - "Tag = {'exoplanet_1au_ring'}," - "GUI = {" - "Name = '1 AU Size Comparison Circle'," - "Path = '" + guiPath + "'," - "Description = \"A circle with a radius of 1 Astronomical Unit. That is, its " - "size corresponds to the size of Earth's orbit.\"" - "}" - "}"; - - queueAddSceneGraphNodeScript(circle); - - // Habitable Zone - bool hasTeff = !std::isnan(system.starData.teff); - bool hasLuminosity = !std::isnan(system.starData.luminosity); - - if (hasTeff && hasLuminosity) { - constexpr std::string_view description = - "The habitable zone is the region around a star in which an Earth-like " - "planet can potentially have liquid water on its surface." - "

" - "The inner boundary is where the greenhouse gases in the atmosphere " - "would trap any incoming infrared radiation, leading to the planet " - "surface becoming so hot that water boils away. The outer boundary is where " - "the greenhouse effect would not be able to maintain surface temperature " - "above freezing anywhere on the planet"; - - const std::filesystem::path hzTexture = module->habitableZoneTexturePath(); - bool isHzEnabled = module->showHabitableZone(); - bool useOptimistic = module->useOptimisticZone(); - float opacity = module->habitableZoneOpacity(); - - const std::string zoneDiscNode = "{" - "Identifier = '" + starIdentifier + "_HZ_Disc'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderableHabitableZone'," - "Enabled = " + (isHzEnabled ? "true" : "false") + "," - "Texture = openspace.absPath('" + formatPathToLua(hzTexture) + "')," - "Luminosity = " + std::to_string(system.starData.luminosity) + "," - "EffectiveTemperature = " + std::to_string(system.starData.teff) + "," - "Optimistic = " + (useOptimistic ? "true" : "false") + "," - "Opacity = " + std::to_string(opacity) + "" - "}," - "Transform = {" - "Rotation = {" - "Type = 'StaticRotation'," - "Rotation = " + ghoul::to_string(meanOrbitPlaneRotationMatrix) + "" - "}" - "}," - "Tag = {'exoplanet_habitable_zone'}," - "GUI = {" - "Name = '" + starName + " Habitable Zone'," - "Path = '" + guiPath + "'," - "Description = '" + std::string(description) + "'" - "}" - "}"; - - queueAddSceneGraphNodeScript(zoneDiscNode); - - // Star glare - if (starColor.has_value()) { - // This is a little magic to make the size of the glare dependent on the - // size and the temperature of the star. It's kind of based on the fact that - // the luminosity of a star is proportional to: (radius^2)*(temperature^4) - // Maybe a better option would be to compute the size based on the aboslute - // magnitude or star luminosity, but for now this looks good enough. - double size = 59.0 * radiusInMeter; - if (hasTeff) { - constexpr float sunTeff = 5780.f; - size *= std::pow(system.starData.teff / sunTeff, 2.0); - } - - const std::filesystem::path glareTexture = module->starGlareTexturePath(); - - const std::string starGlare = "{" - "Identifier = '" + starIdentifier + "_Glare'," - "Parent = '" + starIdentifier + "'," - "Renderable = {" - "Type = 'RenderablePlaneImageLocal'," - "Size = " + ghoul::to_string(size) + "," - "Origin = 'Center'," - "Billboard = true," - "Texture = openspace.absPath('" - + formatPathToLua(glareTexture) + - "')," - "BlendMode = 'Additive'," - "Opacity = 0.65," - "MultiplyColor = " + ghoul::to_string(*starColor) + "" - "}," - "GUI = {" - "Name = '" + sanitizedStarName + " Glare'," - "Path = '" + guiPath + "'" - "}" - "}"; - - queueAddSceneGraphNodeScript(starGlare); - } - } -} - std::vector hostStarsWithSufficientData() { using namespace openspace; using namespace exoplanets; @@ -702,22 +106,21 @@ std::vector hostStarsWithSufficientData() { if (!module->hasDataFiles()) { // If no data file path has been configured at all, we just bail out early here - LINFO("No data path was configured for the exoplanets"); - return {}; + throw ghoul::lua::LuaError("No data path was configured for the exoplanets"); } const std::filesystem::path lutPath = module->lookUpTablePath(); std::ifstream lookupTableFile(absPath(lutPath)); if (!lookupTableFile.good()) { - LERROR(std::format("Failed to open lookup table file '{}'", lutPath)); - return {}; + throw ghoul::lua::LuaError(std::format( + "Failed to open lookup table file '{}'", lutPath + )); } const std::filesystem::path binPath = module->exoplanetsDataPath(); std::ifstream data(absPath(binPath), std::ios::in | std::ios::binary); if (!data.good()) { - LERROR(std::format("Failed to open data file '{}'", binPath)); - return {}; + throw ghoul::lua::LuaError(std::format("Failed to open data file '{}'", binPath)); } std::vector names; @@ -761,37 +164,34 @@ std::vector hostStarsWithSufficientData() { } /** - * Add one or multiple exoplanet systems to the scene, as specified by the input. An input - * string should be the name of the system host star. + * Return an object containing the information needed to add a specific exoplanet system. + * The data is retrieved from the module's prepared datafile for exoplanets. This file is + * in a binary format, for fast retrieval during runtime. + * + * \param starName The name of the star to get the information for. + * + * \return An object of the type [ExoplanetSystemData](#exoplanets_exoplanet_system_data) + * that can be used to create the scene graph nodes for the exoplanet system */ -[[codegen::luawrap]] void addExoplanetSystem( - std::variant> starNames) -{ - std::vector starsToAdd; +[[codegen::luawrap]] ghoul::Dictionary systemData(std::string starName){ + using namespace openspace; - if (std::holds_alternative(starNames)) { - // The user provided a single name - const std::string starName = std::get(starNames); - starsToAdd.push_back(starName); - } - else { - starsToAdd = std::get>(starNames); - } - - for (const std::string& starName : starsToAdd) { - openspace::exoplanets::ExoplanetSystem systemData = - findExoplanetSystemInData(starName); - - if (systemData.planetsData.empty()) { - LERROR(std::format("Exoplanet system '{}' could not be found", starName)); - return; - } - - createExoplanetSystem(starName, systemData); + exoplanets::ExoplanetSystem systemData = findSystemInData(starName); + + if (systemData.planetsData.empty()) { + throw ghoul::lua::LuaError(std::format( + "Exoplanet system '{}' could not be found", starName + )); } + return systemData.toDataDictionary(); } +/** + * Remove a loaded exoplanet system. + * + * \param starName The name of the host star for the system to remove. + */ [[codegen::luawrap]] void removeExoplanetSystem(std::string starName) { using namespace openspace; using namespace exoplanets; @@ -810,6 +210,8 @@ std::vector hostStarsWithSufficientData() { * Returns a list with names of the host star of all the exoplanet systems * that have sufficient data for generating a visualization, based on the * module's loaded data file. + * + * \return A list of exoplanet host star names. */ [[codegen::luawrap]] std::vector listOfExoplanets() { std::vector names = hostStarsWithSufficientData(); @@ -830,6 +232,10 @@ listOfExoplanetsDeprecated() return listOfExoplanets(); } +/** + * Lists the names of the host stars of all exoplanet systems that have sufficient + * data for generating a visualization, and prints the list to the console. + */ [[codegen::luawrap]] void listAvailableExoplanetSystems() { std::vector names = hostStarsWithSufficientData(); @@ -846,26 +252,24 @@ listOfExoplanetsDeprecated() )); } -/** - * Load a set of exoplanets based on custom data, in the form of a CSV file, and add - * them to the rendering. Can be used to load custom datasets, or more recent planets - * than what are included in the internal data file that is released with OpenSpace. - * - * The format and column names in the CSV sould be the same as the ones provided by the - * NASA Exoplanet Archive. https://exoplanetarchive.ipac.caltech.edu/ - * - * We recommend downloading the file from the Exoplanet Archive's Composite data table, - * where multiple sources are combined into one row per planet. - * https://exoplanetarchive.ipac.caltech.edu - * /cgi-bin/TblView/nph-tblView?app=ExoTbls&config=PSCompPars - * - * Please remember to include all columns in the file download, as missing data columns - * may lead to an incomplete visualization. - * - * Also, avoid loading too large files of planets, as each added system will affect the - * rendering performance. - */ -[[codegen::luawrap]] void loadExoplanetsFromCsv(std::string csvFile) { + /** + * Load a set of exoplanet information based on custom data in the form of a CSV file. + * + * The format and column names in the CSV should be the same as the ones provided by the + * [NASA Exoplanet Archive](https://exoplanetarchive.ipac.caltech.edu/). + * + * When dowloading the data from the archive we recommend including all columns, since a + * few required ones are not selected by default. + * + * \param csvFile A path to the CSV file to load the data from. + * + * \return A list of objects of the type + * [ExoplanetSystemData](#exoplanets_exoplanet_system_data), that can be used to + * create the scene graph nodes for the exoplanet systems + */ +[[codegen::luawrap]] std::vector loadSystemDataFromCsv( + std::string csvFile) +{ using namespace openspace; using namespace exoplanets; @@ -873,8 +277,7 @@ listOfExoplanetsDeprecated() std::ifstream inputDataFile(csvFile); if (!inputDataFile.good()) { - LERROR(std::format("Failed to open input file '{}'", csvFile)); - return; + throw ghoul::lua::LuaError(std::format("Failed to open input file '{}'", csvFile)); } std::vector columnNames = @@ -886,6 +289,8 @@ listOfExoplanetsDeprecated() std::map hostNameToSystemDataMap; + LINFO(std::format("Reading exoplanet data from file '{}'", csvFile)); + // Parse the file line by line to compose system information std::string row; while (ghoul::getline(inputDataFile, row)) { @@ -896,8 +301,6 @@ listOfExoplanetsDeprecated() module->teffToBvConversionFilePath() ); - LINFO(std::format("Reading data for planet '{}'", planetData.name)); - if (!hasSufficientData(planetData.dataEntry)) { LWARNING(std::format( "Insufficient data for exoplanet '{}'", planetData.name @@ -905,6 +308,8 @@ listOfExoplanetsDeprecated() continue; } + LINFO(std::format("Loaded data for planet '{}'", planetData.name)); + auto found = hostNameToSystemDataMap.find(planetData.host); if (found != hostNameToSystemDataMap.end()) { // Found a match. Add the planet to the system data @@ -918,6 +323,7 @@ listOfExoplanetsDeprecated() // No host found. Add a new one ExoplanetSystem system; sanitizeNameString(planetData.name); + system.starName = planetData.host; system.planetNames.push_back(planetData.name); system.planetsData.push_back(planetData.dataEntry); updateStarDataFromNewPlanet(system.starData, planetData.dataEntry); @@ -926,20 +332,13 @@ listOfExoplanetsDeprecated() } } - // Add all the added exoplanet systems - using K = const std::string; - using V = ExoplanetSystem; - for (const std::pair& entry : hostNameToSystemDataMap) { - const std::string& hostName = entry.first; - const ExoplanetSystem& data = entry.second; - createExoplanetSystem(hostName, data); - } + std::vector result; + result.reserve(hostNameToSystemDataMap.size()); - LINFO(std::format( - "Read data for {} exoplanet systems from CSV file: {}. Please wait until " - "they are all finished initializing. You may have to reload the user interface.", - hostNameToSystemDataMap.size(), csvFile - )); + for (auto const& [_, system]: hostNameToSystemDataMap) { + result.push_back(system.toDataDictionary()); + } + return result; } #include "exoplanetsmodule_lua_codegen.cpp" diff --git a/modules/exoplanets/scripts/systemcreation.lua b/modules/exoplanets/scripts/systemcreation.lua new file mode 100644 index 0000000000..e10f4f532b --- /dev/null +++ b/modules/exoplanets/scripts/systemcreation.lua @@ -0,0 +1,641 @@ +openspace.exoplanets.documentation = { + { + Name = "addExoplanetSystem", + Arguments = { + { "starName", "String" } + }, + Documentation = [[ + Add a specific exoplanet system to the scene, based on a star name. + + Note that the formatting of the name must match the one in the dataset. That is, it + must match the name as given in the [NASA Exoplanet Archive](https://exoplanetarchive.ipac.caltech.edu/). + + \\param starName The name of the star + ]] + }, + { + Name = "addExoplanetSystems", + Arguments = { + { "listOfStarNames", "String[]" } + }, + Documentation = [[ + Add multiple exoplanet systems to the scene, based on a list of names. + + Note that the formatting of the name must match the one in the dataset. That is, + they must match the names as given in the [NASA Exoplanet Archive](https://exoplanetarchive.ipac.caltech.edu/). + + \\param listOfStarNames A list of star names for which to create the exoplanet systems + ]] + }, + { + Name = "loadExoplanetsFromCsv", + Arguments = { + { "csvFile", "String" } + }, + Documentation = [[ + Load a set of exoplanets based on custom data, in the form of a CSV file, and add + them to the rendering. Can be used to load custom datasets, or more recent planets + than what are included in the internal data file that is released with OpenSpace. + + The format and column names in the CSV should be the same as the ones provided by + the [NASA Exoplanet Archive](https://exoplanetarchive.ipac.caltech.edu/). + + We recommend downloading the file from the [Exoplanet Archive's Composite data table](https://exoplanetarchive.ipac.caltech.edu/cgi-bin/TblView/nph-tblView?app=ExoTbls&config=PSCompPars), + where multiple sources are combined into one row per planet. + + Please remember to include all columns in the file download, as missing data columns + may lead to an incomplete visualization. + + Also, avoid loading too large files of planets, as each added system will affect the + rendering performance. + + \\param csvFile A path to a .csv file that contains the data for the exoplanets + ]] + } +} + +openspace.exoplanets.addExoplanetSystem = function (starName) + local data = openspace.exoplanets.systemData(starName) + addExoplanetSystem(data) +end + +openspace.exoplanets.addExoplanetSystems = function (listOfStarNames) + for _,starName in pairs(listOflistOfStarNamesNames) do + local data = openspace.exoplanets.systemData(starName) + addExoplanetSystem(data) + end +end + +openspace.exoplanets.loadExoplanetsFromCsv = function (csvFile) + local dataList = openspace.exoplanets.loadSystemDataFromCsv(csvFile) + for _,data in pairs(dataList) do + addExoplanetSystem(data) + end +end + +----------------------------------------------------------------------------------- +-- Some Settings and things that will be used for the creation +----------------------------------------------------------------------------------- + +local ExoplanetsGuiPath = "/Milky Way/Exoplanets/Exoplanet Systems/"; + +-- @TODO (2024-10-11, emmbr) We should add a way of getting constants like this from +-- OpenSpace instead (for example though the Lua API) so we don't have to redefine them +-- everywhere we need a value for e.g. the Earth radius +local SolarRadius = 6.95700E8 +local EarthRadius = 6371E3 +local JupiterRadius = 7.1492E7 +local AstronomicalUnit = 149597871E3 + +-- Constants for different categories of sizes of planets (in Earth radii) +-- Source: https://www.nasa.gov/image-article/sizes-of-known-exoplanets/ +local MaxRadius = { + Terrestrial = 1.25, + SuperEarth = 2.0, + NeptuneLike = 6.0 +} + +-- Planets will be colored based on their size (from thresholds above), and +-- described based on the planet types +local PlanetType = { + Terrestrial = { + Description = string.format( + "Terrestrial planets (R < %.2f Earth radii)", + MaxRadius.Terrestrial + ), + Color = { 0.32, 0.2, 0.1 }, + ColorName = "Brown" + }, + SuperEarth = { + Description = string.format( + "Super-Earths (%.0f < R < %.0f Earth radii)", + MaxRadius.Terrestrial, MaxRadius.SuperEarth + ), + Color = { 1.0, 0.76, 0.65 }, + ColorName = "Beige" + }, + NeptuneLike = { + Description = string.format( + "Neptune-like planets (%.0f < R < %.0f Earth radii)", + MaxRadius.SuperEarth, MaxRadius.NeptuneLike + ), + Color = { 0.22, 0.49, 0.50 }, + ColorName = "Blue" + }, + GasGiant = { + Description = string.format( + "Gas giants or larger planets (R > %.0f Earth radii)", + MaxRadius.NeptuneLike + ), + Color = { 0.55, 0.34, 0.39 }, + ColorName = "Wine red" + } +} + +local SizeColorLayerDescription = string.format( + [[This layer gives a fixed color to the planet surface based on the + planet radius. The planets are split into four categories based on + their radius (in Earth radii). 1) %s are %s, 2) %s are %s, 3) + %s are %s, and 4) %s are %s.]], + PlanetType.Terrestrial.Description, PlanetType.Terrestrial.ColorName, + PlanetType.SuperEarth.Description, PlanetType.SuperEarth.ColorName, + PlanetType.NeptuneLike.Description, PlanetType.NeptuneLike.ColorName, + PlanetType.GasGiant.Description, PlanetType.GasGiant.ColorName +); + +function planetTypeKey(radiusInMeter) + if radiusInMeter < MaxRadius.Terrestrial * EarthRadius then + return "Terrestrial" + elseif radiusInMeter < MaxRadius.SuperEarth * EarthRadius then + return "SuperEarth" + elseif radiusInMeter < MaxRadius.NeptuneLike * EarthRadius then + return "NeptuneLike" + else + return "GasGiant" + end +end + +function hasValue(v) + return v ~= nil +end + +----------------------------------------------------------------------------------- +-- This is the function that adds the scene graph nodes for each exoplanet system. +-- Edit this to change the visuals of the created exoplanet systems +----------------------------------------------------------------------------------- +function addExoplanetSystem(data) + if openspace.isEmpty(data) then + -- No data was found + return + end + + local starIdentifier = data.SystemId + local guiPath = ExoplanetsGuiPath .. data.StarName + + if openspace.hasSceneGraphNode(starIdentifier) then + openspace.printError( + "Adding of exoplanet system '" .. data.StarName .. "' failed. " .. + "The system has already been added" + ) + return + end + + -------------------------------------------------------------------- + -- Star Globe + -------------------------------------------------------------------- + local starTexture = openspace.propertyValue("Modules.Exoplanets.StarTexture") + local starNoDataTexture = openspace.propertyValue("Modules.Exoplanets.NoDataTexture") + + local colorLayers = {} + if hasValue(data.StarColor) and hasValue(data.StarRadius) then + -- If a star color was computed, there was enough data to visualize the star + colorLayers = { + { + Identifier = "StarColor", + Type = "SolidColor", + Color = data.StarColor, + BlendMode = "Normal", + Enabled = true + }, + { + Identifier = "StarTexture", + FilePath = openspace.absPath(starTexture), + BlendMode = "Color", + Enabled = true + } + } + else + colorLayers = { + { + Identifier = "NoDataStarTexture", + FilePath = openspace.absPath(starNoDataTexture), + BlendMode = "Color", + Enabled = true + } + } + end + + local starSizeAndTempInfo = function (data) + if hasValue(data.StarRadius) and hasValue(data.StarTeff) then + return string.format( + "It has a size of %.2f solar radii and an effective temperature of %.0f Kelvin", + data.StarRadius / SolarRadius, data.StarTeff + ) + elseif hasValue(data.StarTeff) then + return string.format( + "Its size is uknown, but it has an effective temperature of %.0f Kelvin", + data.StarTeff + ) + elseif hasValue(data.StarRadius) then + return string.format( + "It has a size of %.2f solar radii, but its temperature is unknown", + data.StarRadius + ) + else + return "Both its size and temperature is unknown" + end + end + + local Star = { + Identifier = starIdentifier, + Parent = "SolarSystemBarycenter", + Transform = { + Rotation = { + Type = "StaticRotation", + Rotation = data.SystemRotation + }, + Translation = { + Type = "StaticTranslation", + Position = data.Position + } + }, + Renderable = { + Type = "RenderableGlobe", + -- If there is not a value for the radius, render a globe with a default radius, + -- to allow us to navigate to something. Note that it can't be too small, due to + -- precision issues at this distance + Radii = openspace.ternary( + hasValue(data.StarRadius), + data.StarRadius, + 0.1 * SolarRadius + ), + PerformShading = false, + Layers = { + ColorLayers = colorLayers + } + }, + Tag = { "exoplanet_system" }, + GUI = { + Name = data.StarName .. " (Star)", + Path = guiPath, + Description = string.format( + [[The star %s is the host star of an exoplanet system with %d known %s that %s + enough data to be visualized. %s. The system is located at a distance of %.0f + light-years from Earth.]], + data.StarName, + data.NumPlanets, + openspace.ternary(data.NumPlanets > 1, "planets", "planet"), + openspace.ternary(data.NumPlanets > 1, "have", "has"), + starSizeAndTempInfo(data), + data.Distance + ) + } + } + openspace.addSceneGraphNode(Star) + + -------------------------------------------------------------------- + -- Star Label + -------------------------------------------------------------------- + local StarLabel = { + Identifier = starIdentifier .. "_Label", + Parent = starIdentifier, + Renderable = { + Type = "RenderableLabel", + Enabled = false, + Text = data.StarName, + FontSize = 70.0, + Size = 14.17, + MinMaxSize = { 1, 50 }, + EnableFading = true, + FadeUnit = "pc", + FadeDistances = { 1.33, 15.0 }, + FadeWidths = { 1.0, 20.0 } + }, + Tag = { "exoplanet_system_labels" }, + GUI = { + Name = data.StarName .. " Label", + Path = guiPath, + Description = string.format( + "A label for the exoplanet host star %s.", data.StarName + ) + } + } + openspace.addSceneGraphNode(StarLabel) + + -------------------------------------------------------------------- + -- Star Glare + -------------------------------------------------------------------- + local starGlareTexture = openspace.propertyValue("Modules.Exoplanets.StarGlareTexture") + + if hasValue(data.StarColor) and hasValue(data.StarRadius) then + -- This is a little magic to make the size of the glare dependent on the + -- size and the temperature of the star. It's kind of based on the fact that + -- the luminosity of a star is proportional to: (radius^2)*(temperature^4) + -- Maybe a better option would be to compute the size based on the absolute + -- magnitude or star luminosity, but for now this looks good enough. + local size = 59.0 * data.StarRadius + if hasValue(data.StarTeff) then + local SunTeff = 5780.90; + local RelativeTeff = (data.StarTeff / SunTeff) + size = size * RelativeTeff * RelativeTeff; + end + + local StarGlare = { + Identifier = starIdentifier .. "_Glare", + Parent = starIdentifier, + Renderable = { + Type = "RenderablePlaneImageLocal", + Size = size, + Origin = "Center", + Billboard = true, + Texture = openspace.absPath(starGlareTexture), + BlendMode = "Additive", + Opacity = 0.65, + MultiplyColor = data.StarColor + }, + GUI = { + Name = data.StarName .. " Glare", + Path = guiPath, + Description = string.format( + "A glare effect for the star %s. %s", + data.StarName, + openspace.ternary( + hasValue(data.StarTeff), + [[The size of the glare has been computed based on data of the star's + temperature, in a way that's relative to the visualization for our Sun.]], + "The size of the glare is not data-based." + ) + ) + } + } + openspace.addSceneGraphNode(StarGlare) + end + + -------------------------------------------------------------------- + -- 1 AU Comparison Circle + -------------------------------------------------------------------- + local showCircle = openspace.propertyValue("Modules.Exoplanets.ShowComparisonCircle") + local circleColor = openspace.propertyValue("Modules.Exoplanets.ComparisonCircleColor") + + local Circle = { + Identifier = starIdentifier .. "_1AU_Circle", + Parent = starIdentifier, + Renderable = { + Type = "RenderableRadialGrid", + Enabled = showCircle, + Radii = { 0.0, 1.0 }, + Color = circleColor, + CircleSegments = 64, + LineWidth = 2.0 + }, + Transform = { + Rotation = { + Type = "StaticRotation", + Rotation = data.MeanOrbitRotation + }, + Scale = { + Type = "StaticScale", + Scale = AstronomicalUnit + } + }, + Tag = { "exoplanet_1au_ring" }, + GUI = { + Name = "1 AU Size Comparison Circle", + Path = guiPath, + Description = [[A circle with a radius of 1 Astronomical Unit. That is, its + size corresponds to the size of Earth's orbit.]] + } + } + openspace.addSceneGraphNode(Circle) + + -------------------------------------------------------------------- + -- Habitable Zone + -------------------------------------------------------------------- + local showZone = openspace.propertyValue("Modules.Exoplanets.ShowHabitableZone") + local zoneOpacity = openspace.propertyValue("Modules.Exoplanets.HabitableZoneOpacity") + local zoneTexture = openspace.propertyValue("Modules.Exoplanets.HabitableZoneTexture") + local useOptimisticBounds = openspace.propertyValue( + "Modules.Exoplanets.UseOptimisticZone" + ) + + if hasValue(data.StarTeff) and hasValue(data.StarLuminosity) then + local HabitableZoneDisc = { + Identifier = starIdentifier .. "_HZ_Disc", + Parent = starIdentifier, + Renderable = { + Type = "RenderableHabitableZone", + Enabled = showZone, + Texture = openspace.absPath(zoneTexture), + Luminosity = data.StarLuminosity, + EffectiveTemperature = data.StarTeff, + Optimistic = useOptimisticBounds, + Opacity = zoneOpacity + }, + Transform = { + Rotation = { + Type = "StaticRotation", + Rotation = data.MeanOrbitRotation + } + }, + Tag = { "exoplanet_habitable_zone" }, + GUI = { + Name = data.StarName .. " Habitable Zone", + Path = guiPath, + Description = [[ + The habitable zone is the region around a star in which an Earth-like planet + can potentially have liquid water on its surface. The inner boundary is where + the greenhouse gases in the atmosphere would trap any incoming infrared + radiation, leading to the planet surface becoming so hot that water boils away. + The outer boundary is where the greenhouse effect would not be able to maintain + surface temperature above freezing anywhere on the planet. + ]] + } + } + openspace.addSceneGraphNode(HabitableZoneDisc) + end + + -------------------------------------------------------------------- + -- Planets + -------------------------------------------------------------------- + local defaultPlanetTexture = openspace.propertyValue( + "Modules.Exoplanets.PlanetDefaultTexture" + ) + + local addExoplanet = function(id, planetData) + -- This translation will be used for both the trail and globe + local PlanetKeplerTranslation = { + Type = "KeplerTranslation", + Eccentricity = planetData.Eccentricity, + SemiMajorAxis = 0.001 * planetData.SemiMajorAxis, -- km + Inclination = planetData.Inclination, + AscendingNode = planetData.AscendingNode, + ArgumentOfPeriapsis = planetData.ArgumentOfPeriapsis, + MeanAnomaly = 0.0, + Epoch = planetData.Epoch, + Period = planetData.Period * openspace.time.secondsPerDay() + } + + -------------------------------------------------------------------- + -- Planet Globe (if we know how big it is) + -------------------------------------------------------------------- + local ambientIntensity = 0.5 -- High to show the color from size more clearly + + local planetSizeInfo = function (data) + if hasValue(data.Radius) then + return string.format( + "%.2f Earth radii, %.2f Jupiter radii", + planetData.Radius / EarthRadius, + planetData.Radius / JupiterRadius + ) + else + return "unknown" + end + end + + if hasValue(planetData.Radius) then + local planetTypeKey = planetTypeKey(planetData.Radius) + local planetTypeData = PlanetType[planetTypeKey] + + local planetColorLayers = { + { + Identifier = "ColorFromSize", + Name = "Color From Size Classification", + Type = "SolidColor", + Color = planetTypeData.Color, + Enabled = true, + Description = SizeColorLayerDescription + } + } + + -- If a default texture was provided, use it. Also, reduce the ambient intensity + if not openspace.isEmpty(defaultPlanetTexture) then + local PlanetTextureLayer = { + Identifier = "PlanetTexture", + Name = "Planet Texture", + FilePath = openspace.absPath(defaultPlanetTexture), + Enabled = true + } + table.insert(planetColorLayers, PlanetTextureLayer) + ambientIntensity = 0.15 + end + + local Planet = { + Identifier = id, + Parent = starIdentifier, + Transform = { + Translation = PlanetKeplerTranslation + }, + Renderable = { + Type = "RenderableGlobe", + Radii = planetData.Radius, + PerformShading = true, + Layers = { + ColorLayers = planetColorLayers + }, + LightSourceNode = starIdentifier, + AmbientIntensity = ambientIntensity + }, + Tag = { "exoplanet_planet" }, + GUI = { + Name = planetData.Name, + Path = guiPath, + Description = string.format( + [[The exoplanet %s falls into the category of %s. Some key facts: + Radius: %s. + Orbit Period: %.1f (Earth) days. + Orbit Semi-major axis: %.2f (AU). + Orbit Eccentricity: %.2f. %s]], + planetData.Name, + planetTypeData.Description, + planetSizeInfo(planetData), + planetData.Period, + planetData.SemiMajorAxis / AstronomicalUnit, + planetData.Eccentricity, + openspace.ternary( + planetData.HasUsedDefaultValues, + [[OBS! Default values have been used to visualize the orbit (for example for + inclination, eccentricity, or argument of periastron), and hence the data + specified for the orbit might not be reliable.]], + "" + ) + ) + } + } + openspace.addSceneGraphNode(Planet) + end + + -------------------------------------------------------------------- + -- Planet Orbit + -------------------------------------------------------------------- + local trailResolution = 1000.0 + -- Increase the resolution of very eccentric orbits + local EccentricityThreshold = 0.85 + if planetData.Eccentricity > EccentricityThreshold then + trailResolution = trailResolution * 2.0 + end + + local orbitDescription = string.format( + "The orbit trail of the exoplanet %s.", planetData.Name + ) + if planetData.HasUsedDefaultValues then + orbitDescription = orbitDescription .. " " .. [[ + OBS! Default values have been used to visualize the orbit (for example for + inclination, eccentricity, or argument of periastron). The shape or orientation + of the orbit might hence not be completely accurate. + ]] + end + + local Orbit = { + Identifier = id .. "_Trail", + Parent = starIdentifier, + Renderable = { + Type = "RenderableTrailOrbit", + Period = planetData.Period, + Resolution = trailResolution, + Translation = PlanetKeplerTranslation, + Color = { 1.0, 1.0, 1.0 } + }, + Tag = { "exoplanet_trail" }, + GUI = { + Name = planetData.Name .. " Trail", + Path = guiPath, + Description = orbitDescription + } + } + openspace.addSceneGraphNode(Orbit) + + -------------------------------------------------------------------- + -- Planet Orbit Uncertainty + -------------------------------------------------------------------- + local showUncertaintyDisc = openspace.propertyValue("Modules.Exoplanets.ShowHabitableZone") + local discTexture = openspace.propertyValue("Modules.Exoplanets.OrbitDiscTexture") + + if hasValue(planetData.SemiMajorAxisUncertainty) then + local OrbitDisc = { + Identifier = id .. "_Disc", + Parent = starIdentifier, + Transform = { + Rotation = { + Type = "StaticRotation", + Rotation = planetData.OrbitPlaneRotationMatrix + } + }, + Renderable = { + Type = "RenderableOrbitDisc", + Enabled = showUncertaintyDisc, + Texture = openspace.absPath(discTexture), + Size = planetData.SemiMajorAxis, + Eccentricity = planetData.Eccentricity, + Offset = planetData.SemiMajorAxisUncertainty, + Opacity = 0.25 + }, + Tag = { "exoplanet_uncertainty_disc" }, + GUI = { + Name = planetData.Name .. " Disc", + Path = guiPath, + Description = [[ + The width of this disc around the planet's orbit marks the uncertainty of the + orbit (based on the uncertainty of the semi-major axis, and the eccentricity + of the orbit). The wider the disc, the more uncertain the orbit is. + ]] + } + } + openspace.addSceneGraphNode(OrbitDisc) + end + end + + -- Add all the planets that have sufficient data + for planetId,planetData in pairs(data.Planets) do + addExoplanet(planetId, planetData) + end +end diff --git a/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h b/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h index 2a0fc765e6..9c913e337e 100644 --- a/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h +++ b/modules/exoplanets/tasks/exoplanetsdatapreparationtask.h @@ -27,8 +27,7 @@ #include -#include - +#include #include #include #include diff --git a/scripts/core_scripts.lua b/scripts/core_scripts.lua index 89a4bd0ad6..f17ee63820 100644 --- a/scripts/core_scripts.lua +++ b/scripts/core_scripts.lua @@ -88,6 +88,38 @@ openspace.documentation = { target several nodes. If the fade time is not provided then the "OpenSpaceEngine.FadeDuration" property will be used instead. If the third argument (endScript) is provided then that script will be run after the fade is finished.]] + }, + { + Name = "ternary", + Arguments = { + { "condition", "Boolean" }, + { "trueValue", "any" }, + { "falseValue", "any" } + }, + Return = "any", + Documentation = [[ + A utility function to return a specific value based on a True/False condition. + + \\param condition The condition to check against + \\param trueValue The value to return if the condition is True + \\param falseValue The value to return if the condition is False + \\return Either the trueValue of falseValue, depending on if the condition is true + or not + ]] + }, + { + Name = "isEmpty", + Arguments = { + { "object", "any" } + }, + Return = "Boolean", + Documentation = [[ + A utility function to check whether an object is empty or not. Identifies `nil` + objects, Tables without any keys, and empty strings. + + \\param object The object to check + \\return A Boolean that specifies if the object is empty or not + ]] } } @@ -305,3 +337,15 @@ openspace.toggleFade = function(renderable, fadeTime, endScript) openspace.fadeIn(renderable, fadeTime, endScript) end end + +openspace.ternary = function(condition, trueValue, falseValue) + if condition then return trueValue else return falseValue end +end + +openspace.isEmpty = function(v) + if type(v) == "table" then + return next(v) == nil + else + return v == nil or v == '' + end +end