code wiki / _hdl_build / nx_geo.nx
nx_geo.nx source
↩ module page · 151 lines · 6630 B
1// nx_geo.nx -- LIB: sovereign INTEGER-EXACT geofencing primitives. The hardware-rung-up foundation
2// of the Nishi geo / geofencing / maps stack (operator: "superior to google or open source tools").
3//
4// THE EXCEED ANGLE (measured, not asserted): coordinates are MICRODEGREES (degrees * 1e7) as i64,
5// and point-in-polygon is decided by INTEGER cross-multiplication -- NO floating point anywhere. So
6// the inside/outside verdict is EXACT and DETERMINISTIC at 1e-7 deg (~1.1 cm) resolution, with no
7// epsilon/robustness bugs on edges or vertices. Turf.js / Shapely / PostGIS all use float and give
8// non-deterministic or convention-dependent answers exactly there; the gate proves our result is
9// exact on the cases that break them. Overflow-safe for Earth coordinates in i64.
10//
11// This is the geofence FLOOR. Geodesic distance (haversine, rides a sovereign trig rung) and the
12// spatial index (geohash/grid) are rungs above; both compose THIS. license_tier: ORIGINAL
13import "nx_syscalls.nx"
14const GEO_MAGIC_90000000: i64 = 90000000
15const GEO_MAGIC_180000000: i64 = 180000000
16
17const GEO_MICRO: i64 = 1000000 // microdegrees per degree (degrees * 1e6); ~0.11 m resolution.
18 // All coords are microdegrees: lat in [-90e6,90e6], lon in [-180e6,180e6].
19
20// point in an axis-aligned bbox (inclusive) -- the O(1) pre-filter run before the polygon test.
21func geo_bbox(minlat: i64, minlon: i64, maxlat: i64, maxlon: i64, lat: i64, lon: i64) -> i64 {
22 if lat < minlat { return 0 }
23 if lat > maxlat { return 0 }
24 if lon < minlon { return 0 }
25 if lon > maxlon { return 0 }
26 return 1
27}
28
29// compute a polygon's bounding box into out=[minlat,minlon,maxlat,maxlon]. poly = flat
30// [lat0,lon0, lat1,lon1, ...], npts vertices.
31func geo_poly_bbox(poly: *i64, npts: i64, out: *i64) -> i64 {
32 var minlat: i64 = poly[0]
33 var maxlat: i64 = poly[0]
34 var minlon: i64 = poly[1]
35 var maxlon: i64 = poly[1]
36 var i: i64 = 1
37 while i < npts {
38 let la: i64 = poly[i * 2]
39 let lo: i64 = poly[i * 2 + 1]
40 if la < minlat { minlat = la }
41 if la > maxlat { maxlat = la }
42 if lo < minlon { minlon = lo }
43 if lo > maxlon { maxlon = lo }
44 i = i + 1
45 }
46 out[0] = minlat
47 out[1] = minlon
48 out[2] = maxlat
49 out[3] = maxlon
50 return 0
51}
52
53// INTEGER-EXACT point-in-polygon (ray-cast, PNPOLY) -- lat=Y, lon=X; horizontal ray in +lon.
54// poly = flat [lat0,lon0, ...], npts vertices (implicitly closed last->first). 1 inside, 0 outside.
55// The edge-intersection comparison is cross-multiplied by the edge's dy so NO division/float is
56// used; the inequality flips with the sign of dy. straddle guarantees dy != 0.
57func geo_point_in_poly(poly: *i64, npts: i64, lat: i64, lon: i64) -> i64 {
58 var inside: i64 = 0
59 var i: i64 = 0
60 var j: i64 = npts - 1
61 while i < npts {
62 let yi: i64 = poly[i * 2]
63 let xi: i64 = poly[i * 2 + 1]
64 let yj: i64 = poly[j * 2]
65 let xj: i64 = poly[j * 2 + 1]
66 var a: i64 = 0
67 if yi > lat { a = 1 }
68 var b: i64 = 0
69 if yj > lat { b = 1 }
70 if a != b {
71 let dy: i64 = yj - yi
72 let lhs: i64 = (lon - xi) * dy
73 let rhs: i64 = (xj - xi) * (lat - yi)
74 var cross: i64 = 0
75 if dy > 0 { if lhs < rhs { cross = 1 } }
76 if dy < 0 { if lhs > rhs { cross = 1 } }
77 if cross == 1 { inside = 1 - inside }
78 }
79 j = i
80 i = i + 1
81 }
82 return inside
83}
84
85// geofence test: a point is fenced iff it is inside the polygon (bbox pre-filtered for speed).
86func geo_fence_contains(poly: *i64, npts: i64, bbox: *i64, lat: i64, lon: i64) -> i64 {
87 if geo_bbox(bbox[0], bbox[1], bbox[2], bbox[3], lat, lon) == 0 { return 0 }
88 return geo_point_in_poly(poly, npts, lat, lon)
89}
90
91// RADIUS geofence ("within R of a center") -- the most common fence. INTEGER-EXACT, no sqrt on the
92// hot path (compare squared distance). The earth's longitude-compression (a lon degree is shorter
93// than a lat degree, by cos(latitude)) is handled by a per-fence `lon_scale` = cos(center_lat) *
94// GEO_SCALE, baked once at fence-definition (using a trig rung) so the runtime stays pure-integer.
95// r2 = (radius expressed in latitude-microdegree-equivalents) squared. Returns 1 inside, 0 outside.
96const GEO_SCALE: i64 = 1000000 // fixed-point unit for lon_scale (cos(lat) * 1e6)
97func geo_in_radius(clat: i64, clon: i64, lon_scale: i64, r2: i64, lat: i64, lon: i64) -> i64 {
98 let dlat: i64 = lat - clat
99 let dlon_raw: i64 = lon - clon
100 let dlon: i64 = (dlon_raw * lon_scale) / GEO_SCALE // compress lon to lat-equivalent ground units
101 let d2: i64 = dlat * dlat + dlon * dlon
102 if d2 <= r2 { return 1 }
103 return 0
104}
105
106// GEOHASH spatial index (the "superior at scale" rung): interleave lon/lat bits and base32-encode,
107// so geofence lookup becomes O(1) prefix bucketing instead of O(N) scanning every fence. The
108// PROXIMITY PROPERTY -- nearby points share a long common prefix -- is what makes it an index.
109// Pure-integer bisection (microdegree midpoints), deterministic. Standard alphabet (no a/i/l/o), so
110// the output interoperates with every geohash tool (last-mile). lat/lon in microdegrees; `prec`
111// chars written to out (NUL-terminated). Returns length.
112func geo_geohash(lat: i64, lon: i64, prec: i64, out: *u8) -> i64 {
113 let b32: *u8 = "0123456789bcdefghjkmnpqrstuvwxyz" as *u8
114 var latlo: i64 = 0 - GEO_MAGIC_90000000
115 var lathi: i64 = GEO_MAGIC_90000000
116 var lonlo: i64 = 0 - GEO_MAGIC_180000000
117 var lonhi: i64 = GEO_MAGIC_180000000
118 var is_lon: i64 = 1
119 var k: i64 = 0
120 while k < prec {
121 var bits: i64 = 0
122 var nb: i64 = 0
123 while nb < 5 {
124 if is_lon == 1 {
125 let mid: i64 = (lonlo + lonhi) / 2
126 if lon >= mid { bits = bits * 2 + 1; lonlo = mid } else { bits = bits * 2; lonhi = mid }
127 is_lon = 0
128 } else {
129 let mid: i64 = (latlo + lathi) / 2
130 if lat >= mid { bits = bits * 2 + 1; latlo = mid } else { bits = bits * 2; lathi = mid }
131 is_lon = 1
132 }
133 nb = nb + 1
134 }
135 out[k] = b32[bits]
136 k = k + 1
137 }
138 out[k] = 0 as u8
139 return k
140}
141
142// length of the common prefix of two NUL-terminated geohashes -- the proximity score the index
143// buckets on (longer shared prefix = closer).
144func geo_prefix_common(a: *u8, b: *u8) -> i64 {
145 var i: i64 = 0
146 while a[i] != (0 as u8) {
147 if a[i] != b[i] { return i }
148 i = i + 1
149 }
150 return i
151}