add brewery coordinates

This commit is contained in:
Aaron Po
2026-08-23 06:15:09 -04:00
parent 5816634809
commit a5f93e33c5
9 changed files with 157 additions and 43 deletions

View File

@@ -94,6 +94,8 @@ struct EnrichedCity {
struct BreweryAddress { struct BreweryAddress {
City city; City city;
std::string postal_code{}; std::string postal_code{};
double longitude{};
double latitude{};
}; };
/** /**

View File

@@ -66,6 +66,9 @@ struct City {
*/ */
std::string iso3166_1{}; std::string iso3166_1{};
double longitude{};
double latitude{};
/** /**
* @brief Local language codes in priority order. * @brief Local language codes in priority order.
*/ */

View File

@@ -26,6 +26,8 @@ CREATE TABLE IF NOT EXISTS cities (
iso3166_2 TEXT NOT NULL, iso3166_2 TEXT NOT NULL,
country TEXT NOT NULL, country TEXT NOT NULL,
iso3166_1 TEXT NOT NULL, iso3166_1 TEXT NOT NULL,
longitude REAL NOT NULL,
latitude REAL NOT NULL,
local_languages_json TEXT NOT NULL, local_languages_json TEXT NOT NULL,
postal_code_country_format_regex TEXT NOT NULL, postal_code_country_format_regex TEXT NOT NULL,
postal_code_city_regex_json TEXT NOT NULL, postal_code_city_regex_json TEXT NOT NULL,
@@ -53,6 +55,8 @@ CREATE TABLE IF NOT EXISTS brewery_addresses (
brewery_id INTEGER NOT NULL, brewery_id INTEGER NOT NULL,
city_id INTEGER NOT NULL, city_id INTEGER NOT NULL,
postal_code TEXT NOT NULL, postal_code TEXT NOT NULL,
longitude REAL NOT NULL,
latitude REAL NOT NULL,
FOREIGN KEY(brewery_id) REFERENCES breweries(id) ON DELETE CASCADE, FOREIGN KEY(brewery_id) REFERENCES breweries(id) ON DELETE CASCADE,
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
); );
@@ -105,10 +109,12 @@ INSERT INTO cities (
iso3166_2, iso3166_2,
country, country,
iso3166_1, iso3166_1,
longitude,
latitude,
local_languages_json, local_languages_json,
postal_code_country_format_regex, postal_code_country_format_regex,
postal_code_city_regex_json postal_code_city_regex_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?); ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
)sql"; )sql";
inline constexpr std::string_view kInsertBrewerySql = R"sql( inline constexpr std::string_view kInsertBrewerySql = R"sql(
@@ -124,8 +130,10 @@ inline constexpr std::string_view kInsertBreweryAddressSql = R"sql(
INSERT INTO brewery_addresses ( INSERT INTO brewery_addresses (
brewery_id, brewery_id,
city_id, city_id,
postal_code postal_code,
) VALUES (?, ?, ?); longitude,
latitude
) VALUES (?, ?, ?, ?, ?);
)sql"; )sql";
inline constexpr std::string_view kInsertUserSql = R"sql( inline constexpr std::string_view kInsertUserSql = R"sql(
@@ -157,6 +165,8 @@ enum CityBindIndex {
kCityIso31662BindIndex, kCityIso31662BindIndex,
kCityCountryBindIndex, kCityCountryBindIndex,
kCityIso31661BindIndex, kCityIso31661BindIndex,
kCityLongitudeBindIndex,
kCityLatitudeBindIndex,
kCityLanguagesBindIndex, kCityLanguagesBindIndex,
kCityPostalCodeCountryFormatRegexBindIndex, kCityPostalCodeCountryFormatRegexBindIndex,
kCityPostalCodeCityRegexJsonBindIndex, kCityPostalCodeCityRegexJsonBindIndex,
@@ -173,6 +183,8 @@ enum BreweryAddressBindIndex {
kBreweryAddressBreweryIdBindIndex = 1, kBreweryAddressBreweryIdBindIndex = 1,
kBreweryAddressCityIdBindIndex, kBreweryAddressCityIdBindIndex,
kBreweryAddressPostalCodeBindIndex, kBreweryAddressPostalCodeBindIndex,
kBreweryAddressLongitudeBindIndex,
kBreweryAddressLatitudeBindIndex,
}; };
enum UserBindIndex { enum UserBindIndex {

View File

@@ -4,12 +4,55 @@
*/ */
#include <chrono> #include <chrono>
#include <cmath>
#include <format> #include <format>
#include <numbers>
#include <optional> #include <optional>
#include <random>
#include "biergarten_pipeline_orchestrator.h" #include "biergarten_pipeline_orchestrator.h"
#include "services/logging/logger.h" #include "services/logging/logger.h"
struct Coords {
double longitude;
double latitude;
};
// @todo find a better home for this
namespace {
constexpr double kEarthRadiusKm = 6371.0;
double DegreesToRadians(double degrees) {
return degrees * std::numbers::pi / 180.0;
}
double RadiansToDegrees(double radians) {
return radians * 180.0 / std::numbers::pi;
}
} // namespace
static Coords GetRandomCoordsWithinRange(Coords const& centre,
int const distance_km,
std::mt19937& rng) {
std::uniform_real_distribution<double> angle_dist(0.0,
2.0 * std::numbers::pi);
std::uniform_real_distribution<double> unit_dist(0.0, 1.0);
const double angle_radians = angle_dist(rng);
// sqrt() keeps points uniformly distributed over the disc's area rather
// than clustering near the centre.
const double radius_km = distance_km * std::sqrt(unit_dist(rng));
const double delta_latitude_degrees =
RadiansToDegrees(radius_km * std::cos(angle_radians) / kEarthRadiusKm);
const double delta_longitude_degrees = RadiansToDegrees(
radius_km * std::sin(angle_radians) /
(kEarthRadiusKm * std::cos(DegreesToRadians(centre.latitude))));
return Coords{.longitude = centre.longitude + delta_longitude_degrees,
.latitude = centre.latitude + delta_latitude_degrees};
}
void BiergartenPipelineOrchestrator::GenerateBreweries( void BiergartenPipelineOrchestrator::GenerateBreweries(
std::span<const EnrichedCity> cities) { std::span<const EnrichedCity> cities) {
logger_->Log({.level = LogLevel::Info, logger_->Log({.level = LogLevel::Info,
@@ -19,18 +62,28 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
generated_breweries_.clear(); generated_breweries_.clear();
size_t skipped_count = 0; size_t skipped_count = 0;
size_t export_failed_count = 0; size_t export_failed_count = 0;
std::mt19937 rng(std::random_device{}());
const auto generate_record = const auto generate_record =
[this, &skipped_count]( [this, &skipped_count, &rng](
const EnrichedCity& enriched_city) -> std::optional<BreweryRecord> { const EnrichedCity& enriched_city) -> std::optional<BreweryRecord> {
try { try {
const BreweryResult brewery = const BreweryResult brewery =
generator_->GenerateBrewery(enriched_city); generator_->GenerateBrewery(enriched_city);
const std::string postal_code = const std::string postal_code =
postal_code_service_->GeneratePostalCode(enriched_city.location); postal_code_service_->GeneratePostalCode(enriched_city.location);
constexpr int kMaxDistanceFromCentreKm = 5;
const Coords city_centre{.longitude = enriched_city.location.longitude,
.latitude = enriched_city.location.latitude};
const Coords brewery_coords = GetRandomCoordsWithinRange(
city_centre, kMaxDistanceFromCentreKm, rng);
return BreweryRecord{ return BreweryRecord{
.address = BreweryAddress{.city = enriched_city.location, .address = BreweryAddress{.city = enriched_city.location,
.postal_code = postal_code}, .postal_code = postal_code,
.longitude = brewery_coords.longitude,
.latitude = brewery_coords.latitude},
.brewery = brewery}; .brewery = brewery};
} catch (const std::exception& e) { } catch (const std::exception& e) {
++skipped_count; ++skipped_count;

View File

@@ -27,6 +27,8 @@ void BiergartenPipelineOrchestrator::LogResults() const {
{"state_province", city.state_province}, {"state_province", city.state_province},
{"iso3166_2", city.iso3166_2}, {"iso3166_2", city.iso3166_2},
{"postal_code", address.postal_code}, {"postal_code", address.postal_code},
{"longitude", address.longitude},
{"latitude", address.latitude},
}}}); }}});
} }

View File

@@ -39,11 +39,17 @@ std::vector<City> BiergartenPipelineOrchestrator::QueryLocations() {
logger_->Log({.level = LogLevel::Info, logger_->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,
.message = std::format(" Locations available: {}", .message = std::format(
all_locations.size())}); " Locations available: {}\n Sampled Locations: {}",
all_locations.size(), sampled_locations.size())});
for (const auto& location : all_locations) {
logger_->Log({.level = LogLevel::Info, logger_->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,
.message = std::format(" Sampled locations: {}", .message = std::format("{} {} {} {}", location.city,
sampled_locations.size())}); location.country, location.longitude,
location.latitude)});
}
return sampled_locations; return sampled_locations;
} }

View File

@@ -110,13 +110,13 @@ int main(const int argc, char** argv) {
di::bind<ICuratedDataService>().to( di::bind<ICuratedDataService>().to(
[options, [options,
&log_producer]() -> std::unique_ptr<ICuratedDataService> { &log_producer]() -> std::unique_ptr<ICuratedDataService> {
if (options.generator.use_mocked) { // if (options.generator.use_mocked) {
log_producer->Log({.level = LogLevel::Info, // log_producer->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup, // .phase = PipelinePhase::Startup,
.message = "Curated data: mock"}); // .message = "Curated data: mock"});
//
return std::make_unique<MockCuratedDataService>(); // return std::make_unique<MockCuratedDataService>();
} // }
log_producer->Log({.level = LogLevel::Info, log_producer->Log({.level = LogLevel::Info,
.phase = PipelinePhase::Startup, .phase = PipelinePhase::Startup,

View File

@@ -30,6 +30,16 @@ static std::string ReadRequiredString(const boost::json::object& object,
return std::string(text); return std::string(text);
} }
static double ReadRequiredDouble(const boost::json::object& object,
const char* key) {
const boost::json::value* value = object.if_contains(key);
if (value == nullptr || !value->is_double()) {
throw std::runtime_error(
std::format("Missing or invalid double field: {}", key));
}
return value->as_double();
}
static std::vector<std::string> ReadRequiredStringArray( static std::vector<std::string> ReadRequiredStringArray(
const boost::json::object& object, const char* key) { const boost::json::object& object, const char* key) {
const boost::json::value* value = object.if_contains(key); const boost::json::value* value = object.if_contains(key);
@@ -67,7 +77,6 @@ static PostalCodeSpec ReadPostalCodeSpec(const boost::json::object& object) {
} }
namespace { namespace {
boost::json::value ParseJsonFile(const std::filesystem::path& filepath, boost::json::value ParseJsonFile(const std::filesystem::path& filepath,
const char* what) { const char* what) {
std::ifstream input(filepath); std::ifstream input(filepath);
@@ -108,7 +117,6 @@ std::string ReadFirstOfStringArray(const boost::json::object& object,
throw std::runtime_error( throw std::runtime_error(
std::format("Missing or invalid string array field: {}", key)); std::format("Missing or invalid string array field: {}", key));
} }
} // namespace } // namespace
CuratedJsonDataService::CuratedJsonDataService(CuratedDataFilePaths filepaths) CuratedJsonDataService::CuratedJsonDataService(CuratedDataFilePaths filepaths)
@@ -144,6 +152,8 @@ const LocationsList& CuratedJsonDataService::LoadLocations() {
.iso3166_2 = ReadRequiredString(object, "iso3166_2"), .iso3166_2 = ReadRequiredString(object, "iso3166_2"),
.country = ReadRequiredString(object, "country"), .country = ReadRequiredString(object, "country"),
.iso3166_1 = ReadRequiredString(object, "iso3166_1"), .iso3166_1 = ReadRequiredString(object, "iso3166_1"),
.latitude = ReadRequiredDouble(object, "latitude"),
.longitude = ReadRequiredDouble(object, "longitude"),
.local_languages = ReadRequiredStringArray(object, "local_languages"), .local_languages = ReadRequiredStringArray(object, "local_languages"),
.postal_code = ReadPostalCodeSpec(object), .postal_code = ReadPostalCodeSpec(object),
}); });

View File

@@ -66,6 +66,18 @@ sqlite3_int64 SqliteExportService::ResolveCityId(const City& city) {
.index = sqlite_export_service_internal::kCityIso31661BindIndex, .index = sqlite_export_service_internal::kCityIso31661BindIndex,
.value = city.iso3166_1, .value = city.iso3166_1,
.action = "Failed to bind SQLite city ISO 3166-1 code"}); .action = "Failed to bind SQLite city ISO 3166-1 code"});
sqlite_export_service_internal::Bind(
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<double>{
.index = sqlite_export_service_internal::kCityLongitudeBindIndex,
.value = city.longitude,
.action = "Failed to bind SQLite city longitude"});
sqlite_export_service_internal::Bind(
insert_city_stmt_,
sqlite_export_service_internal::BoundParam<double>{
.index = sqlite_export_service_internal::kCityLatitudeBindIndex,
.value = city.latitude,
.action = "Failed to bind SQLite city latitude"});
sqlite_export_service_internal::Bind( sqlite_export_service_internal::Bind(
insert_city_stmt_, insert_city_stmt_,
sqlite_export_service_internal::BoundParam<std::string_view>{ sqlite_export_service_internal::BoundParam<std::string_view>{
@@ -165,6 +177,20 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
kBreweryAddressPostalCodeBindIndex, kBreweryAddressPostalCodeBindIndex,
.value = brewery.address.postal_code, .value = brewery.address.postal_code,
.action = "Failed to bind SQLite brewery address postal code"}); .action = "Failed to bind SQLite brewery address postal code"});
sqlite_export_service_internal::Bind(
insert_brewery_address_stmt_,
sqlite_export_service_internal::BoundParam<double>{
.index = sqlite_export_service_internal::
kBreweryAddressLongitudeBindIndex,
.value = brewery.address.longitude,
.action = "Failed to bind SQLite brewery address longitude"});
sqlite_export_service_internal::Bind(
insert_brewery_address_stmt_,
sqlite_export_service_internal::BoundParam<double>{
.index = sqlite_export_service_internal::
kBreweryAddressLatitudeBindIndex,
.value = brewery.address.latitude,
.action = "Failed to bind SQLite brewery address latitude"});
sqlite_export_service_internal::StepStatement( sqlite_export_service_internal::StepStatement(
db_handle_, insert_brewery_address_stmt_, db_handle_, insert_brewery_address_stmt_,