Skip to content

Commit

Permalink
Merge pull request #31 from paulmach/pm/resample-interval
Browse files Browse the repository at this point in the history
Path resample to given interval
  • Loading branch information
paulmach committed Dec 6, 2015
2 parents c9ea7be + 6682d20 commit e8994de
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 149 deletions.
86 changes: 0 additions & 86 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,92 +151,6 @@ func (p *Path) Transform(projector Projector) *Path {
return p
}

// Resample converts the path into totalPoints-1 evenly spaced segments.
func (p *Path) Resample(totalPoints int) *Path {
// degenerate case
if len(p.PointSet) <= 1 {
return p
}

if totalPoints <= 0 {
p.PointSet = make([]Point, 0)
return p
}

// if all the points are the same, treat as special case.
equal := true
for _, point := range p.PointSet {
if !p.PointSet[0].Equals(&point) {
equal = false
break
}
}

if equal {
if totalPoints > p.Length() {
// extend to be requested length
for p.Length() != totalPoints {
p.PointSet = append(p.PointSet, p.PointSet[0])
}

return p
}

// contract to be requested length
p.PointSet = p.PointSet[:totalPoints]
return p
}

points := make([]Point, 1, totalPoints)
points[0] = p.PointSet[0] // start stays the same

// first distance we're looking for
step := 1
totalDistance := 0.0
distance := 0.0
distances := make([]float64, len(p.PointSet)-1)
for i := 0; i < len(p.PointSet)-1; i++ {
distances[i] = p.PointSet[i].DistanceFrom(&p.PointSet[i+1])
totalDistance += distances[i]
}

currentDistance := totalDistance / float64(totalPoints-1)
currentLine := &Line{} // declare here and update has nice performance benefits
for i := 0; i < len(p.PointSet)-1; i++ {
currentLine.a = p.PointSet[i]
currentLine.b = p.PointSet[i+1]

currentLineDistance := distances[i]
nextDistance := distance + currentLineDistance

for currentDistance <= nextDistance {
// need to add a point
percent := (currentDistance - distance) / currentLineDistance
points = append(points, Point{
currentLine.a[0] + percent*(currentLine.b[0]-currentLine.a[0]),
currentLine.a[1] + percent*(currentLine.b[1]-currentLine.a[1]),
})

// move to the next distance we want
step++
currentDistance = totalDistance * float64(step) / float64(totalPoints-1)
if step == totalPoints-1 { // weird round off error on my machine
currentDistance = totalDistance
}
}

// past the current point in the original line, so move to the next one
distance = nextDistance
}

// end stays the same, to handle round off errors
if totalPoints != 1 { // for 1, we want the first point
points[totalPoints-1] = p.PointSet[len(p.PointSet)-1]
}
(&p.PointSet).SetPoints(points)
return p
}

// Decode is deprecated, use NewPathFromEncoding
func Decode(encoded string, factor ...int) *Path {
return NewPathFromEncoding(encoded, factor...)
Expand Down
162 changes: 162 additions & 0 deletions path_resample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package geo

// Resample converts the path into totalPoints-1 evenly spaced segments.
// Assumes euclidean geometry.
func (p *Path) Resample(totalPoints int) *Path {
if totalPoints <= 0 {
p.PointSet = make([]Point, 0)
return p
}

if p.resampleEdgeCases(totalPoints) {
return p
}

// precomputes the total distance and intermediate distances
total, dists := precomputeDistances(p.PointSet)
p.resample(dists, total, totalPoints)
return p
}

// ResampleWithInterval coverts the path into evenly spaced points of
// about the given distance. The total distance is computed using euclidean
// geometry and then divided by the given distance to get the number of segments.
func (p *Path) ResampleWithInterval(dist float64) *Path {
if dist <= 0 {
p.PointSet = make([]Point, 0)
return p
}

// precomputes the total distance and intermediate distances
total, dists := precomputeDistances(p.PointSet)

totalPoints := int(total/dist) + 1
if p.resampleEdgeCases(totalPoints) {
return p
}

p.resample(dists, total, totalPoints)
return p
}

// ResampleWithGeoInterval converts the path into about evenly spaced points of
// about the given distance. The total distance is computed using spherical (lng/lat) geometry
// and divided by the given distance. The new points are chosen by linearly interpolating
// between two given points. This may not make sense in some contexts, especially if
// the path covers a large range of latitude.
func (p *Path) ResampleWithGeoInterval(meters float64) *Path {
if meters <= 0 {
p.PointSet = make([]Point, 0)
return p
}

// precomputes the total geo distance and intermediate distances
totalDistance := 0.0
distances := make([]float64, len(p.PointSet)-1)
for i := 0; i < len(p.PointSet)-1; i++ {
distances[i] = p.PointSet[i].GeoDistanceFrom(&p.PointSet[i+1])
totalDistance += distances[i]
}

totalPoints := int(totalDistance/meters) + 1
if p.resampleEdgeCases(totalPoints) {
return p
}

p.resample(distances, totalDistance, totalPoints)
return p
}

func (p *Path) resample(distances []float64, totalDistance float64, totalPoints int) {
points := make([]Point, 1, totalPoints)
points[0] = p.PointSet[0] // start stays the same

step := 1
distance := 0.0

currentDistance := totalDistance / float64(totalPoints-1)
currentLine := &Line{} // declare here and update has nice performance benefits
for i := 0; i < len(p.PointSet)-1; i++ {
currentLine.a = p.PointSet[i]
currentLine.b = p.PointSet[i+1]

currentLineDistance := distances[i]
nextDistance := distance + currentLineDistance

for currentDistance <= nextDistance {
// need to add a point
percent := (currentDistance - distance) / currentLineDistance
points = append(points, Point{
currentLine.a[0] + percent*(currentLine.b[0]-currentLine.a[0]),
currentLine.a[1] + percent*(currentLine.b[1]-currentLine.a[1]),
})

// move to the next distance we want
step++
currentDistance = totalDistance * float64(step) / float64(totalPoints-1)
if step == totalPoints-1 { // weird round off error on my machine
currentDistance = totalDistance
}
}

// past the current point in the original line, so move to the next one
distance = nextDistance
}

// end stays the same, to handle round off errors
if totalPoints != 1 { // for 1, we want the first point
points[totalPoints-1] = p.PointSet[len(p.PointSet)-1]
}

(&p.PointSet).SetPoints(points)
return
}

// resampleEdgeCases is used to handle edge case for
// resampling like not enough points and the path is all the same point.
// will return nil if there are no edge cases. If return true if
// one of these edge cases was found and handled.
func (p *Path) resampleEdgeCases(totalPoints int) bool {
// degenerate case
if len(p.PointSet) <= 1 {
return true
}

// if all the points are the same, treat as special case.
equal := true
for _, point := range p.PointSet {
if !p.PointSet[0].Equals(&point) {
equal = false
break
}
}

if equal {
if totalPoints > p.Length() {
// extend to be requested length
for p.Length() != totalPoints {
p.PointSet = append(p.PointSet, p.PointSet[0])
}

return true
}

// contract to be requested length
p.PointSet = p.PointSet[:totalPoints]
return true
}

return false
}

// precomputeDistances precomputes the total distance and intermediate distances.
func precomputeDistances(p PointSet) (float64, []float64) {
total := 0.0
dists := make([]float64, len(p)-1)
for i := 0; i < len(p)-1; i++ {
dists[i] = p[i].DistanceFrom(&p[i+1])
total += dists[i]
}

return total, dists
}
121 changes: 121 additions & 0 deletions path_resample_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package geo

import "testing"

func TestPathResample(t *testing.T) {
p := NewPath()
p.Resample(10) // should not panic

p.Push(NewPoint(0, 0)).Resample(10) // should not panic
p.Push(NewPoint(1.5, 1.5))
p.Push(NewPoint(2, 2))

// resample to 0?
result := p.Clone().Resample(0)
if result.Length() != 0 {
t.Error("path, resample down to zero should be empty line")
}

// resample to 1
result = p.Clone().Resample(1)
answer := NewPath().Push(NewPoint(0, 0))
if !result.Equals(answer) {
t.Error("path, resample down to 1 should be first point")
}

result = p.Clone().Resample(2)
answer = NewPath().Push(NewPoint(0, 0)).Push(NewPoint(2, 2))
if !result.Equals(answer) {
t.Error("path, resample downsampling")
}

result = p.Clone().Resample(5)
answer = NewPath()
answer.Push(NewPoint(0, 0)).Push(NewPoint(0.5, 0.5))
answer.Push(NewPoint(1, 1)).Push(NewPoint(1.5, 1.5))
answer.Push(NewPoint(2, 2))
if !result.Equals(answer) {
t.Error("path, resample upsampling")
t.Error(result)
t.Error(answer)
}

// round off error case, triggered on my laptop
p1 := NewPath().Push(NewPoint(-88.145243, 42.321059)).Push(NewPoint(-88.145232, 42.325902))
p1.Resample(109)
if p1.Length() != 109 {
t.Errorf("path, resample incorrect length, expected 109, got %d", p1.Length())
}

// duplicate points
p = NewPath()
p.Push(NewPoint(1, 0))
p.Push(NewPoint(1, 0))
p.Push(NewPoint(1, 0))

p.Resample(10)
if l := p.Length(); l != 10 {
t.Errorf("path, resample length incorrect, got %d", l)
}

for i := 0; i < p.Length(); i++ {
if !p.GetAt(i).Equals(NewPoint(1, 0)) {
t.Errorf("path, resample not correct point, got %v", p.GetAt(i))
}
}
}

func TestPathResampleWithInterval(t *testing.T) {
p := NewPath()
p.Push(NewPoint(0, 0))
p.Push(NewPoint(0, 10))

p.ResampleWithInterval(5.0)
if l := p.Length(); l != 3 {
t.Errorf("incorrect resample, got %v", l)
}

if v := p.GetAt(1); !v.Equals(NewPoint(0, 5.0)) {
t.Errorf("incorrect point, got %v", v)
}
}

func TestPathResampleWithGeoInterval(t *testing.T) {
p := NewPath()
p.Push(NewPoint(0, 0))
p.Push(NewPoint(0, 10))

d := p.GeoDistance() / 2
p.ResampleWithGeoInterval(d)
if l := p.Length(); l != 3 {
t.Errorf("incorrect resample, got %v", l)
}

if v := p.GetAt(1); !v.Equals(NewPoint(0, 5.0)) {
t.Errorf("incorrect point, got %v", v)
}
}

func TestPathResampleEdgeCases(t *testing.T) {
p := NewPath()
p.Push(NewPoint(0, 0))

if !p.resampleEdgeCases(10) {
t.Errorf("should return false")
}

// duplicate points
p.Push(NewPoint(0, 0))
if !p.resampleEdgeCases(10) {
t.Errorf("should return true")
}

if l := p.Length(); l != 10 {
t.Errorf("should reset to suggested points, got %v", l)
}

p.resampleEdgeCases(5)
if l := p.Length(); l != 5 {
t.Errorf("should shorten if necessary, got %v", l)
}
}
Loading

0 comments on commit e8994de

Please sign in to comment.