232 lines
7.5 KiB
C++
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
|