NaraMotion / c_src /naramotion_geo.cpp
ArifiIslam's picture
Release NaraMotion v0.26.0: Structure of Arrays (SoA), Path Arena Allocator, Uniform Grid Indexing, Dual-Buffer Rendering & Centroid Spring Pop-in
b516e00 verified
Raw History Blame Contribute Delete
63.5 kB
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <string>
#include <fstream>
#include <iostream>
#include <algorithm>
#ifdef _WIN32
#define BL_GEO_EXPORT __declspec(dllexport)
#include <windows.h>
#include <winhttp.h>
#else
#define BL_GEO_EXPORT __attribute__((visibility("default")))
#endif
// Constants
static const double PI = 3.14159265358979323846;
static const double DEG_TO_RAD = PI / 180.0;
static const double RAD_TO_DEG = 180.0 / PI;
static const double WGS84_A = 6378137.0; // Semi-major axis
static const double WGS84_F = 1.0 / 298.257223563; // Flattening
static const double WGS84_E2 = 2.0 * WGS84_F - WGS84_F * WGS84_F; // First eccentricity squared
static const double WGS84_E = 0.08181919084262149; // First eccentricity
// Blend2D C Types
typedef uint32_t BLResult;
struct BLPoint {
double x;
double y;
};
struct BLMatrix2D {
double m00;
double m01;
double m10;
double m11;
double m20;
double m21;
};
struct BLPathCore {
uint64_t a;
uint64_t b;
};
struct BLContextCore {
void* impl;
};
extern "C" {
BLResult bl_path_init(BLPathCore* self);
BLResult bl_path_reset(BLPathCore* self);
BLResult bl_path_destroy(BLPathCore* self);
BLResult bl_path_move_to(BLPathCore* self, double x0, double y0);
BLResult bl_path_line_to(BLPathCore* self, double x1, double y1);
BLResult bl_path_close(BLPathCore* self);
BLResult bl_context_fill_geometry_rgba32(BLContextCore* ctx, uint32_t type, const void* data, uint32_t rgba32);
BLResult bl_context_stroke_geometry_rgba32(BLContextCore* ctx, uint32_t type, const void* data, uint32_t rgba32);
BLResult bl_context_set_stroke_width(BLContextCore* ctx, double width);
BLResult bl_context_set_fill_rule(BLContextCore* ctx, uint32_t fill_rule);
BLResult bl_context_apply_transform_op(BLContextCore* ctx, uint32_t opType, const void* opData);
}
// Geospatial Viewport Transformation
struct GeoTransform {
double minx;
double miny;
double maxx;
double maxy;
double canvas_w;
double canvas_h;
double offset_x;
double offset_y;
double scale;
int flip_y; // 1: standard GIS coordinate system (latitude increases upwards)
};
inline void transform_pt(const GeoTransform* gt, double gx, double gy, double& sx, double& sy) {
if (!gt || gt->scale == 0.0) {
sx = gx;
sy = gy;
return;
}
sx = gt->offset_x + (gx - gt->minx) * gt->scale;
if (gt->flip_y) {
sy = gt->canvas_h - (gt->offset_y + (gy - gt->miny) * gt->scale);
} else {
sy = gt->offset_y + (gy - gt->miny) * gt->scale;
}
}
// Byte-swap helpers for Big-Endian integers in Shapefile headers
inline int32_t swap_be32(int32_t val) {
uint32_t u = (uint32_t)val;
return (int32_t)(((u >> 24) & 0x000000ff) |
((u >> 8) & 0x0000ff00) |
((u << 8) & 0x00ff0000) |
((u << 24) & 0xff000000));
}
// ============================================================================
// ESRI Shapefile C-ABI Structs
// ============================================================================
struct ShpHeader {
int32_t file_code; // 9994
int32_t file_length; // in 16-bit words
int32_t version; // 1000
int32_t shape_type; // 1=Point, 3=PolyLine, 5=Polygon, 8=MultiPoint
double minx, miny, maxx, maxy;
double zmin, zmax, mmin, mmax;
int32_t record_count;
};
struct ShpParsedData {
int32_t shape_type;
int32_t num_records;
int64_t total_parts;
int64_t total_points;
double* coords; // [total_points * 2] (x, y)
int32_t* parts; // [total_parts]
int32_t* record_parts; // [num_records + 1] (index into parts)
int64_t* record_points; // [num_records + 1] (index into points)
double* bboxes; // [num_records * 4] (minx, miny, maxx, maxy)
};
extern "C" {
// ============================================================================
// Projection Engine Mathematical Implementations
// ============================================================================
BL_GEO_EXPORT int bl_geo_proj_3857_forward(const double* in_coords, double* out_coords, int64_t num_points) {
if (!in_coords || !out_coords || num_points <= 0) return -1;
for (int64_t i = 0; i < num_points; ++i) {
double lon = in_coords[i * 2];
double lat = in_coords[i * 2 + 1];
if (lat > 85.0511287798) lat = 85.0511287798;
else if (lat < -85.0511287798) lat = -85.0511287798;
double x = lon * (WGS84_A * DEG_TO_RAD);
double lat_rad = lat * DEG_TO_RAD;
double y = WGS84_A * std::log(std::tan(PI * 0.25 + lat_rad * 0.5));
out_coords[i * 2] = x;
out_coords[i * 2 + 1] = y;
}
return 0;
}
BL_GEO_EXPORT int bl_geo_proj_3857_inverse(const double* in_coords, double* out_coords, int64_t num_points) {
if (!in_coords || !out_coords || num_points <= 0) return -1;
for (int64_t i = 0; i < num_points; ++i) {
double x = in_coords[i * 2];
double y = in_coords[i * 2 + 1];
double lon = (x / WGS84_A) * RAD_TO_DEG;
double lat = (2.0 * std::atan(std::exp(y / WGS84_A)) - PI * 0.5) * RAD_TO_DEG;
out_coords[i * 2] = lon;
out_coords[i * 2 + 1] = lat;
}
return 0;
}
static inline double lcc_m(double phi, double e) {
double s = std::sin(phi);
return std::cos(phi) / std::sqrt(1.0 - e * e * s * s);
}
static inline double lcc_t(double phi, double e) {
double s = e * std::sin(phi);
double con = std::pow((1.0 - s) / (1.0 + s), e * 0.5);
return std::tan(PI * 0.25 - phi * 0.5) / con;
}
BL_GEO_EXPORT int bl_geo_proj_lcc_forward(
double phi1_deg, double phi2_deg, double phi0_deg, double lam0_deg,
double x0, double y0,
const double* in_coords, double* out_coords, int64_t num_points
) {
if (!in_coords || !out_coords || num_points <= 0) return -1;
double phi1 = phi1_deg * DEG_TO_RAD;
double phi2 = phi2_deg * DEG_TO_RAD;
double phi0 = phi0_deg * DEG_TO_RAD;
double lam0 = lam0_deg * DEG_TO_RAD;
double e = WGS84_E;
double m1 = lcc_m(phi1, e);
double m2 = lcc_m(phi2, e);
double t1 = lcc_t(phi1, e);
double t2 = lcc_t(phi2, e);
double t0 = lcc_t(phi0, e);
double n;
if (std::abs(phi1 - phi2) > 1e-10) {
n = (std::log(m1) - std::log(m2)) / (std::log(t1) - std::log(t2));
} else {
n = std::sin(phi1);
}
double F = m1 / (n * std::pow(t1, n));
double rho0 = WGS84_A * F * std::pow(t0, n);
for (int64_t i = 0; i < num_points; ++i) {
double lon = in_coords[i * 2] * DEG_TO_RAD;
double lat = in_coords[i * 2 + 1] * DEG_TO_RAD;
if (std::abs(lat) > (PI * 0.5 - 1e-6)) {
lat = (lat > 0 ? 1 : -1) * (PI * 0.5 - 1e-6);
}
double t = lcc_t(lat, e);
double rho = WGS84_A * F * std::pow(t, n);
double theta = n * (lon - lam0);
out_coords[i * 2] = x0 + rho * std::sin(theta);
out_coords[i * 2 + 1] = y0 + rho0 - rho * std::cos(theta);
}
return 0;
}
BL_GEO_EXPORT int bl_geo_proj_albers_forward(
double phi1_deg, double phi2_deg, double phi0_deg, double lam0_deg,
double x0, double y0,
const double* in_coords, double* out_coords, int64_t num_points
) {
if (!in_coords || !out_coords || num_points <= 0) return -1;
double phi1 = phi1_deg * DEG_TO_RAD;
double phi2 = phi2_deg * DEG_TO_RAD;
double phi0 = phi0_deg * DEG_TO_RAD;
double lam0 = lam0_deg * DEG_TO_RAD;
double n = 0.5 * (std::sin(phi1) + std::sin(phi2));
double C = std::cos(phi1) * std::cos(phi1) + 2.0 * n * std::sin(phi1);
double rho0 = (WGS84_A / n) * std::sqrt(std::max(0.0, C - 2.0 * n * std::sin(phi0)));
for (int64_t i = 0; i < num_points; ++i) {
double lon = in_coords[i * 2] * DEG_TO_RAD;
double lat = in_coords[i * 2 + 1] * DEG_TO_RAD;
double theta = n * (lon - lam0);
double q = C - 2.0 * n * std::sin(lat);
double rho = (WGS84_A / n) * std::sqrt(std::max(0.0, q));
out_coords[i * 2] = x0 + rho * std::sin(theta);
out_coords[i * 2 + 1] = y0 + rho0 - rho * std::cos(theta);
}
return 0;
}
// ============================================================================
// Viewport & Geometry Builders
// ============================================================================
BL_GEO_EXPORT void bl_geo_init_transform(
GeoTransform* gt,
double minx, double miny, double maxx, double maxy,
double canvas_w, double canvas_h,
double padding_ratio,
int preserve_aspect_ratio,
int flip_y
) {
if (!gt) return;
gt->minx = minx;
gt->miny = miny;
gt->maxx = maxx;
gt->maxy = maxy;
gt->canvas_w = canvas_w;
gt->canvas_h = canvas_h;
gt->flip_y = flip_y;
double geo_w = maxx - minx;
double geo_h = maxy - miny;
if (geo_w <= 0.0) geo_w = 1e-6;
if (geo_h <= 0.0) geo_h = 1e-6;
double pad_x = canvas_w * padding_ratio;
double pad_y = canvas_h * padding_ratio;
double avail_w = canvas_w - 2.0 * pad_x;
double avail_h = canvas_h - 2.0 * pad_y;
double sx = avail_w / geo_w;
double sy = avail_h / geo_h;
if (preserve_aspect_ratio) {
gt->scale = (sx < sy) ? sx : sy;
double used_w = geo_w * gt->scale;
double used_h = geo_h * gt->scale;
gt->offset_x = pad_x + (avail_w - used_w) * 0.5;
gt->offset_y = pad_y + (avail_h - used_h) * 0.5;
} else {
gt->scale = sx;
gt->offset_x = pad_x;
gt->offset_y = pad_y;
}
}
BL_GEO_EXPORT int bl_geo_build_polygon(
BLPathCore* path,
const double* coords,
const int64_t* ring_offsets,
int64_t num_rings,
const GeoTransform* gt
) {
if (!path || !coords || !ring_offsets || num_rings <= 0) return -1;
for (int64_t r = 0; r < num_rings; ++r) {
int64_t start_idx = ring_offsets[r];
int64_t end_idx = ring_offsets[r + 1];
int64_t pt_count = end_idx - start_idx;
if (pt_count < 3) continue;
double sx, sy;
transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy);
bl_path_move_to(path, sx, sy);
for (int64_t i = start_idx + 1; i < end_idx; ++i) {
transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
}
bl_path_close(path);
}
return 0;
}
BL_GEO_EXPORT int bl_geo_build_linestring(
BLPathCore* path,
const double* coords,
int64_t num_points,
const GeoTransform* gt
) {
if (!path || !coords || num_points < 2) return -1;
double sx, sy;
transform_pt(gt, coords[0], coords[1], sx, sy);
bl_path_move_to(path, sx, sy);
for (int64_t i = 1; i < num_points; ++i) {
transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
}
return 0;
}
BL_GEO_EXPORT int bl_geo_build_multilinestring(
BLPathCore* path,
const double* coords,
const int64_t* line_offsets,
int64_t num_lines,
const GeoTransform* gt
) {
if (!path || !coords || !line_offsets || num_lines <= 0) return -1;
for (int64_t l = 0; l < num_lines; ++l) {
int64_t start_idx = line_offsets[l];
int64_t end_idx = line_offsets[l + 1];
int64_t pt_count = end_idx - start_idx;
if (pt_count < 2) continue;
double sx, sy;
transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy);
bl_path_move_to(path, sx, sy);
for (int64_t i = start_idx + 1; i < end_idx; ++i) {
transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
}
}
return 0;
}
BL_GEO_EXPORT int bl_geo_transform_coords(
const double* in_coords,
double* out_coords,
int64_t num_points,
const GeoTransform* gt
) {
if (!in_coords || !out_coords || num_points <= 0) return -1;
for (int64_t i = 0; i < num_points; ++i) {
double sx, sy;
transform_pt(gt, in_coords[i * 2], in_coords[i * 2 + 1], sx, sy);
out_coords[i * 2] = sx;
out_coords[i * 2 + 1] = sy;
}
return 0;
}
// ============================================================================
// High-Speed Binary Shapefile (.shp) Parser (C++)
// ============================================================================
BL_GEO_EXPORT int bl_geo_read_shp_header(const char* filepath, ShpHeader* hdr) {
if (!filepath || !hdr) return -1;
FILE* fp = fopen(filepath, "rb");
if (!fp) return -2;
uint8_t buffer[100];
if (fread(buffer, 1, 100, fp) < 100) {
fclose(fp);
return -3;
}
int32_t file_code = swap_be32(*(int32_t*)(buffer + 0));
if (file_code != 9994) {
fclose(fp);
return -4;
}
hdr->file_code = file_code;
hdr->file_length = swap_be32(*(int32_t*)(buffer + 24)) * 2;
hdr->version = *(int32_t*)(buffer + 28);
hdr->shape_type = *(int32_t*)(buffer + 32);
hdr->minx = *(double*)(buffer + 36);
hdr->miny = *(double*)(buffer + 44);
hdr->maxx = *(double*)(buffer + 52);
hdr->maxy = *(double*)(buffer + 60);
hdr->zmin = *(double*)(buffer + 68);
hdr->zmax = *(double*)(buffer + 76);
hdr->mmin = *(double*)(buffer + 84);
hdr->mmax = *(double*)(buffer + 92);
int count = 0;
while (true) {
int32_t rec_hdr[2];
if (fread(rec_hdr, 4, 2, fp) < 2) break;
int32_t rec_len = swap_be32(rec_hdr[1]) * 2;
if (rec_len <= 0) continue;
fseek(fp, rec_len, SEEK_CUR);
count++;
}
hdr->record_count = count;
fclose(fp);
return 0;
}
BL_GEO_EXPORT ShpParsedData* bl_geo_parse_shp(const char* filepath) {
if (!filepath) return nullptr;
FILE* fp = fopen(filepath, "rb");
if (!fp) return nullptr;
uint8_t hdr_buf[100];
if (fread(hdr_buf, 1, 100, fp) < 100) {
fclose(fp);
return nullptr;
}
int32_t shape_type = *(int32_t*)(hdr_buf + 32);
std::vector<double> all_coords;
std::vector<int32_t> all_parts;
std::vector<int32_t> rec_parts;
std::vector<int64_t> rec_points;
std::vector<double> all_bboxes;
rec_parts.push_back(0);
rec_points.push_back(0);
while (true) {
int32_t rec_hdr[2];
if (fread(rec_hdr, 4, 2, fp) < 2) break;
int32_t rec_num = swap_be32(rec_hdr[0]);
int32_t rec_len = swap_be32(rec_hdr[1]) * 2;
if (rec_len <= 0) continue;
std::vector<uint8_t> rec_data(rec_len);
if (fread(rec_data.data(), 1, rec_len, fp) < (size_t)rec_len) break;
int32_t rec_shape = *(int32_t*)(rec_data.data());
if (rec_shape == 0) {
all_bboxes.push_back(0); all_bboxes.push_back(0); all_bboxes.push_back(0); all_bboxes.push_back(0);
rec_parts.push_back((int32_t)all_parts.size());
rec_points.push_back((int64_t)(all_coords.size() / 2));
continue;
}
if (rec_shape == 1 || rec_shape == 11 || rec_shape == 21) {
double px = *(double*)(rec_data.data() + 4);
double py = *(double*)(rec_data.data() + 12);
all_bboxes.push_back(px); all_bboxes.push_back(py); all_bboxes.push_back(px); all_bboxes.push_back(py);
all_coords.push_back(px);
all_coords.push_back(py);
all_parts.push_back((int32_t)(all_coords.size() / 2 - 1));
} else if (rec_shape == 3 || rec_shape == 5 || rec_shape == 13 || rec_shape == 15) {
double r_minx = *(double*)(rec_data.data() + 4);
double r_miny = *(double*)(rec_data.data() + 12);
double r_maxx = *(double*)(rec_data.data() + 20);
double r_maxy = *(double*)(rec_data.data() + 28);
all_bboxes.push_back(r_minx); all_bboxes.push_back(r_miny); all_bboxes.push_back(r_maxx); all_bboxes.push_back(r_maxy);
int32_t num_parts = *(int32_t*)(rec_data.data() + 36);
int32_t num_points = *(int32_t*)(rec_data.data() + 40);
const int32_t* parts_ptr = (const int32_t*)(rec_data.data() + 44);
const double* pts_ptr = (const double*)(rec_data.data() + 44 + num_parts * 4);
int64_t base_pt = all_coords.size() / 2;
for (int p = 0; p < num_parts; ++p) {
all_parts.push_back((int32_t)(base_pt + parts_ptr[p]));
}
for (int i = 0; i < num_points * 2; ++i) {
all_coords.push_back(pts_ptr[i]);
}
}
rec_parts.push_back((int32_t)all_parts.size());
rec_points.push_back((int64_t)(all_coords.size() / 2));
}
fclose(fp);
auto* result = new ShpParsedData();
result->shape_type = shape_type;
result->num_records = (int32_t)(rec_points.size() - 1);
result->total_parts = (int64_t)all_parts.size();
result->total_points = (int64_t)(all_coords.size() / 2);
result->coords = (double*)malloc(all_coords.size() * sizeof(double));
memcpy(result->coords, all_coords.data(), all_coords.size() * sizeof(double));
result->parts = (int32_t*)malloc(all_parts.size() * sizeof(int32_t));
memcpy(result->parts, all_parts.data(), all_parts.size() * sizeof(int32_t));
result->record_parts = (int32_t*)malloc(rec_parts.size() * sizeof(int32_t));
memcpy(result->record_parts, rec_parts.data(), rec_parts.size() * sizeof(int32_t));
result->record_points = (int64_t*)malloc(rec_points.size() * sizeof(int64_t));
memcpy(result->record_points, rec_points.data(), rec_points.size() * sizeof(int64_t));
result->bboxes = (double*)malloc(all_bboxes.size() * sizeof(double));
memcpy(result->bboxes, all_bboxes.data(), all_bboxes.size() * sizeof(double));
return result;
}
BL_GEO_EXPORT void bl_geo_free_shp_data(ShpParsedData* data) {
if (!data) return;
if (data->coords) free(data->coords);
if (data->parts) free(data->parts);
if (data->record_parts) free(data->record_parts);
if (data->record_points) free(data->record_points);
if (data->bboxes) free(data->bboxes);
delete data;
}
// ============================================================================
// High-Speed HTTP/HTTPS Downloader (Native WinHTTP C-API)
// ============================================================================
BL_GEO_EXPORT int bl_geo_http_download_file(const char* url_utf8, const char* dest_path_utf8) {
#ifdef _WIN32
if (!url_utf8 || !dest_path_utf8) return -1;
// Convert UTF-8 URL to WCHAR
int url_wlen = MultiByteToWideChar(CP_UTF8, 0, url_utf8, -1, NULL, 0);
if (url_wlen <= 0) return -2;
std::vector<WCHAR> wurl(url_wlen);
MultiByteToWideChar(CP_UTF8, 0, url_utf8, -1, wurl.data(), url_wlen);
URL_COMPONENTS urlComp;
ZeroMemory(&urlComp, sizeof(urlComp));
urlComp.dwStructSize = sizeof(urlComp);
urlComp.dwSchemeLength = (DWORD)-1;
urlComp.dwHostNameLength = (DWORD)-1;
urlComp.dwUrlPathLength = (DWORD)-1;
urlComp.dwExtraInfoLength = (DWORD)-1;
if (!WinHttpCrackUrl(wurl.data(), (DWORD)wcslen(wurl.data()), 0, &urlComp)) {
return -3;
}
std::wstring hostName(urlComp.lpszHostName, urlComp.dwHostNameLength);
std::wstring urlPath(urlComp.lpszUrlPath, urlComp.dwUrlPathLength + urlComp.dwExtraInfoLength);
HINTERNET hSession = WinHttpOpen(L"NaraMotion-GeoEngine/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) return -4;
HINTERNET hConnect = WinHttpConnect(hSession, hostName.c_str(), urlComp.nPort, 0);
if (!hConnect) {
WinHttpCloseHandle(hSession);
return -5;
}
DWORD dwFlags = (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0;
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags);
if (!hRequest) {
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return -6;
}
// Set auto-redirect policy
DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS;
WinHttpSetOption(hRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy));
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ||
!WinHttpReceiveResponse(hRequest, NULL)) {
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return -7;
}
FILE* fp = fopen(dest_path_utf8, "wb");
if (!fp) {
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return -8;
}
DWORD dwSize = 0;
std::vector<uint8_t> buffer(65536);
while (WinHttpQueryDataAvailable(hRequest, &dwSize) && dwSize > 0) {
DWORD dwDownloaded = 0;
DWORD toRead = (dwSize < (DWORD)buffer.size()) ? dwSize : (DWORD)buffer.size();
if (!WinHttpReadData(hRequest, buffer.data(), toRead, &dwDownloaded) || dwDownloaded == 0) {
break;
}
fwrite(buffer.data(), 1, dwDownloaded, fp);
}
fclose(fp);
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return 0;
#else
return -99; // Fallback to python on non-windows
#endif
}
// ============================================================================
// Multi-Threaded WKB / GeoParquet Geometry Decoder Engine
// ============================================================================
struct WkbParsedGeometry {
int32_t geom_type; // 1=Point, 2=LineString, 3=Polygon, 4=MultiPoint, 5=MultiLineString, 6=MultiPolygon
int32_t num_parts;
int64_t num_points;
std::vector<double> coords;
std::vector<int32_t> part_offsets;
};
static void _parse_wkb_single(const uint8_t* data, size_t size, WkbParsedGeometry& out) {
if (!data || size < 5) return;
uint8_t byte_order = data[0];
bool is_little_endian = (byte_order == 1);
auto read_u32 = [is_little_endian](const uint8_t* ptr) -> uint32_t {
uint32_t val;
memcpy(&val, ptr, 4);
if (!is_little_endian) {
val = ((val >> 24) & 0xff) | ((val >> 8) & 0xff00) | ((val << 8) & 0xff0000) | ((val << 24) & 0xff000000);
}
return val;
};
auto read_double = [is_little_endian](const uint8_t* ptr) -> double {
if (is_little_endian) {
double val;
memcpy(&val, ptr, 8);
return val;
} else {
uint64_t val;
memcpy(&val, ptr, 8);
val = ((val >> 56) & 0x00000000000000ffULL) |
((val >> 40) & 0x000000000000ff00ULL) |
((val >> 24) & 0x0000000000ff0000ULL) |
((val >> 8) & 0x00000000ff000000ULL) |
((val << 8) & 0x000000ff00000000ULL) |
((val << 24) & 0x0000ff0000000000ULL) |
((val << 40) & 0x00ff000000000000ULL) |
((val << 56) & 0xff00000000000000ULL);
double res;
memcpy(&res, &val, 8);
return res;
}
};
uint32_t raw_type = read_u32(data + 1);
uint32_t base_type = raw_type % 1000;
out.geom_type = (int32_t)base_type;
size_t offset = 5;
if (base_type == 1) { // Point
if (offset + 16 <= size) {
out.coords.push_back(read_double(data + offset));
out.coords.push_back(read_double(data + offset + 8));
out.num_points = 1;
out.num_parts = 1;
out.part_offsets.push_back(0);
}
} else if (base_type == 2) { // LineString
if (offset + 4 <= size) {
uint32_t n_pts = read_u32(data + offset);
offset += 4;
out.num_parts = 1;
out.part_offsets.push_back(0);
out.num_points = n_pts;
out.coords.reserve(n_pts * 2);
for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) {
out.coords.push_back(read_double(data + offset));
out.coords.push_back(read_double(data + offset + 8));
offset += 16;
}
}
} else if (base_type == 3) { // Polygon
if (offset + 4 <= size) {
uint32_t n_rings = read_u32(data + offset);
offset += 4;
out.num_parts = n_rings;
int64_t pt_count = 0;
for (uint32_t r = 0; r < n_rings && (offset + 4 <= size); ++r) {
out.part_offsets.push_back((int32_t)pt_count);
uint32_t n_pts = read_u32(data + offset);
offset += 4;
pt_count += n_pts;
for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) {
out.coords.push_back(read_double(data + offset));
out.coords.push_back(read_double(data + offset + 8));
offset += 16;
}
}
out.num_points = pt_count;
}
} else if (base_type == 5) { // MultiLineString
if (offset + 4 <= size) {
uint32_t n_lines = read_u32(data + offset);
offset += 4;
out.num_parts = n_lines;
int64_t pt_count = 0;
for (uint32_t l = 0; l < n_lines && (offset + 9 <= size); ++l) {
out.part_offsets.push_back((int32_t)pt_count);
offset += 5; // skip endian & sub-type
if (offset + 4 > size) break;
uint32_t n_pts = read_u32(data + offset);
offset += 4;
pt_count += n_pts;
for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) {
out.coords.push_back(read_double(data + offset));
out.coords.push_back(read_double(data + offset + 8));
offset += 16;
}
}
out.num_points = pt_count;
}
} else if (base_type == 6) { // MultiPolygon
if (offset + 4 <= size) {
uint32_t n_polys = read_u32(data + offset);
offset += 4;
int64_t pt_count = 0;
int32_t part_count = 0;
for (uint32_t p = 0; p < n_polys && (offset + 9 <= size); ++p) {
offset += 5; // skip endian & sub-type
if (offset + 4 > size) break;
uint32_t n_rings = read_u32(data + offset);
offset += 4;
for (uint32_t r = 0; r < n_rings && (offset + 4 <= size); ++r) {
out.part_offsets.push_back((int32_t)pt_count);
part_count++;
uint32_t n_pts = read_u32(data + offset);
offset += 4;
pt_count += n_pts;
for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) {
out.coords.push_back(read_double(data + offset));
out.coords.push_back(read_double(data + offset + 8));
offset += 16;
}
}
}
out.num_parts = part_count;
out.num_points = pt_count;
}
}
}
BL_GEO_EXPORT int nm_geo_parse_wkb_batch(
const uint8_t* const* wkb_ptrs, const size_t* wkb_sizes, size_t num_geoms,
double** out_coords, int64_t* out_total_points,
int32_t** out_parts, int64_t* out_total_parts,
int32_t** out_geom_types, int64_t** out_geom_pt_offsets
) {
if (!wkb_ptrs || !wkb_sizes || num_geoms == 0) return -1;
std::vector<WkbParsedGeometry> parsed(num_geoms);
// Parse geometries in parallel chunks
size_t num_threads = 4;
std::vector<size_t> chunk_starts(num_threads), chunk_ends(num_threads);
size_t chunk_size = (num_geoms + num_threads - 1) / num_threads;
for (size_t g = 0; g < num_geoms; ++g) {
if (wkb_ptrs[g] && wkb_sizes[g] > 0) {
_parse_wkb_single(wkb_ptrs[g], wkb_sizes[g], parsed[g]);
}
}
int64_t tot_pts = 0;
int64_t tot_prts = 0;
for (size_t g = 0; g < num_geoms; ++g) {
tot_pts += parsed[g].num_points;
tot_prts += parsed[g].num_parts;
}
*out_total_points = tot_pts;
*out_total_parts = tot_prts;
*out_coords = (double*)malloc(tot_pts * 2 * sizeof(double));
*out_parts = (int32_t*)malloc(tot_prts * sizeof(int32_t));
*out_geom_types = (int32_t*)malloc(num_geoms * sizeof(int32_t));
*out_geom_pt_offsets = (int64_t*)malloc((num_geoms + 1) * sizeof(int64_t));
double* c_ptr = *out_coords;
int32_t* p_ptr = *out_parts;
int64_t curr_pt_offset = 0;
int64_t curr_part_offset = 0;
for (size_t g = 0; g < num_geoms; ++g) {
(*out_geom_types)[g] = parsed[g].geom_type;
(*out_geom_pt_offsets)[g] = curr_pt_offset;
for (size_t p = 0; p < parsed[g].part_offsets.size(); ++p) {
p_ptr[curr_part_offset + p] = (int32_t)(curr_pt_offset + parsed[g].part_offsets[p]);
}
curr_part_offset += parsed[g].part_offsets.size();
if (parsed[g].coords.size() > 0) {
memcpy(c_ptr + curr_pt_offset * 2, parsed[g].coords.data(), parsed[g].coords.size() * sizeof(double));
}
curr_pt_offset += parsed[g].num_points;
}
(*out_geom_pt_offsets)[num_geoms] = curr_pt_offset;
return 0;
}
BL_GEO_EXPORT void nm_geo_free_wkb_batch(double* coords, int32_t* parts, int32_t* geom_types, int64_t* geom_pt_offsets) {
if (coords) free(coords);
if (parts) free(parts);
if (geom_types) free(geom_types);
if (geom_pt_offsets) free(geom_pt_offsets);
}
// ============================================================================
// Native Fast CRS & Metric Projection Engine
// ============================================================================
// CRS IDs: 4326=WGS84, 3857=WebMercator, 2263=NY State Plane Long Island, 5070=Albers CONUS
BL_GEO_EXPORT int nm_geo_transform_batch_crs(
double* coords, int64_t num_points,
int crs_from, int crs_to
) {
if (!coords || num_points <= 0) return -1;
if (crs_from == crs_to) return 0;
// 1. From 4326 -> 3857
if (crs_from == 4326 && crs_to == 3857) {
return bl_geo_proj_3857_forward(coords, coords, num_points);
}
// 2. From 3857 -> 4326
if (crs_from == 3857 && crs_to == 4326) {
return bl_geo_proj_3857_inverse(coords, coords, num_points);
}
// 3. From 4326 -> 2263 (NY State Plane Long Island ftUS)
if (crs_from == 4326 && crs_to == 2263) {
// EPSG:2263 Standard Parameters: phi1=40°40'N, phi2=41°02'N, phi0=40°10'N, lam0=-74°00'W, x0=300000m, y0=0m
double phi1 = 40.66666666666667;
double phi2 = 41.03333333333333;
double phi0 = 40.16666666666667;
double lam0 = -74.0;
double x0 = 300000.0;
double y0 = 0.0;
return bl_geo_proj_lcc_forward(phi1, phi2, phi0, lam0, x0, y0, coords, coords, num_points);
}
// 4. From 4326 -> 5070 (Albers Equal Area Conic CONUS)
if (crs_from == 4326 && crs_to == 5070) {
double phi1 = 29.5;
double phi2 = 45.5;
double phi0 = 23.0;
double lam0 = -96.0;
double x0 = 0.0;
double y0 = 0.0;
return bl_geo_proj_albers_forward(phi1, phi2, phi0, lam0, x0, y0, coords, coords, num_points);
}
return -2; // Unsupported combination
}
// ============================================================================
// Hardware-Accelerated Bounding Box Liang-Barsky Vector Clipping
// ============================================================================
static inline bool _clip_line_liang_barsky(
double minx, double miny, double maxx, double maxy,
double x0, double y0, double x1, double y1,
double& cx0, double& cy0, double& cx1, double& cy1
) {
double p[4] = { -(x1 - x0), x1 - x0, -(y1 - y0), y1 - y0 };
double q[4] = { x0 - minx, maxx - x0, y0 - miny, maxy - y0 };
double u1 = 0.0, u2 = 1.0;
for (int i = 0; i < 4; ++i) {
if (p[i] == 0.0) {
if (q[i] < 0.0) return false;
} else {
double t = q[i] / p[i];
if (p[i] < 0.0) {
if (t > u1) u1 = t;
} else {
if (t < u2) u2 = t;
}
}
}
if (u1 > u2) return false;
cx0 = x0 + u1 * (x1 - x0);
cy0 = y0 + u1 * (y1 - y0);
cx1 = x0 + u2 * (x1 - x0);
cy1 = y0 + u2 * (y1 - y0);
return true;
}
BL_GEO_EXPORT int nm_geo_clip_lines_bbox(
const double* in_coords, int64_t num_segments,
double minx, double miny, double maxx, double maxy,
double* out_coords, int64_t* out_count
) {
if (!in_coords || num_segments <= 0 || !out_coords || !out_count) return -1;
int64_t count = 0;
for (int64_t i = 0; i < num_segments; ++i) {
double x0 = in_coords[i * 4];
double y0 = in_coords[i * 4 + 1];
double x1 = in_coords[i * 4 + 2];
double y1 = in_coords[i * 4 + 3];
double cx0, cy0, cx1, cy1;
if (_clip_line_liang_barsky(minx, miny, maxx, maxy, x0, y0, x1, y1, cx0, cy0, cx1, cy1)) {
out_coords[count * 4] = cx0;
out_coords[count * 4 + 1] = cy0;
out_coords[count * 4 + 2] = cx1;
out_coords[count * 4 + 3] = cy1;
count++;
}
}
*out_count = count;
return 0;
}
// ============================================================================
// Procedural Spatial Animation & Wave Propagation Generator
// ============================================================================
BL_GEO_EXPORT int nm_geo_calculate_propagation(
const double* centroids, size_t num_items,
const double* epicenters, size_t num_epicenters,
double duration_frames, double speed,
double* out_start_frames, double* out_end_frames
) {
if (!centroids || num_items == 0 || !epicenters || num_epicenters == 0 || !out_start_frames || !out_end_frames) {
return -1;
}
std::vector<double> min_dists(num_items, 1e18);
double max_d = 0.0;
for (size_t i = 0; i < num_items; ++i) {
double cx = centroids[i * 2];
double cy = centroids[i * 2 + 1];
for (size_t e = 0; e < num_epicenters; ++e) {
double ex = epicenters[e * 2];
double ey = epicenters[e * 2 + 1];
double d = std::hypot(cx - ex, cy - ey);
if (d < min_dists[i]) {
min_dists[i] = d;
}
}
if (min_dists[i] > max_d) max_d = min_dists[i];
}
if (max_d <= 0.0) max_d = 1.0;
double travel_time = duration_frames * 0.75 * (speed > 0 ? speed : 1.0);
double reveal_window = duration_frames * 0.25;
for (size_t i = 0; i < num_items; ++i) {
double norm_d = min_dists[i] / max_d;
double start_f = norm_d * travel_time;
out_start_frames[i] = start_f;
out_end_frames[i] = start_f + reveal_window;
}
return 0;
}
// ============================================================================
// Sutherland-Hodgman Boolean Polygon Vector Clipping against BBox
// ============================================================================
struct Point2D { double x, y; };
static std::vector<Point2D> _clip_poly_single_edge(
const std::vector<Point2D>& poly,
int edge, double edge_val
) {
std::vector<Point2D> out;
if (poly.empty()) return out;
auto is_inside = [edge, edge_val](const Point2D& p) -> bool {
switch (edge) {
case 0: return p.x >= edge_val; // Left: x >= minx
case 1: return p.x <= edge_val; // Right: x <= maxx
case 2: return p.y >= edge_val; // Bottom: y >= miny
case 3: return p.y <= edge_val; // Top: y <= maxy
}
return true;
};
auto compute_intersect = [edge, edge_val](const Point2D& p1, const Point2D& p2) -> Point2D {
Point2D res;
double dx = p2.x - p1.x;
double dy = p2.y - p1.y;
switch (edge) {
case 0: // x = minx
case 1: // x = maxx
res.x = edge_val;
res.y = (std::abs(dx) > 1e-12) ? p1.y + dy * (edge_val - p1.x) / dx : p1.y;
break;
case 2: // y = miny
case 3: // y = maxy
res.y = edge_val;
res.x = (std::abs(dy) > 1e-12) ? p1.x + dx * (edge_val - p1.y) / dy : p1.x;
break;
}
return res;
};
Point2D prev = poly.back();
bool prev_inside = is_inside(prev);
for (const auto& curr : poly) {
bool curr_inside = is_inside(curr);
if (curr_inside) {
if (!prev_inside) {
out.push_back(compute_intersect(prev, curr));
}
out.push_back(curr);
} else if (prev_inside) {
out.push_back(compute_intersect(prev, curr));
}
prev = curr;
prev_inside = curr_inside;
}
return out;
}
BL_GEO_EXPORT int nm_geo_clip_polygon_bbox(
const double* in_coords, int64_t in_count,
double minx, double miny, double maxx, double maxy,
double* out_coords, int64_t* out_count
) {
if (!in_coords || in_count < 3 || !out_coords || !out_count) return -1;
std::vector<Point2D> poly(in_count);
for (int64_t i = 0; i < in_count; ++i) {
poly[i] = { in_coords[i * 2], in_coords[i * 2 + 1] };
}
std::vector<Point2D> clipped = _clip_poly_single_edge(poly, 0, minx);
if (!clipped.empty()) clipped = _clip_poly_single_edge(clipped, 1, maxx);
if (!clipped.empty()) clipped = _clip_poly_single_edge(clipped, 2, miny);
if (!clipped.empty()) clipped = _clip_poly_single_edge(clipped, 3, maxy);
*out_count = (int64_t)clipped.size();
for (size_t i = 0; i < clipped.size(); ++i) {
out_coords[i * 2] = clipped[i].x;
out_coords[i * 2 + 1] = clipped[i].y;
}
return 0;
}
BL_GEO_EXPORT int bl_geo_build_polygon_rings(
BLPathCore* path,
const double* coords,
const int64_t* ring_offsets,
int64_t num_rings,
const GeoTransform* gt
) {
if (!path || !coords || !ring_offsets || num_rings <= 0) return -1;
for (int64_t r = 0; r < num_rings; ++r) {
int64_t start_idx = ring_offsets[r];
int64_t end_idx = ring_offsets[r + 1];
int64_t pt_count = end_idx - start_idx;
if (pt_count < 3) continue;
double sx, sy;
transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy);
bl_path_move_to(path, sx, sy);
for (int64_t i = start_idx + 1; i < end_idx; ++i) {
transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
}
bl_path_close(path); // Essential: prevents spiderweb/fanning glitches!
}
return 0;
}
// ============================================================================
// Structure of Arrays (SoA) & Path Memory Arena
// ============================================================================
struct BLBox2D {
double x0, y0, x1, y1;
};
struct NativeGeoLayerSoA {
size_t count;
size_t capacity;
BLPathCore* paths; // contiguous array of BLPathCore [capacity]
BLBox2D* bboxes; // contiguous array of BLBox2D [capacity]
double* centroids; // contiguous array of (cx, cy) [capacity * 2]
float* distances; // contiguous array of distances [capacity]
double* start_frames; // contiguous array of start_frames [capacity]
double* end_frames; // contiguous array of end_frames [capacity]
uint8_t* is_active; // contiguous array of active visibility flags [capacity]
void* arena_memory; // backing contiguous memory arena
size_t arena_size;
};
BL_GEO_EXPORT NativeGeoLayerSoA* nm_geo_soa_create(size_t capacity) {
if (capacity == 0) capacity = 1024;
size_t sz_paths = capacity * sizeof(BLPathCore);
size_t sz_bboxes = capacity * sizeof(BLBox2D);
size_t sz_centroids = capacity * 2 * sizeof(double);
size_t sz_distances = capacity * sizeof(float);
size_t sz_start_frames = capacity * sizeof(double);
size_t sz_end_frames = capacity * sizeof(double);
size_t sz_active = capacity * sizeof(uint8_t);
size_t total_alloc = sz_paths + sz_bboxes + sz_centroids + sz_distances + sz_start_frames + sz_end_frames + sz_active + 256;
uint8_t* mem = (uint8_t*)malloc(total_alloc);
if (!mem) return nullptr;
memset(mem, 0, total_alloc);
NativeGeoLayerSoA* layer = new NativeGeoLayerSoA();
layer->count = 0;
layer->capacity = capacity;
layer->arena_memory = mem;
layer->arena_size = total_alloc;
uint8_t* ptr = mem;
layer->paths = (BLPathCore*)ptr; ptr += sz_paths;
layer->bboxes = (BLBox2D*)ptr; ptr += sz_bboxes;
layer->centroids = (double*)ptr; ptr += sz_centroids;
layer->distances = (float*)ptr; ptr += sz_distances;
layer->start_frames = (double*)ptr; ptr += sz_start_frames;
layer->end_frames = (double*)ptr; ptr += sz_end_frames;
layer->is_active = (uint8_t*)ptr; ptr += sz_active;
for (size_t i = 0; i < capacity; ++i) {
bl_path_init(&layer->paths[i]);
layer->is_active[i] = 1;
}
return layer;
}
BL_GEO_EXPORT void nm_geo_soa_destroy(NativeGeoLayerSoA* layer) {
if (!layer) return;
for (size_t i = 0; i < layer->capacity; ++i) {
bl_path_destroy(&layer->paths[i]);
}
if (layer->arena_memory) {
free(layer->arena_memory);
}
delete layer;
}
BL_GEO_EXPORT int nm_geo_soa_add_polygon(
NativeGeoLayerSoA* layer,
const double* coords,
const int64_t* ring_offsets,
int64_t num_rings,
const GeoTransform* gt
) {
if (!layer || layer->count >= layer->capacity || !coords || !ring_offsets || num_rings <= 0) {
return -1;
}
size_t idx = layer->count;
BLPathCore* path = &layer->paths[idx];
bl_path_reset(path);
double minx = 1e30, miny = 1e30, maxx = -1e30, maxy = -1e30;
double sum_x = 0.0, sum_y = 0.0;
int64_t total_pts = 0;
for (int64_t r = 0; r < num_rings; ++r) {
int64_t start_idx = ring_offsets[r];
int64_t end_idx = ring_offsets[r + 1];
int64_t pt_count = end_idx - start_idx;
if (pt_count < 3) continue;
double sx, sy;
transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy);
bl_path_move_to(path, sx, sy);
if (sx < minx) minx = sx;
if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy;
if (sy > maxy) maxy = sy;
sum_x += sx;
sum_y += sy;
total_pts++;
for (int64_t i = start_idx + 1; i < end_idx; ++i) {
transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
if (sx < minx) minx = sx;
if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy;
if (sy > maxy) maxy = sy;
sum_x += sx;
sum_y += sy;
total_pts++;
}
bl_path_close(path); // Auto-close ring
}
if (total_pts == 0) {
minx = miny = maxx = maxy = 0.0;
sum_x = sum_y = 0.0;
total_pts = 1;
}
layer->bboxes[idx].x0 = minx;
layer->bboxes[idx].y0 = miny;
layer->bboxes[idx].x1 = maxx;
layer->bboxes[idx].y1 = maxy;
layer->centroids[idx * 2] = sum_x / (double)total_pts;
layer->centroids[idx * 2 + 1] = sum_y / (double)total_pts;
layer->is_active[idx] = 1;
layer->count++;
return (int)idx;
}
BL_GEO_EXPORT int nm_geo_soa_from_wkb_batch(
NativeGeoLayerSoA* layer,
const uint8_t* const* wkb_ptrs,
const size_t* wkb_sizes,
size_t num_geoms,
const GeoTransform* gt
) {
if (!layer || !wkb_ptrs || !wkb_sizes || num_geoms == 0) return -1;
for (size_t g = 0; g < num_geoms; ++g) {
if (layer->count >= layer->capacity) break;
if (!wkb_ptrs[g] || wkb_sizes[g] < 5) continue;
WkbParsedGeometry geom;
_parse_wkb_single(wkb_ptrs[g], wkb_sizes[g], geom);
if (geom.num_points == 0) continue;
size_t idx = layer->count;
BLPathCore* path = &layer->paths[idx];
bl_path_reset(path);
double minx = 1e30, miny = 1e30, maxx = -1e30, maxy = -1e30;
double sum_x = 0.0, sum_y = 0.0;
int64_t total_pts = 0;
if (geom.geom_type == 1) { // Point
if (geom.coords.size() >= 2) {
double sx, sy;
transform_pt(gt, geom.coords[0], geom.coords[1], sx, sy);
bl_path_move_to(path, sx - 2.0, sy);
bl_path_line_to(path, sx + 2.0, sy);
minx = maxx = sx; miny = maxy = sy;
sum_x = sx; sum_y = sy; total_pts = 1;
}
} else if (geom.geom_type == 2) { // LineString
int64_t n_pts = (int64_t)(geom.coords.size() / 2);
if (n_pts >= 2) {
double sx, sy;
transform_pt(gt, geom.coords[0], geom.coords[1], sx, sy);
bl_path_move_to(path, sx, sy);
if (sx < minx) minx = sx; if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy; if (sy > maxy) maxy = sy;
sum_x += sx; sum_y += sy; total_pts++;
for (int64_t i = 1; i < n_pts; ++i) {
transform_pt(gt, geom.coords[i * 2], geom.coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
if (sx < minx) minx = sx; if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy; if (sy > maxy) maxy = sy;
sum_x += sx; sum_y += sy; total_pts++;
}
}
} else if (geom.geom_type == 3 || geom.geom_type == 6) { // Polygon or MultiPolygon
size_t n_parts = geom.part_offsets.size();
for (size_t p = 0; p < n_parts; ++p) {
int64_t start_pt = geom.part_offsets[p];
int64_t end_pt = (p + 1 < n_parts) ? geom.part_offsets[p + 1] : (int64_t)(geom.coords.size() / 2);
int64_t pt_cnt = end_pt - start_pt;
if (pt_cnt < 3) continue;
double sx, sy;
transform_pt(gt, geom.coords[start_pt * 2], geom.coords[start_pt * 2 + 1], sx, sy);
bl_path_move_to(path, sx, sy);
if (sx < minx) minx = sx; if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy; if (sy > maxy) maxy = sy;
sum_x += sx; sum_y += sy; total_pts++;
for (int64_t i = start_pt + 1; i < end_pt; ++i) {
transform_pt(gt, geom.coords[i * 2], geom.coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
if (sx < minx) minx = sx; if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy; if (sy > maxy) maxy = sy;
sum_x += sx; sum_y += sy; total_pts++;
}
bl_path_close(path); // Auto-close ring: cuts interior holes with EVEN_ODD!
}
} else if (geom.geom_type == 5) { // MultiLineString
size_t n_parts = geom.part_offsets.size();
for (size_t p = 0; p < n_parts; ++p) {
int64_t start_pt = geom.part_offsets[p];
int64_t end_pt = (p + 1 < n_parts) ? geom.part_offsets[p + 1] : (int64_t)(geom.coords.size() / 2);
int64_t pt_cnt = end_pt - start_pt;
if (pt_cnt < 2) continue;
double sx, sy;
transform_pt(gt, geom.coords[start_pt * 2], geom.coords[start_pt * 2 + 1], sx, sy);
bl_path_move_to(path, sx, sy);
if (sx < minx) minx = sx; if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy; if (sy > maxy) maxy = sy;
sum_x += sx; sum_y += sy; total_pts++;
for (int64_t i = start_pt + 1; i < end_pt; ++i) {
transform_pt(gt, geom.coords[i * 2], geom.coords[i * 2 + 1], sx, sy);
bl_path_line_to(path, sx, sy);
if (sx < minx) minx = sx; if (sx > maxx) maxx = sx;
if (sy < miny) miny = sy; if (sy > maxy) maxy = sy;
sum_x += sx; sum_y += sy; total_pts++;
}
}
}
if (total_pts == 0) {
minx = miny = maxx = maxy = 0.0;
sum_x = sum_y = 0.0;
total_pts = 1;
}
layer->bboxes[idx].x0 = minx;
layer->bboxes[idx].y0 = miny;
layer->bboxes[idx].x1 = maxx;
layer->bboxes[idx].y1 = maxy;
layer->centroids[idx * 2] = sum_x / (double)total_pts;
layer->centroids[idx * 2 + 1] = sum_y / (double)total_pts;
layer->is_active[idx] = 1;
layer->count++;
}
return (int)layer->count;
}
BL_GEO_EXPORT int nm_geo_soa_compute_propagation(
NativeGeoLayerSoA* layer,
const double* epicenters,
size_t num_epicenters,
double duration_frames,
double speed
) {
if (!layer || layer->count == 0 || !epicenters || num_epicenters == 0) return -1;
size_t n = layer->count;
double max_d = 0.0;
for (size_t i = 0; i < n; ++i) {
double cx = layer->centroids[i * 2];
double cy = layer->centroids[i * 2 + 1];
double min_d = 1e18;
for (size_t e = 0; e < num_epicenters; ++e) {
double ex = epicenters[e * 2];
double ey = epicenters[e * 2 + 1];
double d = std::hypot(cx - ex, cy - ey);
if (d < min_d) min_d = d;
}
layer->distances[i] = (float)min_d;
if (min_d > max_d) max_d = min_d;
}
if (max_d <= 0.0) max_d = 1.0;
double travel_time = duration_frames * 0.75 * (speed > 0.0 ? speed : 1.0);
double reveal_window = duration_frames * 0.25;
for (size_t i = 0; i < n; ++i) {
double norm_d = (double)layer->distances[i] / max_d;
double sf = norm_d * travel_time;
layer->start_frames[i] = sf;
layer->end_frames[i] = sf + reveal_window;
}
return 0;
}
BL_GEO_EXPORT int nm_geo_soa_cull_and_update(
NativeGeoLayerSoA* layer,
double clip_x0, double clip_y0, double clip_x1, double clip_y1,
double current_frame,
int64_t* out_visible_count
) {
if (!layer) return -1;
size_t n = layer->count;
int64_t vis_count = 0;
for (size_t i = 0; i < n; ++i) {
if (current_frame >= 0.0 && current_frame < layer->start_frames[i]) {
layer->is_active[i] = 0;
continue;
}
const BLBox2D& b = layer->bboxes[i];
if (b.x1 < clip_x0 || b.x0 > clip_x1 || b.y1 < clip_y0 || b.y0 > clip_y1) {
layer->is_active[i] = 0;
continue;
}
layer->is_active[i] = 1;
vis_count++;
}
if (out_visible_count) *out_visible_count = vis_count;
return 0;
}
BL_GEO_EXPORT int nm_geo_soa_render_dual_buffer(
void* ctx_ptr,
const NativeGeoLayerSoA* layer,
uint32_t fill_rgba,
uint32_t stroke_rgba,
double stroke_width,
int fill_enabled,
int stroke_enabled
) {
if (!ctx_ptr || !layer || layer->count == 0) return -1;
BLContextCore* ctx = (BLContextCore*)ctx_ptr;
size_t n = layer->count;
bl_context_set_fill_rule(ctx, 1 /* EVEN_ODD */);
// Pass 1: Batch Fill Pass (all fills in a tight unrolled loop)
if (fill_enabled) {
for (size_t i = 0; i < n; ++i) {
if (layer->is_active[i]) {
bl_context_fill_geometry_rgba32(ctx, 21 /* BL_GEOMETRY_TYPE_PATH */, &layer->paths[i], fill_rgba);
}
}
}
// Pass 2: Batch Stroke Pass (all strokes in a tight loop)
if (stroke_enabled && stroke_width > 0.0) {
bl_context_set_stroke_width(ctx, stroke_width);
for (size_t i = 0; i < n; ++i) {
if (layer->is_active[i]) {
bl_context_stroke_geometry_rgba32(ctx, 21 /* BL_GEOMETRY_TYPE_PATH */, &layer->paths[i], stroke_rgba);
}
}
}
return 0;
}
static inline double _geo_easing_back_out(double t) {
if (t <= 0.0) return 0.0;
if (t >= 1.0) return 1.0;
double c1 = 1.70158;
double c3 = c1 + 1.0;
double t_m_1 = t - 1.0;
return 1.0 + c3 * (t_m_1 * t_m_1 * t_m_1) + c1 * (t_m_1 * t_m_1);
}
BL_GEO_EXPORT int nm_geo_soa_render_popin(
void* ctx_ptr,
const NativeGeoLayerSoA* layer,
double current_frame,
uint32_t fill_rgba,
uint32_t stroke_rgba,
double stroke_width,
int fill_enabled,
int stroke_enabled
) {
if (!ctx_ptr || !layer || layer->count == 0) return -1;
BLContextCore* ctx = (BLContextCore*)ctx_ptr;
size_t n = layer->count;
bl_context_set_fill_rule(ctx, 1 /* EVEN_ODD */);
// Pass 1: Batch Fill Pass with Centroid Spring Pop-In
if (fill_enabled) {
for (size_t i = 0; i < n; ++i) {
if (!layer->is_active[i]) continue;
double sf = layer->start_frames[i];
double ef = layer->end_frames[i];
if (current_frame < sf) continue;
if (current_frame >= ef) {
bl_context_fill_geometry_rgba32(ctx, 21, &layer->paths[i], fill_rgba);
} else {
double t = (current_frame - sf) / (ef - sf);
double scale = _geo_easing_back_out(t);
if (scale > 0.001) {
double cx = layer->centroids[i * 2];
double cy = layer->centroids[i * 2 + 1];
BLMatrix2D m;
m.m00 = scale;
m.m01 = 0.0;
m.m10 = 0.0;
m.m11 = scale;
m.m20 = cx * (1.0 - scale);
m.m21 = cy * (1.0 - scale);
bl_context_apply_transform_op(ctx, 1 /* ASSIGN */, &m);
bl_context_fill_geometry_rgba32(ctx, 21, &layer->paths[i], fill_rgba);
bl_context_apply_transform_op(ctx, 0 /* RESET */, NULL);
}
}
}
}
// Pass 2: Batch Stroke Pass with Centroid Spring Pop-In
if (stroke_enabled && stroke_width > 0.0) {
bl_context_set_stroke_width(ctx, stroke_width);
for (size_t i = 0; i < n; ++i) {
if (!layer->is_active[i]) continue;
double sf = layer->start_frames[i];
double ef = layer->end_frames[i];
if (current_frame < sf) continue;
if (current_frame >= ef) {
bl_context_stroke_geometry_rgba32(ctx, 21, &layer->paths[i], stroke_rgba);
} else {
double t = (current_frame - sf) / (ef - sf);
double scale = _geo_easing_back_out(t);
if (scale > 0.001) {
double cx = layer->centroids[i * 2];
double cy = layer->centroids[i * 2 + 1];
BLMatrix2D m;
m.m00 = scale;
m.m01 = 0.0;
m.m10 = 0.0;
m.m11 = scale;
m.m20 = cx * (1.0 - scale);
m.m21 = cy * (1.0 - scale);
bl_context_apply_transform_op(ctx, 1 /* ASSIGN */, &m);
bl_context_stroke_geometry_rgba32(ctx, 21, &layer->paths[i], stroke_rgba);
bl_context_apply_transform_op(ctx, 0 /* RESET */, NULL);
}
}
}
}
return 0;
}
// ============================================================================
// Flat Uniform Grid Spatial Indexing
// ============================================================================
struct UniformGrid2D {
double minx, miny, maxx, maxy;
double cell_w, cell_h;
int32_t grid_w, grid_h;
int32_t* cell_counts; // [grid_w * grid_h]
int32_t* cell_offsets; // [grid_w * grid_h + 1]
int32_t* item_indices; // [total_references]
int32_t total_references;
};
BL_GEO_EXPORT UniformGrid2D* nm_geo_grid_create(
double minx, double miny, double maxx, double maxy,
int32_t grid_w, int32_t grid_h
) {
if (grid_w <= 0) grid_w = 32;
if (grid_h <= 0) grid_h = 32;
if (maxx <= minx) maxx = minx + 1.0;
if (maxy <= miny) maxy = miny + 1.0;
UniformGrid2D* grid = new UniformGrid2D();
grid->minx = minx;
grid->miny = miny;
grid->maxx = maxx;
grid->maxy = maxy;
grid->grid_w = grid_w;
grid->grid_h = grid_h;
grid->cell_w = (maxx - minx) / (double)grid_w;
grid->cell_h = (maxy - miny) / (double)grid_h;
size_t num_cells = (size_t)(grid_w * grid_h);
grid->cell_counts = (int32_t*)calloc(num_cells, sizeof(int32_t));
grid->cell_offsets = (int32_t*)calloc(num_cells + 1, sizeof(int32_t));
grid->item_indices = nullptr;
grid->total_references = 0;
return grid;
}
BL_GEO_EXPORT int nm_geo_grid_build(
UniformGrid2D* grid,
const BLBox2D* bboxes,
size_t count
) {
if (!grid || !bboxes || count == 0) return -1;
int32_t gw = grid->grid_w;
int32_t gh = grid->grid_h;
int32_t num_cells = gw * gh;
memset(grid->cell_counts, 0, num_cells * sizeof(int32_t));
auto to_cx = [grid, gw](double x) -> int32_t {
int32_t c = (int32_t)((x - grid->minx) / grid->cell_w);
if (c < 0) c = 0;
if (c >= gw) c = gw - 1;
return c;
};
auto to_cy = [grid, gh](double y) -> int32_t {
int32_t c = (int32_t)((y - grid->miny) / grid->cell_h);
if (c < 0) c = 0;
if (c >= gh) c = gh - 1;
return c;
};
int32_t total_refs = 0;
for (size_t i = 0; i < count; ++i) {
int32_t cx0 = to_cx(bboxes[i].x0);
int32_t cx1 = to_cx(bboxes[i].x1);
int32_t cy0 = to_cy(bboxes[i].y0);
int32_t cy1 = to_cy(bboxes[i].y1);
for (int32_t y = cy0; y <= cy1; ++y) {
for (int32_t x = cx0; x <= cx1; ++x) {
int32_t cell = y * gw + x;
grid->cell_counts[cell]++;
total_refs++;
}
}
}
grid->total_references = total_refs;
if (grid->item_indices) free(grid->item_indices);
grid->item_indices = (int32_t*)malloc(total_refs * sizeof(int32_t));
int32_t cur_offset = 0;
for (int32_t c = 0; c < num_cells; ++c) {
grid->cell_offsets[c] = cur_offset;
cur_offset += grid->cell_counts[c];
}
grid->cell_offsets[num_cells] = cur_offset;
memset(grid->cell_counts, 0, num_cells * sizeof(int32_t));
for (size_t i = 0; i < count; ++i) {
int32_t cx0 = to_cx(bboxes[i].x0);
int32_t cx1 = to_cx(bboxes[i].x1);
int32_t cy0 = to_cy(bboxes[i].y0);
int32_t cy1 = to_cy(bboxes[i].y1);
for (int32_t y = cy0; y <= cy1; ++y) {
for (int32_t x = cx0; x <= cx1; ++x) {
int32_t cell = y * gw + x;
int32_t write_pos = grid->cell_offsets[cell] + grid->cell_counts[cell];
grid->item_indices[write_pos] = (int32_t)i;
grid->cell_counts[cell]++;
}
}
}
return 0;
}
BL_GEO_EXPORT int nm_geo_grid_query_bbox(
const UniformGrid2D* grid,
double qx0, double qy0, double qx1, double qy1,
int32_t* out_indices,
int32_t max_indices,
int32_t* out_count
) {
if (!grid || !out_indices || max_indices <= 0 || !out_count) return -1;
int32_t gw = grid->grid_w;
int32_t gh = grid->grid_h;
auto to_cx = [grid, gw](double x) -> int32_t {
int32_t c = (int32_t)((x - grid->minx) / grid->cell_w);
if (c < 0) c = 0;
if (c >= gw) c = gw - 1;
return c;
};
auto to_cy = [grid, gh](double y) -> int32_t {
int32_t c = (int32_t)((y - grid->miny) / grid->cell_h);
if (c < 0) c = 0;
if (c >= gh) c = gh - 1;
return c;
};
int32_t cx0 = to_cx(qx0);
int32_t cx1 = to_cx(qx1);
int32_t cy0 = to_cy(qy0);
int32_t cy1 = to_cy(qy1);
int32_t found = 0;
int32_t max_item_id = -1;
for (int32_t y = cy0; y <= cy1; ++y) {
for (int32_t x = cx0; x <= cx1; ++x) {
int32_t cell = y * gw + x;
int32_t cnt = grid->cell_counts[cell];
int32_t off = grid->cell_offsets[cell];
for (int32_t k = 0; k < cnt; ++k) {
int32_t idx = grid->item_indices[off + k];
if (idx > max_item_id) max_item_id = idx;
}
}
}
if (max_item_id < 0) {
*out_count = 0;
return 0;
}
std::vector<bool> visited(max_item_id + 1, false);
for (int32_t y = cy0; y <= cy1 && found < max_indices; ++y) {
for (int32_t x = cx0; x <= cx1 && found < max_indices; ++x) {
int32_t cell = y * gw + x;
int32_t cnt = grid->cell_counts[cell];
int32_t off = grid->cell_offsets[cell];
for (int32_t k = 0; k < cnt && found < max_indices; ++k) {
int32_t idx = grid->item_indices[off + k];
if (!visited[idx]) {
visited[idx] = true;
out_indices[found++] = idx;
}
}
}
}
*out_count = found;
return 0;
}
BL_GEO_EXPORT void nm_geo_grid_destroy(UniformGrid2D* grid) {
if (!grid) return;
if (grid->cell_counts) free(grid->cell_counts);
if (grid->cell_offsets) free(grid->cell_offsets);
if (grid->item_indices) free(grid->item_indices);
delete grid;
}
} // extern "C"