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 representation 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 := 360.0; deg > 0; 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) }