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