Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simplify: Visvalingam, by default, keeps 3 points for "areas" #140

Merged
merged 1 commit into from
Jan 11, 2024
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
2 changes: 1 addition & 1 deletion simplify/douglas_peucker.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func DouglasPeucker(threshold float64) *DouglasPeuckerSimplifier {
}
}

func (s *DouglasPeuckerSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineString, []int) {
func (s *DouglasPeuckerSimplifier) simplify(ls orb.LineString, area, wim bool) (orb.LineString, []int) {
mask := make([]byte, len(ls))
mask[0] = 1
mask[len(mask)-1] = 1
Expand Down
2 changes: 1 addition & 1 deletion simplify/douglas_peucker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestDouglasPeucker(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, im := DouglasPeucker(tc.threshold).simplify(tc.ls, true)
v, im := DouglasPeucker(tc.threshold).simplify(tc.ls, false, true)
if !v.Equal(tc.expected) {
t.Log(v)
t.Log(tc.expected)
Expand Down
14 changes: 7 additions & 7 deletions simplify/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package simplify
import "github.com/paulmach/orb"

type simplifier interface {
simplify(orb.LineString, bool) (orb.LineString, []int)
simplify(l orb.LineString, area bool, withIndexMap bool) (orb.LineString, []int)
}

func simplify(s simplifier, geom orb.Geometry) orb.Geometry {
Expand Down Expand Up @@ -64,24 +64,24 @@ func simplify(s simplifier, geom orb.Geometry) orb.Geometry {
}

func lineString(s simplifier, ls orb.LineString) orb.LineString {
return runSimplify(s, ls)
return runSimplify(s, ls, false)
}

func multiLineString(s simplifier, mls orb.MultiLineString) orb.MultiLineString {
for i := range mls {
mls[i] = runSimplify(s, mls[i])
mls[i] = runSimplify(s, mls[i], false)
}
return mls
}

func ring(s simplifier, r orb.Ring) orb.Ring {
return orb.Ring(runSimplify(s, orb.LineString(r)))
return orb.Ring(runSimplify(s, orb.LineString(r), true))
}

func polygon(s simplifier, p orb.Polygon) orb.Polygon {
count := 0
for i := range p {
r := orb.Ring(runSimplify(s, orb.LineString(p[i])))
r := orb.Ring(runSimplify(s, orb.LineString(p[i]), true))
if i != 0 && len(r) <= 2 {
continue
}
Expand Down Expand Up @@ -113,10 +113,10 @@ func collection(s simplifier, c orb.Collection) orb.Collection {
return c
}

func runSimplify(s simplifier, ls orb.LineString) orb.LineString {
func runSimplify(s simplifier, ls orb.LineString, area bool) orb.LineString {
if len(ls) <= 2 {
return ls
}
ls, _ = s.simplify(ls, false)
ls, _ = s.simplify(ls, area, false)
return ls
}
2 changes: 1 addition & 1 deletion simplify/radial.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func Radial(df orb.DistanceFunc, threshold float64) *RadialSimplifier {
}
}

func (s *RadialSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineString, []int) {
func (s *RadialSimplifier) simplify(ls orb.LineString, area, wim bool) (orb.LineString, []int) {
var indexMap []int
if wim {
indexMap = append(indexMap, 0)
Expand Down
2 changes: 1 addition & 1 deletion simplify/radial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestRadial(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, im := Radial(planar.Distance, tc.threshold).simplify(tc.ls, true)
v, im := Radial(planar.Distance, tc.threshold).simplify(tc.ls, false, true)
if !v.Equal(tc.expected) {
t.Log(v)
t.Log(tc.expected)
Expand Down
46 changes: 38 additions & 8 deletions simplify/visvalingam.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@ var _ orb.Simplifier = &VisvalingamSimplifier{}
// performs the vivalingham algorithm.
type VisvalingamSimplifier struct {
Threshold float64
ToKeep int

// If 0 defaults to 2 for line, 3 for non-closed rings and 4 for closed rings.
// The intent is to maintain valid geometry after simplification, however it
// is still possible for the simplification to create self-intersections.
ToKeep int
}

// Visvalingam creates a new VisvalingamSimplifier.
// If minPointsToKeep is 0 the algorithm will keep at least 2 points for lines,
// 3 for non-closed rings and 4 for closed rings. However it is still possible
// for the simplification to create self-intersections.
func Visvalingam(threshold float64, minPointsToKeep int) *VisvalingamSimplifier {
return &VisvalingamSimplifier{
Threshold: threshold,
Expand All @@ -25,19 +32,42 @@ func Visvalingam(threshold float64, minPointsToKeep int) *VisvalingamSimplifier

// VisvalingamThreshold runs the Visvalingam-Whyatt algorithm removing
// triangles whose area is below the threshold.
// Will keep at least 2 points for lines, 3 for non-closed rings and 4 for closed rings.
// The intent is to maintain valid geometry after simplification, however it
// is still possible for the simplification to create self-intersections.
func VisvalingamThreshold(threshold float64) *VisvalingamSimplifier {
return Visvalingam(threshold, 0)
}

// VisvalingamKeep runs the Visvalingam-Whyatt algorithm removing
// triangles of minimum area until we're down to `toKeep` number of points.
func VisvalingamKeep(toKeep int) *VisvalingamSimplifier {
return Visvalingam(math.MaxFloat64, toKeep)
// triangles of minimum area until we're down to `minPointsToKeep` number of points.
// If minPointsToKeep is 0 the algorithm will keep at least 2 points for lines,
// 3 for non-closed rings and 4 for closed rings. However it is still possible
// for the simplification to create self-intersections.
func VisvalingamKeep(minPointsToKeep int) *VisvalingamSimplifier {
return Visvalingam(math.MaxFloat64, minPointsToKeep)
}

func (s *VisvalingamSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineString, []int) {
func (s *VisvalingamSimplifier) simplify(ls orb.LineString, area, wim bool) (orb.LineString, []int) {
if len(ls) <= 1 {
return ls, nil
}

toKeep := s.ToKeep
if toKeep == 0 {
if area {
if ls[0] == ls[len(ls)-1] {
toKeep = 4
} else {
toKeep = 3
}
} else {
toKeep = 2
}
}

var indexMap []int
if len(ls) <= s.ToKeep {
if len(ls) <= toKeep {
if wim {
// create identify map
indexMap = make([]int, len(ls))
Expand Down Expand Up @@ -89,7 +119,7 @@ func (s *VisvalingamSimplifier) simplify(ls orb.LineString, wim bool) (orb.LineS
// run through the reduction process
for len(heap) > 0 {
current := heap.Pop()
if current.area > threshold || len(ls)-removed <= s.ToKeep {
if current.area > threshold || len(ls)-removed <= toKeep {
break
}

Expand Down Expand Up @@ -153,7 +183,7 @@ type visItem struct {
next *visItem
previous *visItem

index int // interal index in heap, for removal and update
index int // internal index in heap, for removal and update
}

func (h *minHeap) Push(item *visItem) {
Expand Down
45 changes: 42 additions & 3 deletions simplify/visvalingam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestVisvalingamThreshold(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, im := VisvalingamThreshold(tc.threshold).simplify(tc.ls, true)
v, im := VisvalingamThreshold(tc.threshold).simplify(tc.ls, false, true)
if !v.Equal(tc.expected) {
t.Log(v)
t.Log(tc.expected)
Expand All @@ -49,6 +49,45 @@ func TestVisvalingamThreshold(t *testing.T) {
}
}

func TestVisvalingamThreshold_area(t *testing.T) {
cases := []struct {
name string
r orb.Ring
expected orb.Ring
indexMap []int
}{
{
name: "reduction",
r: orb.Ring{{0, 0}, {1, 0}, {1, 0.5}, {1, 1}, {0.5, 1}, {0, 1}},
expected: orb.Ring{{0, 0}, {1, 0}, {0, 1}},
indexMap: []int{0, 1, 5},
},
{
name: "reduction closed",
r: orb.Ring{{0, 0}, {1, 0}, {1, 0.5}, {1, 1}, {0.5, 1}, {0, 1}, {0, 0}},
expected: orb.Ring{{0, 0}, {1, 0}, {1, 1}, {0, 0}},
indexMap: []int{0, 1, 3, 6},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, im := VisvalingamThreshold(10).simplify(orb.LineString(tc.r), true, true)
if !v.Equal(orb.LineString(tc.expected)) {
t.Log(v)
t.Log(tc.expected)
t.Errorf("incorrect ring")
}

if !reflect.DeepEqual(im, tc.indexMap) {
t.Log(im)
t.Log(tc.indexMap)
t.Errorf("incorrect index map")
}
})
}
}

func TestVisvalingamKeep(t *testing.T) {
cases := []struct {
name string
Expand Down Expand Up @@ -96,7 +135,7 @@ func TestVisvalingamKeep(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, im := VisvalingamKeep(tc.keep).simplify(tc.ls, true)
v, im := VisvalingamKeep(tc.keep).simplify(tc.ls, false, true)
if !v.Equal(tc.expected) {
t.Log(v)
t.Log(tc.expected)
Expand Down Expand Up @@ -181,7 +220,7 @@ func TestVisvalingam(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v, im := Visvalingam(tc.threshold, tc.keep).simplify(tc.ls, true)
v, im := Visvalingam(tc.threshold, tc.keep).simplify(tc.ls, false, true)
if !v.Equal(tc.expected) {
t.Log(v)
t.Log(tc.expected)
Expand Down
Loading