mono-cpp/packages/gadm_reader/src/gadm_reader.cpp

232 lines
7.5 KiB
C++

#include "gadm_reader/gadm_reader.h"
#include <algorithm>
#include <fstream>
#include <sstream>
#include <rapidjson/document.h>
namespace gadm {
// ── Helpers ─────────────────────────────────────────────────────────────────
std::string country_code(const std::string& gid) {
auto dot = gid.find('.');
return (dot != std::string::npos) ? gid.substr(0, dot) : gid;
}
int infer_level(const std::string& gid) {
// Count dots: "ABW" → 0, "AFG.1_1" → 1, "AFG.1.1_1" → 2
int dots = 0;
for (char c : gid) {
if (c == '.') dots++;
}
return dots;
}
static std::string read_file(const std::string& path) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs.is_open()) return "";
std::ostringstream oss;
oss << ifs.rdbuf();
return oss.str();
}
/// Parse a coord array [lon, lat] → geo::Coord
static geo::Coord parse_coord(const rapidjson::Value& arr) {
if (arr.IsArray() && arr.Size() >= 2) {
return {arr[0].GetDouble(), arr[1].GetDouble()};
}
return {};
}
/// Parse a ring array [[lon,lat], [lon,lat], ...] → vector<Coord>
static std::vector<geo::Coord> parse_ring(const rapidjson::Value& arr) {
std::vector<geo::Coord> ring;
if (!arr.IsArray()) return ring;
ring.reserve(arr.Size());
for (rapidjson::SizeType i = 0; i < arr.Size(); ++i) {
ring.push_back(parse_coord(arr[i]));
}
return ring;
}
/// Parse weighted centers [[lon, lat, weight], ...]
static std::vector<std::array<double, 3>> parse_weighted_centers(
const rapidjson::Value& arr) {
std::vector<std::array<double, 3>> centers;
if (!arr.IsArray()) return centers;
centers.reserve(arr.Size());
for (rapidjson::SizeType i = 0; i < arr.Size(); ++i) {
const auto& c = arr[i];
if (c.IsArray() && c.Size() >= 3) {
centers.push_back({c[0].GetDouble(), c[1].GetDouble(), c[2].GetDouble()});
}
}
return centers;
}
/// Get a double from properties, with fallback
static double get_double(const rapidjson::Value& props, const char* key,
double fallback = 0.0) {
if (props.HasMember(key) && props[key].IsNumber()) {
return props[key].GetDouble();
}
return fallback;
}
/// Get a bool from properties, with fallback
static bool get_bool(const rapidjson::Value& props, const char* key,
bool fallback = true) {
if (props.HasMember(key) && props[key].IsBool()) {
return props[key].GetBool();
}
return fallback;
}
/// Get a string from properties, checking GID_0, GID_1, GID_2, etc.
static std::string get_gid(const rapidjson::Value& props) {
// Try GID_5 down to GID_0, return the most specific one found
for (int lvl = 5; lvl >= 0; --lvl) {
std::string key = "GID_" + std::to_string(lvl);
if (props.HasMember(key.c_str()) && props[key.c_str()].IsString()) {
return props[key.c_str()].GetString();
}
}
return "";
}
/// Get the name (NAME_0, NAME_1, ... NAME_5)
static std::string get_name(const rapidjson::Value& props) {
for (int lvl = 5; lvl >= 0; --lvl) {
std::string key = "NAME_" + std::to_string(lvl);
if (props.HasMember(key.c_str()) && props[key.c_str()].IsString()) {
return props[key.c_str()].GetString();
}
}
return "";
}
/// Parse a single GeoJSON Feature object into a gadm::Feature
static Feature parse_feature(const rapidjson::Value& feat) {
Feature f;
// Properties
if (feat.HasMember("properties") && feat["properties"].IsObject()) {
const auto& props = feat["properties"];
f.gid = get_gid(props);
f.name = get_name(props);
f.level = infer_level(f.gid);
f.ghsPopulation = get_double(props, "ghsPopulation");
f.ghsBuiltWeight = get_double(props, "ghsBuiltWeight");
f.ghsPopMaxDensity = get_double(props, "ghsPopMaxDensity");
f.ghsBuiltMax = get_double(props, "ghsBuiltMax");
f.isOuter = get_bool(props, "isOuter");
if (props.HasMember("ghsPopCenter") && props["ghsPopCenter"].IsArray()) {
f.ghsPopCenter = parse_coord(props["ghsPopCenter"]);
}
if (props.HasMember("ghsBuiltCenter") && props["ghsBuiltCenter"].IsArray()) {
f.ghsBuiltCenter = parse_coord(props["ghsBuiltCenter"]);
}
if (props.HasMember("ghsPopCenters") && props["ghsPopCenters"].IsArray()) {
f.ghsPopCenters = parse_weighted_centers(props["ghsPopCenters"]);
}
if (props.HasMember("ghsBuiltCenters") && props["ghsBuiltCenters"].IsArray()) {
f.ghsBuiltCenters = parse_weighted_centers(props["ghsBuiltCenters"]);
}
}
// Geometry
if (feat.HasMember("geometry") && feat["geometry"].IsObject()) {
const auto& geom = feat["geometry"];
std::string gtype;
if (geom.HasMember("type") && geom["type"].IsString()) {
gtype = geom["type"].GetString();
}
if (geom.HasMember("coordinates") && geom["coordinates"].IsArray()) {
const auto& coords = geom["coordinates"];
if (gtype == "Polygon") {
// coordinates: [ [ring], [hole], ... ]
for (rapidjson::SizeType r = 0; r < coords.Size(); ++r) {
f.rings.push_back(parse_ring(coords[r]));
}
} else if (gtype == "MultiPolygon") {
// coordinates: [ [ [ring], [hole] ], [ [ring] ], ... ]
for (rapidjson::SizeType p = 0; p < coords.Size(); ++p) {
if (coords[p].IsArray()) {
for (rapidjson::SizeType r = 0; r < coords[p].Size(); ++r) {
f.rings.push_back(parse_ring(coords[p][r]));
}
}
}
}
}
}
// Compute bbox and area from first ring (outer boundary)
if (!f.rings.empty() && !f.rings[0].empty()) {
f.bbox = geo::bbox(f.rings[0]);
f.areaSqKm = geo::area_sq_km(f.rings[0]);
}
return f;
}
// ── Public API ──────────────────────────────────────────────────────────────
BoundaryResult load_boundary_file(const std::string& filepath) {
BoundaryResult result;
std::string json = read_file(filepath);
if (json.empty()) {
result.error = "Failed to read file: " + filepath;
return result;
}
rapidjson::Document doc;
doc.Parse(json.c_str());
if (doc.HasParseError()) {
result.error = "JSON parse error in: " + filepath;
return result;
}
// Expect a FeatureCollection
if (!doc.HasMember("features") || !doc["features"].IsArray()) {
result.error = "Missing 'features' array in: " + filepath;
return result;
}
const auto& features = doc["features"];
result.features.reserve(features.Size());
for (rapidjson::SizeType i = 0; i < features.Size(); ++i) {
result.features.push_back(parse_feature(features[i]));
}
return result;
}
BoundaryResult load_boundary(const std::string& gid, int targetLevel,
const std::string& cacheDir) {
// Try: cacheDir/boundary_{gid}_{level}.json
std::string path = cacheDir + "/boundary_" + gid + "_" + std::to_string(targetLevel) + ".json";
auto result = load_boundary_file(path);
if (result.error.empty()) return result;
// Fallback: cacheDir/boundary_{countryCode}_{level}.json
std::string cc = country_code(gid);
if (cc != gid) {
path = cacheDir + "/boundary_" + cc + "_" + std::to_string(targetLevel) + ".json";
result = load_boundary_file(path);
if (result.error.empty()) return result;
}
// Both failed
result.error = "No boundary file found for gid=" + gid + " level=" + std::to_string(targetLevel) + " in " + cacheDir;
return result;
}
} // namespace gadm