diff --git a/planar/simplify/debug.go b/planar/simplify/debug.go new file mode 100644 index 00000000..68b56fa5 --- /dev/null +++ b/planar/simplify/debug.go @@ -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) + } +} diff --git a/planar/simplify/douglaspeucker.go b/planar/simplify/douglaspeucker.go new file mode 100644 index 00000000..abe78ef8 --- /dev/null +++ b/planar/simplify/douglaspeucker.go @@ -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 +} diff --git a/planar/simplify/douglaspeucker_test.go b/planar/simplify/douglaspeucker_test.go new file mode 100644 index 00000000..e3bd4d3e --- /dev/null +++ b/planar/simplify/douglaspeucker_test.go @@ -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) }) + } +}