1package geojson 2 3import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 8 "github.com/tidwall/gjson" 9 "github.com/tidwall/tile38/geojson/poly" 10) 11 12const ( 13 point = 0 14 multiPoint = 1 15 lineString = 2 16 multiLineString = 3 17 polygon = 4 18 multiPolygon = 5 19 geometryCollection = 6 20 feature = 7 21 featureCollection = 8 22) 23 24var ( 25 errNotEnoughData = errors.New("not enough data") 26 errTooMuchData = errors.New("too much data") 27 errInvalidData = errors.New("invalid data") 28) 29 30var ( // json errors 31 fmtErrTypeIsUnknown = "The type '%s' is unknown" 32 errInvalidTypeMember = errors.New("Type member is invalid. Expecting a string") 33 errInvalidCoordinates = errors.New("Coordinates member is invalid. Expecting an array") 34 errCoordinatesRequired = errors.New("Coordinates member is required") 35 errInvalidGeometries = errors.New("Geometries member is invalid. Expecting an array") 36 errGeometriesRequired = errors.New("Geometries member is required") 37 errInvalidGeometryMember = errors.New("Geometry member is invalid. Expecting an object") 38 errGeometryMemberRequired = errors.New("Geometry member is required") 39 errInvalidFeaturesMember = errors.New("Features member is invalid. Expecting an array") 40 errFeaturesMemberRequired = errors.New("Features member is required") 41 errInvalidFeature = errors.New("Invalid feature in collection") 42 errInvalidPropertiesMember = errors.New("Properties member in invalid. Expecting an array") 43 errInvalidCoordinatesValue = errors.New("Coordinates member has an invalid value") 44 errLineStringInvalidCoordinates = errors.New("Coordinates must be an array of two or more positions") 45 errInvalidNumberOfPositionValues = errors.New("Position must have two or more numbers") 46 errInvalidPositionValue = errors.New("Position has an invalid value") 47 errCoordinatesMustBeArray = errors.New("Coordinates member must be an array of positions") 48 errMustBeALinearRing = errors.New("Polygon must have at least 4 positions and the first and last position must be the same") 49 errBBoxInvalidType = errors.New("BBox member is an invalid. Expecting an array") 50 errBBoxInvalidNumberOfValues = errors.New("BBox member requires exactly 4 or 6 values") 51 errBBoxInvalidValue = errors.New("BBox has an invalid value") 52 errInvalidGeometry = errors.New("Invalid geometry in collection") 53) 54 55const nilz = 0 56 57// Object is a geojson object 58type Object interface { 59 bboxPtr() *BBox 60 hasPositions() bool 61 appendJSON(dst []byte) []byte 62 63 // WithinBBox detects if the object is fully contained inside a bbox. 64 WithinBBox(bbox BBox) bool 65 // IntersectsBBox detects if the object intersects a bbox. 66 IntersectsBBox(bbox BBox) bool 67 // Within detects if the object is fully contained inside another object. 68 Within(o Object) bool 69 // Intersects detects if the object intersects another object. 70 Intersects(o Object) bool 71 // Nearby detects if the object is nearby a position. 72 Nearby(center Position, meters float64) bool 73 // CalculatedBBox is exterior bbox containing the object. 74 CalculatedBBox() BBox 75 // CalculatedPoint is a point representation of the object. 76 CalculatedPoint() Position 77 // JSON is the json representation of the object. This might not be exactly the same as the original. 78 JSON() string 79 // String returns a string representation of the object. This may be JSON or something else. 80 String() string 81 // PositionCount return the number of coordinates. 82 PositionCount() int 83 // Weight returns the in-memory size of the object. 84 Weight() int 85 // MarshalJSON allows the object to be encoded in json.Marshal calls. 86 MarshalJSON() ([]byte, error) 87 // Geohash converts the object to a geohash value. 88 Geohash(precision int) (string, error) 89 // IsBBoxDefined returns true if the object has a defined bbox. 90 IsBBoxDefined() bool 91 // IsGeometry return true if the object is a geojson geometry object. false if it something else. 92 IsGeometry() bool 93} 94 95func positionBBox(i int, bbox BBox, ps []Position) (int, BBox) { 96 for _, p := range ps { 97 if i == 0 { 98 bbox.Min = p 99 bbox.Max = p 100 } else { 101 if p.X < bbox.Min.X { 102 bbox.Min.X = p.X 103 } 104 if p.Y < bbox.Min.Y { 105 bbox.Min.Y = p.Y 106 } 107 if p.X > bbox.Max.X { 108 bbox.Max.X = p.X 109 } 110 if p.Y > bbox.Max.Y { 111 bbox.Max.Y = p.Y 112 } 113 } 114 i++ 115 } 116 return i, bbox 117} 118 119func isLinearRing(ps []Position) bool { 120 return len(ps) >= 4 && ps[0] == ps[len(ps)-1] 121} 122 123// ObjectJSON parses geojson and returns an Object 124func ObjectJSON(json string) (Object, error) { 125 return objectMap(json, root) 126} 127 128var ( 129 root = 0 // accept all types 130 gcoll = 1 // accept only geometries 131 feat = 2 // accept only geometries 132 fcoll = 3 // accept only features 133) 134 135func objectMap(json string, from int) (Object, error) { 136 var err error 137 res := gjson.Get(json, "type") 138 if res.Type != gjson.String { 139 return nil, errInvalidTypeMember 140 } 141 typ := res.String() 142 if from != root { 143 switch from { 144 case gcoll, feat: 145 switch typ { 146 default: 147 return nil, fmt.Errorf(fmtErrTypeIsUnknown, typ) 148 case "Point", "MultiPoint", "LineString", "MultiLineString", "Polygon", "MultiPolygon", "GeometryCollection": 149 } 150 case fcoll: 151 switch typ { 152 default: 153 return nil, fmt.Errorf(fmtErrTypeIsUnknown, typ) 154 case "Feature": 155 } 156 } 157 } 158 159 var o Object 160 switch typ { 161 default: 162 return nil, fmt.Errorf(fmtErrTypeIsUnknown, typ) 163 case "Point": 164 o, err = fillSimplePointOrPoint(fillLevel1Map(json)) 165 case "MultiPoint": 166 o, err = fillMultiPoint(fillLevel2Map(json)) 167 case "LineString": 168 o, err = fillLineString(fillLevel2Map(json)) 169 case "MultiLineString": 170 o, err = fillMultiLineString(fillLevel3Map(json)) 171 case "Polygon": 172 o, err = fillPolygon(fillLevel3Map(json)) 173 case "MultiPolygon": 174 o, err = fillMultiPolygon(fillLevel4Map(json)) 175 case "GeometryCollection": 176 o, err = fillGeometryCollectionMap(json) 177 case "Feature": 178 o, err = fillFeatureMap(json) 179 case "FeatureCollection": 180 o, err = fillFeatureCollectionMap(json) 181 } 182 return o, err 183} 184 185func withinObjectShared(g Object, o Object, pin func(v Polygon) bool) bool { 186 bbp := o.bboxPtr() 187 if bbp != nil { 188 if !g.WithinBBox(*bbp) { 189 return false 190 } 191 if o.IsBBoxDefined() { 192 return true 193 } 194 } 195 switch v := o.(type) { 196 default: 197 return false 198 case Point: 199 return g.WithinBBox(v.CalculatedBBox()) 200 case SimplePoint: 201 return g.WithinBBox(v.CalculatedBBox()) 202 case MultiPoint: 203 for i := range v.Coordinates { 204 if g.Within(Point{Coordinates: v.Coordinates[i]}) { 205 return true 206 } 207 } 208 return false 209 case LineString: 210 if len(v.Coordinates) == 0 { 211 return false 212 } 213 switch g := g.(type) { 214 default: 215 return false 216 case SimplePoint: 217 return poly.Point(Position{X: g.X, Y: g.Y, Z: 0}).IntersectsLineString(polyPositions(v.Coordinates)) 218 case Point: 219 return poly.Point(g.Coordinates).IntersectsLineString(polyPositions(v.Coordinates)) 220 case MultiPoint: 221 if len(v.Coordinates) == 0 { 222 return false 223 } 224 for _, p := range v.Coordinates { 225 if !poly.Point(p).IntersectsLineString(polyPositions(v.Coordinates)) { 226 return false 227 } 228 } 229 return true 230 } 231 case MultiLineString: 232 for i := range v.Coordinates { 233 if g.Within(v.getLineString(i)) { 234 return true 235 } 236 } 237 return false 238 case Polygon: 239 if len(v.Coordinates) == 0 { 240 return false 241 } 242 return pin(v) 243 case MultiPolygon: 244 for i := range v.Coordinates { 245 if pin(v.getPolygon(i)) { 246 return true 247 } 248 } 249 return false 250 case Feature: 251 return g.Within(v.Geometry) 252 case FeatureCollection: 253 if len(v.Features) == 0 { 254 return false 255 } 256 for _, f := range v.Features { 257 if !g.Within(f) { 258 return false 259 } 260 } 261 return true 262 case GeometryCollection: 263 if len(v.Geometries) == 0 { 264 return false 265 } 266 for _, f := range v.Geometries { 267 if !g.Within(f) { 268 return false 269 } 270 } 271 return true 272 } 273} 274 275func intersectsObjectShared(g Object, o Object, pin func(v Polygon) bool) bool { 276 bbp := o.bboxPtr() 277 if bbp != nil { 278 if !g.IntersectsBBox(*bbp) { 279 return false 280 } 281 if o.IsBBoxDefined() { 282 return true 283 } 284 } 285 switch v := o.(type) { 286 default: 287 return false 288 case Point: 289 return g.IntersectsBBox(v.CalculatedBBox()) 290 case SimplePoint: 291 return g.IntersectsBBox(v.CalculatedBBox()) 292 case MultiPoint: 293 for i := range v.Coordinates { 294 if (Point{Coordinates: v.Coordinates[i]}).Intersects(g) { 295 return true 296 } 297 } 298 return false 299 case LineString: 300 if g, ok := g.(LineString); ok { 301 a := polyPositions(g.Coordinates) 302 b := polyPositions(v.Coordinates) 303 return a.LineStringIntersectsLineString(b) 304 } 305 return o.Intersects(g) 306 case MultiLineString: 307 for i := range v.Coordinates { 308 if g.Intersects(v.getLineString(i)) { 309 return true 310 } 311 } 312 return false 313 case Polygon: 314 if len(v.Coordinates) == 0 { 315 return false 316 } 317 return pin(v) 318 case MultiPolygon: 319 for _, coords := range v.Coordinates { 320 if pin(Polygon{Coordinates: coords}) { 321 return true 322 } 323 } 324 return false 325 case Feature: 326 return g.Intersects(v.Geometry) 327 case FeatureCollection: 328 if len(v.Features) == 0 { 329 return false 330 } 331 for _, f := range v.Features { 332 if g.Intersects(f) { 333 return true 334 } 335 } 336 return false 337 case GeometryCollection: 338 if len(v.Geometries) == 0 { 339 return false 340 } 341 for _, f := range v.Geometries { 342 if g.Intersects(f) { 343 return true 344 } 345 } 346 return false 347 } 348} 349 350// CirclePolygon returns a Polygon around the radius. 351func CirclePolygon(x, y, meters float64, steps int) Polygon { 352 if steps < 3 { 353 steps = 3 354 } 355 p := Polygon{ 356 Coordinates: [][]Position{make([]Position, steps+1)}, 357 } 358 center := Position{X: x, Y: y, Z: 0} 359 step := 360.0 / float64(steps) 360 i := 0 361 for deg := 360.0; deg > 0; deg -= step { 362 c := Position(poly.Point(center.Destination(meters, deg))) 363 p.Coordinates[0][i] = c 364 i++ 365 } 366 p.Coordinates[0][i] = p.Coordinates[0][0] 367 return p 368} 369 370// The object's calculated bounding box must intersect the radius of the circle to pass. 371func nearbyObjectShared(g Object, x, y float64, meters float64) bool { 372 if !g.hasPositions() { 373 return false 374 } 375 center := Position{X: x, Y: y, Z: 0} 376 bbox := g.CalculatedBBox() 377 if bbox.Min.X == bbox.Max.X && bbox.Min.Y == bbox.Max.Y { 378 // just a point, return is point is inside of the circle 379 return center.DistanceTo(bbox.Min) <= meters 380 } 381 circlePoly := CirclePolygon(x, y, meters, 12) 382 return g.Intersects(circlePoly) 383} 384 385func jsonMarshalString(s string) []byte { 386 for i := 0; i < len(s); i++ { 387 if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 { 388 b, _ := json.Marshal(s) 389 return b 390 } 391 } 392 b := make([]byte, len(s)+2) 393 b[0] = '"' 394 copy(b[1:], s) 395 b[len(b)-1] = '"' 396 return b 397} 398 399func stripWhitespace(s string) string { 400 var p []byte 401 var str bool 402 var escs int 403 for i := 0; i < len(s); i++ { 404 c := s[i] 405 if str { 406 // We're inside of a string. Look out for '"' and '\' characters. 407 if c == '\\' { 408 // Increment the escape character counter. 409 escs++ 410 } else { 411 if c == '"' && escs%2 == 0 { 412 // We reached the end of string 413 str = false 414 } 415 // Reset the escape counter 416 escs = 0 417 } 418 } else if c == '"' { 419 // We encountared a double quote character. 420 str = true 421 } else if c <= ' ' { 422 // Ignore the whitespace 423 if p == nil { 424 p = []byte(s[:i]) 425 } 426 continue 427 } 428 // Append the character 429 if p != nil { 430 p = append(p, c) 431 } 432 } 433 if p == nil { 434 return s 435 } 436 return string(p) 437} 438