go-geom
attempts to implement efficient, standards-compatible OGC-style
geometries for Go. This document describes some of the key ideas required to
understand its implementation.
go-geom
is an evolution of the techniques developed for the OpenLayers 3
geometry library,
designed to efficiently handle large geometries in a resource-constrained,
garbage-collected environment, but adapted to the Go programming language and
its type system.
There are three priniciple 2D geometry types: Point
s, LineString
s, and
Polygon
s.
OGC extends these three into collections of the principle types: MultiPoint
,
MultiLineString
, and MultiPolygon
. This gives 3 geometry types * 2
multi-or-not-multi = 6 combinations.
On top of this, there are multiple combinations of dimensions, e.g. 2D (XY), 3D (XYZ), 2D varying over time/distance (XYM), and 3D varying over time/distance (XYZM).
3 geometry types * 2 multi-or-not-multi * 4 different dimensionalities = 24 distinct types.
Go has neither generics, nor macros, nor a rich type system. go-geom
attempts
to manage this combinatorial explosion while maintaining an idiomatic Go API,
implementation efficiency. and high runtime performance.
go-geom
exploits structural similarity between different geometry types to
share code. Consider:
-
A
Point
consists of a single coordinate. This single coordinate is ageom.Coord
. -
A
LineString
,LinearRing
, andMultiPoint
consist of a collection of coordinates. They all have different semantics (aLineString
is ordered, aLinearRing
is ordered and closed, aMultiPoint
is neither ordered nor closed) yet all share a similar underlying structure. -
A
Polygon
and aMultiLineString
are a collection of collections of coordinates. Again, the semantics vary: aPolygon
is a weakly ordered collection ofLinearRing
s (the firstLinearRing
is the outer boundary, subsequentLinearRing
s are inner boundaries (holes)). AMultiLineString
is an unordered collection ofLineString
s. -
A
MultiPolygon
is an unordered collection ofPolygon
s.
go-geom
makes these structural similarities explicit:
-
A
Point
is ageom.Coord
, also known asgeom0
. -
LineString
s,LinearRing
s, and andMultiPoint
s are[]geom.Coord
, also known asgeom1
. -
Polygon
s andMultiLineString
s are[][]geom.Coord
, also known asgeom2
. -
MultiPolygon
s are[][][]geom.Coord
, also known asgeom3
.
Under the hood, go-geom
uses Go's structural composition to share common
code. For example, LineString
s, LinearRing
s, and MultiPoint
s all embed a
single anonymous geom1
.
The hierarchy of embedding is:
geom0
+- geom1
+- geom2
+- geom3
Note that geom2
and geom3
independently embed geom1
. Despite their
numerical ordering, geom2
and geom3
are separate branches of the geometry
tree.
We can exploit these structural similarities to share code. For example, calculating the bounds of a geometry only involves finding the minimum and maximum values in each dimension, which can be found by iterating over all coordinates in the geometry. The semantic meaning of these coordinates - whether they're points on a line, or points on a polygon inner or outer boundary, or something else - does not matter. Therefore, as long as we can treat any geometry as a collection of coordinates, we can use the same code to calculate bounds across all geometry types.
Similarly, we can exploit higher-level similarities. For example, the "length"
of a MultiLineString
is the sum of the lengths of its component
LineString
s, and the "length" (perimeter) of a Polygon
is the sum of the
lengths (perimeters) of its component LinearRing
s.
At the time of writing (2016), CPUs are fast, cache hits are quite fast, cache misses are slow, memory is very slow, and garbage collection takes an eternity.
Typical geometry libraries use multiple levels of nested arrays, e.g. a
[][][]float64
for a polygon. This requires multiple levels of indirection to
access a single coordinate value, and as different sub-arrays might be stored
in different parts of memory, is more likely to lead to cache miss.
In contrast, go-geom
packs all the coordinates for a geometry, whatever its
structure, into a single []float64
. The underlying array is stored in a
single blob of memory. Most operations do a linear scan over the array, which
is particularly cache friendly. There are also fewer objects for the garbage
collector to manage.
Parts of the underlying array can be shared between multitple objects. For
example, retrieving the outer ring of a Polygon
returns a LinearRing
that
references the coordinates of the Polygon
. No coordinate data are copied.