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