Skip to content

Commit

Permalink
Polygon index (#406)
Browse files Browse the repository at this point in the history
* Polygon index

* Polyline files
  • Loading branch information
irees authored Feb 6, 2025
1 parent 1af6d51 commit 612e597
Show file tree
Hide file tree
Showing 13 changed files with 784 additions and 106 deletions.
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tidwall/rtree v1.10.0
github.com/twpayne/go-geom v1.4.1
github.com/twpayne/go-polyline v1.1.1
google.golang.org/protobuf v1.33.0
Expand Down Expand Up @@ -65,10 +66,11 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
github.com/tidwall/geoindex v1.7.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
24 changes: 16 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/cities v0.1.0 h1:CVNkmMf7NEC9Bvokf5GoSsArHCKRMTgLuubRTHnH0mE=
github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4=
github.com/tidwall/geoindex v1.7.0 h1:jtk41sfgwIt8MEDyC3xyKSj75iXXf6rjReJGDNPtR5o=
github.com/tidwall/geoindex v1.7.0/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/rtree v1.10.0 h1:+EcI8fboEaW1L3/9oW/6AMoQ8HiEIHyR7bQOGnmz4Mg=
github.com/tidwall/rtree v1.10.0/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ=
github.com/twpayne/go-geom v1.4.1 h1:LeivFqaGBRfyg0XJJ9pkudcptwhSSrYN9KZUW6HcgdA=
github.com/twpayne/go-geom v1.4.1/go.mod h1:k/zktXdL+qnA6OgKsdEGUTA17jbQ2ZPTUa3CCySuGpE=
github.com/twpayne/go-kml v1.5.2/go.mod h1:kz8jAiIz6FIdU2Zjce9qGlVtgFYES9vt7BTPBHf5jl4=
Expand All @@ -187,16 +195,16 @@ github.com/twpayne/go-polyline v1.1.1/go.mod h1:ybd9IWWivW/rlXPXuuckeKUyF3yrIim+
github.com/twpayne/go-waypoint v0.0.0-20200706203930-b263a7f6e4e8/go.mod h1:qj5pHncxKhu9gxtZEYWypA/z097sxhFlbTyOyt9gcnU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -210,8 +218,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
53 changes: 53 additions & 0 deletions testdata/tlxy/tz-example.polyline

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions tlxy/bbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import (
"strings"
)

// BoundingBox is a simple bounding box.
type BoundingBox struct {
MinLon float64 `json:"min_lon"`
MinLat float64 `json:"min_lat"`
MaxLon float64 `json:"max_lon"`
MaxLat float64 `json:"max_lat"`
}

// Contains returns true if the point is within the bounding box.
func (v *BoundingBox) Contains(pt Point) bool {
if pt.Lon >= v.MinLon && pt.Lon <= v.MaxLon && pt.Lat >= v.MinLat && pt.Lat <= v.MaxLat {
return true
}
return false
}

// ParseBbox parses a bounding box from a string.
func ParseBbox(v string) (BoundingBox, error) {
r := BoundingBox{}
if s := strings.Split(v, ","); len(s) == 4 {
Expand Down
94 changes: 72 additions & 22 deletions tlxy/cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@ import (
"math"
)

// SegmentClosestPoint returns the point (and position) on AB closest to P.
// SegmentClosestPoint calculates the closest point on a line segment AB to a given point P,
// and returns both the closest point and the distance to it.
//
// Given three points:
// - a: Start point of line segment
// - b: End point of line segment
// - p: Point to find closest position to
//
// Returns:
// - Point: The closest point on segment AB to point P
// - float64: The distance between point P and the closest point
//
// The algorithm first checks if P is closest to either endpoint.
// If not, it projects P onto line AB and clamps the result to the segment.
// Distance calculation assumes a simplified 2D Euclidean space suitable for small distances.
func SegmentClosestPoint(a, b, p Point) (Point, float64) {
// ported from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
// check ends
Expand All @@ -29,8 +43,26 @@ func SegmentClosestPoint(a, b, p Point) (Point, float64) {
return ret, Distance2d(ret, p)
}

// LineClosestPoint returns the point (and position) on line closest to point.
// LineClosestPoint finds the nearest point on a line (represented as a slice of Points) to a given point.
// Based on go-geom DistanceFromPointToLineString
// It returns three values:
// - The closest point on the line
// - The index of the line segment containing the closest point (0-based)
// - The normalized position along the line (0.0 to 1.0) where the closest point lies
//
// The line must contain at least 2 points. If the line has length 0 (single point or empty),
// it returns the input point, index 0, and position 0.
//
// The calculation uses Haversine distance for geographic coordinates.
//
// Parameters:
// - line: A slice of Points representing the line segments
// - point: The reference Point to find the closest position to
//
// Returns:
// - Point: The closest point on the line
// - int: Index of the segment containing the closest point
// - float64: Normalized position along the line (0.0 to 1.0)
func LineClosestPoint(line []Point, point Point) (Point, int, float64) {
position := 0.0
length := LengthHaversine(line)
Expand Down Expand Up @@ -60,8 +92,23 @@ func LineClosestPoint(line []Point, point Point) (Point, int, float64) {
return minp, minidx, position / length
}

// CutBetweenPoints attempts to cut a line based on the
// relative positions of two nearby points projected onto the line.
// CutBetweenPoints extracts a portion of a line between two points by finding the closest segments.
// It takes a line (represented as a slice of Points), and two points (from and to) as input.
// The function finds the closest segments to both input points and returns a new line that starts
// at the projection of 'from' on its closest segment and ends at the projection of 'to' on its closest segment.
// The returned line includes all original vertices between these segments.
//
// Parameters:
// - line: []Point - Input line as a slice of Points
// - from: Point - Starting point to cut from
// - to: Point - Ending point to cut to
//
// Returns:
// - []Point - A new slice of Points representing the cut portion of the original line
// - nil if the input line has fewer than 2 points
//
// Note: The function assumes the input points are relatively close to the line.
// The search for the end point starts from the start point's segment to maintain proper ordering.
func CutBetweenPoints(line []Point, from Point, to Point) []Point {
startPoint := Point{}
startNear := 1_000_000.0
Expand Down Expand Up @@ -95,23 +142,27 @@ func CutBetweenPoints(line []Point, from Point, to Point) []Point {
return coords
}

// func CutBetweenPoints(line []Point, startPoint Point, endPoint Point) []Point {
// spt, sidx, _ := LineClosestPoint(line, startPoint)
// ept, eidx, _ := LineClosestPoint(line, endPoint)
// if eidx < sidx {
// return nil
// }
// if DistanceHaversine(startPoint, spt) > 1000 || DistanceHaversine(endPoint, ept) > 1000 {
// return nil
// }
// var ret []Point
// ret = append(ret, spt)
// ret = append(ret, line[sidx:eidx]...)
// ret = append(ret, ept)
// return ret
// }

// CutBetweenPositions is similar to CutBetweenPoints but takes absolute positions.
// CutBetweenPositions returns a slice of points representing a segment of the input line
// between the specified start and end distances along the line.
//
// The function takes a line represented as a slice of Points, a slice of cumulative distances
// along the line (dists), and start/end distances (startDist, endDist) indicating where to cut.
//
// It returns a new slice containing:
// - An interpolated point at startDist
// - All original points between start and end positions
// - An interpolated point at endDist
//
// Returns nil if the cut positions cannot be determined (e.g. distances out of range).
//
// Parameters:
// - line: Slice of Points representing the polyline
// - dists: Slice of cumulative distances along the line
// - startDist: Distance along the line where cut should start
// - endDist: Distance along the line where cut should end
//
// Returns:
// - Slice of Points representing the cut segment, or nil if cut is not possible
func CutBetweenPositions(line []Point, dists []float64, startDist float64, endDist float64) []Point {
spt, ept, sidx, eidx, ok := cutBetweenPositions(line, dists, startDist, endDist)
if !ok {
Expand All @@ -124,7 +175,6 @@ func CutBetweenPositions(line []Point, dists []float64, startDist float64, endDi
return ret
}

// CutBetweenPositions is similar to CutBetweenPoints but takes absolute positions.
func cutBetweenPositions(line []Point, dists []float64, startDist float64, endDist float64) (Point, Point, int, int, bool) {
for i := 0; i < len(dists)-1; i++ {
if startDist >= dists[i] && startDist <= dists[i+1] {
Expand Down
23 changes: 0 additions & 23 deletions tlxy/line.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package tlxy

import (
"math"

"github.com/twpayne/go-polyline"
)

type Line []Point
Expand All @@ -13,27 +11,6 @@ type LineM struct {
Data []float64
}

func DecodePolyline(p string) ([]Point, error) {
return DecodePolylineBytes([]byte(p))
}

func DecodePolylineBytes(p []byte) ([]Point, error) {
coords, _, err := polyline.DecodeCoords(p)
var ret []Point
for _, c := range coords {
ret = append(ret, Point{Lon: c[1], Lat: c[0]})
}
return ret, err
}

func EncodePolyline(coords []Point) []byte {
var g [][]float64
for _, c := range coords {
g = append(g, []float64{c.Lat, c.Lon})
}
return polyline.EncodeCoords(g)
}

// LineRelativePositionsFallback returns the relative position along the line for each point.
// TODO: use Haversine
func LineRelativePositionsFallback(line []Point) []float64 {
Expand Down
49 changes: 0 additions & 49 deletions tlxy/line_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package tlxy

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestLineRelativePositions(t *testing.T) {
Expand Down Expand Up @@ -114,53 +112,6 @@ func TestContains(t *testing.T) {
}
}

func TestDecodePolyline(t *testing.T) {
check := "yfttIf{jR?B@BBDD?@ANDH@XHrAj@t@Z"
expect := []Point{
{Lon: -3.1738, Lat: 55.97821},
{Lon: -3.17382, Lat: 55.97821},
{Lon: -3.17384, Lat: 55.978199},
{Lon: -3.17387, Lat: 55.978179},
{Lon: -3.17387, Lat: 55.978149},
{Lon: -3.17386, Lat: 55.978139},
{Lon: -3.17389, Lat: 55.978059},
{Lon: -3.17390, Lat: 55.978009},
{Lon: -3.17395, Lat: 55.977879},
{Lon: -3.17417, Lat: 55.977459},
{Lon: -3.17431, Lat: 55.977189},
}
p, err := DecodePolyline(check)
if err != nil {
t.Fatal(err)
}
if len(expect) != len(p) {
t.Fatal("unequal length")
}
for i := range expect {
assert.InDelta(t, expect[i].Lon, p[i].Lon, 0.001)
assert.InDelta(t, expect[i].Lat, p[i].Lat, 0.001)
}
}

func TestEncodePolyline(t *testing.T) {
expect := "yfttIf{jR?B@BBDD?@ANDH@XHrAj@t@Z"
check := []Point{
{Lon: -3.1738, Lat: 55.97821},
{Lon: -3.17382, Lat: 55.97821},
{Lon: -3.17384, Lat: 55.978199},
{Lon: -3.17387, Lat: 55.978179},
{Lon: -3.17387, Lat: 55.978149},
{Lon: -3.17386, Lat: 55.978139},
{Lon: -3.17389, Lat: 55.978059},
{Lon: -3.17390, Lat: 55.978009},
{Lon: -3.17395, Lat: 55.977879},
{Lon: -3.17417, Lat: 55.977459},
{Lon: -3.17431, Lat: 55.977189},
}
p := EncodePolyline(check)
assert.Equal(t, expect, string(p))
}

func TestLineSimilarity(t *testing.T) {
// TODO
type testcase struct {
Expand Down
Loading

0 comments on commit 612e597

Please sign in to comment.