Better polygon detection logic

Refactored shared detection logic.
Fixed linestring, multilinestring, and multipoint bugs.
Added more geojson unit tests.
This commit is contained in:
Josh Baker 2018-03-09 17:22:38 -07:00
parent b1b7eda877
commit 4c2c1dd186
15 changed files with 817 additions and 327 deletions

294
geojson/detect.go Normal file
View File

@ -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)
}

362
geojson/detect_test.go Normal file
View File

@ -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")
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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.

View File

@ -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.