/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2022 * * * * 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 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::string& path) { std::string resPath = path; std::replace(resPath.begin(), resPath.end(), '\\', '/'); return resPath; } openspace::exoplanets::ExoplanetSystem findExoplanetSystemInData( std::string_view starName) { using namespace openspace; using namespace exoplanets; const ExoplanetsModule* module = global::moduleEngine->module(); const std::string binPath = module->exoplanetsDataPath(); std::ifstream data(absPath(binPath), std::ios::in | std::ios::binary); if (!data.good()) { LERROR(fmt::format("Failed to open exoplanets data file: '{}'", binPath)); return ExoplanetSystem(); } const std::string lutPath = module->lookUpTablePath(); std::ifstream lut(absPath(lutPath)); if (!lut.good()) { LERROR(fmt::format("Failed to open exoplanets look-up table: '{}'", lutPath)); return ExoplanetSystem(); } ExoplanetSystem system; // 1. search lut for the starname and return the corresponding location // 2. go to that location in the data file // 3. read sizeof(exoplanet) bytes into an exoplanet object. ExoplanetDataEntry p; std::string line; while (std::getline(lut, line)) { std::istringstream ss(line); std::string name; std::getline(ss, name, ','); if (name.substr(0, name.length() - 2) != starName) { continue; } std::string location_s; std::getline(ss, location_s); long location = std::stol(location_s.c_str()); data.seekg(location); data.read(reinterpret_cast(&p), sizeof(ExoplanetDataEntry)); sanitizeNameString(name); if (!hasSufficientData(p)) { LWARNING(fmt::format("Insufficient data for exoplanet: '{}'", name)); continue; } system.planetNames.push_back(name); system.planetsData.push_back(p); // Star data - Should not vary between planets, but one data entry might lack data // for the host star while another does not. So for every planet, update star data // if needed const glm::vec3 pos = glm::vec3(p.positionX, p.positionY, p.positionZ); if (system.starData.position != pos && isValidPosition(pos)) { system.starData.position = pos; } if (system.starData.radius != p.rStar && !std::isnan(p.rStar)) { system.starData.radius = p.rStar; } if (system.starData.bv != p.bmv && !std::isnan(p.bmv)) { system.starData.bv = p.bmv; } if (system.starData.teff != p.teff && !std::isnan(p.teff)) { system.starData.teff = p.teff; } if (system.starData.luminosity != p.luminosity && !std::isnan(p.luminosity)) { system.starData.luminosity = p.luminosity; } } system.starName = starName; return system; } void createExoplanetSystem(const std::string& starName) { using namespace openspace; using namespace exoplanets; const std::string starIdentifier = createIdentifier(starName); std::string sanitizedStarName = starName; sanitizeNameString(sanitizedStarName); const std::string guiPath = fmt::format("{}{}", ExoplanetsGuiPath, sanitizedStarName); SceneGraphNode* existingStarNode = sceneGraphNode(starIdentifier); if (existingStarNode) { LERROR(fmt::format( "Adding of exoplanet system '{}' failed. The system has already been added", starName )); return; } ExoplanetSystem system = findExoplanetSystemInData(starName); if (system.planetsData.empty()) { LERROR(fmt::format("Exoplanet system '{}' could not be found", starName)); return; } const glm::vec3 starPosInParsec = system.starData.position; if (!isValidPosition(starPosInParsec)) { LERROR(fmt::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::string starTexture = module->starTexturePath(); 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::string noDataTexture = module->noDataTexturePath(); colorLayers = "{" "Identifier = 'NoDataStarTexture'," "FilePath = openspace.absPath('" + formatPathToLua(noDataTexture) + "')," "BlendMode = 'Color'," "Enabled = true" "}"; } const std::string starGlobeRenderableString = "Renderable = {" "Type = 'RenderableGlobe'," "Radii = " + std::to_string(radiusInMeter) + "," "SegmentsPerPatch = 64," "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 + "'" "}" "}"; global::scriptEngine->queueScript( "openspace.addSceneGraphNode(" + starParent + ");", scripting::ScriptEngine::RemoteScripting::Yes ); // Planets for (size_t i = 0; i < system.planetNames.size(); i++) { 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 = createIdentifier(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) + "" "}"; const std::string planetNode = "{" "Identifier = '" + planetIdentifier + "'," "Parent = '" + starIdentifier + "'," "Renderable = {" "Type = 'RenderableGlobe'," "Enabled = " + enabled + "," "Radii = " + std::to_string(planetRadius) + "," // in meters "SegmentsPerPatch = 64," "PerformShading = false," "Layers = {}" "}," "Transform = { " "Translation = " + planetKeplerTranslation + "" "}," "GUI = {" "Name = '" + planetName + "'," "Path = '" + guiPath + "'" "}" "}"; int trailResolution = 1000; // Increase the resolution for highly eccentric orbits const 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 + "'" "}" "}"; global::scriptEngine->queueScript( "openspace.addSceneGraphNode(" + planetTrailNode + ");" "openspace.addSceneGraphNode(" + planetNode + ");", scripting::ScriptEngine::RemoteScripting::Yes ); 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::string discTexture = module->orbitDiscTexturePath(); const std::string discNode = "{" "Identifier = '" + planetIdentifier + "_Disc'," "Parent = '" + starIdentifier + "'," "Renderable = {" "Type = 'RenderableOrbitDisc'," "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) + "" "}" "}," "GUI = {" "Name = '" + planetName + " Disc'," "Path = '" + guiPath + "'" "}" "}"; global::scriptEngine->queueScript( "openspace.addSceneGraphNode(" + discNode + ");", scripting::ScriptEngine::RemoteScripting::Yes ); } } 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(); const std::string isCircleEnabledString = isCircleEnabled ? "true" : "false"; // 1 AU Size Comparison Circle const std::string circle = "{" "Identifier = '" + starIdentifier + "_1AU_Circle'," "Parent = '" + starIdentifier + "'," "Renderable = {" "Type = 'RenderableRadialGrid'," "Enabled = " + isCircleEnabledString + "," "Radii = { 0.0, 1.0 }," "CircleSegments = 64," "LineWidth = 2.0," "}," "Transform = {" "Rotation = {" "Type = 'StaticRotation'," "Rotation = " + ghoul::to_string(meanOrbitPlaneRotationMatrix) + "" "}," "Scale = {" "Type = 'StaticScale'," "Scale = " + std::to_string(distanceconstants::AstronomicalUnit) + "" "}" "}," "GUI = {" "Name = '1 AU Size Comparison Circle'," "Path = '" + guiPath + "'" "}" "}"; global::scriptEngine->queueScript( "openspace.addSceneGraphNode(" + circle + ");", scripting::ScriptEngine::RemoteScripting::Yes ); // 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::string hzTexture = module->habitableZoneTexturePath(); bool isHzEnabled = module->showHabitableZone(); const std::string isHzEnabledString = isHzEnabled ? "true" : "false"; bool useOptimistic = module->useOptimisticZone(); const std::string useOptimisticString = useOptimistic ? "true" : "false"; float opacity = module->habitableZoneOpacity(); const std::string zoneDiscNode = "{" "Identifier = '" + starIdentifier + "_HZ_Disc'," "Parent = '" + starIdentifier + "'," "Renderable = {" "Type = 'RenderableHabitableZone'," "Enabled = " + isHzEnabledString + "," "Texture = openspace.absPath('" + formatPathToLua(hzTexture) + "')," "Luminosity = " + std::to_string(system.starData.luminosity) + "," "EffectiveTemperature = " + std::to_string(system.starData.teff) + "," "Optimistic = " + useOptimisticString + "," "Opacity = " + std::to_string(opacity) + "" "}," "Transform = {" "Rotation = {" "Type = 'StaticRotation'," "Rotation = " + ghoul::to_string(meanOrbitPlaneRotationMatrix) + "" "}" "}," "GUI = {" "Name = '" + starName + " Habitable Zone'," "Path = '" + guiPath + "'," "Description = '" + std::string(description) + "'" "}" "}"; global::scriptEngine->queueScript( "openspace.addSceneGraphNode(" + zoneDiscNode + ");", scripting::ScriptEngine::RemoteScripting::Yes ); // 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::string 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 + "'" "}" "}"; global::scriptEngine->queueScript( "openspace.addSceneGraphNode(" + starGlare + ");", scripting::ScriptEngine::RemoteScripting::Yes ); } } } std::vector hostStarsWithSufficientData() { using namespace openspace; using namespace exoplanets; const ExoplanetsModule* module = global::moduleEngine->module(); 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 {}; } const std::string lutPath = module->lookUpTablePath(); std::ifstream lookupTableFile(absPath(lutPath)); if (!lookupTableFile.good()) { LERROR(fmt::format("Failed to open lookup table file '{}'", lutPath)); return {}; } const std::string binPath = module->exoplanetsDataPath(); std::ifstream data(absPath(binPath), std::ios::in | std::ios::binary); if (!data.good()) { LERROR(fmt::format("Failed to open data file '{}'", binPath)); return {}; } std::vector names; std::string line; // Read number of lines int nExoplanets = 0; while (std::getline(lookupTableFile, line)) { ++nExoplanets; } lookupTableFile.clear(); lookupTableFile.seekg(0); names.reserve(nExoplanets); ExoplanetDataEntry p; while (std::getline(lookupTableFile, line)) { std::stringstream ss(line); std::string name; std::getline(ss, name, ','); // Remove the last two characters, that specify the planet name = name.substr(0, name.size() - 2); // Don't want to list systems where there is not enough data to visualize. // So, test if there is before adding the name to the list. std::string location_s; std::getline(ss, location_s); long location = std::stol(location_s.c_str()); data.seekg(location); data.read(reinterpret_cast(&p), sizeof(ExoplanetDataEntry)); if (hasSufficientData(p)) { names.push_back(name); } } // For easier read, sort by names and remove duplicates std::sort(names.begin(), names.end()); names.erase(std::unique(names.begin(), names.end()), names.end()); return names; } /** * 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. */ [[codegen::luawrap]] void addExoplanetSystem( std::variant> starNames) { if (std::holds_alternative(starNames)) { // The user provided a single name std::string starName = std::get(starNames); createExoplanetSystem(starName); } else { std::vector sns = std::get>(starNames); for (const std::string& starName : sns) { createExoplanetSystem(starName); } } } [[codegen::luawrap]] void removeExoplanetSystem(std::string starName) { using namespace openspace; using namespace exoplanets; const std::string starIdentifier = createIdentifier(std::move(starName)); global::scriptEngine->queueScript( "openspace.removeSceneGraphNode('" + starIdentifier + "');", scripting::ScriptEngine::RemoteScripting::Yes ); } [[codegen::luawrap]] std::vector getListOfExoplanets() { std::vector names = hostStarsWithSufficientData(); return names; } [[codegen::luawrap]] void listAvailableExoplanetSystems() { std::vector names = hostStarsWithSufficientData(); std::string output; for (auto it = names.begin(); it != names.end(); ++it) { output += *it + ", "; } output.pop_back(); output.pop_back(); LINFO(fmt::format( "There is data available for the following {} exoplanet systems: {}", names.size(), output )); } #include "exoplanetsmodule_lua_codegen.cpp" } // namespace