3
// Copyright (C) 2001-2024, the Celestia Development Team
4
// Original version by Chris Laurel <claurel@gmail.com>
6
// This program is free software; you can redistribute it and/or
7
// modify it under the terms of the GNU General Public License
8
// as published by the Free Software Foundation; either version 2
9
// of the License, or (at your option) any later version.
11
#include "stardbbuilder.h"
24
#include <boost/smart_ptr/intrusive_ptr.hpp>
26
#include <Eigen/Geometry>
28
#include <fmt/format.h>
30
#include <celastro/astro.h>
31
#include <celmath/geomutil.h>
32
#include <celmath/mathlib.h>
33
#include <celutil/binaryread.h>
34
#include <celutil/fsutils.h>
35
#include <celutil/gettext.h>
36
#include <celutil/logger.h>
37
#include <celutil/timer.h>
38
#include <celutil/tokenizer.h>
40
#include "meshmanager.h"
41
#include "octreebuilder.h"
44
#include "stellarclass.h"
47
using namespace std::string_view_literals;
49
namespace astro = celestia::astro;
50
namespace engine = celestia::engine;
51
namespace ephem = celestia::ephem;
52
namespace math = celestia::math;
53
namespace util = celestia::util;
57
struct StarDatabaseBuilder::StcHeader
59
explicit StcHeader(const fs::path&);
60
explicit StcHeader(fs::path&&) = delete;
64
DataDisposition disposition{ DataDisposition::Add };
66
AstroCatalog::IndexNumber catalogNumber{ AstroCatalog::InvalidIndex };
67
std::vector<std::string> names;
70
StarDatabaseBuilder::StcHeader::StcHeader(const fs::path& _path) :
76
struct fmt::formatter<StarDatabaseBuilder::StcHeader> : formatter<std::string_view>
78
format_context::iterator format(const StarDatabaseBuilder::StcHeader& header, format_context& ctx)
80
fmt::basic_memory_buffer<char> data;
81
fmt::format_to(std::back_inserter(data), "line {}", header.lineNumber);
82
if (header.catalogNumber <= Star::MaxTychoCatalogNumber)
83
fmt::format_to(std::back_inserter(data), " - HIP {}", header.catalogNumber);
84
if (!header.names.empty())
85
fmt::format_to(std::back_inserter(data), " - {}", header.names.front());
86
return formatter<std::string_view>::format(std::string_view(data.data(), data.size()), ctx);
93
// In testing, changing SPLIT_THRESHOLD from 100 to 50 nearly
94
// doubled the number of nodes in the tree, but provided only between a
95
// 0 to 5 percent frame rate improvement.
96
constexpr engine::OctreeObjectIndex StarOctreeSplitThreshold = 75;
98
// The octree node into which a star is placed is dependent on two properties:
99
// its obsPosition and its luminosity--the fainter the star, the deeper the node
100
// in which it will reside. Each node stores an absolute magnitude; no child
101
// of the node is allowed contain a star brighter than this value, making it
102
// possible to determine quickly whether or not to cull subtrees.
104
struct StarOctreeTraits
106
using ObjectType = Star;
107
using PrecisionType = float;
109
static Eigen::Vector3f getPosition(const ObjectType&);
110
static float getRadius(const ObjectType&);
111
static float getMagnitude(const ObjectType&);
112
static float applyDecay(float);
115
inline Eigen::Vector3f
116
StarOctreeTraits::getPosition(const ObjectType& obj)
118
return obj.getPosition();
122
StarOctreeTraits::getRadius(const ObjectType& obj)
124
return obj.getOrbitalRadius();
128
StarOctreeTraits::getMagnitude(const ObjectType& obj)
130
return obj.getAbsoluteMagnitude();
134
StarOctreeTraits::applyDecay(float factor)
136
// Decrease in luminosity by factor of 4
137
// -2.5 * log10(1.0 / 4.0) = 1.50515 (nearest float)
138
return factor + 1.50515f;
141
constexpr float STAR_OCTREE_MAGNITUDE = 6.0f;
143
// We can't compute the intrinsic brightness of the star from
144
// the apparent magnitude if the star is within a few AU of the
146
constexpr float VALID_APPMAG_DISTANCE_THRESHOLD = 1e-5f;
148
constexpr std::string_view STARSDAT_MAGIC = "CELSTARS"sv;
149
constexpr std::uint16_t StarDBVersion = 0x0100;
153
// stars.dat header structure
156
StarsDatHeader() = delete;
157
char magic[8]; //NOSONAR
158
std::uint16_t version;
159
std::uint32_t counter;
162
// stars.dat record structure
165
StarsDatRecord() = delete;
166
AstroCatalog::IndexNumber catNo;
171
std::uint16_t spectralType;
176
static_assert(std::is_standard_layout_v<StarsDatHeader>);
177
static_assert(std::is_standard_layout_v<StarsDatRecord>);
180
parseStarsDatHeader(std::istream& in, std::uint32_t& nStarsInFile)
182
std::array<char, sizeof(StarsDatHeader)> header;
183
if (!in.read(header.data(), header.size()).good()) /* Flawfinder: ignore */
186
// Verify the magic string
187
if (auto magic = std::string_view(header.data() + offsetof(StarsDatHeader, magic), STARSDAT_MAGIC.size());
188
magic != STARSDAT_MAGIC)
193
// Verify the version
194
if (auto version = util::fromMemoryLE<std::uint16_t>(header.data() + offsetof(StarsDatHeader, version));
195
version != StarDBVersion)
200
// Read the star count
201
nStarsInFile = util::fromMemoryLE<std::uint32_t>(header.data() + offsetof(StarsDatHeader, counter));
206
stcError(const StarDatabaseBuilder::StcHeader& header, std::string_view msg)
208
GetLogger()->error(_("Error in .stc file ({}): {}\n"), header, msg);
212
stcWarn(const StarDatabaseBuilder::StcHeader& header, std::string_view msg)
214
GetLogger()->warn(_("Warning in .stc file ({}): {}\n"), header, msg);
218
parseStcHeader(Tokenizer& tokenizer, StarDatabaseBuilder::StcHeader& header)
220
header.lineNumber = tokenizer.getLineNumber();
222
header.isStar = true;
224
// Parse the disposition--either Add, Replace, or Modify. The disposition
225
// may be omitted. The default value is Add.
226
header.disposition = DataDisposition::Add;
227
if (auto tokenValue = tokenizer.getNameValue(); tokenValue.has_value())
229
if (*tokenValue == "Modify")
231
header.disposition = DataDisposition::Modify;
232
tokenizer.nextToken();
234
else if (*tokenValue == "Replace")
236
header.disposition = DataDisposition::Replace;
237
tokenizer.nextToken();
239
else if (*tokenValue == "Add")
241
header.disposition = DataDisposition::Add;
242
tokenizer.nextToken();
246
// Parse the object type--either Star or Barycenter. The object type
247
// may be omitted. The default is Star.
248
if (auto tokenValue = tokenizer.getNameValue(); tokenValue.has_value())
250
if (*tokenValue == "Star")
252
header.isStar = true;
254
else if (*tokenValue == "Barycenter")
256
header.isStar = false;
260
stcError(header, _("unrecognized object type"));
263
tokenizer.nextToken();
266
// Parse the catalog number; it may be omitted if a name is supplied.
267
header.catalogNumber = AstroCatalog::InvalidIndex;
268
if (auto tokenValue = tokenizer.getNumberValue(); tokenValue.has_value())
270
header.catalogNumber = static_cast<AstroCatalog::IndexNumber>(*tokenValue);
271
tokenizer.nextToken();
274
header.names.clear();
275
if (auto tokenValue = tokenizer.getStringValue(); tokenValue.has_value())
277
for (std::string_view remaining = *tokenValue; !remaining.empty();)
279
auto pos = remaining.find(':');
280
if (std::string_view name = remaining.substr(0, pos);
281
!name.empty() && std::find(header.names.cbegin(), header.names.cend(), name) == header.names.cend())
283
header.names.emplace_back(name);
286
if (pos == std::string_view::npos || header.names.size() == StarDatabase::MAX_STAR_NAMES)
289
remaining = remaining.substr(pos + 1);
292
tokenizer.nextToken();
294
else if (header.catalogNumber == AstroCatalog::InvalidIndex)
296
stcError(header, _("entry missing name and catalog number"));
304
checkSpectralType(const StarDatabaseBuilder::StcHeader& header,
305
const AssociativeArray* starData,
307
boost::intrusive_ptr<StarDetails>& newDetails)
309
const std::string* spectralType = starData->getString("SpectralType");
312
if (spectralType != nullptr)
313
stcWarn(header, _("ignoring SpectralType on Barycenter"));
314
newDetails = StarDetails::GetBarycenterDetails();
316
else if (spectralType != nullptr)
318
newDetails = StarDetails::GetStarDetails(StellarClass::parse(*spectralType));
319
if (newDetails == nullptr)
321
stcError(header, _("invalid SpectralType"));
325
else if (header.disposition != DataDisposition::Modify || star->isBarycenter())
327
stcError(header, _("missing SpectralType on Star"));
335
checkPolarCoordinates(const StarDatabaseBuilder::StcHeader& header,
336
const AssociativeArray* starData,
338
std::optional<Eigen::Vector3f>& position)
340
constexpr unsigned int has_ra = 1;
341
constexpr unsigned int has_dec = 2;
342
constexpr unsigned int has_distance = 4;
343
constexpr unsigned int has_all = has_ra | has_dec | has_distance;
345
unsigned int status = 0;
347
auto raValue = starData->getAngle<double>("RA", astro::DEG_PER_HRA, 1.0);
348
auto decValue = starData->getAngle<double>("Dec");
349
auto distanceValue = starData->getLength<double>("Distance", astro::KM_PER_LY<double>);
350
status = (static_cast<unsigned int>(raValue.has_value()) * has_ra)
351
| (static_cast<unsigned int>(decValue.has_value()) * has_dec)
352
| (static_cast<unsigned int>(distanceValue.has_value()) * has_distance);
357
if (status == has_all)
359
position = astro::equatorialToCelestialCart(*raValue, *decValue, *distanceValue).cast<float>();
363
if (header.disposition != DataDisposition::Modify)
365
stcError(header, _("incomplete set of coordinates RA/Dec/Distance specified"));
369
// Partial modification of polar coordinates
370
assert(star != nullptr);
372
// Convert from Celestia's coordinate system
373
const Eigen::Vector3f& p = star->getPosition();
374
Eigen::Vector3d v = math::XRotation(math::degToRad(astro::J2000Obliquity)) * Eigen::Vector3f(p.x(), -p.z(), p.y()).cast<double>();
375
// Disable Sonar on the below: suggests using value-or which would eagerly-evaluate the replacement value
376
double distance = distanceValue.has_value() ? *distanceValue : v.norm(); //NOSONAR
377
double ra = raValue.has_value() ? *raValue : (math::radToDeg(std::atan2(v.y(), v.x())) / astro::DEG_PER_HRA); //NOSONAR
378
double dec = decValue.has_value() ? *decValue : math::radToDeg(std::asin(std::clamp(v.z(), -1.0, 1.0))); //NOSONAR
380
position = astro::equatorialToCelestialCart(ra, dec, distance).cast<float>();
385
checkMagnitudes(const StarDatabaseBuilder::StcHeader& header,
386
const AssociativeArray* starData,
389
std::optional<float>& absMagnitude,
390
std::optional<float>& extinction)
392
assert(header.disposition != DataDisposition::Modify || star != nullptr);
393
absMagnitude = starData->getNumber<float>("AbsMag");
394
auto appMagnitude = starData->getNumber<float>("AppMag");
398
if (absMagnitude.has_value())
399
stcWarn(header, _("AbsMag ignored on Barycenter"));
400
if (appMagnitude.has_value())
401
stcWarn(header, _("AppMag ignored on Barycenter"));
402
absMagnitude = 30.0f;
406
extinction = starData->getNumber<float>("Extinction");
407
if (extinction.has_value() && distance < VALID_APPMAG_DISTANCE_THRESHOLD)
409
stcWarn(header, _("Extinction ignored for stars close to the origin"));
410
extinction = std::nullopt;
413
if (absMagnitude.has_value())
415
if (appMagnitude.has_value())
416
stcWarn(header, _("AppMag ignored when AbsMag is supplied"));
418
else if (appMagnitude.has_value())
420
if (distance < VALID_APPMAG_DISTANCE_THRESHOLD)
422
stcError(header, _("AppMag cannot be used close to the origin"));
426
float extinctionValue = 0.0;
427
if (extinction.has_value())
428
extinctionValue = *extinction;
429
else if (header.disposition == DataDisposition::Modify)
430
extinctionValue = star->getExtinction() * distance;
432
absMagnitude = astro::appToAbsMag(*appMagnitude, distance) - extinctionValue;
434
else if (header.disposition != DataDisposition::Modify || star->isBarycenter())
436
stcError(header, _("no magnitude defined for star"));
444
mergeStarDetails(boost::intrusive_ptr<StarDetails>& existingDetails,
445
const boost::intrusive_ptr<StarDetails>& referenceDetails)
447
if (referenceDetails == nullptr)
450
if (existingDetails->shared())
452
// If there are no extended information values set, just
453
// use the new reference details object
454
existingDetails = referenceDetails;
458
// There are custom details: copy the new data into the
460
existingDetails->mergeFromStandard(referenceDetails.get());
465
applyTemperatureBoloCorrection(const StarDatabaseBuilder::StcHeader& header,
466
const AssociativeArray* starData,
467
boost::intrusive_ptr<StarDetails>& details)
469
auto bolometricCorrection = starData->getNumber<float>("BoloCorrection");
470
if (bolometricCorrection.has_value())
473
stcWarn(header, _("BoloCorrection is ignored on Barycenters"));
475
StarDetails::setBolometricCorrection(details, *bolometricCorrection);
478
if (auto temperature = starData->getNumber<float>("Temperature"); temperature.has_value())
482
stcWarn(header, _("Temperature is ignored on Barycenters"));
484
else if (*temperature > 0.0)
486
StarDetails::setTemperature(details, *temperature);
487
if (!bolometricCorrection.has_value())
489
// if we change the temperature, recalculate the bolometric
490
// correction using formula from formula for main sequence
491
// stars given in B. Cameron Reed (1998), "The Composite
492
// Observational-Theoretical HR Diagram", Journal of the Royal
493
// Astronomical Society of Canada, Vol 92. p36.
495
double logT = std::log10(static_cast<double>(*temperature)) - 4.0;
496
double bc = -8.499 * std::pow(logT, 4) + 13.421 * std::pow(logT, 3)
497
- 8.131 * logT * logT - 3.901 * logT - 0.438;
499
StarDetails::setBolometricCorrection(details, static_cast<float>(bc));
504
stcWarn(header, _("Temperature value must be greater than zero"));
510
applyCustomDetails(const StarDatabaseBuilder::StcHeader& header,
511
const AssociativeArray* starData,
512
boost::intrusive_ptr<StarDetails>& details)
514
if (const auto* mesh = starData->getString("Mesh"); mesh != nullptr)
518
stcWarn(header, _("Mesh is ignored on Barycenters"));
520
else if (auto meshPath = util::U8FileName(*mesh); meshPath.has_value())
522
using engine::GeometryInfo;
523
using engine::GetGeometryManager;
524
ResourceHandle geometryHandle = GetGeometryManager()->getHandle(GeometryInfo(*meshPath,
526
Eigen::Vector3f::Zero(),
529
StarDetails::setGeometry(details, geometryHandle);
533
stcError(header, _("invalid filename in Mesh"));
537
if (const auto* texture = starData->getString("Texture"); texture != nullptr)
540
stcWarn(header, _("Texture is ignored on Barycenters"));
541
else if (auto texturePath = util::U8FileName(*texture); texturePath.has_value())
542
StarDetails::setTexture(details, MultiResTexture(*texturePath, *header.path));
544
stcError(header, _("invalid filename in Texture"));
547
if (auto rotationModel = CreateRotationModel(starData, *header.path, 1.0); rotationModel != nullptr)
550
stcWarn(header, _("Rotation is ignored on Barycenters"));
552
StarDetails::setRotationModel(details, rotationModel);
555
if (auto semiAxes = starData->getLengthVector<float>("SemiAxes"); semiAxes.has_value())
558
stcWarn(header, _("SemiAxes is ignored on Barycenters"));
559
else if (semiAxes->minCoeff() >= 0.0)
560
StarDetails::setEllipsoidSemiAxes(details, *semiAxes);
562
stcWarn(header, _("SemiAxes must be greater than zero"));
565
if (auto radius = starData->getLength<float>("Radius"); radius.has_value())
568
stcWarn(header, _("Radius is ignored on Barycenters"));
569
else if (*radius >= 0.0)
570
StarDetails::setRadius(details, *radius);
572
stcWarn(header, _("Radius must be greater than zero"));
575
applyTemperatureBoloCorrection(header, starData, details);
577
if (const auto* infoUrl = starData->getString("InfoURL"); infoUrl != nullptr)
578
StarDetails::setInfoURL(details, *infoUrl);
581
} // end unnamed namespace
583
StarDatabaseBuilder::~StarDatabaseBuilder() = default;
586
StarDatabaseBuilder::loadBinary(std::istream& in)
589
std::uint32_t nStarsInFile;
590
if (!parseStarsDatHeader(in, nStarsInFile))
593
constexpr std::uint32_t BUFFER_RECORDS = UINT32_C(4096) / sizeof(StarsDatRecord);
594
std::vector<char> buffer(sizeof(StarsDatRecord) * BUFFER_RECORDS);
595
std::uint32_t nStarsRemaining = nStarsInFile;
596
while (nStarsRemaining > 0)
598
std::uint32_t recordsToRead = std::min(BUFFER_RECORDS, nStarsRemaining);
599
if (!in.read(buffer.data(), sizeof(StarsDatRecord) * recordsToRead).good()) /* Flawfinder: ignore */
602
const char* ptr = buffer.data();
603
for (std::uint32_t i = 0; i < recordsToRead; ++i)
605
auto catNo = util::fromMemoryLE<AstroCatalog::IndexNumber>(ptr + offsetof(StarsDatRecord, catNo));
606
Eigen::Vector3f position(util::fromMemoryLE<float>(ptr + offsetof(StarsDatRecord, x)),
607
util::fromMemoryLE<float>(ptr + offsetof(StarsDatRecord, y)),
608
util::fromMemoryLE<float>(ptr + offsetof(StarsDatRecord, z)));
609
auto absMag = util::fromMemoryLE<std::int16_t>(ptr + offsetof(StarsDatRecord, absMag));
610
auto spectralType = util::fromMemoryLE<std::uint16_t>(ptr + offsetof(StarsDatRecord, spectralType));
612
boost::intrusive_ptr<StarDetails> details = nullptr;
613
if (StellarClass sc; sc.unpackV1(spectralType))
614
details = StarDetails::GetStarDetails(sc);
616
if (details == nullptr)
618
GetLogger()->error(_("Bad spectral type in star database, star #{}\n"), catNo);
622
Star& star = unsortedStars.emplace_back(catNo, details);
623
star.setPosition(position);
624
star.setAbsoluteMagnitude(static_cast<float>(absMag) / 256.0f);
626
ptr += sizeof(StarsDatRecord);
629
nStarsRemaining -= recordsToRead;
635
auto loadTime = timer.getTime();
637
GetLogger()->debug("StarDatabase::read: nStars = {}, time = {} ms\n", nStarsInFile, loadTime);
638
GetLogger()->info(_("{} stars in binary database\n"), unsortedStars.size());
640
// Create the temporary list of stars sorted by catalog number; this
641
// will be used to lookup stars during file loading. After loading is
642
// complete, the stars are sorted into an octree and this list gets
644
binFileCatalogNumberIndex.reserve(unsortedStars.size());
645
for (Star& star : unsortedStars)
646
binFileCatalogNumberIndex.push_back(&star);
648
std::sort(binFileCatalogNumberIndex.begin(), binFileCatalogNumberIndex.end(),
649
[](const Star* star0, const Star* star1) { return star0->getIndex() < star1->getIndex(); });
654
/*! Load an STC file with star definitions. Each definition has the form:
656
* [disposition] [object type] [catalog number] [name]
661
* Disposition is either Add, Replace, or Modify; Add is the default.
662
* Object type is either Star or Barycenter, with Star the default
663
* It is an error to omit both the catalog number and the name.
665
* The dispositions are slightly more complicated than suggested by
666
* their names. Every star must have an unique catalog number. But
667
* instead of generating an error, Adding a star with a catalog
668
* number that already exists will actually replace that star. Here
669
* are how all of the possibilities are handled:
671
* <name> or <number> already exists:
672
* Add <name> : new star
673
* Add <number> : replace star
674
* Replace <name> : replace star
675
* Replace <number> : replace star
676
* Modify <name> : modify star
677
* Modify <number> : modify star
679
* <name> or <number> doesn't exist:
680
* Add <name> : new star
681
* Add <number> : new star
682
* Replace <name> : new star
683
* Replace <number> : new star
684
* Modify <name> : error
685
* Modify <number> : error
688
StarDatabaseBuilder::load(std::istream& in, const fs::path& resourcePath)
690
Tokenizer tokenizer(&in);
691
Parser parser(&tokenizer);
694
std::string domain = resourcePath.string();
695
const char *d = domain.c_str();
696
bindtextdomain(d, d); // domain name is the same as resource path
701
StcHeader header(resourcePath);
702
while (tokenizer.nextToken() != Tokenizer::TokenEnd)
704
if (!parseStcHeader(tokenizer, header))
707
// now goes the star definition
708
tokenizer.pushBack();
709
const Value starDataValue = parser.readValue();
710
const Hash* starData = starDataValue.getHash();
711
if (starData == nullptr)
713
GetLogger()->error(_("Bad star definition at line {}.\n"), tokenizer.getLineNumber());
717
if (header.disposition != DataDisposition::Add && header.catalogNumber == AstroCatalog::InvalidIndex)
718
header.catalogNumber = starDB->namesDB->findCatalogNumberByName(header.names.front(), false);
720
Star* star = findWhileLoading(header.catalogNumber);
723
if (header.disposition == DataDisposition::Modify)
725
GetLogger()->error(_("Modify requested for nonexistent star.\n"));
729
if (header.catalogNumber == AstroCatalog::InvalidIndex)
731
header.catalogNumber = nextAutoCatalogNumber;
732
--nextAutoCatalogNumber;
736
if (createOrUpdateStar(header, starData, star))
738
loadCategories(header, starData, domain);
740
if (!header.names.empty())
742
starDB->namesDB->erase(header.catalogNumber);
743
for (const auto& name : header.names)
744
starDB->namesDB->add(header.catalogNumber, name);
753
StarDatabaseBuilder::setNameDatabase(std::unique_ptr<StarNameDatabase>&& nameDB)
755
starDB->namesDB = std::move(nameDB);
758
std::unique_ptr<StarDatabase>
759
StarDatabaseBuilder::finish()
761
GetLogger()->info(_("Total star count: {}\n"), unsortedStars.size());
766
// Resolve all barycenters; this can't be done before star sorting. There's
767
// still a bug here: final orbital radii aren't available until after
768
// the barycenters have been resolved, and these are required when building
769
// the octree. This will only rarely cause a problem, but it still needs
771
for (const auto [starIdx, barycenterIdx] : barycenters)
773
Star* star = starDB->find(starIdx);
774
Star* barycenter = starDB->find(barycenterIdx);
775
assert(star != nullptr);
776
assert(barycenter != nullptr);
777
if (star != nullptr && barycenter != nullptr)
779
StarDetails::setOrbitBarycenter(star->details, barycenter);
780
StarDetails::addOrbitingStar(barycenter->details, star);
784
for (const auto& [catalogNumber, category] : categories)
786
Star* star = starDB->find(catalogNumber);
787
UserCategory::addObject(star, category);
790
return std::move(starDB);
793
/*! Load star data from a property list into a star instance.
796
StarDatabaseBuilder::createOrUpdateStar(const StcHeader& header,
797
const AssociativeArray* starData,
800
boost::intrusive_ptr<StarDetails> newDetails = nullptr;
801
if (!checkSpectralType(header, starData, star, newDetails))
804
std::optional<Eigen::Vector3f> position = std::nullopt;
805
std::optional<AstroCatalog::IndexNumber> barycenterNumber = std::nullopt;
806
std::shared_ptr<const ephem::Orbit> orbit = nullptr;
807
if (!checkStcPosition(header, starData, star, position, barycenterNumber, orbit))
810
std::optional<float> absMagnitude = std::nullopt;
811
std::optional<float> extinction = std::nullopt;
813
if (position.has_value())
815
distance = position->norm();
819
assert(star != nullptr);
820
distance = star->getPosition().norm();
823
if (!checkMagnitudes(header, starData, star, distance, absMagnitude, extinction))
828
assert(newDetails != nullptr);
829
star = &unsortedStars.emplace_back(header.catalogNumber, newDetails);
830
stcFileCatalogNumberIndex[header.catalogNumber] = star;
832
else if (header.disposition == DataDisposition::Modify)
834
mergeStarDetails(star->details, newDetails);
838
assert(newDetails != nullptr);
839
star->details = newDetails;
842
if (position.has_value())
843
star->setPosition(*position);
845
if (absMagnitude.has_value())
846
star->setAbsoluteMagnitude(*absMagnitude);
848
if (extinction.has_value())
849
star->setExtinction(*extinction / distance);
851
if (barycenterNumber == AstroCatalog::InvalidIndex)
852
barycenters.erase(header.catalogNumber);
853
else if (barycenterNumber.has_value())
854
barycenters[header.catalogNumber] = *barycenterNumber;
856
if (orbit != nullptr)
857
StarDetails::setOrbit(star->details, orbit);
859
applyCustomDetails(header, starData, star->details);
864
StarDatabaseBuilder::checkStcPosition(const StarDatabaseBuilder::StcHeader& header,
865
const AssociativeArray* starData,
867
std::optional<Eigen::Vector3f>& position,
868
std::optional<AstroCatalog::IndexNumber>& barycenterNumber,
869
std::shared_ptr<const ephem::Orbit>& orbit) const
871
position = std::nullopt;
872
barycenterNumber = std::nullopt;
874
if (!checkPolarCoordinates(header, starData, star, position))
877
if (auto positionValue = starData->getLengthVector<float>("Position", astro::KM_PER_LY<double>);
878
positionValue.has_value())
880
if (position.has_value())
881
stcWarn(header, _("ignoring RA/Dec/Distance in favor of Position"));
882
position = *positionValue;
885
if (!checkBarycenter(header, starData, position, barycenterNumber))
888
// we consider a star to have a barycenter if it has an OrbitBarycenter defined
889
// or the star is modified without overriding its position, and it has no other
890
// position overrides.
891
bool hasBarycenter = (barycenterNumber.has_value() && *barycenterNumber != AstroCatalog::InvalidIndex)
892
|| (header.disposition == DataDisposition::Modify
893
&& !position.has_value()
894
&& barycenters.find(header.catalogNumber) != barycenters.end());
896
if (auto newOrbit = CreateOrbit(Selection(), starData, *header.path, true); newOrbit != nullptr)
899
orbit = std::move(newOrbit);
901
stcWarn(header, _("ignoring orbit for object without OrbitBarycenter"));
903
else if (hasBarycenter && star != nullptr && star->getOrbit() == nullptr)
905
stcError(header, _("no orbit specified for star with OrbitBarycenter"));
913
StarDatabaseBuilder::checkBarycenter(const StarDatabaseBuilder::StcHeader& header,
914
const AssociativeArray* starData,
915
std::optional<Eigen::Vector3f>& position,
916
std::optional<AstroCatalog::IndexNumber>& barycenterNumber) const
918
// If we override RA/Dec/Position, remove the barycenter
919
if (position.has_value())
920
barycenterNumber = AstroCatalog::InvalidIndex;
922
const Value* orbitBarycenterValue = starData->getValue("OrbitBarycenter");
923
if (orbitBarycenterValue == nullptr)
926
if (auto bcNumber = orbitBarycenterValue->getNumber(); bcNumber.has_value())
928
barycenterNumber = static_cast<AstroCatalog::IndexNumber>(*bcNumber);
930
else if (auto bcName = orbitBarycenterValue->getString(); bcName != nullptr)
932
barycenterNumber = starDB->namesDB->findCatalogNumberByName(*bcName, false);
936
stcError(header, _("OrbitBarycenter should be either a string or an integer"));
940
if (*barycenterNumber == header.catalogNumber)
942
stcError(header, _("OrbitBarycenter cycle detected"));
946
if (const Star* barycenter = findWhileLoading(*barycenterNumber); barycenter != nullptr)
948
if (position.has_value())
949
stcWarn(header, "ignoring stellar coordinates in favor of OrbitBarycenter");
950
position = barycenter->getPosition();
954
stcError(header, _("OrbitBarycenter refers to nonexistent star"));
958
for (auto it = barycenters.find(*barycenterNumber); it != barycenters.end(); it = barycenters.find(it->second))
960
if (it->second == header.catalogNumber)
962
stcError(header, _("OrbitBarycenter cycle detected"));
971
StarDatabaseBuilder::loadCategories(const StcHeader& header,
972
const AssociativeArray* starData,
973
const std::string& domain)
975
if (header.disposition == DataDisposition::Replace)
976
categories.erase(header.catalogNumber);
978
const Value* categoryValue = starData->getValue("Category");
979
if (categoryValue == nullptr)
982
if (const std::string* categoryName = categoryValue->getString(); categoryName != nullptr)
984
if (categoryName->empty())
987
addCategory(header.catalogNumber, *categoryName, domain);
991
const ValueArray *arr = categoryValue->getArray();
995
for (const auto& it : *arr)
997
const std::string* categoryName = it.getString();
998
if (categoryName == nullptr || categoryName->empty())
1001
addCategory(header.catalogNumber, *categoryName, domain);
1006
StarDatabaseBuilder::addCategory(AstroCatalog::IndexNumber catalogNumber,
1007
const std::string& name,
1008
const std::string& domain)
1010
auto category = UserCategory::findOrAdd(name, domain);
1011
if (category == UserCategoryId::Invalid)
1014
auto [start, end] = categories.equal_range(catalogNumber);
1017
categories.emplace(catalogNumber, category);
1021
if (std::any_of(start, end, [category](const auto& it) { return it.second == category; }))
1024
categories.emplace_hint(end, catalogNumber, category);
1027
/*! While loading the star catalogs, this function must be called instead of
1028
* find(). The final catalog number index for stars cannot be built until
1029
* after all stars have been loaded. During catalog loading, there are two
1030
* separate indexes: one for the binary catalog and another index for stars
1031
* loaded from stc files. They binary catalog index is a sorted array, while
1032
* the stc catalog index is an STL map. Since the binary file can be quite
1033
* large, we want to avoid creating a map with as many nodes as there are
1034
* stars. Stc files should collectively contain many fewer stars, and stars
1035
* in an stc file may reference each other (barycenters). Thus, a dynamic
1036
* structure like a map is both practical and essential.
1039
StarDatabaseBuilder::findWhileLoading(AstroCatalog::IndexNumber catalogNumber) const
1041
if (catalogNumber == AstroCatalog::InvalidIndex)
1044
// First check for stars loaded from the binary database
1045
if (auto it = std::lower_bound(binFileCatalogNumberIndex.cbegin(), binFileCatalogNumberIndex.cend(),
1047
[](const Star* star, AstroCatalog::IndexNumber catNum) { return star->getIndex() < catNum; });
1048
it != binFileCatalogNumberIndex.cend() && (*it)->getIndex() == catalogNumber)
1053
// Next check for stars loaded from an stc file
1054
if (auto it = stcFileCatalogNumberIndex.find(catalogNumber); it != stcFileCatalogNumberIndex.end())
1062
StarDatabaseBuilder::buildOctree()
1064
// This should only be called once for the database
1065
GetLogger()->debug("Sorting stars into octree . . .\n");
1066
auto starCount = static_cast<engine::OctreeObjectIndex>(unsortedStars.size());
1068
float absMag = astro::appToAbsMag(STAR_OCTREE_MAGNITUDE,
1069
StarDatabase::STAR_OCTREE_ROOT_SIZE * celestia::numbers::sqrt3_v<float>);
1071
auto root = engine::makeDynamicOctree<StarOctreeTraits>(std::move(unsortedStars),
1072
Eigen::Vector3f(1000.0f, 1000.0f, 1000.0f),
1073
StarDatabase::STAR_OCTREE_ROOT_SIZE,
1075
StarOctreeSplitThreshold);
1077
GetLogger()->debug("Spatially sorting stars for improved locality of reference . . .\n");
1078
starDB->octreeRoot = root->build();
1080
GetLogger()->debug("{} stars total\nOctree has {} nodes and {} stars.\n",
1082
starDB->octreeRoot->nodeCount(),
1083
starDB->octreeRoot->size());
1085
unsortedStars.clear();
1089
StarDatabaseBuilder::buildIndexes()
1091
// This should only be called once for the database
1092
// assert(catalogNumberIndexes[0] == nullptr);
1094
GetLogger()->info("Building catalog number indexes . . .\n");
1096
auto nStars = starDB->octreeRoot->size();
1098
starDB->catalogNumberIndex.clear();
1099
starDB->catalogNumberIndex.reserve(nStars);
1100
for (std::uint32_t i = 0; i < nStars; ++i)
1101
starDB->catalogNumberIndex.push_back(i);
1103
const auto& octreeRoot = *starDB->octreeRoot;
1104
std::sort(starDB->catalogNumberIndex.begin(), starDB->catalogNumberIndex.end(),
1105
[&octreeRoot](std::uint32_t idx0, std::uint32_t idx1)
1107
return octreeRoot[idx0].getIndex() < octreeRoot[idx1].getIndex();