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