tile38/geojson/object.go

393 lines
10 KiB
Go

package geojson
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/geojson/poly"
)
const (
point = 0
multiPoint = 1
lineString = 2
multiLineString = 3
polygon = 4
multiPolygon = 5
geometryCollection = 6
feature = 7
featureCollection = 8
)
var (
errNotEnoughData = errors.New("not enough data")
errTooMuchData = errors.New("too much data")
errInvalidData = errors.New("invalid data")
)
var ( // json errors
fmtErrTypeIsUnknown = "The type '%s' is unknown"
errInvalidTypeMember = errors.New("Type member is invalid. Expecting a string")
errInvalidCoordinates = errors.New("Coordinates member is invalid. Expecting an array")
errCoordinatesRequired = errors.New("Coordinates member is required.")
errInvalidGeometries = errors.New("Geometries member is invalid. Expecting an array")
errGeometriesRequired = errors.New("Geometries member is required.")
errInvalidGeometryMember = errors.New("Geometry member is invalid. Expecting an object")
errGeometryMemberRequired = errors.New("Geometry member is required.")
errInvalidFeaturesMember = errors.New("Features member is invalid. Expecting an array")
errFeaturesMemberRequired = errors.New("Features member is required")
errInvalidFeature = errors.New("Invalid feature in collection")
errInvalidPropertiesMember = errors.New("Properties member in invalid. Expecting an array")
errInvalidCoordinatesValue = errors.New("Coordinates member has an invalid value")
errLineStringInvalidCoordinates = errors.New("Coordinates must be an array of two or more positions")
errInvalidNumberOfPositionValues = errors.New("Position must have two or more numbers")
errInvalidPositionValue = errors.New("Position has an invalid value")
errCoordinatesMustBeArray = errors.New("Coordinates member must be an array of positions")
errMustBeALinearRing = errors.New("Polygon must have at least 4 positions and the first and last position must be the same")
errBBoxInvalidType = errors.New("BBox member is an invalid. Expecting an array")
errBBoxInvalidNumberOfValues = errors.New("BBox member requires exactly 4 or 6 values")
errBBoxInvalidValue = errors.New("BBox has an invalid value")
errInvalidGeometry = errors.New("Invalid geometry in collection")
)
const nilz = 0
// Object is a geojson object
type Object interface {
bboxPtr() *BBox
hasPositions() bool
// WithinBBox detects if the object is fully contained inside a bbox.
WithinBBox(bbox BBox) bool
// IntersectsBBox detects if the object intersects a bbox.
IntersectsBBox(bbox BBox) bool
// Within detects if the object is fully contained inside another object.
Within(o Object) bool
// Intersects detects if the object intersects another object.
Intersects(o Object) bool
// Nearby detects if the object is nearby a position.
Nearby(center Position, meters float64) bool
// CalculatedBBox is exterior bbox containing the object.
CalculatedBBox() BBox
// CalculatedPoint is a point representation of the object.
CalculatedPoint() Position
// JSON is the json representation of the object. This might not be exactly the same as the original.
JSON() string
// String returns a string represenation of the object. This may be JSON or something else.
String() string
// PositionCount return the number of coordinates.
PositionCount() int
// Weight returns the in-memory size of the object.
Weight() int
// MarshalJSON allows the object to be encoded in json.Marshal calls.
MarshalJSON() ([]byte, error)
// Geohash converts the object to a geohash value.
Geohash(precision int) (string, error)
// IsBBoxDefined returns true if the object has a defined bbox.
IsBBoxDefined() bool
// IsGeometry return true if the object is a geojson geometry object. false if it something else.
IsGeometry() bool
}
func writeHeader(buf *bytes.Buffer, objType byte, bbox *BBox, isCordZ bool) {
header := objType
if bbox != nil {
header |= 1 << 4
if bbox.Min.Z != nilz || bbox.Max.Z != nilz {
header |= 1 << 5
}
}
if isCordZ {
header |= 1 << 6
}
buf.WriteByte(header)
if bbox != nil {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Min.X))
buf.Write(b)
binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Min.Y))
buf.Write(b)
if bbox.Min.Z != nilz || bbox.Max.Z != nilz {
binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Min.Z))
buf.Write(b)
}
binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Max.X))
buf.Write(b)
binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Max.Y))
buf.Write(b)
if bbox.Min.Z != nilz || bbox.Max.Z != nilz {
binary.LittleEndian.PutUint64(b, math.Float64bits(bbox.Max.Z))
buf.Write(b)
}
}
}
func positionBBox(i int, bbox BBox, ps []Position) (int, BBox) {
for _, p := range ps {
if i == 0 {
bbox.Min = p
bbox.Max = p
} else {
if p.X < bbox.Min.X {
bbox.Min.X = p.X
}
if p.Y < bbox.Min.Y {
bbox.Min.Y = p.Y
}
if p.X > bbox.Max.X {
bbox.Max.X = p.X
}
if p.Y > bbox.Max.Y {
bbox.Max.Y = p.Y
}
}
i++
}
return i, bbox
}
func isLinearRing(ps []Position) bool {
return len(ps) >= 4 && ps[0] == ps[len(ps)-1]
}
// ObjectJSON parses geojson and returns an Object
func ObjectJSON(json string) (Object, error) {
return objectMap(json, root)
}
var (
root = 0 // accept all types
gcoll = 1 // accept only geometries
feat = 2 // accept only geometries
fcoll = 3 // accept only features
)
func objectMap(json string, from int) (Object, error) {
var err error
res := gjson.Get(json, "type")
if res.Type != gjson.String {
return nil, errInvalidTypeMember
}
typ := res.String()
if from != root {
switch from {
case gcoll, feat:
switch typ {
default:
return nil, fmt.Errorf(fmtErrTypeIsUnknown, typ)
case "Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon", "GeometryCollection":
}
case fcoll:
switch typ {
default:
return nil, fmt.Errorf(fmtErrTypeIsUnknown, typ)
case "Feature":
}
}
}
var o Object
switch typ {
default:
return nil, fmt.Errorf(fmtErrTypeIsUnknown, typ)
case "Point":
o, _, err = fillSimplePointOrPoint(fillLevel1Map(json))
case "MultiPoint":
o, _, err = fillMultiPoint(fillLevel2Map(json))
case "LineString":
o, _, err = fillLineString(fillLevel2Map(json))
case "MultiLineString":
o, _, err = fillMultiLineString(fillLevel3Map(json))
case "Polygon":
o, _, err = fillPolygon(fillLevel3Map(json))
case "MultiPolygon":
o, _, err = fillMultiPolygon(fillLevel4Map(json))
case "GeometryCollection":
o, _, err = fillGeometryCollectionMap(json)
case "Feature":
o, _, err = fillFeatureMap(json)
case "FeatureCollection":
o, _, err = fillFeatureCollectionMap(json)
}
return o, err
}
func withinObjectShared(g Object, o Object, pin func(v Polygon) bool, mpin func(v MultiPolygon) bool) bool {
bbp := o.bboxPtr()
if bbp != nil {
return g.WithinBBox(*bbp)
}
switch v := o.(type) {
default:
return false
case Polygon:
if len(v.Coordinates) == 0 {
return false
}
return pin(v)
case MultiPolygon:
if len(v.Coordinates) == 0 {
return false
}
return mpin(v)
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, mpin func(v MultiPolygon) bool) bool {
bbp := o.bboxPtr()
if bbp != nil {
return g.IntersectsBBox(*bbp)
}
switch v := o.(type) {
default:
return false
case Polygon:
if len(v.Coordinates) == 0 {
return false
}
return pin(v)
case MultiPolygon:
if len(v.Coordinates) == 0 {
return false
}
return mpin(v)
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 {
steps = 3
}
p := Polygon{
Coordinates: [][]Position{make([]Position, steps+1)},
}
center := Position{X: x, Y: y, Z: 0}
step := 360.0 / float64(steps)
i := 0
for deg := float64(0); deg < 360; deg += step {
c := Position(poly.Point(center.Destination(meters, deg)))
p.Coordinates[0][i] = c
i++
}
p.Coordinates[0][i] = p.Coordinates[0][0]
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 {
b, _ := json.Marshal(s)
return b
}
}
b := make([]byte, len(s)+2)
b[0] = '"'
copy(b[1:], s)
b[len(b)-1] = '"'
return b
}
func stripWhitespace(s string) string {
var p []byte
var str bool
var escs int
for i := 0; i < len(s); i++ {
c := s[i]
if str {
// We're inside of a string. Look out for '"' and '\' characters.
if c == '\\' {
// Increment the escape character counter.
escs++
} else {
if c == '"' && escs%2 == 0 {
// We reached the end of string
str = false
}
// Reset the escape counter
escs = 0
}
} else if c == '"' {
// We encountared a double quote character.
str = true
} else if c <= ' ' {
// Ignore the whitespace
if p == nil {
p = []byte(s[:i])
}
continue
}
// Append the character
if p != nil {
p = append(p, c)
}
}
if p == nil {
return s
}
return string(p)
}