Celestia

Форк
0
/
solarsys.cpp 
1333 строки · 47.8 Кб
1
// solarsys.cpp
2
//
3
// Copyright (C) 2001-2009, the Celestia Development Team
4
// Original version by Chris Laurel <claurel@gmail.com>
5
//
6
// Solar system catalog parser.
7
//
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.
12

13
#include <cassert>
14
#include <cstddef>
15
#include <cstring>
16
#include <istream>
17
#include <limits>
18
#include <memory>
19
#include <string>
20
#include <string_view>
21
#include <vector>
22

23
#include <Eigen/Geometry>
24
#include <fmt/printf.h>
25

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"
35
#include "body.h"
36
#include "category.h"
37
#include "hash.h"
38
#include "frame.h"
39
#include "frametree.h"
40
#include "location.h"
41
#include "meshmanager.h"
42
#include "parseobject.h"
43
#include "parser.h"
44
#include "solarsys.h"
45
#include "surface.h"
46
#include "texmanager.h"
47
#include "universe.h"
48
#include "value.h"
49

50
// size_t and strncmp are used by the gperf output code
51
using std::size_t;
52
using std::strncmp;
53
using namespace std::string_view_literals;
54

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;
60

61
namespace
62
{
63

64
enum BodyType
65
{
66
    ReferencePoint,
67
    NormalBody,
68
    SurfaceObject,
69
    UnknownBodyType,
70
};
71

72

73
/*!
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.
79

80
  An ssc file contains zero or more definitions of this form:
81

82
  \code
83
  [disposition] [item type] "name" "parent name"
84
  {
85
     ...object info fields...
86
  }
87
  \endcode
88

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.
96

97
  All dispositions are equivalent to add if no item of the same name
98
  already exists.
99

100
  The item type is one of Body, Location, or AltSurface, defaulting to
101
  Body when no type is given.
102

103
  The name and parent name are both mandatory.
104
*/
105

106
void sscError(const Tokenizer& tok,
107
              const std::string& msg)
108
{
109
    GetLogger()->error(_("Error in .ssc file (line {}): {}\n"),
110
                      tok.getLineNumber(), msg);
111
}
112

113
// Object class properties
114
constexpr auto CLASSES_UNCLICKABLE           = BodyClassification::Invisible |
115
                                               BodyClassification::Diffuse;
116

117
// lookup table generated by gperf (solarsys.gperf)
118
#include "solarsys.inc"
119

120
BodyClassification GetClassificationId(std::string_view className)
121
{
122
    auto ptr = ClassificationMap::getClassification(className.data(), className.size());
123
    return ptr == nullptr
124
        ? BodyClassification::Unknown
125
        : ptr->classification;
126
}
127

128

129
//! Maximum depth permitted for nested frames.
130
unsigned int MaxFrameDepth = 50;
131

132
bool isFrameCircular(const ReferenceFrame& frame, ReferenceFrame::FrameType frameType)
133
{
134
    return frame.nestingDepth(MaxFrameDepth, frameType) > MaxFrameDepth;
135
}
136

137

138

139
std::unique_ptr<Location>
140
CreateLocation(const Hash* locationData,
141
               const Body* body)
142
{
143
    auto location = std::make_unique<Location>();
144

145
    auto longlat = locationData->getSphericalTuple("LongLat").value_or(Eigen::Vector3d::Zero());
146
    Eigen::Vector3f position = body->geodeticToCartesian(longlat).cast<float>();
147
    location->setPosition(position);
148

149
    auto size = locationData->getLength<float>("Size").value_or(1.0f);
150
    location->setSize(size);
151

152
    auto importance = locationData->getNumber<float>("Importance").value_or(-1.0f);
153
    location->setImportance(importance);
154

155
    if (const std::string* featureTypeName = locationData->getString("Type"); featureTypeName != nullptr)
156
        location->setFeatureType(Location::parseFeatureType(*featureTypeName));
157

158
    if (auto labelColor = locationData->getColor("LabelColor"); labelColor.has_value())
159
    {
160
        location->setLabelColor(*labelColor);
161
        location->setLabelColorOverridden(true);
162
    }
163

164
    return location;
165
}
166

167
template<typename Dst, typename Flag>
168
inline void SetOrUnset(Dst &dst, Flag flag, bool cond)
169
{
170
    if (cond)
171
        dst |= flag;
172
    else
173
        dst &= ~flag;
174
}
175

176
std::optional<fs::path>
177
GetFilename(const Hash& hash, std::string_view key, const char* errorMessage)
178
{
179
    const std::string* value = hash.getString(key);
180
    if (value == nullptr)
181
        return std::nullopt;
182

183
    auto result = util::U8FileName(*value);
184
    if (!result.has_value())
185
        GetLogger()->error(errorMessage);
186

187
    return result;
188
}
189

190

191
void FillinSurface(const Hash* surfaceData,
192
                   Surface* surface,
193
                   const fs::path& path)
194
{
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;
203

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");
210

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;
215

216
    auto bumpHeight = surfaceData->getNumber<float>("BumpHeight").value_or(2.5f);
217

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);
221

222
    SetOrUnset(baseFlags, TextureInfo::CompressTexture, compressTexture);
223

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));
232

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);
239

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);
245

246
    if (overlayTexture.has_value())
247
        surface->overlayTexture.setTexture(*overlayTexture, path, baseFlags);
248
}
249

250

251
Selection GetParentObject(PlanetarySystem* system)
252
{
253
    Selection parent;
254
    Body* primary = system->getPrimaryBody();
255
    if (primary != nullptr)
256
        parent = Selection(primary);
257
    else
258
        parent = Selection(system->getStar());
259

260
    return parent;
261
}
262

263

264
TimelinePhase::SharedConstPtr CreateTimelinePhase(Body* body,
265
                                                  Universe& universe,
266
                                                  const Hash* phaseData,
267
                                                  const fs::path& path,
268
                                                  const ReferenceFrame::SharedConstPtr& defaultOrbitFrame,
269
                                                  const ReferenceFrame::SharedConstPtr& defaultBodyFrame,
270
                                                  bool isFirstPhase,
271
                                                  bool isLastPhase,
272
                                                  double previousPhaseEnd)
273
{
274
    double beginning = previousPhaseEnd;
275
    double ending = std::numeric_limits<double>::infinity();
276

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)
282
    {
283
        GetLogger()->error("Error: Beginning can only be specified for initial phase of timeline.\n");
284
        return nullptr;
285
    }
286

287
    // Ending is required for all phases except for the final one.
288
    bool hasEnding = ParseDate(phaseData, "Ending", ending);
289
    if (!isLastPhase && !hasEnding)
290
    {
291
        GetLogger()->error("Error: Ending is required for all timeline phases other than the final one.\n");
292
        return nullptr;
293
    }
294

295
    // Get the orbit reference frame.
296
    ReferenceFrame::SharedConstPtr orbitFrame;
297
    const Value* frameValue = phaseData->getValue("OrbitFrame");
298
    if (frameValue != nullptr)
299
    {
300
        orbitFrame = CreateReferenceFrame(universe, frameValue, defaultOrbitFrame->getCenter(), body);
301
        if (orbitFrame == nullptr)
302
        {
303
            return nullptr;
304
        }
305
    }
306
    else
307
    {
308
        // No orbit frame specified; use the default frame.
309
        orbitFrame = defaultOrbitFrame;
310
    }
311

312
    // Get the body reference frame
313
    ReferenceFrame::SharedConstPtr bodyFrame;
314
    const Value* bodyFrameValue = phaseData->getValue("BodyFrame");
315
    if (bodyFrameValue != nullptr)
316
    {
317
        bodyFrame = CreateReferenceFrame(universe, bodyFrameValue, defaultBodyFrame->getCenter(), body);
318
        if (bodyFrame == nullptr)
319
        {
320
            return nullptr;
321
        }
322
    }
323
    else
324
    {
325
        // No body frame specified; use the default frame.
326
        bodyFrame = defaultBodyFrame;
327
    }
328

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;
332

333
    // Get the orbit
334
    auto orbit = CreateOrbit(orbitFrame->getCenter(), phaseData, path, usePlanetUnits);
335
    if (!orbit)
336
    {
337
        GetLogger()->error("Error: missing orbit in timeline phase.\n");
338
        return nullptr;
339
    }
340

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());
345
    if (!rotationModel)
346
    {
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();
351
    }
352

353
    auto phase = TimelinePhase::CreateTimelinePhase(universe,
354
                                                    body,
355
                                                    beginning, ending,
356
                                                    orbitFrame,
357
                                                    orbit,
358
                                                    bodyFrame,
359
                                                    rotationModel);
360

361
    // Frame ownership transfered to phase; release local references
362
    return phase;
363
}
364

365

366
std::unique_ptr<Timeline>
367
CreateTimelineFromArray(Body* body,
368
                        Universe& universe,
369
                        const ValueArray* timelineArray,
370
                        const fs::path& path,
371
                        const ReferenceFrame::SharedConstPtr& defaultOrbitFrame,
372
                        const ReferenceFrame::SharedConstPtr& defaultBodyFrame)
373
{
374
    auto timeline = std::make_unique<Timeline>();
375
    double previousEnding = -std::numeric_limits<double>::infinity();
376

377
    if (timelineArray->empty())
378
    {
379
        GetLogger()->error("Error in timeline of '{}': timeline array is empty.\n", body->getName());
380
        return nullptr;
381
    }
382

383
    const auto finalIter = timelineArray->end() - 1;
384
    for (auto iter = timelineArray->begin(); iter != timelineArray->end(); iter++)
385
    {
386
        const Hash* phaseData = iter->getHash();
387
        if (phaseData == nullptr)
388
        {
389
            GetLogger()->error("Error in timeline of '{}': phase {} is not a property group.\n", body->getName(), iter - timelineArray->begin() + 1);
390
            return nullptr;
391
        }
392

393
        bool isFirstPhase = iter == timelineArray->begin();
394
        bool isLastPhase =  iter == finalIter;
395

396
        auto phase = CreateTimelinePhase(body, universe, phaseData,
397
                                         path,
398
                                         defaultOrbitFrame,
399
                                         defaultBodyFrame,
400
                                         isFirstPhase, isLastPhase, previousEnding);
401
        if (phase == nullptr)
402
        {
403
            GetLogger()->error("Error in timeline of '{}', phase {}.\n",
404
                               body->getName(),
405
                               iter - timelineArray->begin() + 1);
406
            return nullptr;
407
        }
408

409
        previousEnding = phase->endTime();
410

411
        timeline->appendPhase(phase);
412
    }
413

414
    return timeline;
415
}
416

417

418
bool CreateTimeline(Body* body,
419
                    PlanetarySystem* system,
420
                    Universe& universe,
421
                    const Hash* planetData,
422
                    const fs::path& path,
423
                    DataDisposition disposition,
424
                    BodyType bodyType)
425
{
426
    FrameTree* parentFrameTree = nullptr;
427
    Selection parentObject = GetParentObject(system);
428
    bool orbitsPlanet = false;
429
    if (parentObject.body())
430
    {
431
        parentFrameTree = parentObject.body()->getOrCreateFrameTree();
432
        //orbitsPlanet = true;
433
    }
434
    else if (parentObject.star())
435
    {
436
        const SolarSystem* solarSystem = universe.getOrCreateSolarSystem(parentObject.star());
437
        parentFrameTree = solarSystem->getFrameTree();
438
    }
439
    else
440
    {
441
        // Bad orbit barycenter specified
442
        return false;
443
    }
444

445
    ReferenceFrame::SharedConstPtr defaultOrbitFrame;
446
    ReferenceFrame::SharedConstPtr defaultBodyFrame;
447
    if (bodyType == SurfaceObject)
448
    {
449
        defaultOrbitFrame = std::make_shared<BodyFixedFrame>(parentObject, parentObject);
450
        defaultBodyFrame = CreateTopocentricFrame(parentObject, parentObject, Selection(body));
451
    }
452
    else
453
    {
454
        defaultOrbitFrame = parentFrameTree->getDefaultReferenceFrame();
455
        defaultBodyFrame = parentFrameTree->getDefaultReferenceFrame();
456
    }
457

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)
462
    {
463
        const ValueArray* timelineArray = value->getArray();
464
        if (timelineArray == nullptr)
465
        {
466
            GetLogger()->error("Error: Timeline must be an array\n");
467
            return false;
468
        }
469

470
        std::unique_ptr<Timeline> timeline = CreateTimelineFromArray(body, universe, timelineArray, path,
471
                                                                     defaultOrbitFrame, defaultBodyFrame);
472

473
        if (!timeline)
474
            return false;
475

476
        body->setTimeline(std::move(timeline));
477
        return true;
478
    }
479

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();
487

488
    // If any new timeline values are specified, we need to overrideOldTimeline will
489
    // be set to true.
490
    bool overrideOldTimeline = false;
491

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)
499
    {
500
        const Timeline* timeline = body->getTimeline();
501
        if (timeline->phaseCount() == 1)
502
        {
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();
510
        }
511
    }
512

513
    // Get the object's orbit reference frame.
514
    bool newOrbitFrame = false;
515
    const Value* frameValue = planetData->getValue("OrbitFrame");
516
    if (frameValue != nullptr)
517
    {
518
        auto frame = CreateReferenceFrame(universe, frameValue, parentObject, body);
519
        if (frame != nullptr)
520
        {
521
            orbitFrame = frame;
522
            newOrbitFrame = true;
523
            overrideOldTimeline = true;
524
        }
525
    }
526

527
    // Get the object's body frame.
528
    bool newBodyFrame = false;
529
    const Value* bodyFrameValue = planetData->getValue("BodyFrame");
530
    if (bodyFrameValue != nullptr)
531
    {
532
        auto frame = CreateReferenceFrame(universe, bodyFrameValue, parentObject, body);
533
        if (frame != nullptr)
534
        {
535
            bodyFrame = frame;
536
            newBodyFrame = true;
537
            overrideOldTimeline = true;
538
        }
539
    }
540

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;
546

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;
550

551
    auto newOrbit = CreateOrbit(orbitFrame->getCenter(), planetData, path, !orbitsPlanet);
552
    if (newOrbit == nullptr && orbit == nullptr)
553
    {
554
        if (body->getTimeline() && disposition == DataDisposition::Modify)
555
        {
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.
562
            return true;
563
        }
564
        else
565
        {
566
            GetLogger()->error("No valid orbit specified for object '{}'. Skipping.\n", body->getName());
567
            return false;
568
        }
569
    }
570

571
    // If a new orbit was given, override any old orbit
572
    if (newOrbit != nullptr)
573
    {
574
        orbit = newOrbit;
575
        overrideOldTimeline = true;
576
    }
577

578
    // Get the rotation model for this body
579
    double syncRotationPeriod = orbit->getPeriod();
580
    auto newRotationModel = CreateRotationModel(planetData, path, syncRotationPeriod);
581

582
    // If a new rotation model was given, override the old one
583
    if (newRotationModel != nullptr)
584
    {
585
        rotationModel = newRotationModel;
586
        overrideOldTimeline = true;
587
    }
588

589
    // If there was no rotation model specified, nor a previous rotation model to
590
    // override, create the default one.
591
    if (rotationModel == nullptr)
592
    {
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);
597
    }
598

599
    if (ParseDate(planetData, "Beginning", beginning))
600
        overrideOldTimeline = true;
601
    if (ParseDate(planetData, "Ending", ending))
602
        overrideOldTimeline = true;
603

604
    // Something went wrong if the disposition isn't modify and no timeline
605
    // is to be created.
606
    assert(disposition == DataDisposition::Modify || overrideOldTimeline);
607

608
    if (overrideOldTimeline)
609
    {
610
        if (beginning >= ending)
611
        {
612
            GetLogger()->error("Beginning time must be before Ending time.\n");
613
            return false;
614
        }
615

616
        // We finally have an orbit, rotation model, frames, and time range. Create
617
        // the object timeline.
618
        auto phase = TimelinePhase::CreateTimelinePhase(universe,
619
                                                        body,
620
                                                        beginning, ending,
621
                                                        orbitFrame,
622
                                                        orbit,
623
                                                        bodyFrame,
624
                                                        rotationModel);
625

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)
630
        {
631
            GetLogger()->error("Internal error creating TimelinePhase.\n");
632
            return false;
633
        }
634

635
        auto timeline = std::make_unique<Timeline>();
636
        timeline->appendPhase(phase);
637

638
        body->setTimeline(std::move(timeline));
639

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))
645
        {
646
            GetLogger()->error("Orbit frame for '{}' is nested too deep (probably circular)\n", body->getName());
647
            return false;
648
        }
649

650
        if (newBodyFrame && isFrameCircular(*body->getBodyFrame(0.0), ReferenceFrame::OrientationFrame))
651
        {
652
            GetLogger()->error("Body frame for '{}' is nested too deep (probably circular)\n", body->getName());
653
            return false;
654
        }
655
    }
656

657
    return true;
658
}
659

660
void
661
ReadMesh(const Hash& planetData, Body& body, const fs::path& path)
662
{
663
    using engine::GeometryInfo;
664
    using engine::GetGeometryManager;
665

666
    auto mesh = planetData.getString("Mesh"sv);
667
    if (mesh == nullptr)
668
        return;
669

670
    ResourceHandle geometryHandle;
671
    float geometryScale = 1.0f;
672
    if (auto geometry = util::U8FileName(*mesh); geometry.has_value())
673
    {
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)
677

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();
681

682
        geometryHandle = GetGeometryManager()->getHandle(GeometryInfo(*geometry, path, geometryCenter, 1.0f, isNormalized));
683
    }
684
    else
685
    {
686
        // Some add-ons appear to be using Mesh "" to switch off the geometry
687
        if (!mesh->empty())
688
            GetLogger()->error("Invalid filename in Mesh\n");
689
        geometryHandle = GetGeometryManager()->getHandle(GeometryInfo({}));
690
    }
691

692
    body.setGeometry(geometryHandle);
693
    body.setGeometryScale(geometryScale);
694
}
695

696
void ReadAtmosphere(Body* body,
697
                    const Hash* atmosData,
698
                    const fs::path& path,
699
                    DataDisposition disposition)
700
{
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);
706

707
    if (atmosphere == nullptr)
708
    {
709
        newAtmosphere = std::make_unique<Atmosphere>();
710
        atmosphere = newAtmosphere.get();
711
    }
712

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;
723

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;
735

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);
741

742
    if (auto cloudTexture = GetFilename(*atmosData, "CloudMap"sv, "Invalid filename in CloudMap\n");
743
        cloudTexture.has_value())
744
    {
745
        atmosphere->cloudTexture.setTexture(*cloudTexture,
746
                                            path,
747
                                            TextureInfo::WrapTexture);
748
    }
749

750
    if (auto cloudNormalMap = GetFilename(*atmosData, "CloudNormalMap"sv, "Invalid filename in CloudNormalMap\n");
751
        cloudNormalMap.has_value())
752
    {
753
        atmosphere->cloudNormalMap.setTexture(*cloudNormalMap,
754
                                              path,
755
                                              TextureInfo::WrapTexture | TextureInfo::LinearColorspace);
756
    }
757

758
    if (auto cloudShadowDepth = atmosData->getNumber<float>("CloudShadowDepth"); cloudShadowDepth.has_value())
759
    {
760
        cloudShadowDepth = std::clamp(*cloudShadowDepth, 0.0f, 1.0f);
761
        atmosphere->cloudShadowDepth = *cloudShadowDepth;
762
    }
763

764
    if (newAtmosphere != nullptr)
765
        bodyFeaturesManager->setAtmosphere(body, std::move(newAtmosphere));
766
}
767

768

769
void ReadRings(Body* body,
770
               const Hash* ringsData,
771
               const fs::path& path,
772
               DataDisposition disposition)
773
{
774
    auto inner = ringsData->getLength<float>("Inner");
775
    auto outer = ringsData->getLength<float>("Outer");
776

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);
782

783
    if (rings == nullptr)
784
    {
785
        if (!inner.has_value() || !outer.has_value())
786
        {
787
            GetLogger()->error(_("Ring system needs inner and outer radii.\n"));
788
            return;
789
        }
790

791
        newRings = std::make_unique<RingSystem>(*inner, *outer);
792
        rings = newRings.get();
793
    }
794
    else
795
    {
796
        if (inner.has_value())
797
            rings->innerRadius = *inner;
798
        if (outer.has_value())
799
            rings->outerRadius = *outer;
800
    }
801

802
    if (auto color = ringsData->getColor("Color"); color.has_value())
803
        rings->color = *color;
804

805
    if (auto textureName = GetFilename(*ringsData, "Texture"sv, "Invalid filename in rings Texture\n");
806
        textureName.has_value())
807
    {
808
        rings->texture = MultiResTexture(*textureName, path);
809
    }
810

811
    if (newRings != nullptr)
812
        bodyFeaturesManager->setRings(body, std::move(newRings));
813
}
814

815

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,
821
                 Universe& universe,
822
                 Body* existingBody,
823
                 const Hash* planetData,
824
                 const fs::path& path,
825
                 DataDisposition disposition,
826
                 BodyType bodyType)
827
{
828
    Body* body = nullptr;
829

830
    if (disposition == DataDisposition::Modify || disposition == DataDisposition::Replace)
831
        body = existingBody;
832

833
    if (body == nullptr)
834
    {
835
        body = system->addBody(name);
836
        // If the body doesn't exist, always treat the disposition as 'Add'
837
        disposition = DataDisposition::Add;
838

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)
842
        {
843
            body->setClassification(BodyClassification::SurfaceFeature);
844
        }
845
    }
846

847
    if (!CreateTimeline(body, system, universe, planetData, path, disposition, bodyType))
848
    {
849
        // No valid timeline given; give up.
850
        if (body != existingBody)
851
            system->removeBody(body);
852
        return nullptr;
853
    }
854

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.
862
    //
863
    // If the body also has a mesh, it is always scaled in x, y, and z by
864
    // the maximum semiaxis, never anisotropically.
865

866
    auto radius = static_cast<double>(body->getRadius());
867
    bool radiusSpecified = false;
868
    if (auto rad = planetData->getLength<double>("Radius"); rad.has_value())
869
    {
870
        radius = *rad;
871
        body->setSemiAxes(Eigen::Vector3f::Constant((float) radius));
872
        radiusSpecified = true;
873
    }
874

875
    bool semiAxesSpecified = false;
876
    auto semiAxes = planetData->getVector3<double>("SemiAxes");
877

878
    if (semiAxes.has_value())
879
    {
880
        if ((*semiAxes).x() <= 0.0 || (*semiAxes).y() <= 0.0 || (*semiAxes).z() <= 0.0)
881
        {
882
            GetLogger()->error(_("Invalid SemiAxes value for object {}: [{}, {}, {}]\n"),
883
                               name,
884
                               (*semiAxes).x(),
885
                               (*semiAxes).y(),
886
                               (*semiAxes).z());
887
            semiAxes.reset();
888
        }
889
    }
890

891
    if (radiusSpecified && semiAxes.has_value())
892
    {
893
        // If the radius has been specified, treat SemiAxes as dimensionless
894
        // (ignore units) and multiply the SemiAxes by the Radius.
895
        *semiAxes *= radius;
896
    }
897

898
    if (semiAxes.has_value())
899
    {
900
        // Swap y and z to match internal coordinate system
901
        semiAxes->tail<2>().reverseInPlace();
902
        body->setSemiAxes(semiAxes->cast<float>());
903
        semiAxesSpecified = true;
904
    }
905

906
    if (!semiAxesSpecified)
907
    {
908
        auto oblateness = planetData->getNumber<float>("Oblateness");
909
        if (oblateness.has_value())
910
        {
911
            if (*oblateness >= 0.0f && *oblateness < 1.0f)
912
            {
913
                body->setSemiAxes(body->getRadius() * Eigen::Vector3f(1.0f, 1.0f - *oblateness, 1.0f));
914
            }
915
            else
916
            {
917
                GetLogger()->error(_("Invalid Oblateness value for object {}: {}\n"), name, *oblateness);
918
            }
919
        }
920
    }
921

922
    BodyClassification classification = body->getClassification();
923
    if (const std::string* classificationName = planetData->getString("Class"); classificationName != nullptr)
924
        classification = GetClassificationId(*classificationName);
925

926
    if (classification == BodyClassification::Unknown)
927
    {
928
        // Try to guess the type
929
        if (system->getPrimaryBody() != nullptr)
930
            classification = radius > 0.1 ? BodyClassification::Moon : BodyClassification::Spacecraft;
931
        else
932
            classification = radius < 1000.0 ? BodyClassification::Asteroid : BodyClassification::Planet;
933
    }
934
    body->setClassification(classification);
935

936
    if (classification == BodyClassification::Invisible)
937
        body->setVisible(false);
938

939
    // Set default properties for the object based on its classification
940
    if (util::is_set(classification, CLASSES_UNCLICKABLE))
941
        body->setClickable(false);
942

943
    // TODO: should be own class
944
    if (const auto *infoURL = planetData->getString("InfoURL"); infoURL != nullptr)
945
        body->setInfoURL(BuildInfoURL(*infoURL, path));
946

947
    if (auto albedo = planetData->getNumber<float>("Albedo"); albedo.has_value())
948
    {
949
        // TODO: make this warn
950
        GetLogger()->verbose("Deprecated parameter Albedo used in {} definition.\nUse GeomAlbedo & BondAlbedo instead.\n", name);
951
        body->setGeomAlbedo(*albedo);
952
    }
953

954
    if (auto albedo = planetData->getNumber<float>("GeomAlbedo"); albedo.has_value())
955
    {
956
        if (*albedo > 0.0)
957
        {
958
            body->setGeomAlbedo(*albedo);
959
            // Set the BondAlbedo and Reflectivity values if it is <1, otherwise as 1.
960
            if (*albedo > 1.0f)
961
                albedo = 1.0f;
962
            body->setBondAlbedo(*albedo);
963
            body->setReflectivity(*albedo);
964
        }
965
        else
966
        {
967
            GetLogger()->error(_("Incorrect GeomAlbedo value: {}\n"), *albedo);
968
        }
969
    }
970

971
    if (auto reflectivity = planetData->getNumber<float>("Reflectivity"); reflectivity.has_value())
972
    {
973
        if (*reflectivity >= 0.0f && *reflectivity <= 1.0f)
974
            body->setReflectivity(*reflectivity);
975
        else
976
            GetLogger()->error(_("Incorrect Reflectivity value: {}\n"), *reflectivity);
977
    }
978

979
    if (auto albedo = planetData->getNumber<float>("BondAlbedo"); albedo.has_value())
980
    {
981
        if (*albedo >= 0.0f && *albedo <= 1.0f)
982
            body->setBondAlbedo(*albedo);
983
        else
984
            GetLogger()->error(_("Incorrect BondAlbedo value: {}\n"), *albedo);
985
    }
986

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);
995

996
    if (auto orientation = planetData->getRotation("Orientation"); orientation.has_value())
997
        body->setGeometryOrientation(*orientation);
998

999
    Surface surface;
1000
    if (disposition == DataDisposition::Modify)
1001
        surface = body->getSurface();
1002
    else
1003
        surface.color = Color(1.0f, 1.0f, 1.0f);
1004

1005
    FillinSurface(planetData, &surface, path);
1006
    body->setSurface(surface);
1007

1008
    ReadMesh(*planetData, *body, path);
1009

1010
    // Read the atmosphere
1011
    if (const Value* atmosDataValue = planetData->getValue("Atmosphere"); atmosDataValue != nullptr)
1012
    {
1013
        if (const Hash* atmosData = atmosDataValue->getHash(); atmosData == nullptr)
1014
            GetLogger()->error(_("Atmosphere must be an associative array.\n"));
1015
        else
1016
            ReadAtmosphere(body, atmosData, path, disposition);
1017
    }
1018

1019
    // Read the ring system
1020
    if (const Value* ringsDataValue = planetData->getValue("Rings"); ringsDataValue != nullptr)
1021
    {
1022
        if (const Hash* ringsData = ringsDataValue->getHash(); ringsData == nullptr)
1023
            GetLogger()->error(_("Rings must be an associative array.\n"));
1024
        else
1025
            ReadRings(body, ringsData, path, disposition);
1026
    }
1027

1028
    auto bodyFeaturesManager = GetBodyFeaturesManager();
1029

1030
    // Read comet tail color
1031
    if (auto cometTailColor = planetData->getColor("TailColor"); cometTailColor.has_value())
1032
        bodyFeaturesManager->setCometTailColor(body, *cometTailColor);
1033

1034
    if (auto clickable = planetData->getBoolean("Clickable"); clickable.has_value())
1035
        body->setClickable(*clickable);
1036

1037
    if (auto visible = planetData->getBoolean("Visible"); visible.has_value())
1038
        body->setVisible(*visible);
1039

1040
    if (auto orbitColor = planetData->getColor("OrbitColor"); orbitColor.has_value())
1041
    {
1042
        bodyFeaturesManager->setOrbitColor(body, *orbitColor);
1043
        bodyFeaturesManager->setOrbitColorOverridden(body, true);
1044
    }
1045

1046
    return body;
1047
}
1048

1049

1050
// Create a barycenter object using the values from a hash
1051
Body* CreateReferencePoint(const std::string& name,
1052
                           PlanetarySystem* system,
1053
                           Universe& universe,
1054
                           Body* existingBody,
1055
                           const Hash* refPointData,
1056
                           const fs::path& path,
1057
                           DataDisposition disposition)
1058
{
1059
    Body* body = nullptr;
1060
    if (disposition == DataDisposition::Modify || disposition == DataDisposition::Replace)
1061
    {
1062
        body = existingBody;
1063
    }
1064

1065
    if (body == nullptr)
1066
    {
1067
        body = system->addBody(name);
1068
        // If the point doesn't exist, always treat the disposition as 'Add'
1069
        disposition = DataDisposition::Add;
1070
    }
1071

1072
    body->setSemiAxes(Eigen::Vector3f::Ones());
1073
    body->setClassification(BodyClassification::Invisible);
1074
    body->setVisible(false);
1075
    body->setClickable(false);
1076

1077
    if (!CreateTimeline(body, system, universe, refPointData, path, disposition, ReferencePoint))
1078
    {
1079
        // No valid timeline given; give up.
1080
        if (body != existingBody)
1081
            system->removeBody(body);
1082
        return nullptr;
1083
    }
1084

1085
    // Reference points can be marked visible; no geometry is shown, but the label and orbit
1086
    // will be.
1087
    if (auto visible = refPointData->getBoolean("Visible"); visible.has_value())
1088
    {
1089
        body->setVisible(*visible);
1090
    }
1091

1092
    if (auto clickable = refPointData->getBoolean("Clickable"); clickable.has_value())
1093
    {
1094
        body->setClickable(*clickable);
1095
    }
1096

1097
    if (auto orbitColor = refPointData->getColor("OrbitColor"); orbitColor.has_value())
1098
    {
1099
        GetBodyFeaturesManager()->setOrbitColor(body, *orbitColor);
1100
        GetBodyFeaturesManager()->setOrbitColorOverridden(body, true);
1101
    }
1102

1103
    return body;
1104
}
1105
} // end unnamed namespace
1106

1107
bool LoadSolarSystemObjects(std::istream& in,
1108
                            Universe& universe,
1109
                            const fs::path& directory)
1110
{
1111
    Tokenizer tokenizer(&in);
1112
    Parser parser(&tokenizer);
1113

1114
#ifdef ENABLE_NLS
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
1118
#endif
1119

1120
    while (tokenizer.nextToken() != Tokenizer::TokenEnd)
1121
    {
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())
1125
        {
1126
            if (*tokenValue == "Add")
1127
            {
1128
                disposition = DataDisposition::Add;
1129
                tokenizer.nextToken();
1130
            }
1131
            else if (*tokenValue == "Replace")
1132
            {
1133
                disposition = DataDisposition::Replace;
1134
                tokenizer.nextToken();
1135
            }
1136
            else if (*tokenValue == "Modify")
1137
            {
1138
                disposition = DataDisposition::Modify;
1139
                tokenizer.nextToken();
1140
            }
1141
        }
1142

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())
1146
        {
1147
            itemType = *tokenValue;
1148
            tokenizer.nextToken();
1149
        }
1150

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())
1155
        {
1156
            nameList = *tokenValue;
1157
        }
1158
        else
1159
        {
1160
            sscError(tokenizer, "object name expected");
1161
            return false;
1162
        }
1163

1164
        tokenizer.nextToken();
1165
        std::string parentName;
1166
        if (auto tokenValue = tokenizer.getStringValue(); tokenValue.has_value())
1167
        {
1168
            parentName = *tokenValue;
1169
        }
1170
        else
1171
        {
1172
            sscError(tokenizer, "bad parent object name");
1173
            return false;
1174
        }
1175

1176
        const Value objectDataValue = parser.readValue();
1177
        const Hash* objectData = objectDataValue.getHash();
1178
        if (objectData == nullptr)
1179
        {
1180
            sscError(tokenizer, "{ expected");
1181
            return false;
1182
        }
1183

1184
        Selection parent = universe.findPath(parentName, {});
1185
        PlanetarySystem* parentSystem = nullptr;
1186

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())
1191
        {
1192
            names.push_back("");
1193
        }
1194
        else
1195
        {
1196
            std::string::size_type startPos   = 0;
1197
            while (startPos != std::string::npos)
1198
            {
1199
                std::string::size_type next   = nameList.find(':', startPos);
1200
                std::string::size_type length = std::string::npos;
1201
                if (next != std::string::npos)
1202
                {
1203
                    length = next - startPos;
1204
                    ++next;
1205
                }
1206
                names.push_back(nameList.substr(startPos, length));
1207
                startPos   = next;
1208
            }
1209
        }
1210
        std::string primaryName = names.front();
1211

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;
1219

1220
        if (bodyType != UnknownBodyType)
1221
        {
1222
            //bool orbitsPlanet = false;
1223
            if (parent.star() != nullptr)
1224
            {
1225
                const SolarSystem* solarSystem = universe.getOrCreateSolarSystem(parent.star());
1226
                parentSystem = solarSystem->getPlanets();
1227
            }
1228
            else if (parent.body() != nullptr)
1229
            {
1230
                // Parent is a planet or moon
1231
                parentSystem = parent.body()->getOrCreateSatellites();
1232
            }
1233
            else
1234
            {
1235
                sscError(tokenizer, fmt::sprintf(_("parent body '%s' of '%s' not found.\n"), parentName, primaryName));
1236
            }
1237

1238
            if (parentSystem != nullptr)
1239
            {
1240
                Body* existingBody = parentSystem->find(primaryName);
1241
                if (existingBody)
1242
                {
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();
1247
                }
1248

1249
                Body* body;
1250
                if (bodyType == ReferencePoint)
1251
                    body = CreateReferencePoint(primaryName, parentSystem, universe, existingBody, objectData, directory, disposition);
1252
                else
1253
                    body = CreateBody(primaryName, parentSystem, universe, existingBody, objectData, directory, disposition, bodyType);
1254

1255
                if (body != nullptr)
1256
                {
1257
                    UserCategory::loadCategories(body, *objectData, disposition, directory.string());
1258
                    if (disposition == DataDisposition::Add)
1259
                        for (const auto& name : names)
1260
                            body->addAlias(name);
1261
                }
1262
            }
1263
        }
1264
        else if (itemType == "AltSurface")
1265
        {
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));
1271
            else
1272
                sscError(tokenizer, _("bad alternate surface"));
1273
        }
1274
        else if (itemType == "Location")
1275
        {
1276
            if (parent.body() != nullptr)
1277
            {
1278
                std::unique_ptr<Location> location = CreateLocation(objectData, parent.body());
1279
                if (location != nullptr)
1280
                {
1281
                    UserCategory::loadCategories(location.get(), *objectData, disposition, directory.string());
1282
                    location->setName(primaryName);
1283
                    GetBodyFeaturesManager()->addLocation(parent.body(), std::move(location));
1284
                }
1285
                else
1286
                {
1287
                    sscError(tokenizer, _("bad location"));
1288
                }
1289
            }
1290
            else
1291
            {
1292
                sscError(tokenizer, fmt::sprintf(_("parent body '%s' of '%s' not found.\n"), parentName, primaryName));
1293
            }
1294
        }
1295
    }
1296

1297
    // TODO: Return some notification if there's an error parsing the file
1298
    return true;
1299
}
1300

1301

1302
SolarSystem::SolarSystem(Star* _star) :
1303
    star(_star)
1304
{
1305
    planets = std::make_unique<PlanetarySystem>(star);
1306
    frameTree = std::make_unique<FrameTree>(star);
1307
}
1308

1309
SolarSystem::~SolarSystem() = default;
1310

1311

1312
Star* SolarSystem::getStar() const
1313
{
1314
    return star;
1315
}
1316

1317
Eigen::Vector3f SolarSystem::getCenter() const
1318
{
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();
1323
}
1324

1325
PlanetarySystem* SolarSystem::getPlanets() const
1326
{
1327
    return planets.get();
1328
}
1329

1330
FrameTree* SolarSystem::getFrameTree() const
1331
{
1332
    return frameTree.get();
1333
}
1334

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.