1 | /* |
2 | * Copyright 2015 Aerospike, Inc. |
3 | * |
4 | * Portions may be licensed to Aerospike, Inc. under one or more |
5 | * contributor license agreements. |
6 | * |
7 | * Licensed under the Apache License, Version 2.0 (the "License"); you |
8 | * may not use this file except in compliance with the License. You |
9 | * may obtain a copy of the License at |
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * |
12 | * Unless required by applicable law or agreed to in writing, software |
13 | * distributed under the License is distributed on an "AS IS" BASIS, |
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
15 | * implied. See the License for the specific language governing |
16 | * permissions and limitations under the License. |
17 | */ |
18 | |
19 | #include <memory> |
20 | #include <iostream> |
21 | #include <iomanip> |
22 | #include <stdexcept> |
23 | |
24 | #include <jansson.h> |
25 | |
26 | #include <s2.h> |
27 | #include <s2cap.h> |
28 | #include <s2cellid.h> |
29 | #include <s2polygon.h> |
30 | #include <s2regionunion.h> |
31 | #include <s2latlng.h> |
32 | |
33 | #include "geospatial/scoped.h" |
34 | #include "geospatial/throwstream.h" |
35 | #include "geospatial/geojson.h" |
36 | |
37 | using namespace std; |
38 | |
39 | namespace { |
40 | |
41 | S2Point |
42 | traverse_point(json_t * coord) |
43 | { |
44 | if (! coord) { |
45 | throwstream(runtime_error, "missing coordinates" ); |
46 | } |
47 | |
48 | if (! json_is_array(coord)) { |
49 | throwstream(runtime_error, "coordinates are not array" ); |
50 | } |
51 | |
52 | if (json_array_size(coord) != 2) { |
53 | throwstream(runtime_error, "expected 2 coordinates, saw " |
54 | << json_array_size(coord)); |
55 | } |
56 | |
57 | double lngval; |
58 | json_t * lng = json_array_get(coord, 0); |
59 | if (json_is_real(lng)) { |
60 | lngval = json_real_value(lng); |
61 | } |
62 | else if (json_is_integer(lng)) { |
63 | lngval = double(json_integer_value(lng)); |
64 | } |
65 | else { |
66 | throwstream(runtime_error, "longitude not numeric value" ); |
67 | } |
68 | |
69 | double latval; |
70 | json_t * lat = json_array_get(coord, 1); |
71 | if (json_is_real(lat)) { |
72 | latval = json_real_value(lat); |
73 | } |
74 | else if (json_is_integer(lat)) { |
75 | latval = double(json_integer_value(lat)); |
76 | } |
77 | else { |
78 | throwstream(runtime_error, "latitude not numeric value" ); |
79 | } |
80 | |
81 | // cout << setprecision(15) << latval << ", " << lngval << endl; |
82 | |
83 | S2LatLng latlng = S2LatLng::FromDegrees(latval, lngval).Normalized(); |
84 | if (! latlng.is_valid()) { |
85 | throwstream(runtime_error, "invalid latitude-longitude" ); |
86 | } |
87 | return latlng.ToPoint(); |
88 | } |
89 | |
90 | S2Loop * |
91 | traverse_loop(json_t * vertices) |
92 | { |
93 | if (! vertices) { |
94 | throwstream(runtime_error, "missing vertices" ); |
95 | } |
96 | |
97 | if (! json_is_array(vertices)) { |
98 | throwstream(runtime_error, "vertices are not array" ); |
99 | } |
100 | |
101 | vector<S2Point> points; |
102 | |
103 | for (size_t ii = 0; ii < json_array_size(vertices); ++ii) { |
104 | points.push_back(traverse_point(json_array_get(vertices, ii))); |
105 | } |
106 | |
107 | // Remove duplicate points. |
108 | for (size_t ii = 1; ii < points.size(); ++ii) { |
109 | if (points[ii - 1] == points[ii]) { |
110 | points.erase(points.begin() + ii); |
111 | --ii; |
112 | } |
113 | } |
114 | |
115 | if (points.size() < 4) { |
116 | throwstream(runtime_error, "loop contains less than 4 points" ); |
117 | } |
118 | if (points[0] != points[points.size()-1]) { |
119 | throwstream(runtime_error, "loop not closed" ); |
120 | } |
121 | points.pop_back(); |
122 | |
123 | auto_ptr<S2Loop> loop(new S2Loop(points)); |
124 | loop->Normalize(); |
125 | return loop.release(); |
126 | } |
127 | |
128 | S2Polygon * |
129 | traverse_polygon(json_t * loops) |
130 | { |
131 | if (! loops) { |
132 | throwstream(runtime_error, "missing polygon body" ); |
133 | } |
134 | |
135 | if (! json_is_array(loops)) { |
136 | throwstream(runtime_error, "polygon body is not array" ); |
137 | } |
138 | |
139 | vector<S2Loop *> loopv; |
140 | try |
141 | { |
142 | for (size_t ii = 0; ii < json_array_size(loops); ++ii) { |
143 | loopv.push_back(traverse_loop(json_array_get(loops, ii))); |
144 | } |
145 | |
146 | return new S2Polygon(&loopv); |
147 | } |
148 | catch (...) |
149 | { |
150 | for (size_t ii = 0; ii < loopv.size(); ++ii) { |
151 | delete loopv[ii]; |
152 | } |
153 | throw; |
154 | } |
155 | } |
156 | |
157 | void process_point(GeoJSON::GeometryHandler & geohand, json_t * coord) |
158 | { |
159 | geohand.handle_point(S2CellId::FromPoint(traverse_point(coord))); |
160 | } |
161 | |
162 | void |
163 | process_polygon(GeoJSON::GeometryHandler & geohand, json_t * coord) |
164 | { |
165 | if (! coord) { |
166 | throwstream(runtime_error, "missing coordinates" ); |
167 | } |
168 | |
169 | if (! json_is_array(coord)) { |
170 | throwstream(runtime_error, "coordinates are not array" ); |
171 | } |
172 | |
173 | S2Polygon * poly = traverse_polygon(coord); |
174 | if (geohand.handle_region(poly)) { |
175 | delete poly; |
176 | } |
177 | } |
178 | |
179 | void |
180 | process_multipolygon(GeoJSON::GeometryHandler & geohand, json_t * coord) |
181 | { |
182 | if (! coord) { |
183 | throwstream(runtime_error, "missing coordinates" ); |
184 | } |
185 | |
186 | if (! json_is_array(coord)) { |
187 | throwstream(runtime_error, "coordinates are not array" ); |
188 | } |
189 | |
190 | auto_ptr<S2RegionUnion> regionsp(new S2RegionUnion); |
191 | |
192 | for (size_t ii = 0; ii < json_array_size(coord); ++ii) { |
193 | regionsp->Add(traverse_polygon(json_array_get(coord, ii))); |
194 | } |
195 | |
196 | if (! geohand.handle_region(regionsp.get())) { |
197 | // Handler took ownership. |
198 | regionsp.release(); |
199 | } |
200 | } |
201 | |
202 | void |
203 | process_circle(GeoJSON::GeometryHandler & geohand, json_t * coord) |
204 | { |
205 | // { |
206 | // "type": "AeroCircle", |
207 | // "coordinates": [[-122.097837, 37.421363], 1000.0] |
208 | // } |
209 | |
210 | if (! coord) { |
211 | throwstream(runtime_error, "missing coordinates" ); |
212 | } |
213 | |
214 | if (! json_is_array(coord)) { |
215 | throwstream(runtime_error, "coordinates are not array" ); |
216 | } |
217 | |
218 | if (json_array_size(coord) != 2) { |
219 | throwstream(runtime_error, "malformed circle coordinate array" ); |
220 | } |
221 | |
222 | S2Point center = traverse_point(json_array_get(coord, 0)); |
223 | |
224 | double radius; |
225 | json_t * radiusobj = json_array_get(coord, 1); |
226 | if (json_is_real(radiusobj)) { |
227 | radius = json_real_value(radiusobj); |
228 | } |
229 | else if (json_is_integer(radiusobj)) { |
230 | radius = double(json_integer_value(radiusobj)); |
231 | } |
232 | else { |
233 | throwstream(runtime_error, "radius not numeric value" ); |
234 | } |
235 | |
236 | S1Angle angle = S1Angle::Radians(radius / geohand.earth_radius_meters()); |
237 | |
238 | auto_ptr<S2Cap> capp(S2Cap::FromAxisAngle(center, angle).Clone()); |
239 | |
240 | if (! geohand.handle_region(capp.get())) { |
241 | // Handler took ownership. |
242 | capp.release(); |
243 | } |
244 | } |
245 | |
246 | void traverse_geometry(GeoJSON::GeometryHandler & geohand, json_t * geom) |
247 | { |
248 | if (! geom) { |
249 | throwstream(runtime_error, "missing geometry element" ); |
250 | } |
251 | |
252 | if (! json_is_object(geom)) { |
253 | throwstream(runtime_error, "geometry is not object" ); |
254 | } |
255 | |
256 | json_t * type = json_object_get(geom, "type" ); |
257 | if (! type) { |
258 | throwstream(runtime_error, "missing geometry type" ); |
259 | } |
260 | |
261 | if (! json_is_string(type)) { |
262 | throwstream(runtime_error, "geometry type is not string" ); |
263 | } |
264 | |
265 | string typestr(json_string_value(type)); |
266 | if (typestr == "Point" ) { |
267 | process_point(geohand, json_object_get(geom, "coordinates" )); |
268 | } |
269 | else if (typestr == "Polygon" ) { |
270 | process_polygon(geohand, json_object_get(geom, "coordinates" )); |
271 | } |
272 | else if (typestr == "MultiPolygon" ) { |
273 | process_multipolygon(geohand, json_object_get(geom, "coordinates" )); |
274 | } |
275 | else if (typestr == "AeroCircle" ) { |
276 | process_circle(geohand, json_object_get(geom, "coordinates" )); |
277 | } |
278 | else { |
279 | throwstream(runtime_error, "unknown geometry type: " << typestr); |
280 | } |
281 | } |
282 | |
283 | } // end namespace |
284 | |
285 | namespace GeoJSON { |
286 | |
287 | void GeometryHandler::handle_point(S2CellId const & i_cellid) |
288 | { |
289 | // nothing by default |
290 | } |
291 | |
292 | bool GeometryHandler::handle_region(S2Region * i_regionp) |
293 | { |
294 | // By default, caller should delete the region. |
295 | return true; |
296 | } |
297 | |
298 | void parse(GeometryHandler & geohand, string const & geostr) |
299 | { |
300 | json_error_t err; |
301 | Scoped<json_t *> geojson(json_loadb(geostr.data(), geostr.size(), 0, &err), |
302 | NULL, json_decref); |
303 | if (! geojson) { |
304 | throwstream(runtime_error, "failed to parse geojson: " |
305 | << err.line << ": " << err.text); |
306 | } |
307 | |
308 | geohand.set_json(geojson); |
309 | |
310 | if (! json_is_object(geojson)) { |
311 | throwstream(runtime_error, "top level geojson element not object" ); |
312 | } |
313 | |
314 | json_t * type = json_object_get(geojson, "type" ); |
315 | if (! type) { |
316 | throwstream(runtime_error, "missing top-level type in geojson element" ); |
317 | } |
318 | |
319 | if (! json_is_string(type)) { |
320 | throwstream(runtime_error, "top-level type is not string" ); |
321 | } |
322 | |
323 | string typestr(json_string_value(type)); |
324 | if (typestr == "Feature" ) { |
325 | traverse_geometry(geohand, json_object_get(geojson, "geometry" )); |
326 | } |
327 | else if (typestr == "Point" ) { |
328 | process_point(geohand, json_object_get(geojson, "coordinates" )); |
329 | } |
330 | else if (typestr == "Polygon" ) { |
331 | process_polygon(geohand, json_object_get(geojson, "coordinates" )); |
332 | } |
333 | else if (typestr == "MultiPolygon" ) { |
334 | process_multipolygon(geohand, json_object_get(geojson, "coordinates" )); |
335 | } |
336 | else if (typestr == "AeroCircle" ) { |
337 | process_circle(geohand, json_object_get(geojson, "coordinates" )); |
338 | } |
339 | else { |
340 | throwstream(runtime_error, "unknown top-level type: " << typestr); |
341 | } |
342 | } |
343 | |
344 | } // end namespace GeoJSON |
345 | |