Skip to content

File syn_geo.c

File List > src > syntropic > util > syn_geo.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_GEO) || SYN_USE_GEO

#include "../util/syn_assert.h"
#include "syn_geo.h"

#include <math.h>
#include <stddef.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define DEG_TO_RAD (M_PI / 180.0)
#define RAD_TO_DEG (180.0 / M_PI)

/* ── WGS84 to ECEF ─────────────────────────────────────────────────────── */

SYN_Status syn_geo_wgs84_to_ecef(double lat_deg, double lon_deg, double alt_m, double *out_x,
                                 double *out_y, double *out_z)
{
    if (out_x == NULL || out_y == NULL || out_z == NULL) {
        return SYN_INVALID_PARAM;
    }

    double lat_rad = lat_deg * DEG_TO_RAD;
    double lon_rad = lon_deg * DEG_TO_RAD;

    double sin_lat = sin(lat_rad);
    double cos_lat = cos(lat_rad);
    double sin_lon = sin(lon_rad);
    double cos_lon = cos(lon_rad);

    /* Prime vertical radius of curvature N(phi) */
    double N = SYN_GEO_WGS84_A / sqrt(1.0 - SYN_GEO_WGS84_E2 * sin_lat * sin_lat);

    *out_x = (N + alt_m) * cos_lat * cos_lon;
    *out_y = (N + alt_m) * cos_lat * sin_lon;
    *out_z = (N * (1.0 - SYN_GEO_WGS84_E2) + alt_m) * sin_lat;

    return SYN_OK;
}

/* ── ECEF to WGS84 (Bowring's Closed-Form Algorithm) ───────────────────── */

SYN_Status syn_geo_ecef_to_wgs84(double x, double y, double z, double *out_lat, double *out_lon,
                                 double *out_alt)
{
    if (out_lat == NULL || out_lon == NULL || out_alt == NULL) {
        return SYN_INVALID_PARAM;
    }

    double a = SYN_GEO_WGS84_A;
    double e2 = SYN_GEO_WGS84_E2;
    double b = a * sqrt(1.0 - e2);
    double ep2 = (a * a - b * b) / (b * b);

    double p = sqrt(x * x + y * y);
    double theta = atan2(z * a, p * b);

    double sin_th = sin(theta);
    double cos_th = cos(theta);

    double lat_rad =
        atan2(z + ep2 * b * sin_th * sin_th * sin_th, p - e2 * a * cos_th * cos_th * cos_th);
    double lon_rad = atan2(y, x);

    double sin_lat = sin(lat_rad);
    double N = a / sqrt(1.0 - e2 * sin_lat * sin_lat);

    *out_lat = lat_rad * RAD_TO_DEG;
    *out_lon = lon_rad * RAD_TO_DEG;
    *out_alt = p / cos(lat_rad) - N;

    return SYN_OK;
}

/* ── ECEF to ENU ───────────────────────────────────────────────────────── */

SYN_Status syn_geo_ecef_to_enu(double x, double y, double z, double ref_lat_deg, double ref_lon_deg,
                               double ref_alt_m, SYN_ENU *out_enu)
{
    if (out_enu == NULL) {
        return SYN_INVALID_PARAM;
    }

    double ref_x, ref_y, ref_z;
    SYN_Status st =
        syn_geo_wgs84_to_ecef(ref_lat_deg, ref_lon_deg, ref_alt_m, &ref_x, &ref_y, &ref_z);
    if (st != SYN_OK)
        return st;

    double dx = x - ref_x;
    double dy = y - ref_y;
    double dz = z - ref_z;

    double ref_lat_rad = ref_lat_deg * DEG_TO_RAD;
    double ref_lon_rad = ref_lon_deg * DEG_TO_RAD;

    double sin_lat = sin(ref_lat_rad);
    double cos_lat = cos(ref_lat_rad);
    double sin_lon = sin(ref_lon_rad);
    double cos_lon = cos(ref_lon_rad);

    /* Rotation matrix R_ENU */
    out_enu->east_m = -sin_lon * dx + cos_lon * dy;
    out_enu->north_m = -sin_lat * cos_lon * dx - sin_lat * sin_lon * dy + cos_lat * dz;
    out_enu->up_m = cos_lat * cos_lon * dx + cos_lat * sin_lon * dy + sin_lat * dz;

    return SYN_OK;
}

/* ── Direct WGS84 to ENU ───────────────────────────────────────────────── */

SYN_Status syn_geo_wgs84_to_enu(double lat_deg, double lon_deg, double alt_m, double ref_lat_deg,
                                double ref_lon_deg, double ref_alt_m, SYN_ENU *out_enu)
{
    if (out_enu == NULL) {
        return SYN_INVALID_PARAM;
    }

    double x, y, z;
    SYN_Status st = syn_geo_wgs84_to_ecef(lat_deg, lon_deg, alt_m, &x, &y, &z);
    if (st != SYN_OK)
        return st;

    return syn_geo_ecef_to_enu(x, y, z, ref_lat_deg, ref_lon_deg, ref_alt_m, out_enu);
}

/* ── Haversine 2D Distance ─────────────────────────────────────────────── */

double syn_geo_haversine_m(double lat1_deg, double lon1_deg, double lat2_deg, double lon2_deg)
{
    double dlat = (lat2_deg - lat1_deg) * DEG_TO_RAD;
    double dlon = (lon2_deg - lon1_deg) * DEG_TO_RAD;

    double lat1_rad = lat1_deg * DEG_TO_RAD;
    double lat2_rad = lat2_deg * DEG_TO_RAD;

    double a = sin(dlat / 2.0) * sin(dlat / 2.0) +
               sin(dlon / 2.0) * sin(dlon / 2.0) * cos(lat1_rad) * cos(lat2_rad);
    double c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));

    return SYN_GEO_WGS84_A * c;
}

/* ── 3D Euclidean Distance ─────────────────────────────────────────────── */

double syn_geo_3d_distance_m(const SYN_ENU *p1, const SYN_ENU *p2)
{
    if (p1 == NULL || p2 == NULL)
        return 0.0;

    double de = p2->east_m - p1->east_m;
    double dn = p2->north_m - p1->north_m;
    double du = p2->up_m - p1->up_m;

    return sqrt(de * de + dn * dn + du * du);
}

/* ── NMEA GGA Bridge ───────────────────────────────────────────────────── */

SYN_Status syn_geo_pos_from_gga(const SYN_NMEA_GGA *gga, SYN_GeoPos *out_pos)
{
    if (gga == NULL || out_pos == NULL) {
        return SYN_INVALID_PARAM;
    }

    out_pos->latitude = gga->latitude;
    out_pos->longitude = gga->longitude;
    out_pos->altitude_m = (double)gga->altitude_m;
    out_pos->fix_type = gga->fix_quality;
    out_pos->valid = gga->valid;

    switch (gga->fix_quality) {
    case SYN_NMEA_FIX_RTK:
        out_pos->accuracy_m = 0.01f; /* 1 cm RTK Fixed accuracy */
        break;
    case SYN_NMEA_FIX_FLOAT_RTK:
        out_pos->accuracy_m = 0.20f; /* 20 cm RTK Float accuracy */
        break;
    case SYN_NMEA_FIX_DGPS:
        out_pos->accuracy_m = 1.00f; /* 1 meter DGPS accuracy */
        break;
    case SYN_NMEA_FIX_GPS:
    case SYN_NMEA_FIX_PPS:
        out_pos->accuracy_m = 2.50f; /* Standard GPS accuracy */
        break;
    default:
        out_pos->accuracy_m = 50.0f; /* Unverified / estimated accuracy */
        break;
    }

    return SYN_OK;
}

#endif /* SYN_USE_GEO */