161 static cg inst{auto_probe_tag{}};
173 auto call_seed =
static_cast<std::uint64_t
>(_engine());
189 throw std::runtime_error(
190 "No city data loaded. Call load() first.");
193 effolkronium::random_local call_engine;
194 call_engine.seed(
static_cast<std::mt19937::result_type
>(
195 (call_seed ^ (call_seed >> 32U))));
198 ? _distribution(call_engine.engine())
199 : _uniform(call_engine.engine());
200 city result = _cities[idx];
201 result._seed = call_seed;
212 auto call_seed =
static_cast<std::uint64_t
>(_engine());
213 return get_city(country, call_seed);
223 std::uint64_t call_seed)
const
227 throw std::runtime_error(
228 "No city data loaded. Call load() first.");
231 auto it = _country_index.find(country);
232 if (it == _country_index.end())
234 throw std::invalid_argument(
235 "No cities found for country code: " + country);
238 effolkronium::random_local call_engine;
239 call_engine.seed(
static_cast<std::mt19937::result_type
>(
240 (call_seed ^ (call_seed >> 32U))));
242 const auto& idx = it->second;
243 auto selected = _weighted
244 ? idx.distribution(call_engine.engine())
245 : std::uniform_int_distribution<std::size_t>(
246 0, idx.city_indices.size() - 1)(
247 call_engine.engine());
248 city result = _cities[idx.city_indices[selected]];
249 result._seed = call_seed;
265 _engine.seed(seed_value);
275 _engine.seed(std::random_device{}());
313 if (!std::filesystem::is_regular_file(path))
316 std::ifstream file{path};
321 while (std::getline(file, line))
325 if (!line.empty() && line.back() ==
'\r')
329 auto t1 = line.find(
'\t');
330 if (t1 == std::string::npos)
332 auto t2 = line.find(
'\t', t1 + 1);
333 if (t2 == std::string::npos)
336 auto key = line.substr(0, t1);
337 auto name = line.substr(t1 + 1, t2 - t1 - 1);
338 _admin1_names[key] = name;
352 const std::string& country_code,
353 const std::string& admin1_code)
const
355 if (admin1_code.empty())
358 auto key = country_code +
"." + admin1_code;
359 auto it = _admin1_names.find(key);
360 if (it != _admin1_names.end())
369 return !_admin1_names.empty();
376 return !_cities.empty();
383 return _cities.size();
394 void load(
const std::filesystem::path& tsv_path)
396 if (!std::filesystem::exists(tsv_path) ||
397 !std::filesystem::is_regular_file(tsv_path))
402 std::ifstream file{tsv_path};
411 if (!std::getline(file, line))
416 while (std::getline(file, line))
424 if (!line.empty() && line.back() ==
'\r')
429 auto c = parse_line(line);
430 if (c.geonameid != 0)
432 _cities.push_back(std::move(c));
449 static constexpr std::array probe_paths = {
450 "resources",
"../resources",
"city-generator/resources"};
452 const char* subfolder =
453 (tier == dataset::full) ?
"full" :
"lite";
455 auto found = std::ranges::find_if(probe_paths, [&](
const char* base) {
457 std::filesystem::path{base} / subfolder /
"cities.tsv";
458 return std::filesystem::is_regular_file(tsv);
460 if (found != probe_paths.end())
462 load(std::filesystem::path{*found} / subfolder /
"cities.tsv");
470 std::vector<city> _cities;
473 std::unordered_map<std::string, std::string> _admin1_names;
476 mutable std::discrete_distribution<std::size_t> _distribution;
479 mutable std::uniform_int_distribution<std::size_t> _uniform;
482 bool _weighted{
true};
487 std::vector<std::size_t> city_indices;
488 mutable std::discrete_distribution<std::size_t> distribution;
491 std::unordered_map<std::string, country_entry> _country_index;
494 std::mt19937_64 _engine{std::random_device{}()};
497 struct auto_probe_tag {};
500 explicit cg(auto_probe_tag )
502 static constexpr std::array probe_paths = {
503 "resources",
"../resources",
"city-generator/resources"};
505 auto found = std::ranges::find_if(probe_paths, [](
const char* p) {
506 return std::filesystem::exists(p) &&
507 std::filesystem::is_directory(p);
509 if (found != probe_paths.end())
511 const std::filesystem::path base{*found};
512 auto lite_tsv = base /
"lite" /
"cities.tsv";
513 auto full_tsv = base /
"full" /
"cities.tsv";
514 if (std::filesystem::is_regular_file(lite_tsv))
518 else if (std::filesystem::is_regular_file(full_tsv))
526 void rebuild_indices()
529 std::vector<double> weights;
530 weights.reserve(_cities.size());
532 std::ranges::transform(_cities, std::back_inserter(weights),
534 return static_cast<double>(
535 std::max<std::uint64_t>(c.population, 1));
538 _distribution = std::discrete_distribution<std::size_t>(
539 weights.begin(), weights.end());
542 if (!_cities.empty())
544 _uniform = std::uniform_int_distribution<std::size_t>(
545 0, _cities.size() - 1);
549 _country_index.clear();
551 for (
auto&& [i, c] : _cities | std::views::enumerate)
553 auto& entry = _country_index[c.country_code];
554 entry.city_indices.push_back(
static_cast<std::size_t
>(i));
558 for (
auto& [code, entry] : _country_index)
560 std::vector<double> cw;
561 cw.reserve(entry.city_indices.size());
563 for (
auto idx : entry.city_indices)
565 cw.push_back(
static_cast<double>(
566 std::max<std::uint64_t>(_cities[idx].population, 1)));
569 entry.distribution = std::discrete_distribution<std::size_t>(
570 cw.begin(), cw.end());
578 static city parse_line(
const std::string& line)
583 std::vector<std::string> fields;
586 for (
auto part : line | std::views::split(
'\t'))
588 fields.emplace_back(std::ranges::begin(part),
589 std::ranges::end(part));
593 static constexpr std::size_t expected_fields{16};
595 if (fields.size() < expected_fields)
602 c.geonameid =
static_cast<std::uint32_t
>(std::stoul(fields[0]));
604 c.asciiname = fields[2];
605 c.latitude = std::stod(fields[3]);
606 c.longitude = std::stod(fields[4]);
607 c.feature_code = fields[5];
608 c.country_code = fields[6];
610 c.admin1_code = fields[8];
611 c.admin2_code = fields[9];
612 c.admin3_code = fields[10];
613 c.admin4_code = fields[11];
617 :
static_cast<std::uint64_t
>(std::stoull(fields[12]));
620 ?
static_cast<std::int16_t
>(-9999)
621 : static_cast<std::int16_t>(std::stoi(fields[13]));
622 c.dem = fields[14].empty()
623 ?
static_cast<std::int16_t
>(0)
624 : static_cast<std::int16_t>(std::stoi(fields[14]));
625 c.timezone = fields[15];