mirror of https://github.com/tidwall/tile38.git
170 lines
5.1 KiB
Go
170 lines
5.1 KiB
Go
package buffer
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
|
|
"github.com/tidwall/geojson"
|
|
"github.com/tidwall/geojson/geo"
|
|
"github.com/tidwall/geojson/geometry"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// TODO: detect of pole and antimeridian crossing and generate
|
|
// valid multigeometries
|
|
|
|
const bufferSteps = 15
|
|
|
|
// Simple performs a very simple buffer operation on a geojson object.
|
|
func Simple(g geojson.Object, meters float64) (geojson.Object, error) {
|
|
if meters <= 0 {
|
|
return g, nil
|
|
}
|
|
if math.IsInf(meters, 0) || math.IsNaN(meters) {
|
|
return g, errors.New("invalid meters")
|
|
}
|
|
switch g := g.(type) {
|
|
case *geojson.Point:
|
|
return bufferSimplePoint(g.Base(), meters), nil
|
|
case *geojson.SimplePoint:
|
|
return bufferSimplePoint(g.Base(), meters), nil
|
|
case *geojson.MultiPoint:
|
|
return bufferSimpleGeometries(g.Base(), meters)
|
|
case *geojson.LineString:
|
|
return bufferSimpleLineString(g, meters)
|
|
case *geojson.MultiLineString:
|
|
return bufferSimpleGeometries(g.Base(), meters)
|
|
case *geojson.Polygon:
|
|
return bufferSimplePolygon(g, meters)
|
|
case *geojson.MultiPolygon:
|
|
return bufferSimpleGeometries(g.Base(), meters)
|
|
case *geojson.FeatureCollection:
|
|
return bufferSimpleFeatures(g.Base(), meters)
|
|
case *geojson.Feature:
|
|
bg, err := Simple(g.Base(), meters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return geojson.NewFeature(bg, g.Members()), nil
|
|
case *geojson.Circle:
|
|
return Simple(g.Primative(), meters)
|
|
case nil:
|
|
return nil, errors.New("cannot buffer nil object")
|
|
default:
|
|
typ := gjson.Get(g.JSON(), "type").String()
|
|
return nil, errors.New("cannot buffer " + typ + " type")
|
|
}
|
|
}
|
|
|
|
func bufferSimplePoint(p geometry.Point, meters float64) *geojson.Polygon {
|
|
meters = geo.NormalizeDistance(meters)
|
|
points := make([]geometry.Point, 0, bufferSteps+1)
|
|
|
|
// calc the four corners
|
|
maxY, _ := geo.DestinationPoint(p.Y, p.X, meters, 0)
|
|
_, maxX := geo.DestinationPoint(p.Y, p.X, meters, 90)
|
|
minY, _ := geo.DestinationPoint(p.Y, p.X, meters, 180)
|
|
_, minX := geo.DestinationPoint(p.Y, p.X, meters, 270)
|
|
|
|
// use the half width of the lat and lon
|
|
lons := (maxX - minX) / 2
|
|
lats := (maxY - minY) / 2
|
|
|
|
// generate the circle polygon
|
|
for th := 0.0; th <= 360.0; th += 360.0 / float64(bufferSteps) {
|
|
radians := (math.Pi / 180) * th
|
|
x := p.X + lons*math.Cos(radians)
|
|
y := p.Y + lats*math.Sin(radians)
|
|
points = append(points, geometry.Point{X: x, Y: y})
|
|
}
|
|
// add last connecting point, make a total of steps+1
|
|
points = append(points, points[0])
|
|
poly := geojson.NewPolygon(
|
|
geometry.NewPoly(points, nil, &geometry.IndexOptions{
|
|
Kind: geometry.None,
|
|
}),
|
|
)
|
|
return poly
|
|
}
|
|
|
|
func bufferSimpleGeometries(objs []geojson.Object, meters float64,
|
|
) (*geojson.GeometryCollection, error) {
|
|
geoms := make([]geojson.Object, len(objs))
|
|
for i := 0; i < len(objs); i++ {
|
|
g, err := Simple(objs[i], meters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
geoms[i] = g
|
|
}
|
|
return geojson.NewGeometryCollection(geoms), nil
|
|
}
|
|
|
|
func bufferSimpleFeatures(objs []geojson.Object, meters float64,
|
|
) (*geojson.FeatureCollection, error) {
|
|
geoms := make([]geojson.Object, len(objs))
|
|
for i := 0; i < len(objs); i++ {
|
|
g, err := Simple(objs[i], meters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
geoms[i] = g
|
|
}
|
|
return geojson.NewFeatureCollection(geoms), nil
|
|
}
|
|
|
|
// appendBufferSimpleSeries buffers a series and appends its parts to dst
|
|
func appendBufferSimpleSeries(dst []geojson.Object, s geometry.Series, meters float64) []geojson.Object {
|
|
nsegs := s.NumSegments()
|
|
for i := 0; i < nsegs; i++ {
|
|
dst = appendSimpleBufferSegment(dst, s.SegmentAt(i), meters, i == 0)
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// appendSimpleBufferSegment buffers a segment and appends its parts to dst
|
|
func appendSimpleBufferSegment(dst []geojson.Object, seg geometry.Segment,
|
|
meters float64, first bool,
|
|
) []geojson.Object {
|
|
if first {
|
|
// endcap A
|
|
dst = append(dst, bufferSimplePoint(seg.A, meters))
|
|
}
|
|
// line polygon
|
|
bear1 := geo.BearingTo(seg.A.Y, seg.A.X, seg.B.Y, seg.B.X)
|
|
lat1, lon1 := geo.DestinationPoint(seg.A.Y, seg.A.X, meters, bear1-90)
|
|
lat2, lon2 := geo.DestinationPoint(seg.A.Y, seg.A.X, meters, bear1+90)
|
|
bear2 := geo.BearingTo(seg.B.Y, seg.B.X, seg.A.Y, seg.A.X)
|
|
lat3, lon3 := geo.DestinationPoint(seg.B.Y, seg.B.X, meters, bear2-90)
|
|
lat4, lon4 := geo.DestinationPoint(seg.B.Y, seg.B.X, meters, bear2+90)
|
|
dst = append(dst, geojson.NewPolygon(
|
|
geometry.NewPoly([]geometry.Point{
|
|
{X: lon1, Y: lat1},
|
|
{X: lon2, Y: lat2},
|
|
{X: lon3, Y: lat3},
|
|
{X: lon4, Y: lat4},
|
|
{X: lon1, Y: lat1},
|
|
}, nil, nil)))
|
|
// endcap B
|
|
dst = append(dst, bufferSimplePoint(seg.B, meters))
|
|
return dst
|
|
}
|
|
|
|
func bufferSimplePolygon(p *geojson.Polygon, meters float64,
|
|
) (*geojson.GeometryCollection, error) {
|
|
var geoms []geojson.Object
|
|
b := p.Base()
|
|
geoms = appendBufferSimpleSeries(geoms, b.Exterior, meters)
|
|
for _, hole := range b.Holes {
|
|
geoms = appendBufferSimpleSeries(geoms, hole, meters)
|
|
}
|
|
geoms = append(geoms, p)
|
|
return geojson.NewGeometryCollection(geoms), nil
|
|
}
|
|
|
|
func bufferSimpleLineString(l *geojson.LineString, meters float64,
|
|
) (*geojson.GeometryCollection, error) {
|
|
geoms := appendBufferSimpleSeries(nil, l.Base(), meters)
|
|
return geojson.NewGeometryCollection(geoms), nil
|
|
}
|