mirror of https://github.com/tidwall/tile38.git
184 lines
4.4 KiB
Go
184 lines
4.4 KiB
Go
package geojson
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/tidwall/geojson/geo"
|
|
"github.com/tidwall/geojson/geometry"
|
|
)
|
|
|
|
// Circle ...
|
|
type Circle struct {
|
|
Object
|
|
center geometry.Point
|
|
meters float64
|
|
haversine float64
|
|
steps int
|
|
km bool
|
|
extra *extra
|
|
}
|
|
|
|
// NewCircle returns an circle object
|
|
func NewCircle(center geometry.Point, meters float64, steps int) *Circle {
|
|
if steps < 3 {
|
|
steps = 3
|
|
}
|
|
g := new(Circle)
|
|
g.center = center
|
|
g.meters = meters
|
|
g.steps = steps
|
|
if meters <= 0 {
|
|
g.Object = NewPoint(center)
|
|
} else {
|
|
meters = geo.NormalizeDistance(meters)
|
|
var points []geometry.Point
|
|
step := 360.0 / float64(steps)
|
|
i := 0
|
|
for deg := 360.0; deg > 0; deg -= step {
|
|
lat, lon := geo.DestinationPoint(center.Y, center.X, meters, deg)
|
|
points = append(points, geometry.Point{X: lon, Y: lat})
|
|
i++
|
|
}
|
|
// TODO: account for the pole and antimerdian. In most cases only a
|
|
// polygon is needed, but when the circle bounds passes the 90/180
|
|
// lines, we need to create a multipolygon
|
|
points = append(points, points[0])
|
|
g.Object = NewPolygon(
|
|
geometry.NewPoly(points, nil, geometry.DefaultIndexOptions),
|
|
)
|
|
g.haversine = geo.DistanceToHaversine(meters)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// AppendJSON ...
|
|
func (g *Circle) AppendJSON(dst []byte) []byte {
|
|
dst = append(dst, `{"type":"Feature","geometry":`...)
|
|
dst = append(dst, `{"type":"Point","coordinates":[`...)
|
|
dst = strconv.AppendFloat(dst, g.center.X, 'f', -1, 64)
|
|
dst = append(dst, ',')
|
|
dst = strconv.AppendFloat(dst, g.center.Y, 'f', -1, 64)
|
|
dst = append(dst, `]},"properties":{"type":"Circle","radius":`...)
|
|
dst = strconv.AppendFloat(dst, g.meters, 'f', -1, 64)
|
|
dst = append(dst, `,"radius_units":"m"}}`...)
|
|
return dst
|
|
}
|
|
|
|
// JSON ...
|
|
func (g *Circle) JSON() string {
|
|
return string(g.AppendJSON(nil))
|
|
}
|
|
|
|
// String ...
|
|
func (g *Circle) String() string {
|
|
return string(g.AppendJSON(nil))
|
|
}
|
|
|
|
// Meters returns the circle's radius
|
|
func (g *Circle) Meters() float64 {
|
|
return g.meters
|
|
}
|
|
|
|
// Center returns the circle's center point
|
|
func (g *Circle) Center() geometry.Point {
|
|
return g.center
|
|
}
|
|
|
|
// Within returns true if circle is contained inside object
|
|
func (g *Circle) Within(obj Object) bool {
|
|
return obj.Contains(g)
|
|
}
|
|
|
|
func (g *Circle) contains(p geometry.Point, allowOnEdge bool) bool {
|
|
h := geo.Haversine(p.Y, p.X, g.center.Y, g.center.X)
|
|
if allowOnEdge {
|
|
return h <= g.haversine
|
|
}
|
|
return h < g.haversine
|
|
}
|
|
|
|
// Contains returns true if the circle contains other object
|
|
func (g *Circle) Contains(obj Object) bool {
|
|
switch other := obj.(type) {
|
|
case *Point:
|
|
return g.contains(other.Center(), false)
|
|
case *Circle:
|
|
return other.Distance(g) < (other.meters + g.meters)
|
|
case *LineString:
|
|
for i := 0; i < other.base.NumPoints(); i++ {
|
|
if geoDistancePoints(other.base.PointAt(i), g.center) > g.meters {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case Collection:
|
|
for _, p := range other.Children() {
|
|
if !g.Contains(p) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
// No simple cases, so using polygon approximation.
|
|
return g.Object.Contains(other)
|
|
}
|
|
}
|
|
|
|
func (g *Circle) intersectsSegment(seg geometry.Segment) bool {
|
|
start, end := seg.A, seg.B
|
|
|
|
// These are faster checks. If they succeed there's no need do complicate things.
|
|
if g.contains(start, true) {
|
|
return true
|
|
}
|
|
if g.contains(end, true) {
|
|
return true
|
|
}
|
|
|
|
// Distance between start and end
|
|
l := geo.DistanceTo(start.Y, start.X, end.Y, end.X)
|
|
|
|
// Unit direction vector
|
|
dx := (end.X - start.X) / l
|
|
dy := (end.Y - start.Y) / l
|
|
|
|
// Point of the line closest to the center
|
|
t := dx*(g.center.X-start.X) + dy*(g.center.Y-start.Y)
|
|
px := t*dx + start.X
|
|
py := t*dy + start.Y
|
|
if px < start.X || px > end.X || py < start.Y || py > end.Y {
|
|
// closest point is outside the segment
|
|
return false
|
|
}
|
|
|
|
// Distance from the closest point to the center
|
|
return g.contains(geometry.Point{X: px, Y: py}, true)
|
|
}
|
|
|
|
// Intersects returns true the circle intersects other object
|
|
func (g *Circle) Intersects(obj Object) bool {
|
|
switch other := obj.(type) {
|
|
case *Point:
|
|
return g.contains(other.Center(), true)
|
|
case *Circle:
|
|
return other.Distance(g) <= (other.meters + g.meters)
|
|
case *LineString:
|
|
for i := 0; i < other.base.NumSegments(); i++ {
|
|
if g.intersectsSegment(other.base.SegmentAt(i)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case Collection:
|
|
for _, p := range other.Children() {
|
|
if g.Intersects(p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
default:
|
|
// No simple cases, so using polygon approximation.
|
|
return g.Object.Intersects(obj)
|
|
}
|
|
}
|