Skip to content

Commit

Permalink
add geometry simplification algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmach committed Feb 5, 2018
1 parent 4e8f8d1 commit 6d89f46
Show file tree
Hide file tree
Showing 15 changed files with 1,439 additions and 0 deletions.
11 changes: 11 additions & 0 deletions define.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ type Projection func(Point) Point
type Pointer interface {
Point() Point
}

// A Simplifier is something that can simplify geometry.
type Simplifier interface {
Simplify(g Geometry) Geometry
LineString(ls LineString) LineString
MultiLineString(mls MultiLineString) MultiLineString
Ring(r Ring) Ring
Polygon(p Polygon) Polygon
MultiPolygon(mp MultiPolygon) MultiPolygon
Collection(c Collection) Collection
}
30 changes: 30 additions & 0 deletions planar/distance_from.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ import (
"github.com/paulmach/orb"
)

// DistanceFromSegment returns the point's distance from the segment [a, b].
func DistanceFromSegment(a, b, point orb.Point) float64 {
return math.Sqrt(DistanceFromSegmentSquared(a, b, point))
}

// DistanceFromSegmentSquared returns point's squared distance from the segement [a, b].
func DistanceFromSegmentSquared(a, b, point orb.Point) float64 {
x := a[0]
y := a[1]
dx := b[0] - x
dy := b[1] - y

if dx != 0 || dy != 0 {
t := ((point[0]-x)*dx + (point[1]-y)*dy) / (dx*dx + dy*dy)

if t > 1 {
x = b[0]
y = b[1]
} else if t > 0 {
x += dx * t
y += dy * t
}
}

dx = point[0] - x
dy = point[1] - y

return dx*dx + dy*dy
}

// DistanceFrom returns the distance from the boundary of the geometry in
// the units of the geometry.
func DistanceFrom(g orb.Geometry, p orb.Point) float64 {
Expand Down
51 changes: 51 additions & 0 deletions planar/distance_from_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,57 @@ import (

var epsilon = 1e-6

func TestDistanceFromSegment(t *testing.T) {
a := orb.Point{0, 0}
b := orb.Point{0, 10}

cases := []struct {
name string
point orb.Point
result float64
}{
{
name: "point in middle",
point: orb.Point{1, 5},
result: 1,
},
{
name: "on line",
point: orb.Point{0, 2},
result: 0,
},
{
name: "past start",
point: orb.Point{0, -5},
result: 5,
},
{
name: "past end",
point: orb.Point{0, 13},
result: 3,
},
{
name: "triangle",
point: orb.Point{3, 4},
result: 3,
},
{
name: "triangle off end",
point: orb.Point{3, -4},
result: 5,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := DistanceFromSegment(a, b, tc.point)
if v != tc.result {
t.Errorf("incorrect distance: %v != %v", v, tc.result)
}
})
}
}

func TestDistanceFromWithIndex(t *testing.T) {
for _, g := range orb.AllGeometries {
DistanceFromWithIndex(g, orb.Point{})
Expand Down
71 changes: 71 additions & 0 deletions simplify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
orb/simplify [![Godoc Reference](https://godoc.org/github.com/paulmach/orb?status.png)](https://godoc.org/github.com/paulmach/orb/simplify)
============

This package implements several reducing/simplifing function for `orb.Geometry` types.

Currently implemented:

* [Douglas-Peucker](#dp)
* [Visvalingam](#vis)
* [Radial](#radial)

**Note:** The geometry object CAN be modified, use `Clone()` if a copy is required.

<a name="dp"></a>Douglas-Peucker
--------------------------------

Probably the most popular simplification algorithm. For algorithm details, see
[wikipedia](http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm).

The algorithm is a pass through for 1d geometry, e.g. Point and MultiPoint.
The algorithms can modify the original geometry, use `Clone()` if a copy is required.

Usage:

original := orb.LineString{}
reduced := simplify.DouglasPeucker(threshold).Simplify(original.Clone())

<a name="vis"></a>Visvalingam
-----------------------------

See Mike Bostock's explanation for
[algorithm details](http://bost.ocks.org/mike/simplify/).

The algorithm is a pass through for 1d geometry, e.g. Point and MultiPoint.
The algorithms can modify the original geometry, use `Clone()` if a copy is required.

Usage:

original := orb.Ring{}

// will remove all whose triangle is smaller than `threshold`
reduced := simplify.VisvalingamThreshold(threshold).Simplify(original)

// will remove points until there are only `toKeep` points left.
reduced := simplify.VisvalingamKeep(toKeep).Simplify(original)

// One can also combine the parameters.
// This will continue to remove points until:
// - there are no more below the threshold,
// - or the new path is of length `toKeep`
reduced := simplify.Visvalingam(threshold, toKeep).Simplify(original)

<a name="radial"></a>Radial
---------------------------

Radial reduces the path by removing points that are close together.
A full [algorithm description](http://psimpl.sourceforge.net/radial-distance.html).

The algorithm is a pass through for 1d geometry, like Point and MultiPoint.
The algorithms can modify the original geometry, use `Clone()` if a copy is required.

Usage:

original := geo.Polygon{}

// this method uses a Euclidean distance measure.
reduced := simplify.Radial(planar.Distance, threshold).Simplify(path)

// if the points are in the lng/lat space Radial Geo will
// compute the geo distance between the coordinates.
reduced:= simplify.Radial(geo.Distance, meters).Simplify(path)
178 changes: 178 additions & 0 deletions simplify/benchmarks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package simplify

import (
"encoding/json"
"os"
"testing"

"github.com/paulmach/orb"
"github.com/paulmach/orb/planar"
)

func TestDouglasPeucker_BenchmarkData(t *testing.T) {
cases := []struct {
threshold float64
length int
}{
{0.1, 1118},
{0.5, 257},
{1.0, 144},
{1.5, 95},
{2.0, 71},
{3.0, 46},
{4.0, 39},
{5.0, 33},
}

ls := benchmarkData()

for i, tc := range cases {
r := DouglasPeucker(tc.threshold).LineString(ls.Clone())
if len(r) != tc.length {
t.Errorf("%d: reduced poorly, %d != %d", i, len(r), tc.length)
}
}
}

func BenchmarkDouglasPeucker(b *testing.B) {
ls := benchmarkData()

var data []orb.LineString
for i := 0; i < b.N; i++ {
data = append(data, ls.Clone())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
DouglasPeucker(0.1).LineString(data[i])
}
}

func TestRadial_BenchmarkData(t *testing.T) {
cases := []struct {
threshold float64
length int
}{
{0.1, 8282},
{0.5, 2023},
{1.0, 1043},
{1.5, 703},
{2.0, 527},
{3.0, 350},
{4.0, 262},
{5.0, 209},
}

ls := benchmarkData()
for i, tc := range cases {
ls := Radial(planar.Distance, tc.threshold).LineString(ls.Clone())
if len(ls) != tc.length {
t.Errorf("%d: data reduced poorly: %v != %v", i, len(ls), tc.length)
}
}
}

func BenchmarkRadial(b *testing.B) {
ls := benchmarkData()

var data []orb.LineString
for i := 0; i < b.N; i++ {
data = append(data, ls.Clone())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Radial(planar.Distance, 0.1).LineString(data[i])
}
}

func BenchmarkRadial_DisanceSquared(b *testing.B) {
ls := benchmarkData()

var data []orb.LineString
for i := 0; i < b.N; i++ {
data = append(data, ls.Clone())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Radial(planar.DistanceSquared, 0.1*0.1).LineString(data[i])
}
}

func TestVisvalingam_BenchmarkData(t *testing.T) {
cases := []struct {
threshold float64
length int
}{
{0.1, 867},
{0.5, 410},
{1.0, 293},
{1.5, 245},
{2.0, 208},
{3.0, 169},
{4.0, 151},
{5.0, 135},
}

ls := benchmarkData()
for i, tc := range cases {
r := VisvalingamThreshold(tc.threshold).LineString(ls.Clone())
if len(r) != tc.length {
t.Errorf("%d: data reduced poorly: %v != %v", i, len(ls), tc.length)
}
}
}

func BenchmarkVisvalingam_Threshold(b *testing.B) {
ls := benchmarkData()

var data []orb.LineString
for i := 0; i < b.N; i++ {
data = append(data, ls.Clone())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
VisvalingamThreshold(0.1).LineString(data[i])
}
}

func BenchmarkVisvalingam_Keep(b *testing.B) {
ls := benchmarkData()
toKeep := int(float64(len(ls)) / 1.616)

var data []orb.LineString
for i := 0; i < b.N; i++ {
data = append(data, ls.Clone())
}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
VisvalingamKeep(toKeep).LineString(data[i])
}
}

func benchmarkData() orb.LineString {
// Data taken from the simplify-js example at http://mourner.github.io/simplify-js/
f, err := os.Open("testdata/lisbon2portugal.json")
if err != nil {
panic(err)
}
defer f.Close()

var points []float64
json.NewDecoder(f).Decode(&points)

var ls orb.LineString
for i := 0; i < len(points); i += 2 {
ls = append(ls, orb.Point{points[i], points[i+1]})
}

return ls
}
Loading

0 comments on commit 6d89f46

Please sign in to comment.