2
// C++ Implementation: starname
7
// Author: Toti <root@totibox>, (C) 2005
9
// Copyright: See COPYING file that comes with this distribution
23
#include <system_error>
26
#include <fmt/format.h>
28
#include <celcompat/charconv.h>
29
#include <celutil/binaryread.h>
30
#include <celutil/gettext.h>
31
#include <celutil/greek.h>
32
#include <celutil/logger.h>
33
#include <celutil/timer.h>
35
#include "constellation.h"
37
using namespace std::string_view_literals;
39
namespace compat = celestia::compat;
40
namespace util = celestia::util;
47
constexpr std::string_view CROSSINDEX_MAGIC = "CELINDEX"sv;
48
constexpr std::uint16_t CrossIndexVersion = 0x0100;
50
constexpr std::string_view HDCatalogPrefix = "HD "sv;
51
constexpr std::string_view HIPPARCOSCatalogPrefix = "HIP "sv;
52
constexpr std::string_view TychoCatalogPrefix = "TYC "sv;
53
constexpr std::string_view SAOCatalogPrefix = "SAO "sv;
55
constexpr AstroCatalog::IndexNumber TYC123_MIN = 1u;
56
constexpr AstroCatalog::IndexNumber TYC1_MAX = 9999u; // actual upper limit is 9537 in TYC2
57
constexpr AstroCatalog::IndexNumber TYC2_MAX = 99999u; // actual upper limit is 12121 in TYC2
58
constexpr AstroCatalog::IndexNumber TYC3_MAX = 3u; // from TYC2
60
// In the original Tycho catalog, TYC3 ranges from 1 to 3, so no there is
61
// no chance of overflow in the multiplication. TDSC (Fabricius et al. 2002)
62
// adds one entry with TYC3 = 4 (TYC 2907-1276-4) so permit TYC=4 when the
63
// TYC1 number is <= 2907
64
constexpr inline AstroCatalog::IndexNumber TDSC_TYC3_MAX = 4u;
65
constexpr inline AstroCatalog::IndexNumber TDSC_TYC3_MAX_RANGE_TYC1 = 2907u;
69
// cross-index header structure
70
struct CrossIndexHeader
72
CrossIndexHeader() = delete;
73
char magic[8]; //NOSONAR
74
std::uint16_t version;
77
// cross-index record structure
78
struct CrossIndexRecord
80
CrossIndexRecord() = delete;
81
std::uint32_t catalogNumber;
82
std::uint32_t celCatalogNumber;
87
static_assert(std::is_standard_layout_v<CrossIndexHeader>);
88
static_assert(std::is_standard_layout_v<CrossIndexRecord>);
90
constexpr unsigned int FIRST_NUMBERED_VARIABLE = 335;
92
using CanonicalBuffer = fmt::basic_memory_buffer<char, 256>;
94
// workaround for missing append method in earlier fmt versions
96
appendComponentA(CanonicalBuffer& buffer)
98
buffer.push_back(' ');
99
buffer.push_back('A');
102
// Try parsing the first word of a name as a Flamsteed number or variable star
103
// designation. Single-letter variable star designations are handled by the
104
// Bayer parser due to indistinguishability with case-insensitive lookup.
106
isFlamsteedOrVariable(std::string_view prefix)
108
using compat::from_chars;
109
switch (prefix.size())
114
// Match single-digit Flamsteed number
115
return prefix[0] >= '1' && prefix[0] <= '9';
118
auto p0 = static_cast<unsigned char>(prefix[0]);
119
auto p1 = static_cast<unsigned char>(prefix[1]);
121
// Two-digit Flamsteed number
122
(std::isdigit(p0) && p0 != '0' && std::isdigit(p1)) ||
123
(std::isalpha(p0) && std::isalpha(p1) &&
124
std::tolower(p0) != 'j' && std::tolower(p1) != 'j' &&
129
// check for either Flamsteed or V### format variable star designations
130
std::size_t startNumber = std::tolower(static_cast<unsigned char>(prefix[0])) == 'v'
133
auto endPtr = prefix.data() + prefix.size();
135
auto [ptr, ec] = from_chars(prefix.data() + startNumber, endPtr, value);
136
return ec == std::errc{} && ptr == endPtr &&
137
(startNumber == 0 || value >= FIRST_NUMBERED_VARIABLE);
144
std::string_view letter{ };
145
unsigned int number{ 0 };
148
// Attempts to parse the first word of a star name as a Greek or Latin-letter
149
// Bayer designation, with optional numeric suffix
151
parseBayerLetter(std::string_view prefix)
153
using compat::from_chars;
156
if (auto numberPos = prefix.find_first_of("0123456789"); numberPos == std::string_view::npos)
157
result.letter = prefix;
158
else if (auto [ptr, ec] = from_chars(prefix.data() + numberPos, prefix.data() + prefix.size(), result.number);
159
ec == std::errc{} && ptr == prefix.data() + prefix.size())
160
result.letter = prefix.substr(0, numberPos);
164
if (result.letter.empty())
167
if (auto greek = GetCanonicalGreekAbbreviation(result.letter); !greek.empty())
168
result.letter = greek;
169
else if (result.letter.size() != 1 || !std::isalpha(static_cast<unsigned char>(result.letter[0])))
176
parseSimpleCatalogNumber(std::string_view name,
177
std::string_view prefix,
178
AstroCatalog::IndexNumber& catalogNumber)
180
using compat::from_chars;
181
if (compareIgnoringCase(name, prefix, prefix.size()) != 0)
184
// skip additional whitespace
185
auto pos = name.find_first_not_of(" \t", prefix.size());
186
if (pos == std::string_view::npos)
189
if (auto [ptr, ec] = from_chars(name.data() + pos, name.data() + name.size(), catalogNumber); ec == std::errc{})
191
// Do not match if suffix is present
192
pos = name.find_first_not_of(" \t", ptr - name.data());
193
return pos == std::string_view::npos;
200
parseTychoCatalogNumber(std::string_view name,
201
AstroCatalog::IndexNumber& catalogNumber)
203
using compat::from_chars;
204
if (compareIgnoringCase(name, TychoCatalogPrefix, TychoCatalogPrefix.size()) != 0)
207
// skip additional whitespace
208
auto pos = name.find_first_not_of(" \t", TychoCatalogPrefix.size());
209
if (pos == std::string_view::npos)
212
const char* const end_ptr = name.data() + name.size();
214
std::array<AstroCatalog::IndexNumber, 3> tycParts;
215
auto result = from_chars(name.data() + pos, end_ptr, tycParts[0]);
216
if (result.ec != std::errc{}
217
|| tycParts[0] < TYC123_MIN || tycParts[0] > TYC1_MAX
218
|| result.ptr == end_ptr
219
|| *result.ptr != '-')
224
result = from_chars(result.ptr + 1, end_ptr, tycParts[1]);
225
if (result.ec != std::errc{}
226
|| tycParts[1] < TYC123_MIN || tycParts[1] > TYC2_MAX
227
|| result.ptr == end_ptr
228
|| *result.ptr != '-')
233
if (result = from_chars(result.ptr + 1, end_ptr, tycParts[2]);
234
result.ec == std::errc{}
235
&& tycParts[2] >= TYC123_MIN
236
&& (tycParts[2] <= TYC3_MAX
237
|| (tycParts[2] == TDSC_TYC3_MAX && tycParts[0] <= TDSC_TYC3_MAX_RANGE_TYC1)))
239
// Do not match if suffix is present
240
pos = name.find_first_not_of(" \t", result.ptr - name.data());
241
if (pos != std::string_view::npos)
244
catalogNumber = tycParts[2] * StarNameDatabase::TYC3_MULTIPLIER
245
+ tycParts[1] * StarNameDatabase::TYC2_MULTIPLIER
254
parseCelestiaCatalogNumber(std::string_view name,
255
AstroCatalog::IndexNumber& catalogNumber)
257
using celestia::compat::from_chars;
258
if (name.size() == 0 || name[0] != '#')
261
if (auto [ptr, ec] = from_chars(name.data() + 1, name.data() + name.size(), catalogNumber);
264
// Do not match if suffix is present
265
auto pos = name.find_first_not_of(" \t", ptr - name.data());
266
return pos == std::string_view::npos;
272
// Verify that the cross index file has a correct header
274
checkCrossIndexHeader(std::istream& in)
276
std::array<char, sizeof(CrossIndexHeader)> header;
277
if (!in.read(header.data(), header.size()).good()) /* Flawfinder: ignore */
280
// Verify the magic string
281
if (std::string_view(header.data() + offsetof(CrossIndexHeader, magic), CROSSINDEX_MAGIC.size()) != CROSSINDEX_MAGIC)
283
GetLogger()->error(_("Bad header for cross index\n"));
287
// Verify the version
288
if (auto version = util::fromMemoryLE<std::uint16_t>(header.data() + offsetof(CrossIndexHeader, version));
289
version != CrossIndexVersion)
291
GetLogger()->error(_("Bad version for cross index\n"));
298
} // end unnamed namespace
300
AstroCatalog::IndexNumber
301
StarNameDatabase::findCatalogNumberByName(std::string_view name, bool i18n) const
304
return AstroCatalog::InvalidIndex;
306
AstroCatalog::IndexNumber catalogNumber = findByName(name, i18n);
307
if (catalogNumber != AstroCatalog::InvalidIndex)
308
return catalogNumber;
310
if (parseCelestiaCatalogNumber(name, catalogNumber))
311
return catalogNumber;
312
if (parseSimpleCatalogNumber(name, HIPPARCOSCatalogPrefix, catalogNumber))
313
return catalogNumber;
314
if (parseTychoCatalogNumber(name, catalogNumber))
315
return catalogNumber;
316
if (parseSimpleCatalogNumber(name, HDCatalogPrefix, catalogNumber))
317
return searchCrossIndexForCatalogNumber(StarCatalog::HenryDraper, catalogNumber);
318
if (parseSimpleCatalogNumber(name, SAOCatalogPrefix, catalogNumber))
319
return searchCrossIndexForCatalogNumber(StarCatalog::SAO, catalogNumber);
321
return AstroCatalog::InvalidIndex;
324
// Return the Celestia catalog number for the star with a specified number
326
AstroCatalog::IndexNumber
327
StarNameDatabase::searchCrossIndexForCatalogNumber(StarCatalog catalog, AstroCatalog::IndexNumber number) const
329
auto catalogIndex = static_cast<std::size_t>(catalog);
330
if (catalogIndex >= crossIndices.size())
331
return AstroCatalog::InvalidIndex;
333
const CrossIndex& xindex = crossIndices[catalogIndex];
334
auto iter = std::lower_bound(xindex.begin(), xindex.end(), number,
335
[](const CrossIndexEntry& ent, AstroCatalog::IndexNumber n) { return ent.catalogNumber < n; });
336
return iter == xindex.end() || iter->catalogNumber != number
337
? AstroCatalog::InvalidIndex
338
: iter->celCatalogNumber;
341
AstroCatalog::IndexNumber
342
StarNameDatabase::crossIndex(StarCatalog catalog, AstroCatalog::IndexNumber celCatalogNumber) const
344
auto catalogIndex = static_cast<std::size_t>(catalog);
345
if (catalogIndex >= crossIndices.size())
346
return AstroCatalog::InvalidIndex;
348
const CrossIndex& xindex = crossIndices[catalogIndex];
350
// A simple linear search. We could store cross indices sorted by
351
// both catalog numbers and trade memory for speed
352
auto iter = std::find_if(xindex.begin(), xindex.end(),
353
[celCatalogNumber](const CrossIndexEntry& o) { return celCatalogNumber == o.celCatalogNumber; });
354
return iter == xindex.end()
355
? AstroCatalog::InvalidIndex
356
: iter->catalogNumber;
359
AstroCatalog::IndexNumber
360
StarNameDatabase::findByName(std::string_view name, bool i18n) const
362
if (auto catalogNumber = getCatalogNumberByName(name, i18n);
363
catalogNumber != AstroCatalog::InvalidIndex)
364
return catalogNumber;
366
if (auto pos = name.find(' '); pos != 0 && pos != std::string::npos && pos < name.size() - 1)
368
std::string_view prefix = name.substr(0, pos);
369
std::string_view remainder = name.substr(pos + 1);
371
if (auto catalogNumber = findFlamsteedOrVariable(prefix, remainder, i18n);
372
catalogNumber != AstroCatalog::InvalidIndex)
373
return catalogNumber;
375
if (auto catalogNumber = findBayer(prefix, remainder, i18n);
376
catalogNumber != AstroCatalog::InvalidIndex)
377
return catalogNumber;
380
return findWithComponentSuffix(name, i18n);
383
AstroCatalog::IndexNumber
384
StarNameDatabase::findFlamsteedOrVariable(std::string_view prefix,
385
std::string_view remainder,
388
if (!isFlamsteedOrVariable(prefix))
389
return AstroCatalog::InvalidIndex;
391
auto [constellationAbbrev, suffix] = ParseConstellation(remainder);
392
if (constellationAbbrev.empty() || (!suffix.empty() && suffix.front() != ' '))
393
return AstroCatalog::InvalidIndex;
395
CanonicalBuffer canonical;
396
fmt::format_to(std::back_inserter(canonical), "{} {}{}", prefix, constellationAbbrev, suffix);
397
if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
398
catalogNumber != AstroCatalog::InvalidIndex)
400
return catalogNumber;
404
return AstroCatalog::InvalidIndex;
406
// try appending " A"
407
appendComponentA(canonical);
408
return getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
411
AstroCatalog::IndexNumber
412
StarNameDatabase::findBayer(std::string_view prefix,
413
std::string_view remainder,
416
auto bayerLetter = parseBayerLetter(prefix);
417
if (bayerLetter.letter.empty())
418
return AstroCatalog::InvalidIndex;
420
auto [constellationAbbrev, suffix] = ParseConstellation(remainder);
421
if (constellationAbbrev.empty() || (!suffix.empty() && suffix.front() != ' '))
422
return AstroCatalog::InvalidIndex;
424
return bayerLetter.number == 0
425
? findBayerNoNumber(bayerLetter.letter, constellationAbbrev, suffix, i18n)
426
: findBayerWithNumber(bayerLetter.letter,
433
AstroCatalog::IndexNumber
434
StarNameDatabase::findBayerNoNumber(std::string_view letter,
435
std::string_view constellationAbbrev,
436
std::string_view suffix,
439
CanonicalBuffer canonical;
440
fmt::format_to(std::back_inserter(canonical), "{} {}{}", letter, constellationAbbrev, suffix);
441
if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
442
catalogNumber != AstroCatalog::InvalidIndex)
444
return catalogNumber;
447
// Try appending "1" to the letter, e.g. ALF CVn --> ALF1 CVn
449
fmt::format_to(std::back_inserter(canonical), "{}1 {}{}", letter, constellationAbbrev, suffix);
450
if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
451
catalogNumber != AstroCatalog::InvalidIndex)
453
return catalogNumber;
457
return AstroCatalog::InvalidIndex;
459
// No component suffix, so try appending " A"
461
fmt::format_to(std::back_inserter(canonical), "{} {} A", letter, constellationAbbrev);
462
if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
463
catalogNumber != AstroCatalog::InvalidIndex)
465
return catalogNumber;
468
// Try appending "1" to the letter and a, e.g. ALF CVn --> ALF1 CVn A
469
if (auto [it, size] = fmt::format_to_n(canonical.data(), canonical.size(), "{}1 {} A",
470
letter, constellationAbbrev);
471
size <= canonical.size())
472
return getCatalogNumberByName({canonical.data(), size}, i18n);
474
return AstroCatalog::InvalidIndex;
477
AstroCatalog::IndexNumber
478
StarNameDatabase::findBayerWithNumber(std::string_view letter,
480
std::string_view constellationAbbrev,
481
std::string_view suffix,
484
CanonicalBuffer canonical;
485
fmt::format_to(std::back_inserter(canonical), "{}{} {}{}", letter, number, constellationAbbrev, suffix);
486
if (auto catalogNumber = getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
487
catalogNumber != AstroCatalog::InvalidIndex)
489
return catalogNumber;
493
return AstroCatalog::InvalidIndex;
495
// No component suffix, so try appending " A"
496
appendComponentA(canonical);
497
return getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
500
AstroCatalog::IndexNumber
501
StarNameDatabase::findWithComponentSuffix(std::string_view name, bool i18n) const
503
CanonicalBuffer canonical;
504
fmt::format_to(std::back_inserter(canonical), "{} A", name);
505
return getCatalogNumberByName({canonical.data(), canonical.size()}, i18n);
508
std::unique_ptr<StarNameDatabase>
509
StarNameDatabase::readNames(std::istream& in)
511
using compat::from_chars;
513
constexpr std::size_t maxLength = 1024;
514
auto db = std::make_unique<StarNameDatabase>();
515
std::string buffer(maxLength, '\0');
518
in.getline(buffer.data(), maxLength);
519
// Delimiter is extracted and contributes to gcount() but is not stored
520
std::size_t lineLength;
523
lineLength = static_cast<std::size_t>(in.gcount() - 1);
525
lineLength = static_cast<std::size_t>(in.gcount());
529
auto line = static_cast<std::string_view>(buffer).substr(0, lineLength);
531
if (line.empty() || line.front() == '#')
534
auto pos = line.find(':');
535
if (pos == std::string_view::npos)
538
auto catalogNumber = AstroCatalog::InvalidIndex;
539
if (auto [ptr, ec] = from_chars(line.data(), line.data() + pos, catalogNumber);
540
ec != std::errc{} || ptr != line.data() + pos)
545
// Iterate through the string for names delimited
546
// by ':', and insert them into the star database. Note that
547
// db->add() will skip empty names.
548
line = line.substr(pos + 1);
549
while (!line.empty())
551
pos = line.find(':');
552
std::string_view name = line.substr(0, pos);
553
db->add(catalogNumber, name);
554
if (pos == std::string_view::npos)
556
line = line.substr(pos + 1);
564
StarNameDatabase::loadCrossIndex(StarCatalog catalog, std::istream& in)
568
auto catalogIndex = static_cast<std::size_t>(catalog);
569
if (catalogIndex >= crossIndices.size())
572
if (!checkCrossIndexHeader(in))
575
CrossIndex& xindex = crossIndices[catalogIndex];
578
constexpr std::uint32_t BUFFER_RECORDS = UINT32_C(4096) / sizeof(CrossIndexRecord);
579
std::vector<char> buffer(sizeof(CrossIndexRecord) * BUFFER_RECORDS);
580
bool hasMoreRecords = true;
581
while (hasMoreRecords)
583
std::size_t remainingRecords = BUFFER_RECORDS;
584
in.read(buffer.data(), buffer.size()); /* Flawfinder: ignore */
587
GetLogger()->error(_("Loading cross index failed\n"));
593
auto bytesRead = static_cast<std::uint32_t>(in.gcount());
594
remainingRecords = bytesRead / sizeof(CrossIndexRecord);
595
// disallow partial records
596
if (bytesRead % sizeof(CrossIndexRecord) != 0)
598
GetLogger()->error(_("Loading cross index failed - unexpected EOF\n"));
603
hasMoreRecords = false;
606
xindex.reserve(xindex.size() + remainingRecords);
608
const char* ptr = buffer.data();
609
while (remainingRecords-- > 0)
611
CrossIndexEntry& ent = xindex.emplace_back();
612
ent.catalogNumber = util::fromMemoryLE<AstroCatalog::IndexNumber>(ptr + offsetof(CrossIndexRecord, catalogNumber));
613
ent.celCatalogNumber = util::fromMemoryLE<AstroCatalog::IndexNumber>(ptr + offsetof(CrossIndexRecord, celCatalogNumber));
614
ptr += sizeof(CrossIndexRecord);
618
GetLogger()->debug("Loaded xindex in {} ms\n", timer.getTime());
620
std::sort(xindex.begin(), xindex.end(),
621
[](const auto& lhs, const auto& rhs) { return lhs.catalogNumber < rhs.catalogNumber; });