// stardb.cpp // // Copyright (C) 2001-2009, the Celestia Development Team // Original version by Chris Laurel // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. #include #include #include #include #include #include #include #include #include #include #include #include "stardb.h" #include "astro.h" #include "parser.h" #include "parseobject.h" #include "multitexture.h" #include "meshmanager.h" #include using namespace Eigen; using namespace std; using namespace celmath; using celestia::util::GetLogger; namespace celutil = celestia::util; constexpr const char HDCatalogPrefix[] = "HD "; constexpr const char HIPPARCOSCatalogPrefix[] = "HIP "; constexpr const char TychoCatalogPrefix[] = "TYC "; constexpr const char SAOCatalogPrefix[] = "SAO "; #if 0 constexpr const char GlieseCatalogPrefix[] = "Gliese "; constexpr const char RossCatalogPrefix[] = "Ross "; constexpr const char LacailleCatalogPrefix[] = "Lacaille "; #endif // The size of the root star octree node is also the maximum distance // distance from the Sun at which any star may be located. The current // setting of 1.0e7 light years is large enough to contain the entire // local group of galaxies. A larger value should be OK, but the // performance implications for octree traversal still need to be // investigated. constexpr const float STAR_OCTREE_ROOT_SIZE = 1000000000.0f; constexpr const float STAR_OCTREE_MAGNITUDE = 6.0f; //constexpr const float STAR_EXTRA_ROOM = 0.01f; // Reserve 1% capacity for extra stars constexpr const char FILE_HEADER[] = "CELSTARS"; constexpr const char CROSSINDEX_FILE_HEADER[] = "CELINDEX"; // Used to sort stars by catalog number struct CatalogNumberOrderingPredicate { int unused; CatalogNumberOrderingPredicate() = default; bool operator()(const Star& star0, const Star& star1) const { return (star0.getIndex() < star1.getIndex()); } }; struct CatalogNumberEquivalencePredicate { int unused; CatalogNumberEquivalencePredicate() = default; bool operator()(const Star& star0, const Star& star1) const { return (star0.getIndex() == star1.getIndex()); } }; // Used to sort star pointers by catalog number struct PtrCatalogNumberOrderingPredicate { int unused; PtrCatalogNumberOrderingPredicate() = default; bool operator()(const Star* const & star0, const Star* const & star1) const { return (star0->getIndex() < star1->getIndex()); } }; static bool parseSimpleCatalogNumber(const string& name, const string& prefix, AstroCatalog::IndexNumber* catalogNumber) { char extra[4]; if (compareIgnoringCase(name, prefix, prefix.length()) == 0) { unsigned int num; // Use scanf to see if we have a valid catalog number; it must be // of the form: No additional // characters other than whitespace are allowed after the number. if (sscanf(name.c_str() + prefix.length(), " %u %c", &num, extra) == 1) { *catalogNumber = (AstroCatalog::IndexNumber) num; return true; } } return false; } static bool parseHIPPARCOSCatalogNumber(const string& name, AstroCatalog::IndexNumber* catalogNumber) { return parseSimpleCatalogNumber(name, HIPPARCOSCatalogPrefix, catalogNumber); } static bool parseHDCatalogNumber(const string& name, AstroCatalog::IndexNumber* catalogNumber) { return parseSimpleCatalogNumber(name, HDCatalogPrefix, catalogNumber); } static bool parseTychoCatalogNumber(const string& name, AstroCatalog::IndexNumber* catalogNumber) { int len = strlen(TychoCatalogPrefix); if (compareIgnoringCase(name, TychoCatalogPrefix, len) == 0) { unsigned int tyc1 = 0, tyc2 = 0, tyc3 = 0; if (sscanf(string(name, len, string::npos).c_str(), " %u-%u-%u", &tyc1, &tyc2, &tyc3) == 3) { *catalogNumber = (AstroCatalog::IndexNumber) (tyc3 * 1000000000 + tyc2 * 10000 + tyc1); return true; } } return false; } static bool parseCelestiaCatalogNumber(const string& name, AstroCatalog::IndexNumber* catalogNumber) { char extra[4]; if (name[0] == '#') { unsigned int num; if (sscanf(name.c_str(), "#%u %c", &num, extra) == 1) { *catalogNumber = (AstroCatalog::IndexNumber) num; return true; } } return false; } bool StarDatabase::CrossIndexEntry::operator<(const StarDatabase::CrossIndexEntry& e) const { return catalogNumber < e.catalogNumber; } StarDatabase::StarDatabase() { crossIndexes.resize(MaxCatalog); } StarDatabase::~StarDatabase() { delete [] stars; delete [] catalogNumberIndex; for (const auto index : crossIndexes) delete index; } Star* StarDatabase::find(AstroCatalog::IndexNumber catalogNumber) const { Star refStar; refStar.setIndex(catalogNumber); Star** star = lower_bound(catalogNumberIndex, catalogNumberIndex + nStars, &refStar, PtrCatalogNumberOrderingPredicate()); if (star != catalogNumberIndex + nStars && (*star)->getIndex() == catalogNumber) return *star; else return nullptr; } AstroCatalog::IndexNumber StarDatabase::findCatalogNumberByName(const string& name, bool i18n) const { if (name.empty()) return AstroCatalog::InvalidIndex; AstroCatalog::IndexNumber catalogNumber = AstroCatalog::InvalidIndex; if (namesDB != nullptr) { catalogNumber = namesDB->findCatalogNumberByName(name, i18n); if (catalogNumber != AstroCatalog::InvalidIndex) return catalogNumber; } if (parseCelestiaCatalogNumber(name, &catalogNumber)) { return catalogNumber; } else if (parseHIPPARCOSCatalogNumber(name, &catalogNumber)) { return catalogNumber; } else if (parseTychoCatalogNumber(name, &catalogNumber)) { return catalogNumber; } else if (parseHDCatalogNumber(name, &catalogNumber)) { return searchCrossIndexForCatalogNumber(HenryDraper, catalogNumber); } else if (parseSimpleCatalogNumber(name, SAOCatalogPrefix, &catalogNumber)) { return searchCrossIndexForCatalogNumber(SAO, catalogNumber); } else { return AstroCatalog::InvalidIndex; } } Star* StarDatabase::find(const string& name, bool i18n) const { AstroCatalog::IndexNumber catalogNumber = findCatalogNumberByName(name, i18n); if (catalogNumber != AstroCatalog::InvalidIndex) return find(catalogNumber); else return nullptr; } AstroCatalog::IndexNumber StarDatabase::crossIndex(const Catalog catalog, const AstroCatalog::IndexNumber celCatalogNumber) const { if (static_cast(catalog) >= crossIndexes.size()) return AstroCatalog::InvalidIndex; CrossIndex* xindex = crossIndexes[catalog]; if (xindex == nullptr) return AstroCatalog::InvalidIndex; // A simple linear search. We could store cross indices sorted by // both catalog numbers and trade memory for speed auto iter = std::find_if(xindex->begin(), xindex->end(), [celCatalogNumber](CrossIndexEntry& o){ return celCatalogNumber == o.celCatalogNumber; }); if (iter != xindex->end()) return iter->catalogNumber; return AstroCatalog::InvalidIndex; } // Return the Celestia catalog number for the star with a specified number // in a cross index. AstroCatalog::IndexNumber StarDatabase::searchCrossIndexForCatalogNumber(const Catalog catalog, const AstroCatalog::IndexNumber number) const { if (static_cast(catalog) >= crossIndexes.size()) return AstroCatalog::InvalidIndex; CrossIndex* xindex = crossIndexes[catalog]; if (xindex == nullptr) return AstroCatalog::InvalidIndex; CrossIndexEntry xindexEnt; xindexEnt.catalogNumber = number; CrossIndex::iterator iter = lower_bound(xindex->begin(), xindex->end(), xindexEnt); if (iter == xindex->end() || iter->catalogNumber != number) return AstroCatalog::InvalidIndex; else return iter->celCatalogNumber; } Star* StarDatabase::searchCrossIndex(const Catalog catalog, const AstroCatalog::IndexNumber number) const { AstroCatalog::IndexNumber celCatalogNumber = searchCrossIndexForCatalogNumber(catalog, number); if (celCatalogNumber != AstroCatalog::InvalidIndex) return find(celCatalogNumber); else return nullptr; } vector StarDatabase::getCompletion(const string& name, bool i18n) const { vector completion; // only named stars are supported by completion. if (!name.empty() && namesDB != nullptr) return namesDB->getCompletion(name, i18n); else return completion; } #if 0 static void catalogNumberToString(AstroCatalog::IndexNumber catalogNumber, char* buf, unsigned int bufSize) { // TODO: implement using using fmt::write } #endif static string catalogNumberToString(AstroCatalog::IndexNumber catalogNumber) { if (catalogNumber <= StarDatabase::MAX_HIPPARCOS_NUMBER) { return fmt::sprintf("HIP %d", catalogNumber); } else { AstroCatalog::IndexNumber tyc3 = catalogNumber / 1000000000; catalogNumber -= tyc3 * 1000000000; AstroCatalog::IndexNumber tyc2 = catalogNumber / 10000; catalogNumber -= tyc2 * 10000; AstroCatalog::IndexNumber tyc1 = catalogNumber; return fmt::sprintf("TYC %d-%d-%d", tyc1, tyc2, tyc3); } } // Return the name for the star with specified catalog number. The returned // string will be: // the common name if it exists, otherwise // the Bayer or Flamsteed designation if it exists, otherwise // the HD catalog number if it exists, otherwise // the HIPPARCOS catalog number. // // CAREFUL: // If the star name is not present in the names database, a new // string is constructed to contain the catalog number--keep in // mind that calling this method could possibly incur the overhead // of a memory allocation (though no explcit deallocation is // required as it's all wrapped in the string class.) string StarDatabase::getStarName(const Star& star, bool i18n) const { AstroCatalog::IndexNumber catalogNumber = star.getIndex(); if (namesDB != nullptr) { StarNameDatabase::NumberIndex::const_iterator iter = namesDB->getFirstNameIter(catalogNumber); if (iter != namesDB->getFinalNameIter() && iter->first == catalogNumber) { if (i18n) { const char * local = D_(iter->second.c_str()); if (iter->second != local) return local; } return iter->second; } } /* // Get the HD catalog name if (star.getIndex() != AstroCatalog::InvalidIndex) return fmt::sprintf("HD %d", star.getIndex(Star::HDCatalog)); else */ return catalogNumberToString(catalogNumber); } // A less convenient version of getStarName that writes to a char // array instead of a string. The advantage is that no memory allocation // will every occur. void StarDatabase::getStarName(const Star& star, char* nameBuffer, unsigned int bufferSize, bool i18n) const { assert(bufferSize != 0); AstroCatalog::IndexNumber catalogNumber = star.getIndex(); if (namesDB != nullptr) { StarNameDatabase::NumberIndex::const_iterator iter = namesDB->getFirstNameIter(catalogNumber); if (iter != namesDB->getFinalNameIter() && iter->first == catalogNumber) { if (i18n) { const char * local = D_(iter->second.c_str()); if (iter->second != local) { strncpy(nameBuffer, local, bufferSize); nameBuffer[bufferSize - 1] = '\0'; return; } } strncpy(nameBuffer, iter->second.c_str(), bufferSize); nameBuffer[bufferSize - 1] = '\0'; return; } } strncpy(nameBuffer, catalogNumberToString(catalogNumber).c_str(), bufferSize); nameBuffer[bufferSize - 1] = '\0'; } string StarDatabase::getStarNameList(const Star& star, const unsigned int maxNames) const { string starNames; unsigned int catalogNumber = star.getIndex(); std::set nameSet; bool isNameSetEmpty = true; auto append = [&] (const string &str) { auto inserted = nameSet.insert(str); if (inserted.second) { if (isNameSetEmpty) isNameSetEmpty = false; else starNames += " / "; starNames += str; } }; if (namesDB != nullptr) { StarNameDatabase::NumberIndex::const_iterator iter = namesDB->getFirstNameIter(catalogNumber); while (iter != namesDB->getFinalNameIter() && iter->first == catalogNumber && nameSet.size() < maxNames) { append(D_(iter->second.c_str())); ++iter; } } AstroCatalog::IndexNumber hip = catalogNumber; if (hip != AstroCatalog::InvalidIndex && hip != 0 && nameSet.size() < maxNames) { if (hip <= Star::MaxTychoCatalogNumber) { if (hip >= 1000000) { AstroCatalog::IndexNumber h = hip; AstroCatalog::IndexNumber tyc3 = h / 1000000000; h -= tyc3 * 1000000000; AstroCatalog::IndexNumber tyc2 = h / 10000; h -= tyc2 * 10000; AstroCatalog::IndexNumber tyc1 = h; append(fmt::sprintf("TYC %u-%u-%u", tyc1, tyc2, tyc3)); } else { append(fmt::sprintf("HIP %u", hip)); } } } AstroCatalog::IndexNumber hd = crossIndex(StarDatabase::HenryDraper, hip); if (nameSet.size() < maxNames && hd != AstroCatalog::InvalidIndex) { append(fmt::sprintf("HD %u", hd)); } AstroCatalog::IndexNumber sao = crossIndex(StarDatabase::SAO, hip); if (nameSet.size() < maxNames && sao != AstroCatalog::InvalidIndex) { append(fmt::sprintf("SAO %u", sao)); } return starNames; } void StarDatabase::findVisibleStars(StarHandler& starHandler, const Vector3f& position, const Quaternionf& orientation, float fovY, float aspectRatio, float limitingMag, OctreeProcStats *stats) const { // Compute the bounding planes of an infinite view frustum Hyperplane frustumPlanes[5]; Vector3f planeNormals[5]; Eigen::Matrix3f rot = orientation.toRotationMatrix(); float h = (float) tan(fovY / 2); float w = h * aspectRatio; planeNormals[0] = Vector3f(0.0f, 1.0f, -h); planeNormals[1] = Vector3f(0.0f, -1.0f, -h); planeNormals[2] = Vector3f(1.0f, 0.0f, -w); planeNormals[3] = Vector3f(-1.0f, 0.0f, -w); planeNormals[4] = Vector3f(0.0f, 0.0f, -1.0f); for (int i = 0; i < 5; i++) { planeNormals[i] = rot.transpose() * planeNormals[i].normalized(); frustumPlanes[i] = Hyperplane(planeNormals[i], position); } octreeRoot->processVisibleObjects(starHandler, position, frustumPlanes, limitingMag, STAR_OCTREE_ROOT_SIZE, stats); } void StarDatabase::findCloseStars(StarHandler& starHandler, const Vector3f& position, float radius) const { octreeRoot->processCloseObjects(starHandler, position, radius, STAR_OCTREE_ROOT_SIZE); } StarNameDatabase* StarDatabase::getNameDatabase() const { return namesDB; } void StarDatabase::setNameDatabase(StarNameDatabase* _namesDB) { namesDB = _namesDB; } bool StarDatabase::loadCrossIndex(const Catalog catalog, istream& in) { if (static_cast(catalog) >= crossIndexes.size()) return false; if (crossIndexes[catalog] != nullptr) delete crossIndexes[catalog]; // Verify that the star database file has a correct header { int headerLength = strlen(CROSSINDEX_FILE_HEADER); char* header = new char[headerLength]; if (!in.read(header, headerLength).good() || strncmp(header, CROSSINDEX_FILE_HEADER, headerLength)) { GetLogger()->error(_("Bad header for cross index\n")); delete[] header; return false; } delete[] header; } // Verify the version { std::uint16_t version; if (!celutil::readLE(in, version) || version != 0x0100) { GetLogger()->error(_("Bad version for cross index\n")); return false; } } CrossIndex* xindex = new CrossIndex(); unsigned int record = 0; for (;;) { CrossIndexEntry ent; if (!celutil::readLE(in, ent.catalogNumber)) { if (in.eof()) { break; } GetLogger()->error(_("Loading cross index failed\n")); delete xindex; return false; } if (!celutil::readLE(in, ent.celCatalogNumber)) { GetLogger()->error(_("Loading cross index failed at record {}\n"), record); delete xindex; return false; } xindex->push_back(ent); record++; } sort(xindex->begin(), xindex->end()); crossIndexes[catalog] = xindex; return true; } bool StarDatabase::loadBinary(istream& in) { uint32_t nStarsInFile = 0; // Verify that the star database file has a correct header { int headerLength = strlen(FILE_HEADER); char* header = new char[headerLength]; if (!in.read(header, headerLength).good() || strncmp(header, FILE_HEADER, headerLength)) { delete[] header; return false; } delete[] header; } // Verify the version { std::uint16_t version; if (!celutil::readLE(in, version) || version != 0x0100) { return false; } } // Read the star count if (!celutil::readLE(in, nStarsInFile)) { return false; } unsigned int totalStars = nStars + nStarsInFile; while (((unsigned int) nStars) < totalStars) { AstroCatalog::IndexNumber catNo = 0; float x = 0.0f, y = 0.0f, z = 0.0f; int16_t absMag; uint16_t spectralType; if (!celutil::readLE(in, catNo) || !celutil::readLE(in, x) || !celutil::readLE(in, y) || !celutil::readLE(in, z) || !celutil::readLE(in, absMag) || !celutil::readLE(in, spectralType)) { return false; } Star star; star.setPosition(x, y, z); star.setAbsoluteMagnitude((float) absMag / 256.0f); StarDetails* details = nullptr; StellarClass sc; if (sc.unpackV1(spectralType)) details = StarDetails::GetStarDetails(sc); if (details == nullptr) { GetLogger()->error(_("Bad spectral type in star database, star #{}\n"), nStars); return false; } star.setDetails(details); star.setIndex(catNo); unsortedStars.add(star); nStars++; } if (in.bad()) return false; GetLogger()->debug("StarDatabase::read: nStars = {}\n", nStarsInFile); GetLogger()->info(_("{} stars in binary database\n"), nStars); // Create the temporary list of stars sorted by catalog number; this // will be used to lookup stars during file loading. After loading is // complete, the stars are sorted into an octree and this list gets // replaced. if (unsortedStars.size() > 0) { binFileStarCount = unsortedStars.size(); binFileCatalogNumberIndex = new Star*[binFileStarCount]; for (unsigned int i = 0; i < binFileStarCount; i++) { binFileCatalogNumberIndex[i] = &unsortedStars[i]; } sort(binFileCatalogNumberIndex, binFileCatalogNumberIndex + binFileStarCount, PtrCatalogNumberOrderingPredicate()); } return true; } void StarDatabase::finish() { GetLogger()->info(_("Total star count: {}\n"), nStars); buildOctree(); buildIndexes(); // Delete the temporary indices used only during loading delete[] binFileCatalogNumberIndex; stcFileCatalogNumberIndex.clear(); // Resolve all barycenters; this can't be done before star sorting. There's // still a bug here: final orbital radii aren't available until after // the barycenters have been resolved, and these are required when building // the octree. This will only rarely cause a problem, but it still needs // to be addressed. for (const auto& b : barycenters) { Star* star = find(b.catNo); Star* barycenter = find(b.barycenterCatNo); assert(star != nullptr); assert(barycenter != nullptr); if (star != nullptr && barycenter != nullptr) { star->setOrbitBarycenter(barycenter); barycenter->addOrbitingStar(star); } } barycenters.clear(); } static void stcError(const Tokenizer& tok, const string& msg) { GetLogger()->error(_("Error in .stc file (line {}): {}\n"), tok.getLineNumber(), msg); } /*! Load star data from a property list into a star instance. */ bool StarDatabase::createStar(Star* star, DataDisposition disposition, AstroCatalog::IndexNumber catalogNumber, Hash* starData, const fs::path& path, bool isBarycenter) { StarDetails* details = nullptr; string spectralType; // Get the magnitude and spectral type; if the star is actually // a barycenter placeholder, these fields are ignored. if (isBarycenter) { details = StarDetails::GetBarycenterDetails(); } else { if (starData->getString("SpectralType", spectralType)) { StellarClass sc = StellarClass::parse(spectralType); details = StarDetails::GetStarDetails(sc); if (details == nullptr) { GetLogger()->error(_("Invalid star: bad spectral type.\n")); return false; } } else { // Spectral type is required for new stars if (disposition != DataDisposition::Modify) { GetLogger()->error(_("Invalid star: missing spectral type.\n")); return false; } } } bool modifyExistingDetails = false; if (disposition == DataDisposition::Modify) { StarDetails* existingDetails = star->getDetails(); // If we're modifying an existing star and it already has a // customized details record, we'll just modify that. if (!existingDetails->shared()) { modifyExistingDetails = true; if (details != nullptr) { // If the spectral type was modified, copy the new data // to the custom details record. existingDetails->setSpectralType(details->getSpectralType()); existingDetails->setTemperature(details->getTemperature()); existingDetails->setBolometricCorrection(details->getBolometricCorrection()); if ((existingDetails->getKnowledge() & StarDetails::KnowTexture) == 0) existingDetails->setTexture(details->getTexture()); if ((existingDetails->getKnowledge() & StarDetails::KnowRotation) == 0) existingDetails->setRotationModel(details->getRotationModel()); existingDetails->setVisibility(details->getVisibility()); } details = existingDetails; } else if (details == nullptr) { details = existingDetails; } } string modelName; string textureName; bool hasTexture = starData->getString("Texture", textureName); bool hasModel = starData->getString("Mesh", modelName); RotationModel* rm = CreateRotationModel(starData, path, 1.0); bool hasRotationModel = (rm != nullptr); Vector3d semiAxes = Vector3d::Ones(); bool hasSemiAxes = starData->getLengthVector("SemiAxes", semiAxes); bool hasBarycenter = false; Eigen::Vector3f barycenterPosition; double radius; bool hasRadius = starData->getLength("Radius", radius); double temperature = 0.0; bool hasTemperature = starData->getNumber("Temperature", temperature); // disallow unphysical temperature values if (temperature <= 0.0) { hasTemperature = false; } double bolometricCorrection; bool hasBolometricCorrection = starData->getNumber("BoloCorrection", bolometricCorrection); string infoURL; bool hasInfoURL = starData->getString("InfoURL", infoURL); Orbit* orbit = CreateOrbit(Selection(), starData, path, true); if (hasTexture || hasModel || orbit != nullptr || hasSemiAxes || hasRadius || hasTemperature || hasBolometricCorrection || hasRotationModel || hasInfoURL) { // If the star definition has extended information, clone the // star details so we can customize it without affecting other // stars of the same spectral type. bool free_details = false; if (!modifyExistingDetails) { details = new StarDetails(*details); free_details = true; } if (hasTexture) { details->setTexture(MultiResTexture(textureName, path)); details->addKnowledge(StarDetails::KnowTexture); } if (hasModel) { ResourceHandle geometryHandle = GetGeometryManager()->getHandle(GeometryInfo(modelName, path, Vector3f::Zero(), 1.0f, true)); details->setGeometry(geometryHandle); } if (hasSemiAxes) { details->setEllipsoidSemiAxes(semiAxes.cast()); } if (hasRadius) { details->setRadius((float) radius); details->addKnowledge(StarDetails::KnowRadius); } if (hasTemperature) { details->setTemperature((float) temperature); if (!hasBolometricCorrection) { // if we change the temperature, recalculate the bolometric // correction using formula from formula for main sequence // stars given in B. Cameron Reed (1998), "The Composite // Observational-Theoretical HR Diagram", Journal of the Royal // Astronomical Society of Canada, Vol 92. p36. double logT = log10(temperature) - 4; double bc = -8.499 * pow(logT, 4) + 13.421 * pow(logT, 3) - 8.131 * logT * logT - 3.901 * logT - 0.438; details->setBolometricCorrection((float) bc); } } if (hasBolometricCorrection) { details->setBolometricCorrection((float) bolometricCorrection); } if (hasInfoURL) { details->setInfoURL(infoURL); } if (orbit != nullptr) { details->setOrbit(orbit); // See if a barycenter was specified as well AstroCatalog::IndexNumber barycenterCatNo = AstroCatalog::InvalidIndex; bool barycenterDefined = false; string barycenterName; if (starData->getString("OrbitBarycenter", barycenterName)) { barycenterCatNo = findCatalogNumberByName(barycenterName, false); barycenterDefined = true; } else if (starData->getNumber("OrbitBarycenter", barycenterCatNo)) { barycenterDefined = true; } if (barycenterDefined) { if (barycenterCatNo != AstroCatalog::InvalidIndex) { // We can't actually resolve the barycenter catalog number // to a Star pointer until after all stars have been loaded // and spatially sorted. Just store it in a list to be // resolved after sorting. BarycenterUsage bc; bc.catNo = catalogNumber; bc.barycenterCatNo = barycenterCatNo; barycenters.push_back(bc); // Even though we can't actually get the Star pointer for // the barycenter, we can get the star information. Star* barycenter = findWhileLoading(barycenterCatNo); if (barycenter != nullptr) { hasBarycenter = true; barycenterPosition = barycenter->getPosition(); } } if (!hasBarycenter) { GetLogger()->error(_("Barycenter {} does not exist.\n"), barycenterName); delete rm; if (free_details) delete details; return false; } } } if (hasRotationModel) details->setRotationModel(rm); } if (!modifyExistingDetails) star->setDetails(details); if (disposition != DataDisposition::Modify) star->setIndex(catalogNumber); // Compute the position in rectangular coordinates. If a star has an // orbit and barycenter, it's position is the position of the barycenter. if (hasBarycenter) { star->setPosition(barycenterPosition); } else { double ra = 0.0; double dec = 0.0; double distance = 0.0; if (disposition == DataDisposition::Modify) { Vector3f pos = star->getPosition(); // Convert from Celestia's coordinate system Vector3f v(pos.x(), -pos.z(), pos.y()); v = Quaternionf(AngleAxis((float) astro::J2000Obliquity, Vector3f::UnitX())) * v; distance = v.norm(); if (distance > 0.0) { v.normalize(); ra = radToDeg(std::atan2(v.y(), v.x())) / DEG_PER_HRA; dec = radToDeg(std::asin(v.z())); } } bool modifyPosition = false; if (starData->getAngle("RA", ra, DEG_PER_HRA, 1.0)) { modifyPosition = true; } else { if (disposition != DataDisposition::Modify) { GetLogger()->error(_("Invalid star: missing right ascension\n")); return false; } } if (starData->getAngle("Dec", dec)) { modifyPosition = true; } else { if (disposition != DataDisposition::Modify) { GetLogger()->error(_("Invalid star: missing declination.\n")); return false; } } if (starData->getLength("Distance", distance, KM_PER_LY)) { modifyPosition = true; } else { if (disposition != DataDisposition::Modify) { GetLogger()->error(_("Invalid star: missing distance.\n")); return false; } } // Truncate to floats to match behavior of reading from binary file. // The conversion to rectangular coordinates is still performed at // double precision, however. if (modifyPosition) { float raf = ((float) ra); float decf = ((float) dec); float distancef = ((float) distance); Vector3d pos = astro::equatorialToCelestialCart((double) raf, (double) decf, (double) distancef); star->setPosition(pos.cast()); } } if (isBarycenter) { star->setAbsoluteMagnitude(30.0f); } else { float magnitude = 0.0f; bool magnitudeModified = true; bool absoluteDefined = true; if (!starData->getNumber("AbsMag", magnitude)) { absoluteDefined = false; if (!starData->getNumber("AppMag", magnitude)) { if (disposition != DataDisposition::Modify) { GetLogger()->error(_("Invalid star: missing magnitude.\n")); return false; } else { magnitudeModified = false; } } else { float distance = star->getPosition().norm(); // We can't compute the intrinsic brightness of the star from // the apparent magnitude if the star is within a few AU of the // origin. if (distance < 1e-5f) { GetLogger()->error(_("Invalid star: absolute (not apparent) magnitude must be specified for star near origin\n")); return false; } magnitude = astro::appToAbsMag(magnitude, distance); } } if (magnitudeModified) star->setAbsoluteMagnitude(magnitude); float extinction = 0.0f; if (starData->getNumber("Extinction", extinction)) { float distance = star->getPosition().norm(); if (distance != 0.0f) star->setExtinction(extinction / distance); else extinction = 0.0f; if (!absoluteDefined) star->setAbsoluteMagnitude(star->getAbsoluteMagnitude() - extinction); } } return true; } /*! Load an STC file with star definitions. Each definition has the form: * * [disposition] [object type] [catalog number] [name] * { * [properties] * } * * Disposition is either Add, Replace, or Modify; Add is the default. * Object type is either Star or Barycenter, with Star the default * It is an error to omit both the catalog number and the name. * * The dispositions are slightly more complicated than suggested by * their names. Every star must have an unique catalog number. But * instead of generating an error, Adding a star with a catalog * number that already exists will actually replace that star. Here * are how all of the possibilities are handled: * * or already exists: * Add : new star * Add : replace star * Replace : replace star * Replace : replace star * Modify : modify star * Modify : modify star * * or doesn't exist: * Add : new star * Add : new star * Replace : new star * Replace : new star * Modify : error * Modify : error */ bool StarDatabase::load(istream& in, const fs::path& resourcePath) { Tokenizer tokenizer(&in); Parser parser(&tokenizer); #ifdef ENABLE_NLS string s = resourcePath.string(); const char *d = s.c_str(); bindtextdomain(d, d); // domain name is the same as resource path #endif while (tokenizer.nextToken() != Tokenizer::TokenEnd) { bool isStar = true; // Parse the disposition--either Add, Replace, or Modify. The disposition // may be omitted. The default value is Add. DataDisposition disposition = DataDisposition::Add; if (tokenizer.getTokenType() == Tokenizer::TokenName) { if (tokenizer.getNameValue() == "Modify") { disposition = DataDisposition::Modify; tokenizer.nextToken(); } else if (tokenizer.getNameValue() == "Replace") { disposition = DataDisposition::Replace; tokenizer.nextToken(); } else if (tokenizer.getNameValue() == "Add") { disposition = DataDisposition::Add; tokenizer.nextToken(); } } // Parse the object type--either Star or Barycenter. The object type // may be omitted. The default is Star. if (tokenizer.getTokenType() == Tokenizer::TokenName) { if (tokenizer.getNameValue() == "Star") { isStar = true; } else if (tokenizer.getNameValue() == "Barycenter") { isStar = false; } else { stcError(tokenizer, "unrecognized object type"); return false; } tokenizer.nextToken(); } // Parse the catalog number; it may be omitted if a name is supplied. AstroCatalog::IndexNumber catalogNumber = AstroCatalog::InvalidIndex; if (tokenizer.getTokenType() == Tokenizer::TokenNumber) { catalogNumber = (AstroCatalog::IndexNumber) tokenizer.getNumberValue(); tokenizer.nextToken(); } string objName; string firstName; if (tokenizer.getTokenType() == Tokenizer::TokenString) { // A star name (or names) is present objName = tokenizer.getStringValue(); tokenizer.nextToken(); if (!objName.empty()) { string::size_type next = objName.find(':', 0); firstName = objName.substr(0, next); } } Star* star = nullptr; switch (disposition) { case DataDisposition::Add: // Automatically generate a catalog number for the star if one isn't // supplied. if (catalogNumber == AstroCatalog::InvalidIndex) { catalogNumber = nextAutoCatalogNumber--; } else { star = findWhileLoading(catalogNumber); } break; case DataDisposition::Replace: if (catalogNumber == AstroCatalog::InvalidIndex) { if (!firstName.empty()) { catalogNumber = findCatalogNumberByName(firstName, false); } } if (catalogNumber == AstroCatalog::InvalidIndex) { catalogNumber = nextAutoCatalogNumber--; } else { star = findWhileLoading(catalogNumber); } break; case DataDisposition::Modify: // If no catalog number was specified, try looking up the star by name if (catalogNumber == AstroCatalog::InvalidIndex && !firstName.empty()) { catalogNumber = findCatalogNumberByName(firstName, false); } if (catalogNumber != AstroCatalog::InvalidIndex) { star = findWhileLoading(catalogNumber); } break; } bool isNewStar = star == nullptr; tokenizer.pushBack(); Value* starDataValue = parser.readValue(); if (starDataValue == nullptr) { GetLogger()->error("Error reading star.\n"); return false; } if (starDataValue->getType() != Value::HashType) { GetLogger()->error("Bad star definition.\n"); delete starDataValue; return false; } Hash* starData = starDataValue->getHash(); if (isNewStar) star = new Star(); bool ok = false; if (isNewStar && disposition == DataDisposition::Modify) { GetLogger()->warn("Modify requested for nonexistent star.\n"); } else { ok = createStar(star, disposition, catalogNumber, starData, resourcePath, !isStar); star->loadCategories(starData, disposition, resourcePath.string()); } delete starDataValue; if (ok) { if (isNewStar) { unsortedStars.add(*star); nStars++; delete star; // Add the new star to the temporary (load time) index. stcFileCatalogNumberIndex[catalogNumber] = &unsortedStars[unsortedStars.size() - 1]; } if (namesDB != nullptr && !objName.empty()) { // List of namesDB will replace any that already exist for // this star. namesDB->erase(catalogNumber); // Iterate through the string for names delimited // by ':', and insert them into the star database. // Note that db->add() will skip empty namesDB. string::size_type startPos = 0; while (startPos != string::npos) { string::size_type next = objName.find(':', startPos); string::size_type length = string::npos; if (next != string::npos) { length = next - startPos; ++next; } string starName = objName.substr(startPos, length); namesDB->add(catalogNumber, starName); startPos = next; } } } else { if (isNewStar) delete star; GetLogger()->info("Bad star definition--will continue parsing file.\n"); } } return true; } void StarDatabase::buildOctree() { // This should only be called once for the database // ASSERT(octreeRoot == nullptr); GetLogger()->debug("Sorting stars into octree . . .\n"); float absMag = astro::appToAbsMag(STAR_OCTREE_MAGNITUDE, STAR_OCTREE_ROOT_SIZE * (float) sqrt(3.0)); DynamicStarOctree* root = new DynamicStarOctree(Vector3f(1000.0f, 1000.0f, 1000.0f), absMag); for (unsigned int i = 0; i < unsortedStars.size(); ++i) { root->insertObject(unsortedStars[i], STAR_OCTREE_ROOT_SIZE); } GetLogger()->debug("Spatially sorting stars for improved locality of reference . . .\n"); Star* sortedStars = new Star[nStars]; Star* firstStar = sortedStars; root->rebuildAndSort(octreeRoot, firstStar); // ASSERT((int) (firstStar - sortedStars) == nStars); GetLogger()->debug("{} stars total\nOctree has {} nodes and {} stars.\n", static_cast(firstStar - sortedStars), 1 + octreeRoot->countChildren(), octreeRoot->countObjects()); #ifdef PROFILE_OCTREE vector stats; octreeRoot->computeStatistics(stats); int level = 0; for (const auto& stat : stats) { level++; clog << fmt::sprintf( _("Level %i, %.5f ly, %i nodes, %i stars\n"), level, STAR_OCTREE_ROOT_SIZE / pow(2.0, (double) level), stat.nodeCount, stat.objectCount; } #endif // Clean up . . . //delete[] stars; unsortedStars.clear(); delete root; stars = sortedStars; } void StarDatabase::buildIndexes() { // This should only be called once for the database // assert(catalogNumberIndexes[0] == nullptr); GetLogger()->info("Building catalog number indexes . . .\n"); catalogNumberIndex = new Star*[nStars]; for (int i = 0; i < nStars; ++i) catalogNumberIndex[i] = &stars[i]; sort(catalogNumberIndex, catalogNumberIndex + nStars, PtrCatalogNumberOrderingPredicate()); } /*! While loading the star catalogs, this function must be called instead of * find(). The final catalog number index for stars cannot be built until * after all stars have been loaded. During catalog loading, there are two * separate indexes: one for the binary catalog and another index for stars * loaded from stc files. They binary catalog index is a sorted array, while * the stc catalog index is an STL map. Since the binary file can be quite * large, we want to avoid creating a map with as many nodes as there are * stars. Stc files should collectively contain many fewer stars, and stars * in an stc file may reference each other (barycenters). Thus, a dynamic * structure like a map is both practical and essential. */ Star* StarDatabase::findWhileLoading(AstroCatalog::IndexNumber catalogNumber) const { // First check for stars loaded from the binary database if (binFileCatalogNumberIndex != nullptr) { Star refStar; refStar.setIndex(catalogNumber); Star** star = lower_bound(binFileCatalogNumberIndex, binFileCatalogNumberIndex + binFileStarCount, &refStar, PtrCatalogNumberOrderingPredicate()); if (star != binFileCatalogNumberIndex + binFileStarCount && (*star)->getIndex() == catalogNumber) return *star; } // Next check for stars loaded from an stc file map::const_iterator iter = stcFileCatalogNumberIndex.find(catalogNumber); if (iter != stcFileCatalogNumberIndex.end()) { return iter->second; } // Star not found return nullptr; }