Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions planar/simplify/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package simplify

import (
"log"
"os"
)

const debug = false

var logger *log.Logger

func init() {
if debug {
logger = log.New(os.Stderr, "simplify:", log.Lshortfile|log.LstdFlags)
}
}
97 changes: 97 additions & 0 deletions planar/simplify/douglaspeucker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package simplify

import (
"strings"

"github.com/go-spatial/geom/planar"
)

type DouglasPeucker struct {

// Tolerance is the tolerance used to eliminate points, a tolerance of zero is not eliminate any points.
Tolerance float64

// Dist is the distance function to use, defaults to planar.PerpendicularDistance
Dist planar.PointLineDistanceFunc
}

func (dp DouglasPeucker) Simplify(linestring [][2]float64, isClosed bool) ([][2]float64, error) {
return dp.simplify(0, linestring, isClosed)
}

func (dp DouglasPeucker) simplify(depth uint8, linestring [][2]float64, isClosed bool) ([][2]float64, error) {

// helper function for debugging and tracing the code
var printf = func(msg string, depth uint8, params ...interface{}) {
if debug {
ps := make([]interface{}, 1, len(params)+1)
ps[0] = depth
ps = append(ps, params...)
logger.Printf(strings.Repeat(" ", int(depth*2))+"[%v]"+msg, ps...)
}
}

if dp.Tolerance <= 0 || len(linestring) <= 2 {
if debug {
if dp.Tolerance <= 0 {
printf("skipping due to Tolerance (%v) ≤ zero:", depth, dp.Tolerance)

}
if len(linestring) <= 2 {
printf("skipping due to len(linestring) (%v) ≤ two:", depth, len(linestring))
}
}
return linestring, nil
}

if debug {
printf("starting linestring: %v ; tolerance: %v", depth, linestring, dp.Tolerance)
}

dmax, idx := 0.0, 0
dist := planar.PerpendicularDistance
if dp.Dist != nil {
dist = dp.Dist
}

line := [2][2]float64{linestring[0], linestring[len(linestring)-1]}

if debug {
printf("starting dmax: %v ; idx %v ; line : %v", depth, dmax, idx, line)
}

// Find the point that is the furthest away.
for i := 1; i <= len(linestring)-2; i++ {
d := dist(line, linestring[i])
if d > dmax {
dmax, idx = d, i
}

if debug {
printf("looking at %v ; d : %v dmax %v ", depth, i, d, dmax)
}
}

// If the furtherest point is greater then tolerance, we split at that point, and look again at each
// subsections.
if dmax > dp.Tolerance {
if len(linestring) <= 3 {
if debug {
printf("returning linestring %v", depth, linestring)
}
return linestring, nil
}
rec1, _ := dp.simplify(depth+1, linestring[0:idx], isClosed)
rec2, _ := dp.simplify(depth+1, linestring[idx:], isClosed)
if debug {
printf("returning combined lines: %v %v", depth, rec1, rec2)
}
return append(rec1, rec2...), nil
}

// Drop all points between the end points.
if debug {
printf("dropping all points between the end points: %v", depth, line)
}
return line[:], nil
}
63 changes: 63 additions & 0 deletions planar/simplify/douglaspeucker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package simplify

import (
"flag"
"reflect"
"testing"
)

var ignoreSanityCheck bool

func init() {
flag.BoolVar(&ignoreSanityCheck, "ignoreSanityCheck", false, "ignore sanity checks in test cases.")
}

func TestDouglasPeucker(t *testing.T) {
type tcase struct {
l [][2]float64
dp DouglasPeucker
el [][2]float64
}

fn := func(t *testing.T, tc tcase) {
gl, err := tc.dp.Simplify(tc.l, false)
// Douglas Peucker should never return an error.
// This is more of a sanity check.
if err != nil {
t.Errorf("Douglas Peucker error, expected nil got %v", err)
return
}
if !reflect.DeepEqual(tc.el, gl) {
t.Errorf("simplified points, expected %v got %v", tc.el, gl)
return
}

if ignoreSanityCheck {
return
}

// Let's try it with true, it should not matter, as DP does not care.
// More sanity checking.
gl, _ = tc.dp.Simplify(tc.l, true)

if !reflect.DeepEqual(tc.el, gl) {
t.Errorf("simplified points (true), expected %v got %v", tc.el, gl)
return
}
}

tests := map[string]tcase{
"simple box": {
l: [][2]float64{{0, 0}, {0, 1}, {1, 1}, {1, 0}},
dp: DouglasPeucker{
Tolerance: 0.001,
},
el: [][2]float64{{0, 0}, {0, 1}, {1, 1}, {1, 0}},
},
}

for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) { fn(t, tc) })
}
}