3
// Copyright (C) 2001-2009, the Celestia Development Team
4
// Original version by Chris Laurel <claurel@gmail.com>
6
// Solar system catalog parser.
8
// This program is free software; you can redistribute it and/or
9
// modify it under the terms of the GNU General Public License
10
// as published by the Free Software Foundation; either version 2
11
// of the License, or (at your option) any later version.
23
#include <Eigen/Geometry>
24
#include <fmt/printf.h>
26
#include <celmath/mathlib.h>
27
#include <celutil/color.h>
28
#include <celutil/fsutils.h>
29
#include <celutil/gettext.h>
30
#include <celutil/infourl.h>
31
#include <celutil/logger.h>
32
#include <celutil/stringutils.h>
33
#include <celutil/tokenizer.h>
34
#include "atmosphere.h"
41
#include "meshmanager.h"
42
#include "parseobject.h"
46
#include "texmanager.h"
50
// size_t and strncmp are used by the gperf output code
53
using namespace std::string_view_literals;
55
using celestia::util::GetLogger;
56
namespace engine = celestia::engine;
57
namespace ephem = celestia::ephem;
58
namespace math = celestia::math;
59
namespace util = celestia::util;
74
Solar system catalog (.ssc) files contain items of three different types:
75
bodies, locations, and alternate surfaces. Bodies planets, moons, asteroids,
76
comets, and spacecraft. Locations are points on the surfaces of bodies which
77
may be labelled but aren't rendered. Alternate surfaces are additional
78
surface definitions for bodies.
80
An ssc file contains zero or more definitions of this form:
83
[disposition] [item type] "name" "parent name"
85
...object info fields...
89
The disposition of the object determines what happens if an item with the
90
same parent and same name already exists. It may be one of the following:
91
- Add - Default if none is specified. Add the item even if one of the
92
same name already exists.
93
- Replace - Replace an existing item with the new one
94
- Modify - Modify the existing item, changing the fields that appear
95
in the new definition.
97
All dispositions are equivalent to add if no item of the same name
100
The item type is one of Body, Location, or AltSurface, defaulting to
101
Body when no type is given.
103
The name and parent name are both mandatory.
106
void sscError(const Tokenizer& tok,
107
const std::string& msg)
109
GetLogger()->error(_("Error in .ssc file (line {}): {}\n"),
110
tok.getLineNumber(), msg);
113
// Object class properties
114
constexpr auto CLASSES_UNCLICKABLE = BodyClassification::Invisible |
115
BodyClassification::Diffuse;
117
// lookup table generated by gperf (solarsys.gperf)
118
#include "solarsys.inc"
120
BodyClassification GetClassificationId(std::string_view className)
122
auto ptr = ClassificationMap::getClassification(className.data(), className.size());
123
return ptr == nullptr
124
? BodyClassification::Unknown
125
: ptr->classification;
129
//! Maximum depth permitted for nested frames.
130
unsigned int MaxFrameDepth = 50;
132
bool isFrameCircular(const ReferenceFrame& frame, ReferenceFrame::FrameType frameType)
134
return frame.nestingDepth(MaxFrameDepth, frameType) > MaxFrameDepth;
139
std::unique_ptr<Location>
140
CreateLocation(const Hash* locationData,
143
auto location = std::make_unique<Location>();
145
auto longlat = locationData->getSphericalTuple("LongLat").value_or(Eigen::Vector3d::Zero());
146
Eigen::Vector3f position = body->geodeticToCartesian(longlat).cast<float>();
147
location->setPosition(position);
149
auto size = locationData->getLength<float>("Size").value_or(1.0f);
150
location->setSize(size);
152
auto importance = locationData->getNumber<float>("Importance").value_or(-1.0f);
153
location->setImportance(importance);
155
if (const std::string* featureTypeName = locationData->getString("Type"); featureTypeName != nullptr)
156
location->setFeatureType(Location::parseFeatureType(*featureTypeName));
158
if (auto labelColor = locationData->getColor("LabelColor"); labelColor.has_value())
160
location->setLabelColor(*labelColor);
161
location->setLabelColorOverridden(true);
167
template<typename Dst, typename Flag>
168
inline void SetOrUnset(Dst &dst, Flag flag, bool cond)
176
std::optional<fs::path>
177
GetFilename(const Hash& hash, std::string_view key, const char* errorMessage)
179
const std::string* value = hash.getString(key);
180
if (value == nullptr)
183
auto result = util::U8FileName(*value);
184
if (!result.has_value())
185
GetLogger()->error(errorMessage);
191
void FillinSurface(const Hash* surfaceData,
193
const fs::path& path)
195
if (auto color = surfaceData->getColor("Color"); color.has_value())
196
surface->color = *color;
197
if (auto specularColor = surfaceData->getColor("SpecularColor"); specularColor.has_value())
198
surface->specularColor = *specularColor;
199
if (auto specularPower = surfaceData->getNumber<float>("SpecularPower"); specularPower.has_value())
200
surface->specularPower = *specularPower;
201
if (auto lunarLambert = surfaceData->getNumber<float>("LunarLambert"); lunarLambert.has_value())
202
surface->lunarLambert = *lunarLambert;
204
auto baseTexture = GetFilename(*surfaceData, "Texture"sv, "Invalid filename in Texture\n");
205
auto bumpTexture = GetFilename(*surfaceData, "BumpMap"sv, "Invalid filename in BumpMap\n");
206
auto nightTexture = GetFilename(*surfaceData, "NightTexture"sv, "Invalid filename in NightTexture\n");
207
auto specularTexture = GetFilename(*surfaceData, "SpecularTexture"sv, "Invalid filename in SpecularTexture\n");
208
auto normalTexture = GetFilename(*surfaceData, "NormalMap"sv, "Invalid filename in NormalMap\n");
209
auto overlayTexture = GetFilename(*surfaceData, "OverlayTexture"sv, "Invalid filename in OverlayTexture\n");
211
unsigned int baseFlags = TextureInfo::WrapTexture | TextureInfo::AllowSplitting;
212
unsigned int bumpFlags = TextureInfo::WrapTexture | TextureInfo::AllowSplitting | TextureInfo::LinearColorspace;
213
unsigned int nightFlags = TextureInfo::WrapTexture | TextureInfo::AllowSplitting;
214
unsigned int specularFlags = TextureInfo::WrapTexture | TextureInfo::AllowSplitting;
216
auto bumpHeight = surfaceData->getNumber<float>("BumpHeight").value_or(2.5f);
218
bool blendTexture = surfaceData->getBoolean("BlendTexture").value_or(false);
219
bool emissive = surfaceData->getBoolean("Emissive").value_or(false);
220
bool compressTexture = surfaceData->getBoolean("CompressTexture").value_or(false);
222
SetOrUnset(baseFlags, TextureInfo::CompressTexture, compressTexture);
224
SetOrUnset(surface->appearanceFlags, Surface::BlendTexture, blendTexture);
225
SetOrUnset(surface->appearanceFlags, Surface::Emissive, emissive);
226
SetOrUnset(surface->appearanceFlags, Surface::ApplyBaseTexture, baseTexture.has_value());
227
SetOrUnset(surface->appearanceFlags, Surface::ApplyBumpMap, (bumpTexture.has_value() || normalTexture.has_value()));
228
SetOrUnset(surface->appearanceFlags, Surface::ApplyNightMap, nightTexture.has_value());
229
SetOrUnset(surface->appearanceFlags, Surface::SeparateSpecularMap, specularTexture.has_value());
230
SetOrUnset(surface->appearanceFlags, Surface::ApplyOverlay, overlayTexture.has_value());
231
SetOrUnset(surface->appearanceFlags, Surface::SpecularReflection, surface->specularColor != Color(0.0f, 0.0f, 0.0f));
233
if (baseTexture.has_value())
234
surface->baseTexture.setTexture(*baseTexture, path, baseFlags);
235
if (nightTexture.has_value())
236
surface->nightTexture.setTexture(*nightTexture, path, nightFlags);
237
if (specularTexture.has_value())
238
surface->specularTexture.setTexture(*specularTexture, path, specularFlags);
240
// If both are present, NormalMap overrides BumpMap
241
if (normalTexture.has_value())
242
surface->bumpTexture.setTexture(*normalTexture, path, bumpFlags);
243
else if (bumpTexture.has_value())
244
surface->bumpTexture.setTexture(*bumpTexture, path, bumpHeight, bumpFlags);
246
if (overlayTexture.has_value())
247
surface->overlayTexture.setTexture(*overlayTexture, path, baseFlags);
251
Selection GetParentObject(PlanetarySystem* system)
254
Body* primary = system->getPrimaryBody();
255
if (primary != nullptr)
256
parent = Selection(primary);
258
parent = Selection(system->getStar());
264
TimelinePhase::SharedConstPtr CreateTimelinePhase(Body* body,
266
const Hash* phaseData,
267
const fs::path& path,
268
const ReferenceFrame::SharedConstPtr& defaultOrbitFrame,
269
const ReferenceFrame::SharedConstPtr& defaultBodyFrame,
272
double previousPhaseEnd)
274
double beginning = previousPhaseEnd;
275
double ending = std::numeric_limits<double>::infinity();
277
// Beginning is optional for the first phase of a timeline, and not
278
// allowed for the other phases, where beginning is always the ending
279
// of the previous phase.
280
bool hasBeginning = ParseDate(phaseData, "Beginning", beginning);
281
if (!isFirstPhase && hasBeginning)
283
GetLogger()->error("Error: Beginning can only be specified for initial phase of timeline.\n");
287
// Ending is required for all phases except for the final one.
288
bool hasEnding = ParseDate(phaseData, "Ending", ending);
289
if (!isLastPhase && !hasEnding)
291
GetLogger()->error("Error: Ending is required for all timeline phases other than the final one.\n");
295
// Get the orbit reference frame.
296
ReferenceFrame::SharedConstPtr orbitFrame;
297
const Value* frameValue = phaseData->getValue("OrbitFrame");
298
if (frameValue != nullptr)
300
orbitFrame = CreateReferenceFrame(universe, frameValue, defaultOrbitFrame->getCenter(), body);
301
if (orbitFrame == nullptr)
308
// No orbit frame specified; use the default frame.
309
orbitFrame = defaultOrbitFrame;
312
// Get the body reference frame
313
ReferenceFrame::SharedConstPtr bodyFrame;
314
const Value* bodyFrameValue = phaseData->getValue("BodyFrame");
315
if (bodyFrameValue != nullptr)
317
bodyFrame = CreateReferenceFrame(universe, bodyFrameValue, defaultBodyFrame->getCenter(), body);
318
if (bodyFrame == nullptr)
325
// No body frame specified; use the default frame.
326
bodyFrame = defaultBodyFrame;
329
// Use planet units (AU for semimajor axis) if the center of the orbit
330
// reference frame is a star.
331
bool usePlanetUnits = orbitFrame->getCenter().star() != nullptr;
334
auto orbit = CreateOrbit(orbitFrame->getCenter(), phaseData, path, usePlanetUnits);
337
GetLogger()->error("Error: missing orbit in timeline phase.\n");
341
// Get the rotation model
342
// TIMELINE-TODO: default rotation model is UniformRotation with a period
343
// equal to the orbital period. Should we do something else?
344
auto rotationModel = CreateRotationModel(phaseData, path, orbit->getPeriod());
347
// TODO: Should distinguish between a missing rotation model (where it's
348
// appropriate to use a default one) and a bad rotation model (where
349
// we should report an error.)
350
rotationModel = ephem::ConstantOrientation::identity();
353
auto phase = TimelinePhase::CreateTimelinePhase(universe,
361
// Frame ownership transfered to phase; release local references
366
std::unique_ptr<Timeline>
367
CreateTimelineFromArray(Body* body,
369
const ValueArray* timelineArray,
370
const fs::path& path,
371
const ReferenceFrame::SharedConstPtr& defaultOrbitFrame,
372
const ReferenceFrame::SharedConstPtr& defaultBodyFrame)
374
auto timeline = std::make_unique<Timeline>();
375
double previousEnding = -std::numeric_limits<double>::infinity();
377
if (timelineArray->empty())
379
GetLogger()->error("Error in timeline of '{}': timeline array is empty.\n", body->getName());
383
const auto finalIter = timelineArray->end() - 1;
384
for (auto iter = timelineArray->begin(); iter != timelineArray->end(); iter++)
386
const Hash* phaseData = iter->getHash();
387
if (phaseData == nullptr)
389
GetLogger()->error("Error in timeline of '{}': phase {} is not a property group.\n", body->getName(), iter - timelineArray->begin() + 1);
393
bool isFirstPhase = iter == timelineArray->begin();
394
bool isLastPhase = iter == finalIter;
396
auto phase = CreateTimelinePhase(body, universe, phaseData,
400
isFirstPhase, isLastPhase, previousEnding);
401
if (phase == nullptr)
403
GetLogger()->error("Error in timeline of '{}', phase {}.\n",
405
iter - timelineArray->begin() + 1);
409
previousEnding = phase->endTime();
411
timeline->appendPhase(phase);
418
bool CreateTimeline(Body* body,
419
PlanetarySystem* system,
421
const Hash* planetData,
422
const fs::path& path,
423
DataDisposition disposition,
426
FrameTree* parentFrameTree = nullptr;
427
Selection parentObject = GetParentObject(system);
428
bool orbitsPlanet = false;
429
if (parentObject.body())
431
parentFrameTree = parentObject.body()->getOrCreateFrameTree();
432
//orbitsPlanet = true;
434
else if (parentObject.star())
436
const SolarSystem* solarSystem = universe.getOrCreateSolarSystem(parentObject.star());
437
parentFrameTree = solarSystem->getFrameTree();
441
// Bad orbit barycenter specified
445
ReferenceFrame::SharedConstPtr defaultOrbitFrame;
446
ReferenceFrame::SharedConstPtr defaultBodyFrame;
447
if (bodyType == SurfaceObject)
449
defaultOrbitFrame = std::make_shared<BodyFixedFrame>(parentObject, parentObject);
450
defaultBodyFrame = CreateTopocentricFrame(parentObject, parentObject, Selection(body));
454
defaultOrbitFrame = parentFrameTree->getDefaultReferenceFrame();
455
defaultBodyFrame = parentFrameTree->getDefaultReferenceFrame();
458
// If there's an explicit timeline definition, parse that. Otherwise, we'll do
459
// things the old way.
460
const Value* value = planetData->getValue("Timeline");
461
if (value != nullptr)
463
const ValueArray* timelineArray = value->getArray();
464
if (timelineArray == nullptr)
466
GetLogger()->error("Error: Timeline must be an array\n");
470
std::unique_ptr<Timeline> timeline = CreateTimelineFromArray(body, universe, timelineArray, path,
471
defaultOrbitFrame, defaultBodyFrame);
476
body->setTimeline(std::move(timeline));
480
// Information required for the object timeline.
481
ReferenceFrame::SharedConstPtr orbitFrame;
482
ReferenceFrame::SharedConstPtr bodyFrame;
483
std::shared_ptr<const ephem::Orbit> orbit = nullptr;
484
std::shared_ptr<const ephem::RotationModel> rotationModel = nullptr;
485
double beginning = -std::numeric_limits<double>::infinity();
486
double ending = std::numeric_limits<double>::infinity();
488
// If any new timeline values are specified, we need to overrideOldTimeline will
490
bool overrideOldTimeline = false;
492
// The interaction of Modify with timelines is slightly complicated. If the timeline
493
// is specified by putting the OrbitFrame, Orbit, BodyFrame, or RotationModel directly
494
// in the object definition (i.e. not inside a Timeline structure), it will completely
495
// replace the previous timeline if it contained more than one phase. Otherwise, the
496
// properties of the single phase will be modified individually, for compatibility with
497
// Celestia versions 1.5.0 and earlier.
498
if (disposition == DataDisposition::Modify)
500
const Timeline* timeline = body->getTimeline();
501
if (timeline->phaseCount() == 1)
503
auto phase = timeline->getPhase(0).get();
504
orbitFrame = phase->orbitFrame();
505
bodyFrame = phase->bodyFrame();
506
orbit = phase->orbit();
507
rotationModel = phase->rotationModel();
508
beginning = phase->startTime();
509
ending = phase->endTime();
513
// Get the object's orbit reference frame.
514
bool newOrbitFrame = false;
515
const Value* frameValue = planetData->getValue("OrbitFrame");
516
if (frameValue != nullptr)
518
auto frame = CreateReferenceFrame(universe, frameValue, parentObject, body);
519
if (frame != nullptr)
522
newOrbitFrame = true;
523
overrideOldTimeline = true;
527
// Get the object's body frame.
528
bool newBodyFrame = false;
529
const Value* bodyFrameValue = planetData->getValue("BodyFrame");
530
if (bodyFrameValue != nullptr)
532
auto frame = CreateReferenceFrame(universe, bodyFrameValue, parentObject, body);
533
if (frame != nullptr)
537
overrideOldTimeline = true;
541
// If no orbit or body frame was specified, use the default ones
542
if (orbitFrame == nullptr)
543
orbitFrame = defaultOrbitFrame;
544
if (bodyFrame == nullptr)
545
bodyFrame = defaultBodyFrame;
547
// If the center of the is a star, orbital element units are
548
// in AU; otherwise, use kilometers.
549
orbitsPlanet = orbitFrame->getCenter().star() == nullptr;
551
auto newOrbit = CreateOrbit(orbitFrame->getCenter(), planetData, path, !orbitsPlanet);
552
if (newOrbit == nullptr && orbit == nullptr)
554
if (body->getTimeline() && disposition == DataDisposition::Modify)
556
// The object definition is modifying an existing object with a multiple phase
557
// timeline, but no orbit definition was given. This can happen for completely
558
// sensible reasons, such a Modify definition that just changes visual properties.
559
// Or, the definition may try to change other timeline phase properties such as
560
// the orbit frame, but without providing an orbit. In both cases, we'll just
561
// leave the original timeline alone.
566
GetLogger()->error("No valid orbit specified for object '{}'. Skipping.\n", body->getName());
571
// If a new orbit was given, override any old orbit
572
if (newOrbit != nullptr)
575
overrideOldTimeline = true;
578
// Get the rotation model for this body
579
double syncRotationPeriod = orbit->getPeriod();
580
auto newRotationModel = CreateRotationModel(planetData, path, syncRotationPeriod);
582
// If a new rotation model was given, override the old one
583
if (newRotationModel != nullptr)
585
rotationModel = newRotationModel;
586
overrideOldTimeline = true;
589
// If there was no rotation model specified, nor a previous rotation model to
590
// override, create the default one.
591
if (rotationModel == nullptr)
593
// If no rotation model is provided, use a default rotation model--
594
// a uniform rotation that's synchronous with the orbit (appropriate
595
// for nearly all natural satellites in the solar system.)
596
rotationModel = CreateDefaultRotationModel(syncRotationPeriod);
599
if (ParseDate(planetData, "Beginning", beginning))
600
overrideOldTimeline = true;
601
if (ParseDate(planetData, "Ending", ending))
602
overrideOldTimeline = true;
604
// Something went wrong if the disposition isn't modify and no timeline
606
assert(disposition == DataDisposition::Modify || overrideOldTimeline);
608
if (overrideOldTimeline)
610
if (beginning >= ending)
612
GetLogger()->error("Beginning time must be before Ending time.\n");
616
// We finally have an orbit, rotation model, frames, and time range. Create
617
// the object timeline.
618
auto phase = TimelinePhase::CreateTimelinePhase(universe,
626
// We've already checked that beginning < ending; nothing else should go
627
// wrong during the creation of a TimelinePhase.
628
assert(phase != nullptr);
629
if (phase == nullptr)
631
GetLogger()->error("Internal error creating TimelinePhase.\n");
635
auto timeline = std::make_unique<Timeline>();
636
timeline->appendPhase(phase);
638
body->setTimeline(std::move(timeline));
640
// Check for circular references in frames; this can only be done once the timeline
641
// has actually been set.
642
// TIMELINE-TODO: This check is not comprehensive; it won't find recursion in
643
// multiphase timelines.
644
if (newOrbitFrame && isFrameCircular(*body->getOrbitFrame(0.0), ReferenceFrame::PositionFrame))
646
GetLogger()->error("Orbit frame for '{}' is nested too deep (probably circular)\n", body->getName());
650
if (newBodyFrame && isFrameCircular(*body->getBodyFrame(0.0), ReferenceFrame::OrientationFrame))
652
GetLogger()->error("Body frame for '{}' is nested too deep (probably circular)\n", body->getName());
661
ReadMesh(const Hash& planetData, Body& body, const fs::path& path)
663
using engine::GeometryInfo;
664
using engine::GetGeometryManager;
666
auto mesh = planetData.getString("Mesh"sv);
670
ResourceHandle geometryHandle;
671
float geometryScale = 1.0f;
672
if (auto geometry = util::U8FileName(*mesh); geometry.has_value())
674
auto geometryCenter = planetData.getVector3<float>("MeshCenter"sv).value_or(Eigen::Vector3f::Zero());
675
// TODO: Adjust bounding radius if model center isn't
676
// (0.0f, 0.0f, 0.0f)
678
bool isNormalized = planetData.getBoolean("NormalizeMesh"sv).value_or(true);
679
if (auto meshScale = planetData.getLength<float>("MeshScale"sv); meshScale.has_value())
680
geometryScale = meshScale.value();
682
geometryHandle = GetGeometryManager()->getHandle(GeometryInfo(*geometry, path, geometryCenter, 1.0f, isNormalized));
686
// Some add-ons appear to be using Mesh "" to switch off the geometry
688
GetLogger()->error("Invalid filename in Mesh\n");
689
geometryHandle = GetGeometryManager()->getHandle(GeometryInfo({}));
692
body.setGeometry(geometryHandle);
693
body.setGeometryScale(geometryScale);
696
void ReadAtmosphere(Body* body,
697
const Hash* atmosData,
698
const fs::path& path,
699
DataDisposition disposition)
701
auto bodyFeaturesManager = GetBodyFeaturesManager();
702
std::unique_ptr<Atmosphere> newAtmosphere = nullptr;
703
Atmosphere* atmosphere = nullptr;
704
if (disposition == DataDisposition::Modify)
705
atmosphere = bodyFeaturesManager->getAtmosphere(body);
707
if (atmosphere == nullptr)
709
newAtmosphere = std::make_unique<Atmosphere>();
710
atmosphere = newAtmosphere.get();
713
if (auto height = atmosData->getLength<float>("Height"); height.has_value())
714
atmosphere->height = *height;
715
if (auto color = atmosData->getColor("Lower"); color.has_value())
716
atmosphere->lowerColor = *color;
717
if (auto color = atmosData->getColor("Upper"); color.has_value())
718
atmosphere->upperColor = *color;
719
if (auto color = atmosData->getColor("Sky"); color.has_value())
720
atmosphere->skyColor = *color;
721
if (auto color = atmosData->getColor("Sunset"); color.has_value())
722
atmosphere->sunsetColor = *color;
724
if (auto mieCoeff = atmosData->getNumber<float>("Mie"); mieCoeff.has_value())
725
atmosphere->mieCoeff = *mieCoeff;
726
if (auto mieScaleHeight = atmosData->getLength<float>("MieScaleHeight"))
727
atmosphere->mieScaleHeight = *mieScaleHeight;
728
if (auto miePhaseAsymmetry = atmosData->getNumber<float>("MieAsymmetry"); miePhaseAsymmetry.has_value())
729
atmosphere->miePhaseAsymmetry = *miePhaseAsymmetry;
730
if (auto rayleighCoeff = atmosData->getVector3<float>("Rayleigh"); rayleighCoeff.has_value())
731
atmosphere->rayleighCoeff = *rayleighCoeff;
732
//atmosData->getNumber("RayleighScaleHeight", atmosphere->rayleighScaleHeight);
733
if (auto absorptionCoeff = atmosData->getVector3<float>("Absorption"); absorptionCoeff.has_value())
734
atmosphere->absorptionCoeff = *absorptionCoeff;
736
// Get the cloud map settings
737
if (auto cloudHeight = atmosData->getLength<float>("CloudHeight"); cloudHeight.has_value())
738
atmosphere->cloudHeight = *cloudHeight;
739
if (auto cloudSpeed = atmosData->getNumber<float>("CloudSpeed"); cloudSpeed.has_value())
740
atmosphere->cloudSpeed = math::degToRad(*cloudSpeed);
742
if (auto cloudTexture = GetFilename(*atmosData, "CloudMap"sv, "Invalid filename in CloudMap\n");
743
cloudTexture.has_value())
745
atmosphere->cloudTexture.setTexture(*cloudTexture,
747
TextureInfo::WrapTexture);
750
if (auto cloudNormalMap = GetFilename(*atmosData, "CloudNormalMap"sv, "Invalid filename in CloudNormalMap\n");
751
cloudNormalMap.has_value())
753
atmosphere->cloudNormalMap.setTexture(*cloudNormalMap,
755
TextureInfo::WrapTexture | TextureInfo::LinearColorspace);
758
if (auto cloudShadowDepth = atmosData->getNumber<float>("CloudShadowDepth"); cloudShadowDepth.has_value())
760
cloudShadowDepth = std::clamp(*cloudShadowDepth, 0.0f, 1.0f);
761
atmosphere->cloudShadowDepth = *cloudShadowDepth;
764
if (newAtmosphere != nullptr)
765
bodyFeaturesManager->setAtmosphere(body, std::move(newAtmosphere));
769
void ReadRings(Body* body,
770
const Hash* ringsData,
771
const fs::path& path,
772
DataDisposition disposition)
774
auto inner = ringsData->getLength<float>("Inner");
775
auto outer = ringsData->getLength<float>("Outer");
777
std::unique_ptr<RingSystem> newRings = nullptr;
778
RingSystem* rings = nullptr;
779
auto bodyFeaturesManager = GetBodyFeaturesManager();
780
if (disposition == DataDisposition::Modify)
781
rings = bodyFeaturesManager->getRings(body);
783
if (rings == nullptr)
785
if (!inner.has_value() || !outer.has_value())
787
GetLogger()->error(_("Ring system needs inner and outer radii.\n"));
791
newRings = std::make_unique<RingSystem>(*inner, *outer);
792
rings = newRings.get();
796
if (inner.has_value())
797
rings->innerRadius = *inner;
798
if (outer.has_value())
799
rings->outerRadius = *outer;
802
if (auto color = ringsData->getColor("Color"); color.has_value())
803
rings->color = *color;
805
if (auto textureName = GetFilename(*ringsData, "Texture"sv, "Invalid filename in rings Texture\n");
806
textureName.has_value())
808
rings->texture = MultiResTexture(*textureName, path);
811
if (newRings != nullptr)
812
bodyFeaturesManager->setRings(body, std::move(newRings));
816
// Create a body (planet, moon, spacecraft, etc.) using the values from a
817
// property list. The usePlanetsUnits flags specifies whether period and
818
// semi-major axis are in years and AU rather than days and kilometers.
819
Body* CreateBody(const std::string& name,
820
PlanetarySystem* system,
823
const Hash* planetData,
824
const fs::path& path,
825
DataDisposition disposition,
828
Body* body = nullptr;
830
if (disposition == DataDisposition::Modify || disposition == DataDisposition::Replace)
835
body = system->addBody(name);
836
// If the body doesn't exist, always treat the disposition as 'Add'
837
disposition = DataDisposition::Add;
839
// Set the default classification for new objects based on the body type.
840
// This may be overridden by the Class property.
841
if (bodyType == SurfaceObject)
843
body->setClassification(BodyClassification::SurfaceFeature);
847
if (!CreateTimeline(body, system, universe, planetData, path, disposition, bodyType))
849
// No valid timeline given; give up.
850
if (body != existingBody)
851
system->removeBody(body);
855
// Three values control the shape and size of an ellipsoidal object:
856
// semiAxes, radius, and oblateness. It is an error if neither the
857
// radius nor semiaxes are set. If both are set, the radius is
858
// multipled by each of the specified semiaxis to give the shape of
859
// the body ellipsoid. Oblateness is ignored if semiaxes are provided;
860
// otherwise, the ellipsoid has semiaxes: ( radius, radius, 1-radius ).
861
// These rather complex rules exist to maintain backward compatibility.
863
// If the body also has a mesh, it is always scaled in x, y, and z by
864
// the maximum semiaxis, never anisotropically.
866
auto radius = static_cast<double>(body->getRadius());
867
bool radiusSpecified = false;
868
if (auto rad = planetData->getLength<double>("Radius"); rad.has_value())
871
body->setSemiAxes(Eigen::Vector3f::Constant((float) radius));
872
radiusSpecified = true;
875
bool semiAxesSpecified = false;
876
auto semiAxes = planetData->getVector3<double>("SemiAxes");
878
if (semiAxes.has_value())
880
if ((*semiAxes).x() <= 0.0 || (*semiAxes).y() <= 0.0 || (*semiAxes).z() <= 0.0)
882
GetLogger()->error(_("Invalid SemiAxes value for object {}: [{}, {}, {}]\n"),
891
if (radiusSpecified && semiAxes.has_value())
893
// If the radius has been specified, treat SemiAxes as dimensionless
894
// (ignore units) and multiply the SemiAxes by the Radius.
898
if (semiAxes.has_value())
900
// Swap y and z to match internal coordinate system
901
semiAxes->tail<2>().reverseInPlace();
902
body->setSemiAxes(semiAxes->cast<float>());
903
semiAxesSpecified = true;
906
if (!semiAxesSpecified)
908
auto oblateness = planetData->getNumber<float>("Oblateness");
909
if (oblateness.has_value())
911
if (*oblateness >= 0.0f && *oblateness < 1.0f)
913
body->setSemiAxes(body->getRadius() * Eigen::Vector3f(1.0f, 1.0f - *oblateness, 1.0f));
917
GetLogger()->error(_("Invalid Oblateness value for object {}: {}\n"), name, *oblateness);
922
BodyClassification classification = body->getClassification();
923
if (const std::string* classificationName = planetData->getString("Class"); classificationName != nullptr)
924
classification = GetClassificationId(*classificationName);
926
if (classification == BodyClassification::Unknown)
928
// Try to guess the type
929
if (system->getPrimaryBody() != nullptr)
930
classification = radius > 0.1 ? BodyClassification::Moon : BodyClassification::Spacecraft;
932
classification = radius < 1000.0 ? BodyClassification::Asteroid : BodyClassification::Planet;
934
body->setClassification(classification);
936
if (classification == BodyClassification::Invisible)
937
body->setVisible(false);
939
// Set default properties for the object based on its classification
940
if (util::is_set(classification, CLASSES_UNCLICKABLE))
941
body->setClickable(false);
943
// TODO: should be own class
944
if (const auto *infoURL = planetData->getString("InfoURL"); infoURL != nullptr)
945
body->setInfoURL(BuildInfoURL(*infoURL, path));
947
if (auto albedo = planetData->getNumber<float>("Albedo"); albedo.has_value())
949
// TODO: make this warn
950
GetLogger()->verbose("Deprecated parameter Albedo used in {} definition.\nUse GeomAlbedo & BondAlbedo instead.\n", name);
951
body->setGeomAlbedo(*albedo);
954
if (auto albedo = planetData->getNumber<float>("GeomAlbedo"); albedo.has_value())
958
body->setGeomAlbedo(*albedo);
959
// Set the BondAlbedo and Reflectivity values if it is <1, otherwise as 1.
962
body->setBondAlbedo(*albedo);
963
body->setReflectivity(*albedo);
967
GetLogger()->error(_("Incorrect GeomAlbedo value: {}\n"), *albedo);
971
if (auto reflectivity = planetData->getNumber<float>("Reflectivity"); reflectivity.has_value())
973
if (*reflectivity >= 0.0f && *reflectivity <= 1.0f)
974
body->setReflectivity(*reflectivity);
976
GetLogger()->error(_("Incorrect Reflectivity value: {}\n"), *reflectivity);
979
if (auto albedo = planetData->getNumber<float>("BondAlbedo"); albedo.has_value())
981
if (*albedo >= 0.0f && *albedo <= 1.0f)
982
body->setBondAlbedo(*albedo);
984
GetLogger()->error(_("Incorrect BondAlbedo value: {}\n"), *albedo);
987
if (auto temperature = planetData->getNumber<float>("Temperature"); temperature.has_value() && *temperature > 0.0f)
988
body->setTemperature(*temperature);
989
if (auto tempDiscrepancy = planetData->getNumber<float>("TempDiscrepancy"); tempDiscrepancy.has_value())
990
body->setTempDiscrepancy(*tempDiscrepancy);
991
if (auto mass = planetData->getMass<float>("Mass", 1.0, 1.0); mass.has_value())
992
body->setMass(*mass);
993
if (auto density = planetData->getNumber<float>("Density"); density.has_value())
994
body->setDensity(*density);
996
if (auto orientation = planetData->getRotation("Orientation"); orientation.has_value())
997
body->setGeometryOrientation(*orientation);
1000
if (disposition == DataDisposition::Modify)
1001
surface = body->getSurface();
1003
surface.color = Color(1.0f, 1.0f, 1.0f);
1005
FillinSurface(planetData, &surface, path);
1006
body->setSurface(surface);
1008
ReadMesh(*planetData, *body, path);
1010
// Read the atmosphere
1011
if (const Value* atmosDataValue = planetData->getValue("Atmosphere"); atmosDataValue != nullptr)
1013
if (const Hash* atmosData = atmosDataValue->getHash(); atmosData == nullptr)
1014
GetLogger()->error(_("Atmosphere must be an associative array.\n"));
1016
ReadAtmosphere(body, atmosData, path, disposition);
1019
// Read the ring system
1020
if (const Value* ringsDataValue = planetData->getValue("Rings"); ringsDataValue != nullptr)
1022
if (const Hash* ringsData = ringsDataValue->getHash(); ringsData == nullptr)
1023
GetLogger()->error(_("Rings must be an associative array.\n"));
1025
ReadRings(body, ringsData, path, disposition);
1028
auto bodyFeaturesManager = GetBodyFeaturesManager();
1030
// Read comet tail color
1031
if (auto cometTailColor = planetData->getColor("TailColor"); cometTailColor.has_value())
1032
bodyFeaturesManager->setCometTailColor(body, *cometTailColor);
1034
if (auto clickable = planetData->getBoolean("Clickable"); clickable.has_value())
1035
body->setClickable(*clickable);
1037
if (auto visible = planetData->getBoolean("Visible"); visible.has_value())
1038
body->setVisible(*visible);
1040
if (auto orbitColor = planetData->getColor("OrbitColor"); orbitColor.has_value())
1042
bodyFeaturesManager->setOrbitColor(body, *orbitColor);
1043
bodyFeaturesManager->setOrbitColorOverridden(body, true);
1050
// Create a barycenter object using the values from a hash
1051
Body* CreateReferencePoint(const std::string& name,
1052
PlanetarySystem* system,
1055
const Hash* refPointData,
1056
const fs::path& path,
1057
DataDisposition disposition)
1059
Body* body = nullptr;
1060
if (disposition == DataDisposition::Modify || disposition == DataDisposition::Replace)
1062
body = existingBody;
1065
if (body == nullptr)
1067
body = system->addBody(name);
1068
// If the point doesn't exist, always treat the disposition as 'Add'
1069
disposition = DataDisposition::Add;
1072
body->setSemiAxes(Eigen::Vector3f::Ones());
1073
body->setClassification(BodyClassification::Invisible);
1074
body->setVisible(false);
1075
body->setClickable(false);
1077
if (!CreateTimeline(body, system, universe, refPointData, path, disposition, ReferencePoint))
1079
// No valid timeline given; give up.
1080
if (body != existingBody)
1081
system->removeBody(body);
1085
// Reference points can be marked visible; no geometry is shown, but the label and orbit
1087
if (auto visible = refPointData->getBoolean("Visible"); visible.has_value())
1089
body->setVisible(*visible);
1092
if (auto clickable = refPointData->getBoolean("Clickable"); clickable.has_value())
1094
body->setClickable(*clickable);
1097
if (auto orbitColor = refPointData->getColor("OrbitColor"); orbitColor.has_value())
1099
GetBodyFeaturesManager()->setOrbitColor(body, *orbitColor);
1100
GetBodyFeaturesManager()->setOrbitColorOverridden(body, true);
1105
} // end unnamed namespace
1107
bool LoadSolarSystemObjects(std::istream& in,
1109
const fs::path& directory)
1111
Tokenizer tokenizer(&in);
1112
Parser parser(&tokenizer);
1115
std::string s = directory.string();
1116
const char* d = s.c_str();
1117
bindtextdomain(d, d); // domain name is the same as resource path
1120
while (tokenizer.nextToken() != Tokenizer::TokenEnd)
1122
// Read the disposition; if none is specified, the default is Add.
1123
DataDisposition disposition = DataDisposition::Add;
1124
if (auto tokenValue = tokenizer.getNameValue(); tokenValue.has_value())
1126
if (*tokenValue == "Add")
1128
disposition = DataDisposition::Add;
1129
tokenizer.nextToken();
1131
else if (*tokenValue == "Replace")
1133
disposition = DataDisposition::Replace;
1134
tokenizer.nextToken();
1136
else if (*tokenValue == "Modify")
1138
disposition = DataDisposition::Modify;
1139
tokenizer.nextToken();
1143
// Read the item type; if none is specified the default is Body
1144
std::string itemType("Body");
1145
if (auto tokenValue = tokenizer.getNameValue(); tokenValue.has_value())
1147
itemType = *tokenValue;
1148
tokenizer.nextToken();
1151
// The name list is a string with zero more names. Multiple names are
1152
// delimited by colons.
1153
std::string nameList;
1154
if (auto tokenValue = tokenizer.getStringValue(); tokenValue.has_value())
1156
nameList = *tokenValue;
1160
sscError(tokenizer, "object name expected");
1164
tokenizer.nextToken();
1165
std::string parentName;
1166
if (auto tokenValue = tokenizer.getStringValue(); tokenValue.has_value())
1168
parentName = *tokenValue;
1172
sscError(tokenizer, "bad parent object name");
1176
const Value objectDataValue = parser.readValue();
1177
const Hash* objectData = objectDataValue.getHash();
1178
if (objectData == nullptr)
1180
sscError(tokenizer, "{ expected");
1184
Selection parent = universe.findPath(parentName, {});
1185
PlanetarySystem* parentSystem = nullptr;
1187
std::vector<std::string> names;
1188
// Iterate through the string for names delimited
1189
// by ':', and insert them into the name list.
1190
if (nameList.empty())
1192
names.push_back("");
1196
std::string::size_type startPos = 0;
1197
while (startPos != std::string::npos)
1199
std::string::size_type next = nameList.find(':', startPos);
1200
std::string::size_type length = std::string::npos;
1201
if (next != std::string::npos)
1203
length = next - startPos;
1206
names.push_back(nameList.substr(startPos, length));
1210
std::string primaryName = names.front();
1212
BodyType bodyType = UnknownBodyType;
1213
if (itemType == "Body")
1214
bodyType = NormalBody;
1215
else if (itemType == "ReferencePoint")
1216
bodyType = ReferencePoint;
1217
else if (itemType == "SurfaceObject")
1218
bodyType = SurfaceObject;
1220
if (bodyType != UnknownBodyType)
1222
//bool orbitsPlanet = false;
1223
if (parent.star() != nullptr)
1225
const SolarSystem* solarSystem = universe.getOrCreateSolarSystem(parent.star());
1226
parentSystem = solarSystem->getPlanets();
1228
else if (parent.body() != nullptr)
1230
// Parent is a planet or moon
1231
parentSystem = parent.body()->getOrCreateSatellites();
1235
sscError(tokenizer, fmt::sprintf(_("parent body '%s' of '%s' not found.\n"), parentName, primaryName));
1238
if (parentSystem != nullptr)
1240
Body* existingBody = parentSystem->find(primaryName);
1243
if (disposition == DataDisposition::Add)
1244
sscError(tokenizer, fmt::sprintf(_("warning duplicate definition of %s %s\n"), parentName, primaryName));
1245
else if (disposition == DataDisposition::Replace)
1246
existingBody->setDefaultProperties();
1250
if (bodyType == ReferencePoint)
1251
body = CreateReferencePoint(primaryName, parentSystem, universe, existingBody, objectData, directory, disposition);
1253
body = CreateBody(primaryName, parentSystem, universe, existingBody, objectData, directory, disposition, bodyType);
1255
if (body != nullptr)
1257
UserCategory::loadCategories(body, *objectData, disposition, directory.string());
1258
if (disposition == DataDisposition::Add)
1259
for (const auto& name : names)
1260
body->addAlias(name);
1264
else if (itemType == "AltSurface")
1266
auto surface = std::make_unique<Surface>();
1267
surface->color = Color(1.0f, 1.0f, 1.0f);
1268
FillinSurface(objectData, surface.get(), directory);
1269
if (parent.body() != nullptr)
1270
GetBodyFeaturesManager()->addAlternateSurface(parent.body(), primaryName, std::move(surface));
1272
sscError(tokenizer, _("bad alternate surface"));
1274
else if (itemType == "Location")
1276
if (parent.body() != nullptr)
1278
std::unique_ptr<Location> location = CreateLocation(objectData, parent.body());
1279
if (location != nullptr)
1281
UserCategory::loadCategories(location.get(), *objectData, disposition, directory.string());
1282
location->setName(primaryName);
1283
GetBodyFeaturesManager()->addLocation(parent.body(), std::move(location));
1287
sscError(tokenizer, _("bad location"));
1292
sscError(tokenizer, fmt::sprintf(_("parent body '%s' of '%s' not found.\n"), parentName, primaryName));
1297
// TODO: Return some notification if there's an error parsing the file
1302
SolarSystem::SolarSystem(Star* _star) :
1305
planets = std::make_unique<PlanetarySystem>(star);
1306
frameTree = std::make_unique<FrameTree>(star);
1309
SolarSystem::~SolarSystem() = default;
1312
Star* SolarSystem::getStar() const
1317
Eigen::Vector3f SolarSystem::getCenter() const
1319
// TODO: This is a very simple method at the moment, but it will get
1320
// more complex when planets around multistar systems are supported
1321
// where the planets may orbit the center of mass of two stars.
1322
return star->getPosition();
1325
PlanetarySystem* SolarSystem::getPlanets() const
1327
return planets.get();
1330
FrameTree* SolarSystem::getFrameTree() const
1332
return frameTree.get();