Skip to content

Commit

Permalink
Merge ee74938 into 5c9e9a7
Browse files Browse the repository at this point in the history
  • Loading branch information
peterstace committed Nov 16, 2021
2 parents 5c9e9a7 + ee74938 commit c289358
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 138 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ __Special thanks to Albert Teoh and Sameera Perera for contributing to this rele
- Use the `%w` verb for wrapping errors internally. Note that simplefeatures
does not yet currently expose any sentinel errors or error types.

- **Breaking change**: Changes the `Simplify` package level function to become
a method on the `Geometry` type. Users upgrading can just change function
invocations that look like `simp, err := geom.Simplify(g, tolerance)` to
method invocations that look like `simp, err := g.Simplify(tolerance)`.

- Adds `Simplify` methods to the concrete geometry types `LineString`,
`MultiLineString`, `Polygon`, `MultiPolygon`, and `GeometryCollection`. These
methods may be used if one of these concrete geometries is to be simplified,
rather than converting to a `Geometry`, calling `Simplify`, then converting
back to the concrete geometry type.

- Fixes a bug in Simplify where invalid interior rings would be omitted rather
than producing an error.

## v0.34.0

2021-11-02
Expand Down
123 changes: 2 additions & 121 deletions geom/alg_simplify.go
Original file line number Diff line number Diff line change
@@ -1,125 +1,6 @@
package geom

// Simplify returns a simplified version of the geometry using the
// Ramer-Douglas-Peucker algorithm. Sometimes a simplified geometry can become
// invalid, in which case an error is returned rather than attempting to fix
// the geometry. Validation of the result can be skipped by making use of the
// geometry constructor options.
func Simplify(g Geometry, threshold float64, opts ...ConstructorOption) (Geometry, error) {
s := simplifier{threshold, opts}
switch g.gtype {
case TypeGeometryCollection:
gc, err := s.simplifyGeometryCollection(g.MustAsGeometryCollection())
return gc.AsGeometry(), wrapSimplified(err)
case TypePoint:
return g, nil
case TypeLineString:
ls, err := s.simplifyLineString(g.MustAsLineString())
return ls.AsGeometry(), wrapSimplified(err)
case TypePolygon:
poly, err := s.simplifyPolygon(g.MustAsPolygon())
return poly.AsGeometry(), wrapSimplified(err)
case TypeMultiPoint:
return g, nil
case TypeMultiLineString:
mls, err := s.simplifyMultiLineString(g.MustAsMultiLineString())
return mls.AsGeometry(), wrapSimplified(err)
case TypeMultiPolygon:
mp, err := s.simplifyMultiPolygon(g.MustAsMultiPolygon())
return mp.AsGeometry(), wrapSimplified(err)
default:
panic("unknown geometry: " + g.gtype.String())
}
}

type simplifier struct {
threshold float64
opts []ConstructorOption
}

func (s simplifier) simplifyLineString(ls LineString) (LineString, error) {
seq := ls.Coordinates()
floats := s.ramerDouglasPeucker(nil, seq)
seq = NewSequence(floats, seq.CoordinatesType())
if seq.Length() > 0 && !hasAtLeast2DistinctPointsInSeq(seq) {
return LineString{}, nil
}
return NewLineString(seq, s.opts...)
}

func (s simplifier) simplifyMultiLineString(mls MultiLineString) (MultiLineString, error) {
n := mls.NumLineStrings()
lss := make([]LineString, 0, n)
for i := 0; i < n; i++ {
ls := mls.LineStringN(i)
ls, err := s.simplifyLineString(ls)
if err != nil {
return MultiLineString{}, err
}
if !ls.IsEmpty() {
lss = append(lss, ls)
}
}
return NewMultiLineString(lss, s.opts...), nil
}

func (s simplifier) simplifyPolygon(poly Polygon) (Polygon, error) {
exterior, err := s.simplifyLineString(poly.ExteriorRing())
if err != nil {
return Polygon{}, err
}

// If we don't have at least 4 coordinates, then we can't form a ring, and
// the polygon has collapsed either to a point or a single linear element.
// Both cases are represented by an empty polygon.
if exterior.Coordinates().Length() < 4 {
return Polygon{}, nil
}

n := poly.NumInteriorRings()
rings := make([]LineString, 0, n+1)
rings = append(rings, exterior)
for i := 0; i < n; i++ {
interior, err := s.simplifyLineString(poly.InteriorRingN(i))
if err != nil {
return Polygon{}, err
}
if interior.IsRing() {
rings = append(rings, interior)
}
}
return NewPolygon(rings, s.opts...)
}

func (s simplifier) simplifyMultiPolygon(mp MultiPolygon) (MultiPolygon, error) {
n := mp.NumPolygons()
polys := make([]Polygon, 0, n)
for i := 0; i < n; i++ {
poly, err := s.simplifyPolygon(mp.PolygonN(i))
if err != nil {
return MultiPolygon{}, err
}
if !poly.IsEmpty() {
polys = append(polys, poly)
}
}
return NewMultiPolygon(polys, s.opts...)
}

func (s simplifier) simplifyGeometryCollection(gc GeometryCollection) (GeometryCollection, error) {
n := gc.NumGeometries()
geoms := make([]Geometry, n)
for i := 0; i < n; i++ {
var err error
geoms[i], err = Simplify(gc.GeometryN(i), s.threshold)
if err != nil {
return GeometryCollection{}, err
}
}
return NewGeometryCollection(geoms, s.opts...), nil
}

func (s simplifier) ramerDouglasPeucker(dst []float64, seq Sequence) []float64 {
func ramerDouglasPeucker(dst []float64, seq Sequence, threshold float64) []float64 {
if seq.Length() <= 2 {
return seq.appendAllPoints(dst)
}
Expand All @@ -143,7 +24,7 @@ func (s simplifier) ramerDouglasPeucker(dst []float64, seq Sequence) []float64 {
maxDist = d
}
}
if maxDist <= s.threshold {
if maxDist <= threshold {
break
}
newEnd = maxDistIdx
Expand Down
19 changes: 17 additions & 2 deletions geom/alg_simplify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestSimplify(t *testing.T) {
t.Logf("input: %v", in.AsText())
t.Logf("threshold: %v", tc.threshold)
t.Logf("want: %v", want.AsText())
got, err := geom.Simplify(in, tc.threshold)
got, err := in.Simplify(tc.threshold)
expectNoErr(t, err)
t.Logf("got: %v", got.AsText())
expectGeomEq(t, got, want, geom.IgnoreOrder)
Expand Down Expand Up @@ -123,10 +123,25 @@ func TestSimplifyErrorCases(t *testing.T) {
))`,
1e-5,
},

// Second case for "outer ring becomes invalid after simplification".
// The outer ring becomes invalid because the Ramer-Douglas-Peucker
// algorithm causes the ring (when considered as a LineString) to
// become self-intersected.
{`POLYGON((0 0,0 1,0.9 1,1 1.1,1.1 1,2 1,2 0,1 1.05,0 0))`, 0.2},

// Inner ring becomes invalid after simplification.
{
`POLYGON(
(-1 -1,-1 3,3 3,3 -1,-1 -1),
(0 0,0 1,0.9 1,1 1.1,1.1 1,2 1,2 0,1 1.05,0 0)
)`,
0.2,
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
in := geomFromWKT(t, tc.wkt)
_, err := geom.Simplify(in, tc.threshold)
_, err := in.Simplify(tc.threshold)
expectErr(t, err)
})
}
Expand Down
30 changes: 30 additions & 0 deletions geom/type_geometry.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,3 +908,33 @@ func (g Geometry) Summary() string {
func (g Geometry) String() string {
return g.Summary()
}

// Simplify returns a simplified version of the geometry using the
// Ramer-Douglas-Peucker algorithm. Sometimes a simplified geometry can become
// invalid, in which case an error is returned rather than attempting to fix
// the geometry. Validation of the result can be skipped by making use of the
// geometry constructor options.
func (g Geometry) Simplify(threshold float64, opts ...ConstructorOption) (Geometry, error) {
switch g.gtype {
case TypeGeometryCollection:
c, err := g.MustAsGeometryCollection().Simplify(threshold, opts...)
return c.AsGeometry(), err
case TypePoint:
return g, nil
case TypeLineString:
c := g.MustAsLineString().Simplify(threshold)
return c.AsGeometry(), nil
case TypePolygon:
c, err := g.MustAsPolygon().Simplify(threshold, opts...)
return c.AsGeometry(), err
case TypeMultiPoint:
return g, nil
case TypeMultiLineString:
return g.MustAsMultiLineString().Simplify(threshold).AsGeometry(), nil
case TypeMultiPolygon:
c, err := g.MustAsMultiPolygon().Simplify(threshold, opts...)
return c.AsGeometry(), err
default:
panic("unknown type: " + g.Type().String())
}
}
16 changes: 16 additions & 0 deletions geom/type_geometry_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,19 @@ func (c GeometryCollection) Summary() string {
func (c GeometryCollection) String() string {
return c.Summary()
}

// Simplify returns a simplified version of the GeometryCollection by applying
// Simplify to each child geometry. Any supplied ConstructorOptions will be
// used when simplifying each child geometry.
func (c GeometryCollection) Simplify(threshold float64, opts ...ConstructorOption) (GeometryCollection, error) {
n := c.NumGeometries()
geoms := make([]Geometry, n)
for i := 0; i < n; i++ {
var err error
geoms[i], err = c.GeometryN(i).Simplify(threshold, opts...)
if err != nil {
return GeometryCollection{}, wrapSimplified(err)
}
}
return NewGeometryCollection(geoms, opts...), nil
}
50 changes: 35 additions & 15 deletions geom/type_line_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,39 @@ type LineString struct {
// sequence must contain exactly 0 points, or at least 2 points with distinct
// XY values (otherwise an error is returned).
func NewLineString(seq Sequence, opts ...ConstructorOption) (LineString, error) {
n := seq.Length()
ctorOpts := newOptionSet(opts)
if ctorOpts.skipValidations || n == 0 {
if ctorOpts.skipValidations {
return LineString{seq}, nil
}
if ctorOpts.omitInvalid {
return newLineStringWithOmitInvalid(seq), nil
}

if err := validateLineStringSeq(seq); err != nil {
return LineString{}, err
}
return LineString{seq}, nil
}

func newLineStringWithOmitInvalid(seq Sequence) LineString {
if err := validateLineStringSeq(seq); err != nil {
return LineString{}
}
return LineString{seq}
}

// Valid non-empty LineStrings must have at least 2 *distinct* points.
func validateLineStringSeq(seq Sequence) error {
if seq.Length() == 0 {
return nil
}
if !hasAtLeast2DistinctPointsInSeq(seq) {
if ctorOpts.omitInvalid {
return LineString{}, nil
}
return LineString{}, validationError{
return validationError{
"non-empty linestring contains only one distinct XY value"}
}

// All XY values must be valid.
if err := seq.validate(); err != nil {
if ctorOpts.omitInvalid {
return LineString{}, nil
}
return LineString{}, validationError{err.Error()}
return validationError{err.Error()}
}

return LineString{seq}, nil
return nil
}

func hasAtLeast2DistinctPointsInSeq(seq Sequence) bool {
Expand Down Expand Up @@ -422,3 +431,14 @@ func (s LineString) Summary() string {
func (s LineString) String() string {
return s.Summary()
}

// Simplify returns a simplified version of the LineString using the
// Ramer-Douglas-Peucker algorithm. If the Ramer-Douglas-Peucker algorithm were to create
// an invalid LineString (i.e. one having only a single distinct point), then
// the empty LineString is returned.
func (s LineString) Simplify(threshold float64) LineString {
seq := s.Coordinates()
floats := ramerDouglasPeucker(nil, seq, threshold)
seq = NewSequence(floats, seq.CoordinatesType())
return newLineStringWithOmitInvalid(seq)
}
17 changes: 17 additions & 0 deletions geom/type_multi_line_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,20 @@ func (m MultiLineString) Summary() string {
func (m MultiLineString) String() string {
return m.Summary()
}

// Simplify returns a simplified version of the MultiLineString by using the
// Ramer-Douglas-Peucker algorithm on each of the child LineStrings. If the
// Ramer-Douglas-Peucker were to create an invalid child LineString (i.e. one
// having only a single distinct point), then it is omitted in the output.
// Empty child LineStrings are also omitted from the output.
func (m MultiLineString) Simplify(threshold float64) MultiLineString {
n := m.NumLineStrings()
lss := make([]LineString, 0, n)
for i := 0; i < n; i++ {
ls := m.LineStringN(i).Simplify(threshold)
if !ls.IsEmpty() {
lss = append(lss, ls)
}
}
return NewMultiLineString(lss)
}
20 changes: 20 additions & 0 deletions geom/type_multi_polygon.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,23 @@ func (m MultiPolygon) Summary() string {
func (m MultiPolygon) String() string {
return m.Summary()
}

// Simplify returns a simplified version of the MultiPolygon by applying
// Simplify to each child Polygon and constructing a new MultiPolygon from the
// result. Any supplied ConstructorOptions will be used when simplifying each
// child Polygon, or constructing the final MultiPolygon output.
func (m MultiPolygon) Simplify(threshold float64, opts ...ConstructorOption) (MultiPolygon, error) {
n := m.NumPolygons()
polys := make([]Polygon, 0, n)
for i := 0; i < n; i++ {
poly, err := m.PolygonN(i).Simplify(threshold, opts...)
if err != nil {
return MultiPolygon{}, err
}
if !poly.IsEmpty() {
polys = append(polys, poly)
}
}
simpl, err := NewMultiPolygon(polys, opts...)
return simpl, wrapSimplified(err)
}

0 comments on commit c289358

Please sign in to comment.