mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
add brewery coordinates
This commit is contained in:
@@ -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{};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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},
|
||||||
}}});
|
}}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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_,
|
||||||
|
|||||||
Reference in New Issue
Block a user