1package geojson
2
3import (
4	"github.com/tidwall/gjson"
5	"github.com/tidwall/tile38/geojson/geohash"
6)
7
8// FeatureCollection is a geojson object with the type "FeatureCollection"
9type FeatureCollection struct {
10	Features    []Object
11	BBox        *BBox
12	bboxDefined bool
13}
14
15func fillFeatureCollectionMap(json string) (FeatureCollection, error) {
16	var g FeatureCollection
17	res := gjson.Get(json, "features")
18	switch res.Type {
19	default:
20		return g, errInvalidFeaturesMember
21	case gjson.Null:
22		return g, errFeaturesMemberRequired
23	case gjson.JSON:
24		if !resIsArray(res) {
25			return g, errInvalidFeaturesMember
26		}
27		v := res.Array()
28		g.Features = make([]Object, len(v))
29		for i, res := range v {
30			if res.Type != gjson.JSON {
31				return g, errInvalidFeature
32			}
33			o, err := objectMap(res.Raw, fcoll)
34			if err != nil {
35				return g, err
36			}
37			g.Features[i] = o
38		}
39	}
40	var err error
41	g.BBox, err = fillBBox(json)
42	if err != nil {
43		return g, err
44	}
45	g.bboxDefined = g.BBox != nil
46	if !g.bboxDefined {
47		cbbox := g.CalculatedBBox()
48		g.BBox = &cbbox
49	}
50	return g, err
51}
52
53// Geohash converts the object to a geohash value.
54func (g FeatureCollection) Geohash(precision int) (string, error) {
55	p := g.CalculatedPoint()
56	return geohash.Encode(p.Y, p.X, precision)
57}
58
59// CalculatedPoint is a point representation of the object.
60func (g FeatureCollection) CalculatedPoint() Position {
61	return g.CalculatedBBox().center()
62}
63
64// CalculatedBBox is exterior bbox containing the object.
65func (g FeatureCollection) CalculatedBBox() BBox {
66	if g.BBox != nil {
67		return *g.BBox
68	}
69	var bbox BBox
70	for i, g := range g.Features {
71		if i == 0 {
72			bbox = g.CalculatedBBox()
73		} else {
74			bbox = bbox.union(g.CalculatedBBox())
75		}
76	}
77	return bbox
78}
79
80// PositionCount return the number of coordinates.
81func (g FeatureCollection) PositionCount() int {
82	var res int
83	for _, g := range g.Features {
84		res += g.PositionCount()
85	}
86	if g.BBox != nil {
87		return 2 + res
88	}
89	return res
90}
91
92// Weight returns the in-memory size of the object.
93func (g FeatureCollection) Weight() int {
94	var res int
95	for _, g := range g.Features {
96		res += g.Weight()
97	}
98	return res
99}
100
101// MarshalJSON allows the object to be encoded in json.Marshal calls.
102func (g FeatureCollection) MarshalJSON() ([]byte, error) {
103	return g.appendJSON(nil), nil
104}
105
106func (g FeatureCollection) appendJSON(json []byte) []byte {
107	json = append(json, `{"type":"FeatureCollection","features":[`...)
108	for i, g := range g.Features {
109		if i != 0 {
110			json = append(json, ',')
111		}
112		json = append(json, g.JSON()...)
113	}
114	json = append(json, ']')
115	if g.bboxDefined {
116		json = appendBBoxJSON(json, g.BBox)
117	}
118	return append(json, '}')
119}
120
121// JSON is the json representation of the object. This might not be exactly the same as the original.
122func (g FeatureCollection) JSON() string {
123	return string(g.appendJSON(nil))
124}
125
126// String returns a string representation of the object. This might be JSON or something else.
127func (g FeatureCollection) String() string {
128	return g.JSON()
129}
130
131// Bytes is the bytes representation of the object.
132func (g FeatureCollection) Bytes() []byte {
133	return []byte(g.JSON())
134}
135func (g FeatureCollection) bboxPtr() *BBox {
136	return g.BBox
137}
138func (g FeatureCollection) hasPositions() bool {
139	if g.BBox != nil {
140		return true
141	}
142	for _, g := range g.Features {
143		if g.hasPositions() {
144			return true
145		}
146	}
147	return false
148}
149
150// WithinBBox detects if the object is fully contained inside a bbox.
151func (g FeatureCollection) WithinBBox(bbox BBox) bool {
152	if g.bboxDefined {
153		return rectBBox(g.CalculatedBBox()).InsideRect(rectBBox(bbox))
154	}
155	if len(g.Features) == 0 {
156		return false
157	}
158	for _, g := range g.Features {
159		if !g.WithinBBox(bbox) {
160			return false
161		}
162	}
163	return true
164}
165
166// IntersectsBBox detects if the object intersects a bbox.
167func (g FeatureCollection) IntersectsBBox(bbox BBox) bool {
168	if g.bboxDefined {
169		return rectBBox(g.CalculatedBBox()).IntersectsRect(rectBBox(bbox))
170	}
171	for _, g := range g.Features {
172		if g.IntersectsBBox(bbox) {
173			return true
174		}
175	}
176	return false
177}
178
179// Within detects if the object is fully contained inside another object.
180func (g FeatureCollection) Within(o Object) bool {
181	return withinObjectShared(g, o,
182		func(v Polygon) bool {
183			if len(g.Features) == 0 {
184				return false
185			}
186			for _, f := range g.Features {
187				if !f.Within(o) {
188					return false
189				}
190			}
191			return true
192		},
193	)
194}
195
196// Intersects detects if the object intersects another object.
197func (g FeatureCollection) Intersects(o Object) bool {
198	return intersectsObjectShared(g, o,
199		func(v Polygon) bool {
200			if len(g.Features) == 0 {
201				return false
202			}
203			for _, f := range g.Features {
204
205				if f.Intersects(o) {
206					return true
207				}
208			}
209			return false
210		},
211	)
212}
213
214// Nearby detects if the object is nearby a position.
215func (g FeatureCollection) Nearby(center Position, meters float64) bool {
216	return nearbyObjectShared(g, center.X, center.Y, meters)
217}
218
219// IsBBoxDefined returns true if the object has a defined bbox.
220func (g FeatureCollection) IsBBoxDefined() bool {
221	return g.bboxDefined
222}
223
224// IsGeometry return true if the object is a geojson geometry object. false if it something else.
225func (g FeatureCollection) IsGeometry() bool {
226	return true
227}
228