1 /*
2 * OwnTracks Recorder
3 * Copyright (C) 2015-2016 Jan-Piet Mens <jpmens@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <time.h>
23 #include <ctype.h>
24 #include <curl/curl.h>
25 #include "utstring.h"
26 #include "geo.h"
27 #include "json.h"
28 #include "util.h"
29
30 typedef enum {
31 GOOGLE,
32 OPENCAGE,
33 REVGEOD
34 } geocoder;
35
36 #define GOOGLE_URL "https://maps.googleapis.com/maps/api/geocode/json?latlng=%lf,%lf&sensor=false&language=EN&key=%s"
37
38 #define OPENCAGE_URL "https://api.opencagedata.com/geocode/v1/json?q=%lf+%lf&key=%s&abbrv=1&no_record=1&limit=1&format=json"
39
40 #define REVGEOD_URL "http://%s/rev?lat=%lf&lon=%lf&app=recorder" /* "host:port", lat, lon */
41
42 static CURL *curl;
43
writemem(void * contents,size_t size,size_t nmemb,void * userp)44 static size_t writemem(void *contents, size_t size, size_t nmemb, void *userp)
45 {
46 UT_string *cbuf = (UT_string *)userp;
47 size_t realsize = size * nmemb;
48
49 utstring_bincpy(cbuf, contents, realsize);
50
51 return (realsize);
52 }
53
goog_decode(UT_string * geodata,UT_string * addr,UT_string * cc,UT_string * locality)54 static int goog_decode(UT_string *geodata, UT_string *addr, UT_string *cc, UT_string *locality)
55 {
56 JsonNode *json, *results, *address, *ac, *zeroth, *j;
57
58 /*
59 * We are parsing this. I want the formatted_address in `addr' and
60 * the country code short_name in `cc'
61 *
62 * {
63 * "results" : [
64 * {
65 * "address_components" : [
66 * {
67 * "long_name" : "New Zealand",
68 * "short_name" : "NZ",
69 * "types" : [ "country", "political" ]
70 * }, ...
71 * ],
72 * "formatted_address" : "59 Example Street, Christchurch 8081, New Zealand",
73 */
74
75 if ((json = json_decode(UB(geodata))) == NULL) {
76 return (0);
77 }
78
79 /*
80 * Check for:
81 *
82 * { "error_message" : "You have exceeded your daily request quota for this API. We recommend registering for a key at the Google Developers Console: https://console.developers.google.com/",
83 * "results" : [],
84 * "status" : "OVER_QUERY_LIMIT"
85 * }
86 */
87
88 // printf("%s\n", UB(geodata));
89 if ((j = json_find_member(json, "status")) != NULL) {
90 // printf("}}}}}} %s\n", j->string_);
91 if (strcmp(j->string_, "OK") != 0) {
92 fprintf(stderr, "revgeo: %s (%s)\n", j->string_, UB(geodata));
93 json_delete(json);
94 return (0);
95 }
96 }
97
98 if ((results = json_find_member(json, "results")) != NULL) {
99 if ((zeroth = json_find_element(results, 0)) != NULL) {
100 address = json_find_member(zeroth, "formatted_address");
101 if ((address != NULL) && (address->tag == JSON_STRING)) {
102 utstring_printf(addr, "%s", address->string_);
103 }
104 }
105
106 /* Country */
107 if ((ac = json_find_member(zeroth, "address_components")) != NULL) {
108 JsonNode *comp, *j;
109 int have_cc = 0, have_locality = 0;
110
111 json_foreach(comp, ac) {
112 JsonNode *a;
113
114 if ((j = json_find_member(comp, "types")) != NULL) {
115 json_foreach(a, j) {
116 if ((a->tag == JSON_STRING) && (strcmp(a->string_, "country") == 0)) {
117 JsonNode *c;
118
119 if ((c = json_find_member(comp, "short_name")) != NULL) {
120 utstring_printf(cc, "%s", c->string_);
121 have_cc = 1;
122 break;
123 }
124 } else if ((a->tag == JSON_STRING) && (strcmp(a->string_, "locality") == 0)) {
125 JsonNode *l;
126
127 if ((l = json_find_member(comp, "long_name")) != NULL) {
128 utstring_printf(locality, "%s", l->string_);
129 have_locality = 1;
130 break;
131 }
132 }
133 }
134 }
135 if (have_cc && have_locality)
136 break;
137 }
138 }
139 }
140
141 json_delete(json);
142 return (1);
143 }
144
revgeod_decode(UT_string * geodata,UT_string * addr,UT_string * cc,UT_string * locality)145 static int revgeod_decode(UT_string *geodata, UT_string *addr, UT_string *cc, UT_string *locality)
146 {
147 JsonNode *json, *village, *j, *a;
148
149 /*
150 * We are parsing this data returned from revgeod(1):
151 *
152 * {"address":{"village":"La Terre Noire, 77510 Sablonnières, France","locality":"Sablonnières","cc":"FR","s":"lmdb"}}
153 *
154 */
155
156 if ((json = json_decode(UB(geodata))) == NULL) {
157 return (0);
158 }
159
160 if ((a = json_find_member(json, "address")) != NULL) {
161 if ((village = json_find_member(a, "village")) != NULL) {
162 if (village->tag == JSON_STRING) {
163 utstring_printf(addr, "%s", village->string_);
164 }
165 }
166 if ((j = json_find_member(a, "locality")) != NULL) {
167 if (j->tag == JSON_STRING) {
168 utstring_printf(locality, "%s", j->string_);
169 }
170 }
171 if ((j = json_find_member(a, "cc")) != NULL) {
172 if (j->tag == JSON_STRING) {
173 utstring_printf(cc, "%s", j->string_);
174 }
175 }
176 }
177
178 json_delete(json);
179 return (1);
180 }
181
opencage_decode(UT_string * geodata,UT_string * addr,UT_string * cc,UT_string * locality)182 static int opencage_decode(UT_string *geodata, UT_string *addr, UT_string *cc, UT_string *locality)
183 {
184 JsonNode *json, *results, *address, *ac, *zeroth;
185
186 /*
187 * We are parsing this. I want the formatted in `addr' and
188 * the country code short_name in `cc'
189 *
190 * {
191 * "documentation": "https://geocoder.opencagedata.com/api",
192 * "licenses": [
193 * {
194 * "name": "CC-BY-SA",
195 * "url": "http://creativecommons.org/licenses/by-sa/3.0/"
196 * },
197 * {
198 * "name": "ODbL",
199 * "url": "http://opendatacommons.org/licenses/odbl/summary/"
200 * }
201 * ],
202 * "rate": {
203 * "limit": 2500,
204 * "remaining": 2495,
205 * "reset": 1525392000
206 * },
207 * "results": [
208 * {
209 * ...
210 * "components": {
211 * "city": "Sablonnières",
212 * "country": "France",
213 * "country_code": "fr",
214 * "place": "La Terre Noire",
215 * },
216 * "formatted": "La Terre Noire, 77510 Sablonnières, France",
217 */
218
219 if ((json = json_decode(UB(geodata))) == NULL) {
220 return (0);
221 }
222
223 if ((results = json_find_member(json, "results")) != NULL) {
224 if ((zeroth = json_find_element(results, 0)) != NULL) {
225 address = json_find_member(zeroth, "formatted");
226 if ((address != NULL) && (address->tag == JSON_STRING)) {
227 utstring_printf(addr, "%s", address->string_);
228 }
229 }
230
231 if ((ac = json_find_member(zeroth, "components")) != NULL) {
232
233 /*
234 * {
235 * "ISO_3166-1_alpha-2": "FR",
236 * "_type": "place",
237 * "city": "Sablonnières",
238 * "country": "France",
239 * "country_code": "fr",
240 * "county": "Seine-et-Marne",
241 * "place": "La Terre Noire",
242 * "political_union": "European Union",
243 * "postcode": "77510",
244 * "state": "Île-de-France"
245 * }
246 */
247
248 JsonNode *j;
249
250 if ((j = json_find_member(ac, "country_code")) != NULL) {
251 if (j->tag == JSON_STRING) {
252 char *bp = j->string_;
253 int ch;
254
255 while (*bp) {
256 ch = (islower(*bp)) ? toupper(*bp) : *bp;
257 utstring_printf(cc, "%c", ch);
258 ++bp;
259 }
260 }
261 }
262
263 if ((j = json_find_member(ac, "city")) != NULL) {
264 if (j->tag == JSON_STRING) {
265 utstring_printf(locality, "%s", j->string_);
266 }
267 }
268 }
269 }
270
271 json_delete(json);
272 return (1);
273 }
274
revgeo(struct udata * ud,double lat,double lon,UT_string * addr,UT_string * cc)275 JsonNode *revgeo(struct udata *ud, double lat, double lon, UT_string *addr, UT_string *cc)
276 {
277 static UT_string *url;
278 static UT_string *cbuf; /* Buffer for curl GET */
279 static UT_string *locality = NULL;
280 long http_code;
281 CURLcode res;
282 int rc;
283 JsonNode *geo;
284 time_t now;
285 geocoder geocoder;
286
287 if ((geo = json_mkobject()) == NULL) {
288 return (NULL);
289 }
290
291 if (lat == 0.0L && lon == 0.0L) {
292 utstring_printf(addr, "Unknown (%lf,%lf)", lat, lon);
293 utstring_printf(cc, "__");
294 return (geo);
295 }
296
297 utstring_renew(url);
298 utstring_renew(cbuf);
299 utstring_renew(locality);
300
301 if (!ud->geokey || !*ud->geokey) {
302 utstring_printf(addr, "Unknown (%lf,%lf)", lat, lon);
303 utstring_printf(cc, "__");
304 return (geo);
305 }
306
307 if (strncmp(ud->geokey, "opencage:", strlen("opencage:")) == 0) {
308 utstring_printf(url, OPENCAGE_URL, lat, lon, ud->geokey + strlen("opencage:"));
309 geocoder = OPENCAGE;
310 } else if (strncmp(ud->geokey, "revgeod:", strlen("revgeod:")) == 0) {
311 /* revgeod:localhost:8865 */
312 utstring_printf(url, REVGEOD_URL, ud->geokey + strlen("revgeod:"), lat, lon);
313 geocoder = REVGEOD;
314 } else {
315 utstring_printf(url, GOOGLE_URL, lat, lon, ud->geokey);
316 geocoder = GOOGLE;
317 }
318
319 // fprintf(stderr, "--------------- %s\n", UB(url));
320
321 curl_easy_setopt(curl, CURLOPT_URL, UB(url));
322 curl_easy_setopt(curl, CURLOPT_USERAGENT, "OwnTracks-Recorder/1.0");
323 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
324 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
325 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, GEOCODE_TIMEOUT);
326
327 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writemem);
328 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)cbuf);
329
330 res = curl_easy_perform(curl);
331 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
332
333 if (res != CURLE_OK || http_code != 200) {
334 utstring_printf(addr, "revgeo failed for (%lf,%lf): HTTP status_code==%ld", lat, lon, http_code);
335 utstring_printf(cc, "__");
336 fprintf(stderr, "curl_easy_perform() failed: %s\n",
337 curl_easy_strerror(res));
338 json_delete(geo);
339 return (NULL);
340 }
341
342 switch (geocoder) {
343 case GOOGLE:
344 rc = goog_decode(cbuf, addr, cc, locality);
345 break;
346 case OPENCAGE:
347 rc = opencage_decode(cbuf, addr, cc, locality);
348 break;
349 case REVGEOD:
350 rc = revgeod_decode(cbuf, addr, cc, locality);
351 break;
352 }
353
354 if (!rc) {
355 json_delete(geo);
356 return (NULL);
357 }
358
359 // fprintf(stderr, "revgeo returns %d: %s\n", rc, UB(addr));
360
361 time(&now);
362
363 json_append_member(geo, "cc", json_mkstring(UB(cc)));
364 json_append_member(geo, "addr", json_mkstring(UB(addr)));
365 json_append_member(geo, "tst", json_mknumber((double)now));
366 json_append_member(geo, "locality", (utstring_len(locality) > 0) ?
367 json_mkstring(UB(locality)) : json_mknull());
368 return (geo);
369 }
370
revgeo_init()371 void revgeo_init()
372 {
373 curl = curl_easy_init();
374 }
375
revgeo_free()376 void revgeo_free()
377 {
378 curl_easy_cleanup(curl);
379 curl = NULL;
380 }
381
382 #if 0
383 int main()
384 {
385 double lat = 52.25458, lon = 5.1494;
386 double clat = 51.197500, clon = 6.699179;
387 UT_string *location = NULL, *cc = NULL;
388 JsonNode *json;
389 char *js;
390
391 curl = curl_easy_init();
392
393 utstring_renew(location);
394 utstring_renew(cc);
395
396 if ((json = revgeo(NULL, lat, lon, location, cc)) != NULL) {
397 js = json_stringify(json, " ");
398 printf("%s\n", js);
399 free(js);
400 } else {
401 printf("Cannot get revgeo\n");
402 }
403
404 if ((json = revgeo(NULL, clat, clon, location, cc)) != NULL) {
405 js = json_stringify(json, " ");
406 printf("%s\n", js);
407 free(js);
408 } else {
409 printf("Cannot get revgeo\n");
410 }
411
412 curl_easy_cleanup(curl);
413
414 return (0);
415
416 }
417 #endif
418