From 4c2c1dd186806a344f08c7ef2795274a3fadd917 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Fri, 9 Mar 2018 17:22:38 -0700 Subject: [PATCH] Better polygon detection logic Refactored shared detection logic. Fixed linestring, multilinestring, and multipoint bugs. Added more geojson unit tests. --- geojson/detect.go | 294 +++++++++++++++++++++++++++ geojson/detect_test.go | 362 ++++++++++++++++++++++++++++++++++ geojson/feature.go | 12 +- geojson/featurecollection.go | 28 +-- geojson/geometrycollection.go | 28 +-- geojson/linestring.go | 12 +- geojson/multilinestring.go | 77 ++++---- geojson/multipoint.go | 32 +-- geojson/multipolygon.go | 36 +--- geojson/object.go | 123 ------------ geojson/object_test.go | 69 +++++++ geojson/point.go | 12 +- geojson/poly/intersects.go | 29 ++- geojson/polygon.go | 18 +- geojson/simplepoint.go | 12 +- 15 files changed, 817 insertions(+), 327 deletions(-) create mode 100644 geojson/detect.go create mode 100644 geojson/detect_test.go diff --git a/geojson/detect.go b/geojson/detect.go new file mode 100644 index 00000000..e06b1336 --- /dev/null +++ b/geojson/detect.go @@ -0,0 +1,294 @@ +package geojson + +import ( + "github.com/tidwall/tile38/geojson/poly" +) + +// withinObjectShared returns true if g is within o +func withinObjectShared(g, o Object) bool { + bbp := o.bboxPtr() + if bbp != nil { + if !g.WithinBBox(*bbp) { + return false + } + if o.IsBBoxDefined() { + return true + } + } + switch o := o.(type) { + default: + return false + case SimplePoint: + return g.WithinBBox(o.CalculatedBBox()) + case Point: + return g.WithinBBox(o.CalculatedBBox()) + case MultiPoint: + for i := range o.Coordinates { + if g.Within(o.getPoint(i)) { + return true + } + } + return false + case LineString: + if len(o.Coordinates) == 0 { + return false + } + switch g := g.(type) { + default: + return false + case SimplePoint: + return poly.Point(Position{X: g.X, Y: g.Y, Z: 0}).IntersectsLineString(polyPositions(o.Coordinates)) + case Point: + return poly.Point(g.Coordinates).IntersectsLineString(polyPositions(o.Coordinates)) + case MultiPoint: + if len(o.Coordinates) == 0 { + return false + } + for _, p := range o.Coordinates { + if !poly.Point(p).IntersectsLineString(polyPositions(o.Coordinates)) { + return false + } + } + return true + } + case MultiLineString: + for i := range o.Coordinates { + if g.Within(o.getLineString(i)) { + return true + } + } + return false + case MultiPolygon: + for i := range o.Coordinates { + if g.Within(o.getPolygon(i)) { + return true + } + } + return false + case Feature: + return g.Within(o.Geometry) + case FeatureCollection: + for _, o := range o.Features { + if g.Within(o) { + return true + } + } + return false + case GeometryCollection: + for _, o := range o.Geometries { + if g.Within(o) { + return true + } + } + return false + case Polygon: + if len(o.Coordinates) == 0 { + return false + } + exterior, holes := polyExteriorHoles(o.Coordinates) + switch g := g.(type) { + default: + return false + case SimplePoint: + return poly.Point(Position{X: g.X, Y: g.Y, Z: 0}).Inside(exterior, holes) + case Point: + return poly.Point(g.Coordinates).Inside(exterior, holes) + case MultiPoint: + if len(g.Coordinates) == 0 { + return false + } + for i := range g.Coordinates { + if !g.getPoint(i).Within(o) { + return false + } + } + return true + case LineString: + return polyPositions(g.Coordinates).Inside(exterior, holes) + case MultiLineString: + if len(g.Coordinates) == 0 { + return false + } + for i := range g.Coordinates { + if !g.getLineString(i).Within(o) { + return false + } + } + return true + case Polygon: + if len(g.Coordinates) == 0 { + return false + } + return polyPositions(g.Coordinates[0]).Inside(exterior, holes) + case MultiPolygon: + if len(g.Coordinates) == 0 { + return false + } + for i := range g.Coordinates { + if !g.getPolygon(i).Within(o) { + return false + } + } + return true + case GeometryCollection: + if len(g.Geometries) == 0 { + return false + } + for _, g := range g.Geometries { + if !g.Within(o) { + return false + } + } + return true + case Feature: + return g.Geometry.Within(o) + case FeatureCollection: + if len(g.Features) == 0 { + return false + } + for _, g := range g.Features { + if !g.Within(o) { + return false + } + } + return true + } + } +} + +// intersectsObjectShared detects if g intersects with o +func intersectsObjectShared(g, o Object) bool { + bbp := o.bboxPtr() + if bbp != nil { + if !g.IntersectsBBox(*bbp) { + return false + } + if o.IsBBoxDefined() { + return true + } + } + switch o := o.(type) { + default: + return false + case SimplePoint: + return g.IntersectsBBox(o.CalculatedBBox()) + case Point: + return g.IntersectsBBox(o.CalculatedBBox()) + case MultiPoint: + for i := range o.Coordinates { + if o.getPoint(i).Intersects(g) { + return true + } + } + return false + case LineString: + if g, ok := g.(LineString); ok { + a := polyPositions(g.Coordinates) + b := polyPositions(o.Coordinates) + return a.LineStringIntersectsLineString(b) + } + return o.Intersects(g) + case MultiLineString: + for i := range o.Coordinates { + if g.Intersects(o.getLineString(i)) { + return true + } + } + return false + case MultiPolygon: + for i := range o.Coordinates { + if g.Intersects(o.getPolygon(i)) { + return true + } + } + return false + case Feature: + return g.Intersects(o.Geometry) + case FeatureCollection: + for _, f := range o.Features { + if g.Intersects(f) { + return true + } + } + return false + case GeometryCollection: + for _, f := range o.Geometries { + if g.Intersects(f) { + return true + } + } + return false + case Polygon: + if len(o.Coordinates) == 0 { + return false + } + exterior, holes := polyExteriorHoles(o.Coordinates) + switch g := g.(type) { + default: + return false + case SimplePoint: + return poly.Point(Position{X: g.X, Y: g.Y, Z: 0}).Intersects(exterior, holes) + case Point: + return poly.Point(g.Coordinates).Intersects(exterior, holes) + case MultiPoint: + for i := range g.Coordinates { + if g.getPoint(i).Intersects(o) { + return true + } + } + return false + case LineString: + return polyPositions(g.Coordinates).LineStringIntersects(exterior, holes) + case MultiLineString: + for i := range g.Coordinates { + if g.getLineString(i).Intersects(o) { + return true + } + } + return false + case Polygon: + if len(g.Coordinates) == 0 { + return false + } + return polyPositions(g.Coordinates[0]).Intersects(exterior, holes) + case MultiPolygon: + for i := range g.Coordinates { + if g.getPolygon(i).Intersects(o) { + return true + } + } + return false + case GeometryCollection: + for _, g := range g.Geometries { + if g.Intersects(o) { + return true + } + } + return false + case Feature: + return g.Geometry.Intersects(o) + case FeatureCollection: + for _, g := range g.Features { + if g.Intersects(o) { + return true + } + } + return false + } + } +} + +// The object's calculated bounding box must intersect the radius of the circle to pass. +func nearbyObjectShared(g Object, x, y float64, meters float64) bool { + if !g.hasPositions() { + return false + } + center := Position{X: x, Y: y, Z: 0} + bbox := g.CalculatedBBox() + if bbox.Min.X == bbox.Max.X && bbox.Min.Y == bbox.Max.Y { + // just a point, return is point is inside of the circle + return center.DistanceTo(bbox.Min) <= meters + } + circlePoly := CirclePolygon(x, y, meters, 12) + return g.Intersects(circlePoly) +} diff --git a/geojson/detect_test.go b/geojson/detect_test.go new file mode 100644 index 00000000..ffc55efa --- /dev/null +++ b/geojson/detect_test.go @@ -0,0 +1,362 @@ +package geojson + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +// https://gist.github.com/tidwall/5524c468fa4212b89e9c3532a5b1f355 +var detectJSON = `{"type":"FeatureCollection","features":[ + {"type":"Feature","properties":{"id":"point1"},"geometry":{"type":"Point","coordinates":[-73.98549556732178,40.72198994979771]}}, + {"type":"Feature","properties":{"id":"polygon1"},"geometry":{"type":"Polygon","coordinates":[[[-74.0035629272461,40.71994085251552],[-73.98914337158203,40.71994085251552],[-73.98914337158203,40.72755146730012],[-74.0035629272461,40.72755146730012],[-74.0035629272461,40.71994085251552]]]}}, + {"type":"Feature","properties":{"id":"linestring1"},"geometry":{"type":"LineString","coordinates":[[-73.98382186889648,40.73652697126574],[-73.98821868896482,40.73652697126574],[-73.97420883178711,40.72943772441242]]}}, + {"type":"Feature","properties":{"id":"linestring2"},"geometry":{"type":"LineString","coordinates":[[-73.98146152496338,40.72716120053256],[-73.99098873138428,40.724754504892424]]}}, + {"type":"Feature","properties":{"id":"linestring3"},"geometry":{"type":"LineString","coordinates":[[-73.98386478424072,40.72696606629052],[-73.98090362548828,40.72501469240076],[-73.97837162017821,40.72621804639551]]}}, + {"type":"Feature","properties":{"id":"polygon2","fill":"#0433ff"},"geometry":{"type":"Polygon","coordinates":[[[-73.98661136627197,40.72540497175607],[-73.99064540863037,40.71938791069558],[-73.98807048797607,40.71779411151556],[-73.97571086883545,40.72338850378556],[-73.98017406463623,40.72960033028089],[-73.98661136627197,40.72540497175607]]]}}, + {"type":"Feature","properties":{"id":"polygon3"},"geometry":{"type":"Polygon","coordinates":[[[-73.98352146148682,40.72550254123727],[-73.98579597473145,40.72088409560772],[-73.97914409637451,40.72251034541217],[-73.98017406463623,40.72599038649773],[-73.98352146148682,40.72550254123727]],[[-73.98300647735596,40.72439674540761],[-73.98111820220947,40.72446179272971],[-73.98141860961913,40.7221525738643],[-73.98300647735596,40.72439674540761]]]}}, + {"type":"Feature","properties":{"id":"multipoint1","marker-color":"#941751"},"geometry":{"type":"MultiPoint","coordinates":[[-73.98957252502441,40.72049378974239],[-73.9897871017456,40.720233584560724],[-73.9897871017456,40.721664700472566],[-73.99085998535155,40.720916620993194],[-73.9912462234497,40.720331161623065]]}}, + {"type":"Feature","properties":{"id":"multilinestring1","stroke":"#941751"},"geometry":{"type":"MultiLineString","coordinates":[[[-73.98442268371582,40.72459188718318],[-73.98463726043701,40.72384384060296],[-73.98382186889648,40.72355112443509]],[[-73.9850664138794,40.72358364851732],[-73.98476600646973,40.72485207532725],[-73.9854097366333,40.72491712220435]]]}}, + {"type":"Feature","properties":{"id":"multipolygon1","fill":"#941751"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-73.98021697998047,40.72429917430525],[-73.97892951965332,40.7250472157678],[-73.98000240325926,40.72524235563617],[-73.98021697998047,40.72429917430525]]],[[[-73.97901535034178,40.72452683998823],[-73.9788007736206,40.72345355209305],[-73.97764205932617,40.72410403167144],[-73.97901535034178,40.72452683998823]]]]}}, + {"type":"Feature","properties":{"id":"point2"},"geometry":{"type":"Point","coordinates":[-73.98326396942139,40.723681220668624]}}, + {"type":"Feature","properties":{"id":"point3"},"geometry":{"type":"Point","coordinates":[-73.98196396942139,40.723681220668624]}}, + {"type":"Feature","properties":{"id":"point4"},"geometry":{"type":"Point","coordinates":[-73.9785396942139,40.7238220668624]}} +]}` + +func getByID(id string) Object { + var r gjson.Result + gjson.Get(detectJSON, "features").ForEach(func(_, v gjson.Result) bool { + if v.Get("properties.id").String() == id { + r = v.Get("geometry") + return false + } + return true + }) + if !r.Exists() { + panic("not found '" + id + "'") + } + o, err := ObjectJSON(r.String()) + if err != nil { + panic(err) + } + if p, ok := o.(SimplePoint); ok { + o = Point{Coordinates: Position{X: p.X, Y: p.Y}} + } + return o +} + +func toSimplePoint(p Object) Object { + return SimplePoint{X: p.(Point).Coordinates.X, Y: p.(Point).Coordinates.Y} +} + +// Basic geometry detections +// Point -> Point +// Point -> MultiPoint +// Point -> LineString +// Point -> MultiLineString +// Point -> Polygon +// Point -> MultiPolygon +// MultiPoint -> Point +// MultiPoint -> MultiPoint +// MultiPoint -> LineString +// MultiPoint -> MultiLineString +// MultiPoint -> Polygon +// MultiPoint -> MultiPolygon +// LineString -> Point +// LineString -> MultiPoint +// LineString -> LineString +// LineString -> MultiLineString +// LineString -> Polygon +// LineString -> MultiPolygon +// MultiLineString -> Point +// MultiLineString -> MultiPoint +// MultiLineString -> LineString +// MultiLineString -> MultiLineString +// MultiLineString -> Polygon +// MultiLineString -> MultiPolygon +// Polygon -> Point +// Polygon -> MultiPoint +// Polygon -> LineString +// Polygon -> MultiLineString +// Polygon -> Polygon +// Polygon -> MultiPolygon +// MultiPolygon -> Point +// MultiPolygon -> MultiPoint +// MultiPolygon -> LineString +// MultiPolygon -> MultiLineString +// MultiPolygon -> Polygon +// MultiPolygon -> MultiPolygon + +func TestDetectSimplePointSimplePoint(t *testing.T) { + p1 := toSimplePoint(getByID("point1")) + p2 := toSimplePoint(getByID("point2")) + if p1.Intersects(p2) { + t.Fatal("expected false") + } + if !p1.Intersects(p1) { + t.Fatal("expected true") + } + if p1.Within(p2) { + t.Fatal("expected false") + } + if !p1.Within(p1) { + t.Fatal("expected true") + } +} +func TestDetectPointPoint(t *testing.T) { + p1 := getByID("point1") + p2 := getByID("point2") + if p1.Intersects(p2) { + t.Fatal("expected false") + } + if !p1.Intersects(p1) { + t.Fatal("expected true") + } + if p1.Within(p2) { + t.Fatal("expected false") + } + if !p1.Within(p1) { + t.Fatal("expected true") + } +} +func TestDetectPointMultPoint(t *testing.T) { + p1 := getByID("point1") + mp1 := getByID("multipoint1") + if p1.Intersects(mp1) { + t.Fatal("expected false") + } + if p1.Within(mp1) { + t.Fatal("expected false") + } + pp := Point{Coordinates: mp1.(MultiPoint).Coordinates[0]} + if !pp.Intersects(mp1) { + t.Fatal("expected true") + } + if !pp.Within(mp1) { + t.Fatal("expected true") + } +} +func TestDetectPointLineString(t *testing.T) { + p1 := getByID("point1") + ls1 := getByID("linestring1") + if p1.Intersects(ls1) { + t.Fatal("expected false") + } + if p1.Within(ls1) { + t.Fatal("expected false") + } + pp := Point{Coordinates: ls1.(LineString).Coordinates[0]} + if !pp.Intersects(ls1) { + t.Fatal("expected true") + } + if !pp.Within(ls1) { + t.Fatal("expected true") + } +} +func TestDetectPointMultiLineString(t *testing.T) { + p1 := getByID("point1") + mls1 := getByID("multilinestring1") + if p1.Intersects(mls1) { + t.Fatal("expected false") + } + if p1.Within(mls1) { + t.Fatal("expected false") + } + pp := Point{Coordinates: mls1.(MultiLineString).Coordinates[0][1]} + if !pp.Intersects(mls1) { + t.Fatal("expected true") + } + if !pp.Within(mls1) { + t.Fatal("expected true") + } +} +func TestDetectPointPolygon(t *testing.T) { + p1 := getByID("point1") + pl3 := getByID("polygon3") + if p1.Intersects(pl3) { + t.Fatal("expected false") + } + if p1.Within(pl3) { + t.Fatal("expected false") + } + p2 := getByID("point2") + if !p2.Intersects(pl3) { + t.Fatal("expected true") + } + if !p2.Within(pl3) { + t.Fatal("expected true") + } + p3 := getByID("point3") + if p3.Intersects(pl3) { + t.Fatal("expected false") + } + if p3.Within(pl3) { + t.Fatal("expected false") + } +} + +func TestDetectPointMultiPolygon(t *testing.T) { + p3 := getByID("point3") + p4 := getByID("point4") + mp1 := getByID("multipolygon1") + if p3.Intersects(mp1) { + t.Fatal("expected false") + } + if p3.Within(mp1) { + t.Fatal("expected false") + } + if !p4.Intersects(mp1) { + t.Fatal("expected true") + } + if !p4.Within(mp1) { + t.Fatal("expected true") + } +} + +func TestDetectMultiPointPoint(t *testing.T) { + p1 := getByID("point1") + mp1 := getByID("multipoint1") + if mp1.Intersects(p1) { + t.Fatal("expected false") + } + if mp1.Within(p1) { + t.Fatal("expected false") + } + pp := Point{Coordinates: mp1.(MultiPoint).Coordinates[0]} + if !mp1.Intersects(pp) { + t.Fatal("expected true") + } + if mp1.Within(pp) { + t.Fatal("expected false") + } +} + +func TestDetectMultiPointPolygon(t *testing.T) { + mp1 := getByID("multipoint1") + pl1 := getByID("polygon1") + pl2 := getByID("polygon2") + pl3 := getByID("polygon3") + if !mp1.Intersects(pl1) { + t.Fatal("expected true") + } + if !mp1.Within(pl1) { + t.Fatal("expected true") + } + if !mp1.Intersects(pl2) { + t.Fatal("expected true") + } + if mp1.Within(pl2) { + t.Fatal("expected false") + } + if mp1.Intersects(pl3) { + t.Fatal("expected false") + } + if mp1.Within(pl3) { + t.Fatal("expected false") + } +} + +func TestDetectLineStringLineString(t *testing.T) { + ls1 := getByID("linestring1") + ls2 := getByID("linestring2") + ls3 := getByID("linestring3") + if ls1.Intersects(ls2) { + t.Fatal("expected false") + } + if ls2.Intersects(ls1) { + t.Fatal("expected false") + } + if !ls2.Intersects(ls3) { + t.Fatal("expected true") + } + if !ls2.Intersects(ls3) { + t.Fatal("expected true") + } +} +func TestDetectLineStringPolygon(t *testing.T) { + ls1 := getByID("linestring1") + ls2 := getByID("linestring2") + ls3 := getByID("linestring3") + pl1 := getByID("polygon1") + pl2 := getByID("polygon2") + pl3 := getByID("polygon3") + + if ls1.Intersects(pl1) { + t.Fatal("expected false") + } + if pl1.Intersects(ls1) { + t.Fatal("expected false") + } + if ls1.Intersects(pl3) { + t.Fatal("expected false") + } + if pl3.Intersects(ls1) { + t.Fatal("expected false") + } + if !ls2.Intersects(pl1) { + t.Fatal("expected true") + } + if !pl2.Intersects(ls2) { + t.Fatal("expected true") + } + if ls2.Within(pl1) { + t.Fatal("expected false") + } + if ls2.Intersects(pl3) { + t.Fatal("expected false") + } + if pl3.Intersects(ls2) { + t.Fatal("expected false") + } + if !ls3.Intersects(pl2) { + t.Fatal("expected true") + } + if !pl2.Intersects(ls3) { + t.Fatal("expected true") + } + if !ls3.Within(pl2) { + t.Fatal("expected true") + } + if ls2.Intersects(pl3) { + t.Fatal("expected false") + } + if pl3.Intersects(ls2) { + t.Fatal("expected false") + } + if ls2.Intersects(pl3) { + t.Fatal("expected false") + } + if pl3.Intersects(ls2) { + t.Fatal("expected false") + } + if !ls3.Intersects(pl3) { + t.Fatal("expected true") + } + if !pl3.Intersects(ls3) { + t.Fatal("expected true") + } +} +func TestDetectMultiLineStringPolygon(t *testing.T) { + mls1 := getByID("multilinestring1") + pl1 := getByID("polygon1") + pl2 := getByID("polygon2") + pl3 := getByID("polygon3") + if mls1.Intersects(pl1) { + t.Fatal("expected false") + } + if mls1.Within(pl1) { + t.Fatal("expected false") + } + if !mls1.Intersects(pl2) { + t.Fatal("expected true") + } + if !mls1.Within(pl2) { + t.Fatal("expected true") + } + if !mls1.Intersects(pl3) { + t.Fatal("expected true") + } + if mls1.Within(pl3) { + t.Fatal("expected false") + } +} diff --git a/geojson/feature.go b/geojson/feature.go index 8991378e..56bb28e2 100644 --- a/geojson/feature.go +++ b/geojson/feature.go @@ -192,20 +192,12 @@ func (g Feature) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g Feature) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - return g.Geometry.Within(o) - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g Feature) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - return g.Geometry.Intersects(o) - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/featurecollection.go b/geojson/featurecollection.go index 9870c64d..bdee6cc4 100644 --- a/geojson/featurecollection.go +++ b/geojson/featurecollection.go @@ -178,36 +178,12 @@ func (g FeatureCollection) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g FeatureCollection) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - if len(g.Features) == 0 { - return false - } - for _, f := range g.Features { - if !f.Within(o) { - return false - } - } - return true - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g FeatureCollection) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - if len(g.Features) == 0 { - return false - } - for _, f := range g.Features { - if f.Intersects(o) { - return true - } - } - return false - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/geometrycollection.go b/geojson/geometrycollection.go index ac01af2f..3aacc46a 100644 --- a/geojson/geometrycollection.go +++ b/geojson/geometrycollection.go @@ -177,36 +177,12 @@ func (g GeometryCollection) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g GeometryCollection) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - if len(g.Geometries) == 0 { - return false - } - for _, g := range g.Geometries { - if !g.Within(o) { - return false - } - } - return true - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g GeometryCollection) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - if len(g.Geometries) == 0 { - return false - } - for _, g := range g.Geometries { - if g.Intersects(o) { - return true - } - } - return false - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/linestring.go b/geojson/linestring.go index eee21c22..e4de9d50 100644 --- a/geojson/linestring.go +++ b/geojson/linestring.go @@ -98,20 +98,12 @@ func (g LineString) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g LineString) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - return polyPositions(g.Coordinates).Inside(polyExteriorHoles(v.Coordinates)) - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g LineString) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - return polyPositions(g.Coordinates).LineStringIntersects(polyExteriorHoles(v.Coordinates)) - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/multilinestring.go b/geojson/multilinestring.go index 1e47d072..01123d3a 100644 --- a/geojson/multilinestring.go +++ b/geojson/multilinestring.go @@ -2,7 +2,6 @@ package geojson import ( "github.com/tidwall/tile38/geojson/geohash" - "github.com/tidwall/tile38/geojson/poly" ) // MultiLineString is a geojson object with the type "MultiLineString" @@ -10,32 +9,50 @@ type MultiLineString struct { Coordinates [][]Position BBox *BBox bboxDefined bool + linestrings []LineString } func fillMultiLineString(coordinates [][]Position, bbox *BBox, err error) (MultiLineString, error) { + linestrings := make([]LineString, len(coordinates)) if err == nil { - for _, coordinates := range coordinates { - if len(coordinates) < 2 { - err = errLineStringInvalidCoordinates + for i, ps := range coordinates { + linestrings[i], err = fillLineString(ps, nil, nil) + if err != nil { break } } } bboxDefined := bbox != nil if !bboxDefined { - cbbox := level3CalculatedBBox(coordinates, nil, false) + cbbox := mlCalculatedBBox(linestrings, nil) bbox = &cbbox } return MultiLineString{ Coordinates: coordinates, BBox: bbox, bboxDefined: bboxDefined, + linestrings: linestrings, }, err } +func mlCalculatedBBox(linestrings []LineString, bbox *BBox) BBox { + if bbox != nil { + return *bbox + } + var cbbox BBox + for i, g := range linestrings { + if i == 0 { + cbbox = g.CalculatedBBox() + } else { + cbbox = cbbox.union(g.CalculatedBBox()) + } + } + return cbbox +} + // CalculatedBBox is exterior bbox containing the object. func (g MultiLineString) CalculatedBBox() BBox { - return level3CalculatedBBox(g.Coordinates, g.BBox, false) + return mlCalculatedBBox(g.linestrings, g.BBox) } // CalculatedPoint is a point representation of the object. @@ -93,6 +110,13 @@ func (g MultiLineString) hasPositions() bool { return false } +func (g MultiLineString) getLineString(index int) LineString { + if index < len(g.linestrings) { + return g.linestrings[index] + } + return LineString{Coordinates: g.Coordinates[index]} +} + // WithinBBox detects if the object is fully contained inside a bbox. func (g MultiLineString) WithinBBox(bbox BBox) bool { if g.bboxDefined { @@ -101,15 +125,10 @@ func (g MultiLineString) WithinBBox(bbox BBox) bool { if len(g.Coordinates) == 0 { return false } - for _, ls := range g.Coordinates { - if len(ls) == 0 { + for i := range g.Coordinates { + if !g.getLineString(i).WithinBBox(bbox) { return false } - for _, p := range ls { - if !poly.Point(p).InsideRect(rectBBox(bbox)) { - return false - } - } } return true } @@ -119,8 +138,8 @@ func (g MultiLineString) IntersectsBBox(bbox BBox) bool { if g.bboxDefined { return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox)) } - for _, ls := range g.Coordinates { - if polyPositions(ls).IntersectsRect(rectBBox(bbox)) { + for i := range g.Coordinates { + if g.getLineString(i).IntersectsBBox(bbox) { return true } } @@ -129,36 +148,12 @@ func (g MultiLineString) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g MultiLineString) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - for _, ls := range g.Coordinates { - if !polyPositions(ls).Inside(polyExteriorHoles(v.Coordinates)) { - return false - } - } - return true - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g MultiLineString) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - for _, ls := range g.Coordinates { - if polyPositions(ls).Intersects(polyExteriorHoles(v.Coordinates)) { - return true - } - } - return false - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/multipoint.go b/geojson/multipoint.go index 3328f7c5..8a1af796 100644 --- a/geojson/multipoint.go +++ b/geojson/multipoint.go @@ -25,6 +25,10 @@ func fillMultiPoint(coordinates []Position, bbox *BBox, err error) (MultiPoint, }, err } +func (g MultiPoint) getPoint(index int) Point { + return Point{Coordinates: g.Coordinates[index]} +} + // CalculatedBBox is exterior bbox containing the object. func (g MultiPoint) CalculatedBBox() BBox { return level2CalculatedBBox(g.Coordinates, g.BBox) @@ -108,36 +112,12 @@ func (g MultiPoint) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g MultiPoint) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - for _, p := range g.Coordinates { - if !poly.Point(p).Inside(polyExteriorHoles(v.Coordinates)) { - return false - } - } - return true - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g MultiPoint) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - for _, p := range g.Coordinates { - if poly.Point(p).Intersects(polyExteriorHoles(v.Coordinates)) { - return true - } - } - return true - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/multipolygon.go b/geojson/multipolygon.go index af2dc96a..f55f9d50 100644 --- a/geojson/multipolygon.go +++ b/geojson/multipolygon.go @@ -22,7 +22,7 @@ func fillMultiPolygon(coordinates [][][]Position, bbox *BBox, err error) (MultiP } bboxDefined := bbox != nil if !bboxDefined { - cbbox := calculatedBBox(polygons, nil) + cbbox := mpCalculatedBBox(polygons, nil) bbox = &cbbox } return MultiPolygon{ @@ -33,16 +33,16 @@ func fillMultiPolygon(coordinates [][][]Position, bbox *BBox, err error) (MultiP }, err } -func calculatedBBox(polygons []Polygon, bbox *BBox) BBox { +func mpCalculatedBBox(polygons []Polygon, bbox *BBox) BBox { if bbox != nil { return *bbox } var cbbox BBox - for i, p := range polygons { + for i, g := range polygons { if i == 0 { - cbbox = p.CalculatedBBox() + cbbox = g.CalculatedBBox() } else { - cbbox = cbbox.union(p.CalculatedBBox()) + cbbox = cbbox.union(g.CalculatedBBox()) } } return cbbox @@ -50,7 +50,7 @@ func calculatedBBox(polygons []Polygon, bbox *BBox) BBox { // CalculatedBBox is exterior bbox containing the object. func (g MultiPolygon) CalculatedBBox() BBox { - return calculatedBBox(g.polygons, g.BBox) + return mpCalculatedBBox(g.polygons, g.BBox) } // CalculatedPoint is a point representation of the object. @@ -148,32 +148,12 @@ func (g MultiPolygon) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g MultiPolygon) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - if !v.Within(o) { - return false - } - return true - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g MultiPolygon) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - if v.Intersects(o) { - return true - } - return false - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/object.go b/geojson/object.go index 3ba60653..ac19f4aa 100644 --- a/geojson/object.go +++ b/geojson/object.go @@ -182,114 +182,6 @@ func objectMap(json string, from int) (Object, error) { return o, err } -func withinObjectShared(g Object, o Object, pin func(v Polygon) bool) bool { - bbp := o.bboxPtr() - if bbp != nil { - if !g.WithinBBox(*bbp) { - return false - } - if o.IsBBoxDefined() { - return true - } - } - switch v := o.(type) { - default: - return false - case Point: - return g.WithinBBox(v.CalculatedBBox()) - case SimplePoint: - return g.WithinBBox(v.CalculatedBBox()) - case Polygon: - if len(v.Coordinates) == 0 { - return false - } - return pin(v) - case MultiPolygon: - for i := range v.Coordinates { - if pin(v.getPolygon(i)) { - return true - } - } - return false - case Feature: - return g.Within(v.Geometry) - case FeatureCollection: - if len(v.Features) == 0 { - return false - } - for _, f := range v.Features { - if !g.Within(f) { - return false - } - } - return true - case GeometryCollection: - if len(v.Geometries) == 0 { - return false - } - for _, f := range v.Geometries { - if !g.Within(f) { - return false - } - } - return true - } -} - -func intersectsObjectShared(g Object, o Object, pin func(v Polygon) bool) bool { - bbp := o.bboxPtr() - if bbp != nil { - if !g.IntersectsBBox(*bbp) { - return false - } - if o.IsBBoxDefined() { - return true - } - } - switch v := o.(type) { - default: - return false - case Point: - return g.IntersectsBBox(v.CalculatedBBox()) - case SimplePoint: - return g.IntersectsBBox(v.CalculatedBBox()) - case Polygon: - if len(v.Coordinates) == 0 { - return false - } - return pin(v) - case MultiPolygon: - for _, coords := range v.Coordinates { - if pin(Polygon{Coordinates: coords}) { - return true - } - } - return false - case Feature: - return g.Intersects(v.Geometry) - case FeatureCollection: - if len(v.Features) == 0 { - return false - } - for _, f := range v.Features { - if g.Intersects(f) { - return true - } - } - return false - case GeometryCollection: - if len(v.Geometries) == 0 { - return false - } - for _, f := range v.Geometries { - if g.Intersects(f) { - return true - } - } - return false - } -} - // CirclePolygon returns a Polygon around the radius. func CirclePolygon(x, y, meters float64, steps int) Polygon { if steps < 3 { @@ -310,21 +202,6 @@ func CirclePolygon(x, y, meters float64, steps int) Polygon { return p } -// The object's calculated bounding box must intersect the radius of the circle to pass. -func nearbyObjectShared(g Object, x, y float64, meters float64) bool { - if !g.hasPositions() { - return false - } - center := Position{X: x, Y: y, Z: 0} - bbox := g.CalculatedBBox() - if bbox.Min.X == bbox.Max.X && bbox.Min.Y == bbox.Max.Y { - // just a point, return is point is inside of the circle - return center.DistanceTo(bbox.Min) <= meters - } - circlePoly := CirclePolygon(x, y, meters, 12) - return g.Intersects(circlePoly) -} - func jsonMarshalString(s string) []byte { for i := 0; i < len(s); i++ { if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 { diff --git a/geojson/object_test.go b/geojson/object_test.go index f6e9ef66..d2328441 100644 --- a/geojson/object_test.go +++ b/geojson/object_test.go @@ -42,3 +42,72 @@ func TestCirclePolygon(t *testing.T) { t.Fatal("should intersect") } } + +func TestIssue281(t *testing.T) { + p := testJSON(t, `{"type":"Polygon","coordinates":[[ + [-74.008283,40.718249], + [-74.007339,40.713305], + [-73.999013,40.714866], + [-74.001760,40.720851], + [-74.008283,40.718249] + ]]}`) + + // intersects polygon + ls1 := testJSON(t, `{"type":"LineString","coordinates":[ + [-74.003648,40.717533], + [-73.99575233459473,40.72046126415031], + [-73.99721145629883,40.72338850378556] + ]}`) + + // outside polygon + ls2 := testJSON(t, `{"type":"LineString","coordinates":[ + [-74.007682,40.722998], + [-74.001932,40.728462], + [-74.001846,40.723583] + ]}`) + + // inside polygon + ls3 := testJSON(t, `{"type":"LineString","coordinates":[ + [-74.006910,40.717598], + [-74.006137,40.715387], + [-74.001331,40.715907] + ]}`) + + if !ls1.Intersects(p) { + t.Fatalf("expected true") + } + if !p.Intersects(ls1) { + t.Fatalf("expected true") + } + if ls1.Within(p) { + t.Fatalf("expected false") + } + if p.Within(ls1) { + t.Fatalf("expected false") + } + if ls2.Intersects(p) { + t.Fatalf("expected false") + } + if p.Intersects(ls2) { + t.Fatalf("expected false") + } + if ls2.Within(p) { + t.Fatalf("expected false") + } + if p.Within(ls2) { + t.Fatalf("expected false") + } + if !ls3.Intersects(p) { + t.Fatalf("expected true") + } + if !p.Intersects(ls3) { + t.Fatalf("expected true") + } + if !ls3.Within(p) { + t.Fatalf("expected true") + } + if p.Within(ls3) { + t.Fatalf("expected false") + } + +} diff --git a/geojson/point.go b/geojson/point.go index 59dfed47..fd02b75c 100644 --- a/geojson/point.go +++ b/geojson/point.go @@ -105,20 +105,12 @@ func (g Point) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g Point) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - return poly.Point(g.Coordinates).Inside(polyExteriorHoles(v.Coordinates)) - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g Point) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - return poly.Point(g.Coordinates).Intersects(polyExteriorHoles(v.Coordinates)) - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/poly/intersects.go b/geojson/poly/intersects.go index c6017ece..e8238f96 100644 --- a/geojson/poly/intersects.go +++ b/geojson/poly/intersects.go @@ -1,5 +1,15 @@ package poly +// IntersectsLineString detect if a point intersects a linestring +func (p Point) IntersectsLineString(exterior Polygon) bool { + for j := 0; j < len(exterior); j++ { + if raycast(p, exterior[j], exterior[(j+1)%len(exterior)]).on { + return true + } + } + return false +} + // Intersects detects if a point intersects another polygon func (p Point) Intersects(exterior Polygon, holes []Polygon) bool { return p.Inside(exterior, holes) @@ -7,7 +17,7 @@ func (p Point) Intersects(exterior Polygon, holes []Polygon) bool { // Intersects detects if a rect intersects another polygon func (r Rect) Intersects(exterior Polygon, holes []Polygon) bool { - return r.Polygon().Inside(exterior, holes) + return r.Polygon().Intersects(exterior, holes) } // Intersects detects if a polygon intersects another polygon @@ -15,7 +25,24 @@ func (shape Polygon) Intersects(exterior Polygon, holes []Polygon) bool { return shape.doesIntersects(false, exterior, holes) } +// LineStringIntersectsLineString detects if a linestring intersects a linestring +// assume shape and exterior are actually linestrings +func (shape Polygon) LineStringIntersectsLineString(exterior Polygon) bool { + for i := 0; i < len(shape); i++ { + for j := 0; j < len(exterior); j++ { + if lineintersects( + shape[i], shape[(i+1)%len(shape)], + exterior[j], exterior[(j+1)%len(exterior)], + ) { + return true + } + } + } + return false +} + // LineStringIntersects detects if a polygon intersects a linestring +// assume shape is a linestring func (shape Polygon) LineStringIntersects(exterior Polygon, holes []Polygon) bool { return shape.doesIntersects(true, exterior, holes) } diff --git a/geojson/polygon.go b/geojson/polygon.go index 375b06c3..f1ad09f0 100644 --- a/geojson/polygon.go +++ b/geojson/polygon.go @@ -145,26 +145,12 @@ func (g Polygon) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g Polygon) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - return polyPositions(g.Coordinates[0]).Inside(polyExteriorHoles(v.Coordinates)) - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g Polygon) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - if len(g.Coordinates) == 0 { - return false - } - return polyPositions(g.Coordinates[0]).Intersects(polyExteriorHoles(v.Coordinates)) - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position. diff --git a/geojson/simplepoint.go b/geojson/simplepoint.go index 0d09ee97..01dd7c65 100644 --- a/geojson/simplepoint.go +++ b/geojson/simplepoint.go @@ -87,20 +87,12 @@ func (g SimplePoint) IntersectsBBox(bbox BBox) bool { // Within detects if the object is fully contained inside another object. func (g SimplePoint) Within(o Object) bool { - return withinObjectShared(g, o, - func(v Polygon) bool { - return poly.Point(Position{X: g.X, Y: g.Y, Z: 0}).Inside(polyExteriorHoles(v.Coordinates)) - }, - ) + return withinObjectShared(g, o) } // Intersects detects if the object intersects another object. func (g SimplePoint) Intersects(o Object) bool { - return intersectsObjectShared(g, o, - func(v Polygon) bool { - return poly.Point(Position{X: g.X, Y: g.Y, Z: 0}).Intersects(polyExteriorHoles(v.Coordinates)) - }, - ) + return intersectsObjectShared(g, o) } // Nearby detects if the object is nearby a position.