geodetic 0.8.0 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +68 -3
- data/docs/coordinate-systems/index.md +24 -22
- data/docs/coordinate-systems/s2.md +451 -0
- data/examples/15_s2_geometry.rb +429 -0
- data/lib/geodetic/coordinate/h3.rb +8 -52
- data/lib/geodetic/coordinate/lla.rb +8 -0
- data/lib/geodetic/coordinate/s2.rb +489 -0
- data/lib/geodetic/coordinate.rb +1 -0
- data/lib/geodetic/geos.rb +10 -59
- data/lib/geodetic/native_library.rb +125 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +1 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e747e334297726a887a07b078e0419bc232dd00d90823007bc8fcc96bb8b6fce
|
|
4
|
+
data.tar.gz: 87f53111a3ca2f2d5e5e126f780c1e756f98c415d1a10c66bad8595fc9049940
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c384201adbfeb713cd67d81f3dda4642d0e9efab8b3026cf6fe88f6de869728890ca60e5f9591c12c9d6b034d9c5d9f74aae77a796a327674c129f806466b1d2
|
|
7
|
+
data.tar.gz: 1e8b73525875288901086e755ce87cfe3f2d257ddfe234f55d6eca0b680b0841c8205e05d77f91550ca978618d79c8bf3ad9ce41740b87c92585ea306726ea36
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<td width="50%" valign="top">
|
|
14
14
|
<strong>Key Features</strong><br>
|
|
15
15
|
|
|
16
|
-
- <strong>
|
|
16
|
+
- <strong>19 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC, GEOREF, GARS, H3, S2<br>
|
|
17
17
|
- <strong>Full Bidirectional Conversions</strong> - Every system converts to and from every other system<br>
|
|
18
18
|
- <strong>Distance Calculations</strong> - Vincenty great-circle and straight-line with unit tracking<br>
|
|
19
19
|
- <strong>Bearing Calculations</strong> - Forward azimuth, back azimuth, compass directions, elevation angles<br>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
</tr>
|
|
37
37
|
</table>
|
|
38
38
|
|
|
39
|
-
<p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All
|
|
39
|
+
<p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All 19 coordinate systems support complete bidirectional conversions with high precision. Review the <a href="https://madbomber.github.io/geodetic/">full documentation website</a> and explore the <a href="examples/">runnable examples</a>.</p>
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
@@ -86,6 +86,24 @@ Set `GEODETIC_GEOS_DISABLE=1` to force pure Ruby for all operations, even when G
|
|
|
86
86
|
Geodetic::Geos.available? # => true when libgeos_c is found and not disabled
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Optional: S2 Spherical Geometry Index
|
|
90
|
+
|
|
91
|
+
The S2 coordinate system requires Google's [S2 Geometry library](https://github.com/google/s2geometry) installed on your system. S2 projects a cube onto the sphere and subdivides it into quadrilateral cells via a Hilbert space-filling curve, providing very low distortion (~0.56%) across the entire Earth surface. Cell IDs are 64-bit integers that enable efficient spatial database queries on standard B-tree indexes.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# macOS
|
|
95
|
+
brew install s2geometry
|
|
96
|
+
|
|
97
|
+
# Linux (build from source)
|
|
98
|
+
# See https://github.com/google/s2geometry
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
You can set the `LIBS2_PATH` environment variable to point to a custom `libs2` location. See [S2 documentation](docs/coordinate-systems/s2.md) for detailed API reference and [example 15](examples/15_s2_geometry.rb) for a comprehensive demo.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Geodetic::Coordinate::S2.available? # => true when libs2 is found
|
|
105
|
+
```
|
|
106
|
+
|
|
89
107
|
## Usage
|
|
90
108
|
|
|
91
109
|
### Basic Coordinate Creation
|
|
@@ -132,7 +150,7 @@ Geodetic::Coordinate.systems
|
|
|
132
150
|
# Get short names
|
|
133
151
|
Geodetic::Coordinate.systems.map { |c| c.name.split('::').last }
|
|
134
152
|
# => ["LLA", "ECEF", "UTM", "ENU", "NED", "MGRS", "USNG", "WebMercator",
|
|
135
|
-
# "UPS", "StatePlane", "BNG", "GH36", "GH", "HAM", "OLC", "GEOREF", "GARS", "H3"]
|
|
153
|
+
# "UPS", "StatePlane", "BNG", "GH36", "GH", "HAM", "OLC", "GEOREF", "GARS", "H3", "S2"]
|
|
136
154
|
```
|
|
137
155
|
|
|
138
156
|
### Coordinate Conversions
|
|
@@ -546,6 +564,51 @@ olc.precision # => 10
|
|
|
546
564
|
olc.precision_in_meters # => { lat: 13.9, lng: 13.9 }
|
|
547
565
|
```
|
|
548
566
|
|
|
567
|
+
### S2 Spherical Geometry Index
|
|
568
|
+
|
|
569
|
+
Google's hierarchical spatial index using a cube-on-sphere projection with Hilbert curve ordering. Cell IDs are 64-bit integers displayed as hex tokens. See [S2 documentation](docs/coordinate-systems/s2.md) for the full API.
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
# From a token string or integer
|
|
573
|
+
s2 = Coordinate::S2.new("54906ab14")
|
|
574
|
+
s2 = Coordinate::S2.new(6093487605347778560)
|
|
575
|
+
|
|
576
|
+
# From any coordinate
|
|
577
|
+
s2 = Coordinate::S2.new(lla)
|
|
578
|
+
s2 = lla.to_s2(precision: 20) # level 20
|
|
579
|
+
|
|
580
|
+
# Decode back to LLA
|
|
581
|
+
lla = s2.to_lla
|
|
582
|
+
|
|
583
|
+
# Properties
|
|
584
|
+
s2.level # => 15
|
|
585
|
+
s2.face # => 2 (cube face 0-5)
|
|
586
|
+
s2.cell_id # => 6093487605347778560 (for database storage)
|
|
587
|
+
s2.to_s # => "54906ab14" (token for display)
|
|
588
|
+
|
|
589
|
+
# Hierarchy
|
|
590
|
+
s2.parent(10) # => S2 at level 10
|
|
591
|
+
s2.children # => [S2, S2, S2, S2] at level 16
|
|
592
|
+
|
|
593
|
+
# Neighbors (4 edge-adjacent cells)
|
|
594
|
+
s2.neighbors # => [S2, S2, S2, S2]
|
|
595
|
+
|
|
596
|
+
# Containment
|
|
597
|
+
parent.contains?(child) # => true
|
|
598
|
+
parent.intersects?(child) # => true
|
|
599
|
+
|
|
600
|
+
# Cell area (exact spherical surface area)
|
|
601
|
+
s2.cell_area # => 77544.2 (square meters)
|
|
602
|
+
|
|
603
|
+
# Cell boundary as polygon
|
|
604
|
+
polygon = s2.to_area # => Areas::Polygon with 4 vertices
|
|
605
|
+
polygon.includes?(s2.to_lla) # => true
|
|
606
|
+
|
|
607
|
+
# Database range scans (spatial queries on B-tree indexes)
|
|
608
|
+
s2.range_min # => start of cell ID range
|
|
609
|
+
s2.range_max # => end of cell ID range
|
|
610
|
+
```
|
|
611
|
+
|
|
549
612
|
### Geographic Areas
|
|
550
613
|
|
|
551
614
|
```ruby
|
|
@@ -896,6 +959,8 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
|
|
|
896
959
|
| [`11_wkb_serialization.rb`](examples/11_wkb_serialization.rb) | WKB serialization: `to_wkb`/`to_wkb_hex` on all geometry types, EWKB/SRID, Z-dimension, parsing, roundtrip, and binary/hex file I/O |
|
|
897
960
|
| [`12_geos_benchmark.rb`](examples/12_geos_benchmark.rb) | GEOS performance benchmark: polygon validation, point-in-polygon, path intersection, PreparedGeometry batch containment, and GEOS-only boolean operations |
|
|
898
961
|
| [`13_geos_operations.rb`](examples/13_geos_operations.rb) | GEOS-only operations: boolean overlay (intersection, difference, symmetric difference, union), buffering, convex hull, simplification, validity checking, geometry repair, planar measurements, nearest points, PreparedGeometry, and operation chaining |
|
|
962
|
+
| [`14_geos_map_rendering.rb`](examples/14_geos_map_rendering.rb) | GEOS map rendering: visualizes 8 GEOS operation categories on a single raster map with distinct colors and an embedded legend |
|
|
963
|
+
| [`15_s2_geometry.rb`](examples/15_s2_geometry.rb) | S2 Geometry: construction, round-trip conversion, cell hierarchy (parent/children), edge neighbors, containment/intersection, cell area calculations, cell polygons, database range scans, cross-hash conversions, distance/bearing, arithmetic, serialization (WKT/WKB/GeoJSON), six cube faces, and performance benchmarks |
|
|
899
964
|
|
|
900
965
|
Run any example with:
|
|
901
966
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Coordinate Systems Overview
|
|
2
2
|
|
|
3
|
-
The Geodetic gem supports
|
|
3
|
+
The Geodetic gem supports 19 coordinate systems organized into six categories. All coordinate classes live under `Geodetic::Coordinate`.
|
|
4
4
|
|
|
5
5
|
## Global Systems
|
|
6
6
|
|
|
@@ -47,6 +47,7 @@ The Geodetic gem supports 18 coordinate systems organized into six categories. A
|
|
|
47
47
|
| [**GEOREF**](georef.md) | `Geodetic::Coordinate::GEOREF` | World Geographic Reference System. A geocode system used in aviation and military applications that encodes positions using letter tiles (15° grid), letter degree subdivisions, and numeric minute pairs. Uses a 24-letter alphabet (A-Z excluding I and O). Supports variable precision from 15° tiles (2 chars) down to 0.01-minute resolution (12 chars). Default precision is 8 characters (1-minute resolution). |
|
|
48
48
|
| [**GARS**](gars.md) | `Geodetic::Coordinate::GARS` | Global Area Reference System. An NGA standard that divides the world into 30-minute cells identified by a 3-digit longitude band (001-720) and 2-letter latitude band. Cells are subdivided into 15-minute quadrants (1-4) and 5-minute keypads (1-9, telephone layout). Variable precision: 5 chars (30'), 6 chars (15'), 7 chars (5'). Default precision is 7 characters. |
|
|
49
49
|
| [**H3**](h3.md) | `Geodetic::Coordinate::H3` | H3 Hexagonal Hierarchical Index. Uber's spatial indexing system that divides the globe into hexagonal cells (and 12 pentagons) at 16 resolution levels (0-15). Each cell is a 64-bit integer displayed as a hex string. Unlike the rectangular spatial hashes, `to_area` returns an `Areas::Polygon` with 6 vertices (5 for pentagons) and `neighbors` returns an Array of 6 cells. **Requires `libh3` installed** (`brew install h3` on macOS). |
|
|
50
|
+
| [**S2**](s2.md) | `Geodetic::Coordinate::S2` | S2 Spherical Geometry Index. Google's hierarchical spatial indexing system that projects a cube onto the unit sphere, subdividing into quadrilateral cells via a Hilbert space-filling curve. 31 levels (0-30) with 64-bit cell IDs displayed as hex tokens. Cells are geodesic quadrilaterals with very low distortion (~0.56%). `to_area` returns an `Areas::Polygon` with 4 vertices. `neighbors` returns an Array of 4 edge cells. Cell IDs enable efficient database range scans on standard B-tree indexes. **Requires `libs2` installed** (`brew install s2geometry` on macOS). |
|
|
50
51
|
|
|
51
52
|
## Regional Systems
|
|
52
53
|
|
|
@@ -61,26 +62,27 @@ The Geodetic gem supports 18 coordinate systems organized into six categories. A
|
|
|
61
62
|
|
|
62
63
|
Every coordinate system can convert to every other coordinate system. The table below confirms full interoperability:
|
|
63
64
|
|
|
64
|
-
| From \ To | LLA | ECEF | UTM | ENU | NED | MGRS | USNG | WebMercator | UPS | StatePlane | BNG | GH36 | GH | HAM | OLC | GEOREF | GARS | H3 |
|
|
65
|
-
|
|
66
|
-
| **LLA** | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
67
|
-
| **ECEF** | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
68
|
-
| **UTM** | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
69
|
-
| **ENU** | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
70
|
-
| **NED** | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
71
|
-
| **MGRS** | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
72
|
-
| **USNG** | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
73
|
-
| **WebMercator**| Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
74
|
-
| **UPS** | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
75
|
-
| **StatePlane** | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
76
|
-
| **BNG** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y |
|
|
77
|
-
| **GH36** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y |
|
|
78
|
-
| **GH** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y |
|
|
79
|
-
| **HAM** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y |
|
|
80
|
-
| **OLC** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y |
|
|
81
|
-
| **GEOREF** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y |
|
|
82
|
-
| **GARS** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y |
|
|
83
|
-
| **H3** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- |
|
|
65
|
+
| From \ To | LLA | ECEF | UTM | ENU | NED | MGRS | USNG | WebMercator | UPS | StatePlane | BNG | GH36 | GH | HAM | OLC | GEOREF | GARS | H3 | S2 |
|
|
66
|
+
|-----------|-----|------|-----|-----|-----|------|------|-------------|-----|------------|-----|------|----|----|-----|--------|------|-----|-----|
|
|
67
|
+
| **LLA** | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
68
|
+
| **ECEF** | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
69
|
+
| **UTM** | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
70
|
+
| **ENU** | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
71
|
+
| **NED** | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
72
|
+
| **MGRS** | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
73
|
+
| **USNG** | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
74
|
+
| **WebMercator**| Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
75
|
+
| **UPS** | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
76
|
+
| **StatePlane** | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
77
|
+
| **BNG** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
78
|
+
| **GH36** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y |
|
|
79
|
+
| **GH** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y |
|
|
80
|
+
| **HAM** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y |
|
|
81
|
+
| **OLC** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y |
|
|
82
|
+
| **GEOREF** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y |
|
|
83
|
+
| **GARS** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y |
|
|
84
|
+
| **H3** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y |
|
|
85
|
+
| **S2** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- |
|
|
84
86
|
|
|
85
87
|
## Universal Distance and Bearing Calculations
|
|
86
88
|
|
|
@@ -100,6 +102,6 @@ Conversions typically route through **LLA** or **ECEF** as intermediate steps:
|
|
|
100
102
|
- **ECEF** is the intermediate for local tangent plane systems (ENU, NED), since the rotation from global Cartesian to local frames is straightforward in ECEF.
|
|
101
103
|
- **ENU and NED** convert between each other directly by reordering axes and inverting the vertical component.
|
|
102
104
|
- **MGRS and USNG** route through UTM, which in turn routes through LLA.
|
|
103
|
-
- **WebMercator, UPS, BNG, StatePlane, GH36, GH, HAM, OLC, GEOREF, GARS, and
|
|
105
|
+
- **WebMercator, UPS, BNG, StatePlane, GH36, GH, HAM, OLC, GEOREF, GARS, H3, and S2** all convert through LLA.
|
|
104
106
|
|
|
105
107
|
For example, converting from BNG to NED follows the chain: `BNG -> LLA -> ECEF -> ENU -> NED`. The gem handles this automatically when you call a conversion method.
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# Geodetic::Coordinate::S2
|
|
2
|
+
|
|
3
|
+
## S2 Spherical Geometry Index
|
|
4
|
+
|
|
5
|
+
S2 is Google's hierarchical geospatial indexing system. It projects a cube onto the unit sphere, then recursively subdivides each of the six cube faces into quadrilateral cells using a Hilbert space-filling curve. Each cell is identified by a 64-bit integer called a **cell ID**, typically displayed as a **token** — a hex string with trailing zeros stripped.
|
|
6
|
+
|
|
7
|
+
The name comes from the mathematical notation S² for the unit sphere.
|
|
8
|
+
|
|
9
|
+
**S2 requires the `libs2` C++ library** installed on your system. Without it, all other coordinate systems work normally; S2 operations raise a clear error message with installation instructions.
|
|
10
|
+
|
|
11
|
+
### Prerequisites
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# macOS (Homebrew)
|
|
15
|
+
brew install s2geometry
|
|
16
|
+
|
|
17
|
+
# Linux (build from source)
|
|
18
|
+
git clone https://github.com/google/s2geometry.git
|
|
19
|
+
cd s2geometry
|
|
20
|
+
cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local
|
|
21
|
+
cmake --build build
|
|
22
|
+
sudo cmake --install build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You can also set the `LIBS2_PATH` environment variable to specify a custom library path:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export LIBS2_PATH=/path/to/libs2.dylib
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Key Differences from Other Spatial Hashes
|
|
32
|
+
|
|
33
|
+
| Feature | GH/OLC/GARS/GEOREF/HAM | H3 | S2 |
|
|
34
|
+
|---------|------------------------|-----|-----|
|
|
35
|
+
| Cell shape | Rectangle | Hexagon (6 vertices) | Quadrilateral (4 vertices) |
|
|
36
|
+
| `to_area` returns | `Areas::BoundingBox` | `Areas::Polygon` (6 verts) | `Areas::Polygon` (4 verts) |
|
|
37
|
+
| `neighbors` returns | Hash with 8 cardinal keys | Array of 6 cells | Array of 4 edge cells |
|
|
38
|
+
| Code format | String | 64-bit integer (hex) | 64-bit integer (hex token) |
|
|
39
|
+
| Hierarchy | String prefix | Parent/child by resolution | Parent/child by level (quadtree) |
|
|
40
|
+
| Levels | String length | 0-15 | 0-30 |
|
|
41
|
+
| Projection | Rectangular lat/lng | Icosahedron | Cube-on-sphere |
|
|
42
|
+
| Distortion | High at poles | Low (~1.2x) | Very low (~0.56%) |
|
|
43
|
+
| Spatial ordering | Z-order (GH) / none | None guaranteed | Hilbert curve (locality) |
|
|
44
|
+
| Dependency | None (pure Ruby) | `libh3` (C) | `libs2` (C++) |
|
|
45
|
+
|
|
46
|
+
S2 is a **2D coordinate system** (no altitude). Conversions to/from other systems go through LLA as the intermediary. Each token represents a quadrilateral cell; the coordinate's point value is the cell's centroid.
|
|
47
|
+
|
|
48
|
+
### Cell ID vs Token
|
|
49
|
+
|
|
50
|
+
A cell ID and a token represent the same cell in two formats:
|
|
51
|
+
|
|
52
|
+
- **cell_id** — the raw 64-bit unsigned integer. Use this for database storage and range queries. Standard B-tree indexes on a `BIGINT` column give you spatial queries for free.
|
|
53
|
+
|
|
54
|
+
- **token** — the cell_id printed as hex with trailing zeros stripped. Use this for display, logging, and human-readable interchange. Shorter tokens represent coarser (larger) cells.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
cell = S2.new(seattle, precision: 15)
|
|
58
|
+
cell.cell_id # => 6093487605347778560
|
|
59
|
+
cell.to_s # => "54906ab14"
|
|
60
|
+
|
|
61
|
+
# Equivalence: token is just compact hex of cell_id
|
|
62
|
+
"54906ab14".ljust(16, '0').to_i(16) # => 6093487605347778560
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Tokens naturally reveal the hierarchy — a parent's token is always a prefix of its descendants:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Level 0: "5" (face 2)
|
|
69
|
+
Level 5: "5494"
|
|
70
|
+
Level 10: "54906b"
|
|
71
|
+
Level 15: "54906ab14"
|
|
72
|
+
Level 20: "54906ab12f1"
|
|
73
|
+
Level 30: "54906ab12f10f899"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For **database applications**, store the cell_id as a `BIGINT`. Spatial containment queries become range scans:
|
|
77
|
+
|
|
78
|
+
```sql
|
|
79
|
+
-- Find all points inside a level-12 cell
|
|
80
|
+
SELECT * FROM locations
|
|
81
|
+
WHERE s2_cell_id BETWEEN :range_min AND :range_max
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The Hilbert curve ordering guarantees that spatially nearby cells have numerically nearby cell_ids, so these range scans are efficient on standard B-tree indexes — no spatial index extension required.
|
|
85
|
+
|
|
86
|
+
## Constructor
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# From a token string
|
|
90
|
+
coord = Geodetic::Coordinate::S2.new("54906ab14")
|
|
91
|
+
|
|
92
|
+
# From a token with 0x prefix
|
|
93
|
+
coord = Geodetic::Coordinate::S2.new("0x54906ab14")
|
|
94
|
+
|
|
95
|
+
# From a 64-bit cell ID integer
|
|
96
|
+
coord = Geodetic::Coordinate::S2.new(6093487605347778560)
|
|
97
|
+
|
|
98
|
+
# From any coordinate (converts via LLA)
|
|
99
|
+
coord = Geodetic::Coordinate::S2.new(lla_coord)
|
|
100
|
+
coord = Geodetic::Coordinate::S2.new(utm_coord, precision: 20)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Parameter | Type | Default | Description |
|
|
104
|
+
|-------------|-------------------------|---------|------------------------------------------|
|
|
105
|
+
| `source` | String, Integer, or Coord | -- | An S2 token, cell ID integer, or coordinate |
|
|
106
|
+
| `precision` | Integer | 15 | S2 cell level (0-30) |
|
|
107
|
+
|
|
108
|
+
Raises `ArgumentError` if the source string is empty, contains invalid hex characters, or does not produce a valid S2 cell ID. String input is case-insensitive (normalized to lowercase). The `0x` prefix is stripped automatically.
|
|
109
|
+
|
|
110
|
+
## Attributes
|
|
111
|
+
|
|
112
|
+
| Attribute | Type | Access | Description |
|
|
113
|
+
|-----------|---------|-----------|-----------------------------------|
|
|
114
|
+
| `code` | String | read-only | The token string representation |
|
|
115
|
+
| `cell_id` | Integer | read-only | The 64-bit S2 cell ID |
|
|
116
|
+
|
|
117
|
+
S2 is **immutable** — there are no setter methods.
|
|
118
|
+
|
|
119
|
+
## Levels
|
|
120
|
+
|
|
121
|
+
S2 uses "level" (0-30) for its hierarchy. Higher level means smaller cells. Each level subdivides cells by 4 (quadtree), so area decreases by roughly 4x per level.
|
|
122
|
+
|
|
123
|
+
| Level | Approximate Cell Area | Approximate Edge Length |
|
|
124
|
+
|-------|----------------------|------------------------|
|
|
125
|
+
| 0 | 85,201,316 km² | ~9,200 km (face cell) |
|
|
126
|
+
| 1 | 21,300,329 km² | ~4,600 km |
|
|
127
|
+
| 5 | 79,002 km² | ~281 km |
|
|
128
|
+
| 10 | 79.4 km² | ~8.9 km |
|
|
129
|
+
| 12 | 5.0 km² | ~2.2 km |
|
|
130
|
+
| 15 | 77,544 m² (default) | ~278 m |
|
|
131
|
+
| 18 | 1,212 m² | ~35 m |
|
|
132
|
+
| 20 | 75.7 m² | ~8.7 m |
|
|
133
|
+
| 24 | 0.30 m² (2,958 cm²) | ~54 cm |
|
|
134
|
+
| 28 | 11.55 cm² | ~3.4 cm |
|
|
135
|
+
| 30 | 0.72 cm² | ~0.85 cm |
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
coord.level # => 15 (alias: coord.precision)
|
|
139
|
+
coord.cell_area # => 77544.2 (square meters)
|
|
140
|
+
coord.precision_in_meters # => { lat: ~278, lng: ~278, area_m2: ~77544 }
|
|
141
|
+
S2.average_cell_area(15) # => 79349.9 (average across all level-15 cells)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## The Six Cube Faces
|
|
145
|
+
|
|
146
|
+
S2 projects a cube onto the sphere, giving 6 face cells at level 0:
|
|
147
|
+
|
|
148
|
+
| Face | Direction | Center | Token |
|
|
149
|
+
|------|-------------|----------------|-------|
|
|
150
|
+
| 0 | +X (front) | (0°, 0°) | `1` |
|
|
151
|
+
| 1 | +Y (right) | (0°, 90°E) | `3` |
|
|
152
|
+
| 2 | +Z (top) | (90°N, 0°) | `5` |
|
|
153
|
+
| 3 | -X (back) | (0°, 180°) | `7` |
|
|
154
|
+
| 4 | -Y (left) | (0°, 90°W) | `9` |
|
|
155
|
+
| 5 | -Z (bottom) | (90°S, 0°) | `b` |
|
|
156
|
+
|
|
157
|
+
The cube-on-sphere projection limits distortion to approximately 0.56% across the entire Earth surface — far less than rectangular projections which distort heavily near the poles.
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
coord.face # => 2 (Seattle is on face 2, the +Z/north pole face)
|
|
161
|
+
coord.face_cell? # => false (only level-0 cells are face cells)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Checking Availability
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
Geodetic::Coordinate::S2.available? # => true if libs2 is found
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Conversions
|
|
171
|
+
|
|
172
|
+
All conversions chain through LLA. The datum parameter defaults to `Geodetic::WGS84`.
|
|
173
|
+
|
|
174
|
+
### Instance Methods
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
coord.to_lla # => LLA (centroid of the cell)
|
|
178
|
+
coord.to_ecef
|
|
179
|
+
coord.to_utm
|
|
180
|
+
coord.to_enu(reference_lla)
|
|
181
|
+
coord.to_ned(reference_lla)
|
|
182
|
+
coord.to_mgrs
|
|
183
|
+
coord.to_usng
|
|
184
|
+
coord.to_web_mercator
|
|
185
|
+
coord.to_ups
|
|
186
|
+
coord.to_state_plane(zone_code)
|
|
187
|
+
coord.to_bng
|
|
188
|
+
coord.to_gh36
|
|
189
|
+
coord.to_gh
|
|
190
|
+
coord.to_ham
|
|
191
|
+
coord.to_olc
|
|
192
|
+
coord.to_georef
|
|
193
|
+
coord.to_gars
|
|
194
|
+
coord.to_h3 # (requires libh3)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Class Methods
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
S2.from_lla(lla_coord)
|
|
201
|
+
S2.from_ecef(ecef_coord)
|
|
202
|
+
S2.from_utm(utm_coord)
|
|
203
|
+
S2.from_web_mercator(wm_coord)
|
|
204
|
+
S2.from_gh(gh_coord)
|
|
205
|
+
S2.from_h3(h3_coord)
|
|
206
|
+
# ... and all other coordinate systems
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### LLA Convenience Methods
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
lla = Geodetic::Coordinate::LLA.new(lat: 47.6062, lng: -122.3321)
|
|
213
|
+
s2 = lla.to_s2 # default level 15
|
|
214
|
+
s2 = lla.to_s2(precision: 20) # level 20
|
|
215
|
+
|
|
216
|
+
lla = Geodetic::Coordinate::LLA.from_s2(s2)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Serialization
|
|
220
|
+
|
|
221
|
+
### `to_s(format = nil)`
|
|
222
|
+
|
|
223
|
+
Returns the token string. Pass `:integer` to get the 64-bit cell ID.
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
coord = S2.new("54906ab14")
|
|
227
|
+
coord.to_s # => "54906ab14"
|
|
228
|
+
coord.to_s(:integer) # => 6093487605347778560
|
|
229
|
+
coord.cell_id # => 6093487605347778560
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `to_a`
|
|
233
|
+
|
|
234
|
+
Returns `[lat, lng]` of the cell centroid.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
coord.to_a # => [47.605..., -122.334...]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `from_string` / `from_array`
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
S2.from_string("54906ab14") # from token
|
|
244
|
+
S2.from_array([47.6062, -122.3321]) # from [lat, lng]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### WKT, WKB, GeoJSON
|
|
248
|
+
|
|
249
|
+
S2 coordinates support all standard serialization formats:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
coord.to_wkt # => "POINT(-122.334371 47.605024)"
|
|
253
|
+
coord.to_wkt(srid: 4326) # => "SRID=4326;POINT(-122.334371 47.605024)"
|
|
254
|
+
coord.to_wkb_hex # => "010100000014dbfb5466955ec0..."
|
|
255
|
+
coord.to_geojson # => {"type" => "Point", "coordinates" => [...]}
|
|
256
|
+
|
|
257
|
+
# Cell polygon serialization
|
|
258
|
+
coord.to_area.to_wkt # => "POLYGON((-122.33392 47.606536, ...))"
|
|
259
|
+
coord.to_area.to_geojson # => {"type" => "Polygon", ...}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Cell Hierarchy
|
|
263
|
+
|
|
264
|
+
S2 cells form a strict quadtree: each cell has exactly 4 children and 1 parent (except level-0 face cells which have no parent).
|
|
265
|
+
|
|
266
|
+
### Parent
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
coord = S2.new("54906ab14") # level 15
|
|
270
|
+
parent = coord.parent # level 14 (one level up)
|
|
271
|
+
parent = coord.parent(10) # level 10 (5 levels up)
|
|
272
|
+
parent = coord.parent(0) # level 0 (face cell)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Raises `ArgumentError` if the target level is not coarser (lower number) than the current level.
|
|
276
|
+
|
|
277
|
+
### Children
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
coord = S2.new("54906ab14") # level 15
|
|
281
|
+
children = coord.children # => [S2, S2, S2, S2] at level 16
|
|
282
|
+
children.length # => 4
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Raises `ArgumentError` on leaf cells (level 30).
|
|
286
|
+
|
|
287
|
+
### Ancestry Traversal
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
leaf = S2.new(seattle, precision: 30)
|
|
291
|
+
[30, 20, 10, 0].each do |lvl|
|
|
292
|
+
ancestor = lvl == 30 ? leaf : leaf.parent(lvl)
|
|
293
|
+
puts "Level #{lvl}: #{ancestor.to_s}"
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Neighbors
|
|
298
|
+
|
|
299
|
+
Returns the 4 edge-adjacent cells as an Array. Unlike rectangular hashes (which return 8 directional neighbors), S2 cells are quadrilaterals with exactly 4 edges.
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
coord = S2.new("54906ab14")
|
|
303
|
+
neighbors = coord.neighbors
|
|
304
|
+
# => [S2, S2, S2, S2]
|
|
305
|
+
|
|
306
|
+
neighbors.length # => 4
|
|
307
|
+
neighbors.first.level # => 15 (same level as source)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Containment and Intersection
|
|
311
|
+
|
|
312
|
+
S2 cells have strict hierarchical containment — a cell contains exactly its descendants and intersects exactly its ancestors and descendants.
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
coarse = S2.new(seattle, precision: 10)
|
|
316
|
+
fine = S2.new(seattle, precision: 20)
|
|
317
|
+
|
|
318
|
+
coarse.contains?(fine) # => true
|
|
319
|
+
fine.contains?(coarse) # => false
|
|
320
|
+
coarse.intersects?(fine) # => true
|
|
321
|
+
fine.intersects?(coarse) # => true
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Database Range Scans
|
|
325
|
+
|
|
326
|
+
Every S2 cell defines a contiguous range of cell IDs that covers all its descendants. This enables spatial queries as simple integer range scans.
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
cell = S2.new(seattle, precision: 12)
|
|
330
|
+
cell.range_min # => 6093487531259592705
|
|
331
|
+
cell.range_max # => 6093487668698546175
|
|
332
|
+
cell.cell_id # => 6093487599767896064 (between min and max)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Any descendant cell's ID falls within `[range_min, range_max]`:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
child = S2.new(seattle, precision: 20)
|
|
339
|
+
child.cell_id >= cell.range_min # => true
|
|
340
|
+
child.cell_id <= cell.range_max # => true
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Cell Area
|
|
344
|
+
|
|
345
|
+
S2 provides exact cell area calculations using the C++ library's spherical geometry.
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
coord.cell_area # => 77544.2 (square meters for level 15)
|
|
349
|
+
S2.average_cell_area(15) # => 79349.9 (global average for level 15)
|
|
350
|
+
coord.precision_in_meters # => { lat: 278.5, lng: 278.5, area_m2: 77544.2 }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Cell areas are computed as exact spherical surface area (steradians) multiplied by R², giving results in square meters.
|
|
354
|
+
|
|
355
|
+
## Cell Polygon (to_area)
|
|
356
|
+
|
|
357
|
+
The `to_area` method returns the quadrilateral cell boundary as an `Areas::Polygon` with 4 vertices.
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
area = coord.to_area
|
|
361
|
+
# => Geodetic::Areas::Polygon
|
|
362
|
+
|
|
363
|
+
area.includes?(coord.to_lla) # => true (centroid is inside the cell)
|
|
364
|
+
area.boundary.length # => 5 (4 vertices + closing point)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
S2 cell boundaries are geodesics (great circle arcs), not straight lines in lat/lng space. The polygon approximation uses the 4 corner vertices connected by straight edges.
|
|
368
|
+
|
|
369
|
+
## Leaf and Face Cells
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
coord.leaf? # => true if level == 30 (smallest possible cell, ~0.7 cm²)
|
|
373
|
+
coord.face_cell? # => true if level == 0 (one of 6 cube faces, ~85M km²)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Equality
|
|
377
|
+
|
|
378
|
+
Two S2 instances are equal if their token strings match exactly.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
S2.new("54906ab14") == S2.new("54906ab14") # => true
|
|
382
|
+
S2.new("54906ab14") == S2.new(6093487605347778560) # => true
|
|
383
|
+
S2.new("54906ab14") == S2.new("54906ab1c") # => false
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## `valid?`
|
|
387
|
+
|
|
388
|
+
Returns `true` if the cell ID has valid structure (correct face bits, properly positioned sentinel bit).
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
coord.valid? # => true
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Universal Distance and Bearing Methods
|
|
395
|
+
|
|
396
|
+
S2 supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
seattle = S2.new(Coordinate::LLA.new(lat: 47.6062, lng: -122.3321), precision: 15)
|
|
400
|
+
nyc = S2.new(Coordinate::LLA.new(lat: 40.7128, lng: -74.0060), precision: 15)
|
|
401
|
+
|
|
402
|
+
seattle.distance_to(nyc) # => Distance (~3,876 km)
|
|
403
|
+
seattle.straight_line_distance_to(nyc) # => Distance
|
|
404
|
+
seattle.bearing_to(nyc) # => Bearing (~83°, roughly east)
|
|
405
|
+
seattle.elevation_to(nyc) # => Float (degrees)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Geodetic Arithmetic
|
|
409
|
+
|
|
410
|
+
S2 cells support the standard arithmetic operators:
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
s2 = S2.new(seattle, precision: 15)
|
|
414
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 90)
|
|
415
|
+
|
|
416
|
+
s2 * v # => LLA (translated cell center)
|
|
417
|
+
s2 + v # => Segment (from center to destination)
|
|
418
|
+
s2 + distance # => Circle (centered on cell center)
|
|
419
|
+
s2 + other_s2 # => Segment (between cell centers)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Well-Known S2 Tokens
|
|
423
|
+
|
|
424
|
+
| Location | Token (level 30) | Token (level 15) | Face |
|
|
425
|
+
|----------|-----------------|-------------------|------|
|
|
426
|
+
| Seattle | `54906ab12f10f899` | `54906ab14` | 2 |
|
|
427
|
+
| New York | `89c25a220cf80969` | `89c25a224` | 4 |
|
|
428
|
+
| Tokyo | `6018f25555544b7f` | `6018f254c` | 3 |
|
|
429
|
+
| London | `487604ce36748fa9` | `487604d04` | 2 |
|
|
430
|
+
| Sydney | `6b12ae3ff6290055` | `6b12ae3fc` | 3 |
|
|
431
|
+
| Null Island (0°, 0°) | `1000000000000001` | `1000000000000004` | 0 |
|
|
432
|
+
|
|
433
|
+
## Implementation Notes
|
|
434
|
+
|
|
435
|
+
Geodetic uses Ruby's `fiddle` (part of the standard library) to call the S2 C++ library directly. Despite S2 being C++ (not C), the key functions are callable through their mangled symbol names. No gem dependency beyond `fiddle` is required. The S2 C++ library must be installed separately.
|
|
436
|
+
|
|
437
|
+
**Functions called via Fiddle:**
|
|
438
|
+
- `S2CellId::S2CellId(S2LatLng const&)` — encode lat/lng to cell ID
|
|
439
|
+
- `S2CellId::ToFaceIJOrientation()` — decode cell ID to face/IJ coordinates
|
|
440
|
+
- `S2CellId::GetEdgeNeighbors()` — 4 adjacent cells
|
|
441
|
+
- `S2Cell::ExactArea()` — spherical surface area in steradians
|
|
442
|
+
- `S2Cell::AverageArea(level)` — average area for a level
|
|
443
|
+
|
|
444
|
+
**Pure Ruby operations** (no FFI overhead):
|
|
445
|
+
- Cell level, face, parent, child — bit manipulation on the 64-bit cell ID
|
|
446
|
+
- Token encode/decode — hex formatting with trailing zero stripping
|
|
447
|
+
- Coordinate transforms — face/IJ → ST → UV → XYZ → lat/lng pipeline
|
|
448
|
+
- Cell vertex computation — 4 corners via the coordinate pipeline
|
|
449
|
+
- Containment/intersection — integer range comparisons
|
|
450
|
+
- Range min/max — bit arithmetic for database scan ranges
|
|
451
|
+
- Validity checks — sentinel bit position verification
|