Skip to content

Commit

Permalink
Polyline files
Browse files Browse the repository at this point in the history
  • Loading branch information
irees committed Feb 6, 2025
1 parent c6912a1 commit 678d3a5
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 152 deletions.
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
265 changes: 185 additions & 80 deletions tlxy/pip_test.go
Original file line number Diff line number Diff line change
@@ -1,92 +1,14 @@
package tlxy

import (
"os"
"testing"

"github.com/interline-io/transitland-lib/internal/testpath"
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/geojson"
)

func TestSanFranciscoPoints(t *testing.T) {
// A very minimal GeoJSON feature for San Francisco :laugh:
sfFeature := &geojson.Feature{
ID: "san_francisco",
Geometry: geom.NewMultiPolygon(geom.XY).MustSetCoords([][][]geom.Coord{
// Main SF peninsula
{{
{-122.517910, 37.708131},
{-122.504800, 37.708131},
{-122.403800, 37.708131},
{-122.377400, 37.708131},
{-122.377400, 37.816239},
{-122.517910, 37.816239},
{-122.517910, 37.708131},
}},
// Treasure Island
{{
{-122.3750, 37.8150},
{-122.3750, 37.8320},
{-122.3650, 37.8320},
{-122.3650, 37.8150},
{-122.3750, 37.8150},
}},
}),
}
// Create Berkeley MultiPolygon feature
berkeleyFeature := &geojson.Feature{
ID: "berkeley",
Geometry: geom.NewMultiPolygon(geom.XY).MustSetCoords([][][]geom.Coord{
{{
{-122.324439, 37.853842},
{-122.324439, 37.900420},
{-122.229052, 37.900420},
{-122.229052, 37.853842},
{-122.324439, 37.853842},
}},
}),
}

// Create PolygonIndex with SF feature
idx, err := NewPolygonIndex(geojson.FeatureCollection{
Features: []*geojson.Feature{sfFeature, berkeleyFeature},
})
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
point Point
wantFeature string
wantFound bool
}{
{"Mission District", Point{-122.4194, 37.7601}, "san_francisco", true},
{"Financial District", Point{-122.4001, 37.7890}, "san_francisco", true},
{"Golden Gate Park", Point{-122.4862, 37.7694}, "san_francisco", true},
{"Berkeley", Point{-122.2729, 37.8715}, "berkeley", true},
{"Pacific Ocean", Point{-122.6000, 37.7500}, "", false},
{"San Jose", Point{-121.8863, 37.3382}, "", false},
{"Treasure Island", Point{-122.3704, 37.8235}, "san_francisco", true},
{"Alcatraz", Point{-122.4229, 37.8267}, "", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
feat, found := idx.FeatureAt(tt.point)
if found != tt.wantFound {
t.Errorf("FeatureAt(%v) found = %v, want %v", tt.point, found, tt.wantFound)
}
if tt.wantFound {
if feat == nil {
t.Errorf("FeatureAt(%v) feature = nil, want non-nil", tt.point)
} else if feat.ID != tt.wantFeature {
t.Errorf("FeatureAt(%v) feature ID = %v, want %v", tt.point, feat.ID, tt.wantFeature)
}
}
})
}
}

func TestPointInPolygon(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -208,3 +130,186 @@ func TestPolygonIndex_EmptyQueries(t *testing.T) {
t.Errorf("FeatureNameAt() on empty index = %v, %v; want \"\", false", name, found)
}
}

// Test with a simple GeoJSON feature collection containing two polygons
func TestPolygonIndex_SanFrancisco(t *testing.T) {
// A very minimal GeoJSON feature for San Francisco :laugh:
testFeatures := `{"type":"FeatureCollection","features":[{"type":"Feature","id":"san_francisco","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[-122.51791,37.708131],[-122.5048,37.708131],[-122.4038,37.708131],[-122.3774,37.708131],[-122.3774,37.816239],[-122.51791,37.816239],[-122.51791,37.708131]]],[[[-122.375,37.815],[-122.365,37.815],[-122.365,37.832],[-122.375,37.832],[-122.375,37.815]]]]}},{"type":"Feature","id":"berkeley","properties":{},"geometry":{"coordinates":[[[-122.31846628706155,37.895115355095655],[-122.31846628706155,37.845986688374026],[-122.22559180139987,37.845986688374026],[-122.22559180139987,37.895115355095655],[-122.31846628706155,37.895115355095655]]],"type":"Polygon"}}]}`
fc := geojson.FeatureCollection{}
if err := fc.UnmarshalJSON([]byte(testFeatures)); err != nil {
t.Fatal(err)
}

// Create PolygonIndex with SF feature
idx, err := NewPolygonIndex(fc)
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
point Point
wantFeature string
wantFound bool
}{
{"Mission District", Point{-122.4194, 37.7601}, "san_francisco", true},
{"Financial District", Point{-122.4001, 37.7890}, "san_francisco", true},
{"Golden Gate Park", Point{-122.4862, 37.7694}, "san_francisco", true},
{"Berkeley", Point{-122.2729, 37.8715}, "berkeley", true},
{"Pacific Ocean", Point{-122.6000, 37.7500}, "", false},
{"San Jose", Point{-121.8863, 37.3382}, "", false},
{"Treasure Island", Point{-122.3704, 37.8235}, "san_francisco", true},
{"Alcatraz", Point{-122.4229, 37.8267}, "", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
feat, found := idx.FeatureAt(tt.point)
if found != tt.wantFound {
t.Errorf("FeatureAt(%v) found = %v, want %v", tt.point, found, tt.wantFound)
}
if tt.wantFound {
if feat == nil {
t.Errorf("FeatureAt(%v) feature = nil, want non-nil", tt.point)
} else if feat.ID != tt.wantFeature {
t.Errorf("FeatureAt(%v) feature ID = %v, want %v", tt.point, feat.ID, tt.wantFeature)
}
}
})
}
}

// Test with polyline encoded features loaded from a file
func TestPolygonIndex_Timezones(t *testing.T) {
type testCase struct {
name string
point Point
expectName string
expectMissing bool
}

tcs := []testCase{
{
name: "new york",
expectName: "America/New_York",
point: Point{Lon: -74.132285, Lat: 40.625665},
},
{
name: "california",
expectName: "America/Los_Angeles",
point: Point{Lon: -122.431297, Lat: 37.773972},
},
{
name: "utah",
expectName: "America/Denver",
point: Point{Lon: -109.056664, Lat: 40.996479},
},
{
name: "colorado",
expectName: "America/Denver",
point: Point{Lon: -109.045685, Lat: 40.997833},
},
{
name: "wyoming",
expectName: "America/Denver",
point: Point{Lon: -109.050133, Lat: 41.002209},
},
{
name: "north dakota",
expectName: "America/Chicago",
point: Point{Lon: -100.964531, Lat: 45.946934},
},
{
name: "georgia",
expectName: "America/New_York",
point: Point{Lon: -82.066697, Lat: 30.370054},
},
{
name: "florida",
expectName: "America/New_York",
point: Point{Lon: -82.046522, Lat: 30.360419},
},
{
name: "saskatchewan",
expectName: "America/Mexico_City",
point: Point{Lon: -102.007904, Lat: 58.269615},
},
{
name: "manitoba",
expectName: "America/Chicago",
point: Point{Lon: -101.982025, Lat: 58.269245},
},
{
name: "texas",
expectName: "America/Chicago",
point: Point{Lon: -94.794261, Lat: 29.289210},
},
{
name: "texas water 1",
expectName: "America/Chicago",
point: Point{Lon: -94.784667, Lat: 29.286234},
},
{
name: "texas water 2",
expectMissing: true,
point: Point{Lon: -94.237, Lat: 26.874},
},
{
name: "canada maidstone 1",
expectName: "America/Denver",
point: Point{Lon: -108.96735, Lat: 53.01851},
},
{
name: "canada maidstone 2",
expectName: "America/Mexico_City",
point: Point{Lon: -108.86594, Lat: 52.99610},
},
{
name: "canada halifax",
expectName: "America/Halifax",
point: Point{Lon: -68.90401, Lat: 47.26115},
},
{
name: "phoenix exclave",
expectName: "America/Phoenix",
point: Point{Lon: -110.7767, Lat: 35.6494},
},
{
name: "phoenix exclave inclave",
expectName: "America/Denver",
point: Point{Lon: -110.1514, Lat: 35.7432},
},
}
fn := testpath.RelPath("testdata/tlxy/tz-example.polyline")
r, err := os.Open(fn)
if err != nil {
t.Fatal(err)
}
fc, err := PolylinesToGeojson(r)
if err != nil {
t.Fatal(err)
}
tzWorld, err := NewPolygonIndex(fc)
if err != nil {
t.Fatal(err)
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
feat, ok := tzWorld.FeatureAt(tc.point)
switch {
case tc.expectMissing:
if ok {
t.Errorf("expected missing, got %v", ok)
}
case feat == nil || !ok:
t.Errorf("expected feature, got nil")
case feat.ID != tc.expectName:
t.Errorf("expected %s, got %s", tc.expectName, feat.ID)
case feat.ID == tc.expectName:
// ok
default:
t.Errorf("unexpected test case")
}
})
}
}
Loading

0 comments on commit 678d3a5

Please sign in to comment.