Skip to content

Commit 22eebea

Browse files
justinhwangclaude
andcommitted
feat: add pure-Go vertexes, directed edges & edge length to x/h3go
Add the Vertex and DirectedEdge index types with their full method and free-function surfaces: Cell.Vertex/Vertexes and Vertex.LatLng/IsValid/ String/Resolution/IndexDigit; Cell.DirectedEdge/DirectedEdges and DirectedEdge.Origin/Destination/Reverse/Cells/Boundary/IsValid plus the string and text-marshal helpers; EdgeLengthRads/Km/M; and the generic Index constraint with IsValidIndex. Verified against the cgo reference over the shared corpus with full in-package coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d1f1a78 commit 22eebea

9 files changed

Lines changed: 1906 additions & 8 deletions

File tree

x/h3go/directededge.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright 2026 Uber Technologies, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package h3go
18+
19+
import "errors"
20+
21+
// DirectedEdge is an H3 index that identifies a directed edge from an origin
22+
// cell to one of its neighbors. The reserved field of the index holds the
23+
// neighbor direction.
24+
type DirectedEdge int64
25+
26+
// DirectedEdge returns the directed edge from c to other. The cells must be
27+
// neighbors at the same resolution, otherwise it returns ErrNotNeighbors.
28+
func (c Cell) DirectedEdge(other Cell) (DirectedEdge, error) {
29+
direction := c.directionForNeighbor(other)
30+
if direction == invalidDigit {
31+
return 0, ErrNotNeighbors
32+
}
33+
34+
edge := c.setMode(directedEdgeMode).setReservedBits(direction)
35+
36+
return DirectedEdge(edge), nil
37+
}
38+
39+
// DirectedEdges returns the six directed edges with c as the origin. For a
40+
// pentagon, the edge in the deleted k direction is omitted.
41+
func (c Cell) DirectedEdges() ([]DirectedEdge, error) {
42+
isPent := c.IsPentagon()
43+
out := make([]DirectedEdge, 0, numCellEdges)
44+
45+
for i := range numCellEdges {
46+
if isPent && i == 0 {
47+
continue
48+
}
49+
50+
edge := c.setMode(directedEdgeMode).setReservedBits(i + 1)
51+
out = append(out, DirectedEdge(edge))
52+
}
53+
54+
return out, nil
55+
}
56+
57+
// IsValid reports whether the index is a valid H3 directed edge.
58+
func (e DirectedEdge) IsValid() bool {
59+
neighborDirection := Cell(e).reservedBits()
60+
if neighborDirection <= centerDigit || neighborDirection >= numDigits {
61+
return false
62+
}
63+
64+
origin, err := e.Origin()
65+
if err != nil {
66+
return false
67+
}
68+
69+
if origin.IsPentagon() && neighborDirection == kAxesDigit {
70+
return false
71+
}
72+
73+
return origin.IsValid()
74+
}
75+
76+
// Origin returns the origin cell of the directed edge.
77+
func (e DirectedEdge) Origin() (Cell, error) {
78+
if Cell(e).mode() != directedEdgeMode {
79+
return 0, ErrDirectedEdgeInvalid
80+
}
81+
82+
return Cell(e).setMode(cellMode).setReservedBits(0), nil
83+
}
84+
85+
// Destination returns the destination cell of the directed edge.
86+
func (e DirectedEdge) Destination() (Cell, error) {
87+
origin, err := e.Origin()
88+
if err != nil {
89+
return 0, err
90+
}
91+
92+
direction := Cell(e).reservedBits()
93+
94+
destination, _, err := origin.neighborRotations(direction, 0)
95+
96+
return destination, err
97+
}
98+
99+
// Reverse returns the directed edge from this edge's destination back to its
100+
// origin.
101+
func (e DirectedEdge) Reverse() (DirectedEdge, error) {
102+
origin, err := e.Origin()
103+
if err != nil {
104+
return 0, err
105+
}
106+
107+
destination, err := e.Destination()
108+
if err != nil {
109+
return 0, err
110+
}
111+
112+
return destination.DirectedEdge(origin)
113+
}
114+
115+
// Cells returns the origin and destination cells of the directed edge, in that
116+
// order.
117+
func (e DirectedEdge) Cells() ([]Cell, error) {
118+
origin, err := e.Origin()
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
destination, err := e.Destination()
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
return []Cell{origin, destination}, nil
129+
}
130+
131+
// Boundary returns the coordinates of the directed edge: the geographic line
132+
// from the center-relative start vertex to the end vertex of the origin cell.
133+
// The boundary may contain an extra vertex where the edge crosses an
134+
// icosahedron face boundary.
135+
func (e DirectedEdge) Boundary() (CellBoundary, error) {
136+
direction := Cell(e).reservedBits()
137+
138+
origin, err := e.Origin()
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
fijk, err := origin.toFaceIjk()
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
startVertex := origin.vertexNumForDirection(direction)
149+
if startVertex == invalidVertexNum {
150+
return nil, ErrDirectedEdgeInvalid
151+
}
152+
153+
res := origin.Resolution()
154+
if origin.IsPentagon() {
155+
return fijk.pentToCellBoundary(res, startVertex, numEdgeCells), nil
156+
}
157+
158+
return fijk.toCellBoundary(res, startVertex, numEdgeCells), nil
159+
}
160+
161+
// Resolution returns the resolution of the directed edge.
162+
func (e DirectedEdge) Resolution() int {
163+
return Cell(e).Resolution()
164+
}
165+
166+
// IndexDigit returns the indexing digit of the directed edge at res, for res in
167+
// [1, maxResolution].
168+
func (e DirectedEdge) IndexDigit(res int) (int, error) {
169+
return Cell(e).IndexDigit(res)
170+
}
171+
172+
// DirectedEdgeFromString returns a DirectedEdge parsed from its hexadecimal
173+
// string representation. Callers should validate it with DirectedEdge.IsValid
174+
// before use.
175+
func DirectedEdgeFromString(s string) DirectedEdge {
176+
//nolint:gosec // an H3 index is a 64-bit value; uint64->int64 is a lossless reinterpretation.
177+
return DirectedEdge(IndexFromString(s))
178+
}
179+
180+
// String returns the hexadecimal string representation of the directed edge.
181+
func (e DirectedEdge) String() string {
182+
//nolint:gosec // an H3 index is a 64-bit value; int64->uint64 is a lossless reinterpretation.
183+
return IndexToString(uint64(e))
184+
}
185+
186+
// MarshalText implements the encoding.TextMarshaler interface.
187+
func (e DirectedEdge) MarshalText() ([]byte, error) {
188+
return []byte(e.String()), nil
189+
}
190+
191+
// UnmarshalText implements the encoding.TextUnmarshaler interface.
192+
func (e *DirectedEdge) UnmarshalText(text []byte) error {
193+
*e = DirectedEdgeFromString(string(text))
194+
if !e.IsValid() {
195+
return errors.New("invalid directed edge index")
196+
}
197+
198+
return nil
199+
}

0 commit comments

Comments
 (0)