recorder/geo.c
2024-02-01 22:37:51 +01:00

445 lines
11 KiB
C

/*
* OwnTracks Recorder
* Copyright (C) 2015-2024 Jan-Piet Mens <jpmens@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <ctype.h>
#include <curl/curl.h>
#include "utstring.h"
#include "geo.h"
#include "json.h"
#include "util.h"
typedef enum {
GOOGLE,
OPENCAGE,
REVGEOD
} geocoder;
#define GOOGLE_URL "https://maps.googleapis.com/maps/api/geocode/json?latlng=%lf,%lf&sensor=false&language=EN&key=%s"
#define OPENCAGE_URL "https://api.opencagedata.com/geocode/v1/json?q=%lf+%lf&key=%s&abbrv=1&no_record=1&limit=1&format=json"
#define REVGEOD_URL "http://%s/rev?lat=%lf&lon=%lf&app=recorder" /* "host:port", lat, lon */
static CURL *curl;
static size_t writemem(void *contents, size_t size, size_t nmemb, void *userp)
{
UT_string *cbuf = (UT_string *)userp;
size_t realsize = size * nmemb;
utstring_bincpy(cbuf, contents, realsize);
return (realsize);
}
static int goog_decode(UT_string *geodata, UT_string *addr, UT_string *cc, UT_string *locality)
{
JsonNode *json, *results, *address, *ac, *zeroth, *j;
/*
* We are parsing this. I want the formatted_address in `addr' and
* the country code short_name in `cc'
*
* {
* "results" : [
* {
* "address_components" : [
* {
* "long_name" : "New Zealand",
* "short_name" : "NZ",
* "types" : [ "country", "political" ]
* }, ...
* ],
* "formatted_address" : "59 Example Street, Christchurch 8081, New Zealand",
*/
if ((json = json_decode(UB(geodata))) == NULL) {
return (0);
}
/*
* Check for:
*
* { "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/",
* "results" : [],
* "status" : "OVER_QUERY_LIMIT"
* }
*/
// printf("%s\n", UB(geodata));
if ((j = json_find_member(json, "status")) != NULL) {
// printf("}}}}}} %s\n", j->string_);
if (strcmp(j->string_, "OK") != 0) {
fprintf(stderr, "revgeo: %s (%s)\n", j->string_, UB(geodata));
json_delete(json);
return (0);
}
}
if ((results = json_find_member(json, "results")) != NULL) {
if ((zeroth = json_find_element(results, 0)) != NULL) {
address = json_find_member(zeroth, "formatted_address");
if ((address != NULL) && (address->tag == JSON_STRING)) {
utstring_printf(addr, "%s", address->string_);
}
}
/* Country */
if ((ac = json_find_member(zeroth, "address_components")) != NULL) {
JsonNode *comp, *j;
int have_cc = 0, have_locality = 0;
json_foreach(comp, ac) {
JsonNode *a;
if ((j = json_find_member(comp, "types")) != NULL) {
json_foreach(a, j) {
if ((a->tag == JSON_STRING) && (strcmp(a->string_, "country") == 0)) {
JsonNode *c;
if ((c = json_find_member(comp, "short_name")) != NULL) {
utstring_printf(cc, "%s", c->string_);
have_cc = 1;
break;
}
} else if ((a->tag == JSON_STRING) && (strcmp(a->string_, "locality") == 0)) {
JsonNode *l;
if ((l = json_find_member(comp, "long_name")) != NULL) {
utstring_printf(locality, "%s", l->string_);
have_locality = 1;
break;
}
}
}
}
if (have_cc && have_locality)
break;
}
}
}
json_delete(json);
return (1);
}
static int revgeod_decode(UT_string *geodata, UT_string *addr, UT_string *cc, UT_string *locality)
{
JsonNode *json, *village, *j, *a;
/*
* We are parsing this data returned from revgeod(1):
*
* {"address":{"village":"La Terre Noire, 77510 Sablonnières, France","locality":"Sablonnières","cc":"FR","s":"lmdb"}}
*
*/
if ((json = json_decode(UB(geodata))) == NULL) {
return (0);
}
if ((a = json_find_member(json, "address")) != NULL) {
if ((village = json_find_member(a, "village")) != NULL) {
if (village->tag == JSON_STRING) {
utstring_printf(addr, "%s", village->string_);
}
}
if ((j = json_find_member(a, "locality")) != NULL) {
if (j->tag == JSON_STRING) {
utstring_printf(locality, "%s", j->string_);
}
}
if ((j = json_find_member(a, "cc")) != NULL) {
if (j->tag == JSON_STRING) {
utstring_printf(cc, "%s", j->string_);
}
}
}
json_delete(json);
return (1);
}
static int opencage_decode(UT_string *geodata, UT_string *addr, UT_string *cc, UT_string *locality, UT_string *tzname)
{
JsonNode *json, *results, *address, *ac, *zeroth;
JsonNode *annotations, *timezone;
/*
* We are parsing this. I want the formatted in `addr' and
* the country code short_name in `cc'
*
* {
* "documentation": "https://geocoder.opencagedata.com/api",
* "licenses": [
* {
* "name": "CC-BY-SA",
* "url": "http://creativecommons.org/licenses/by-sa/3.0/"
* },
* {
* "name": "ODbL",
* "url": "http://opendatacommons.org/licenses/odbl/summary/"
* }
* ],
* "rate": {
* "limit": 2500,
* "remaining": 2495,
* "reset": 1525392000
* },
* "results": [
* {
* "annotations" : {
* "timezone" : {
* "name" : "America/Cancun",
* "now_in_dst" : 0,
* "offset_sec" : -18000,
* "offset_string" : "-0500",
* "short_name" : "EST"
* },
* ...
* "components": {
* "city": "Sablonnières",
* "country": "France",
* "country_code": "fr",
* "place": "La Terre Noire",
* },
* "formatted": "La Terre Noire, 77510 Sablonnières, France",
*/
if ((json = json_decode(UB(geodata))) == NULL) {
return (0);
}
if ((results = json_find_member(json, "results")) != NULL) {
if ((zeroth = json_find_element(results, 0)) != NULL) {
address = json_find_member(zeroth, "formatted");
if ((address != NULL) && (address->tag == JSON_STRING)) {
utstring_printf(addr, "%s", address->string_);
}
}
if ((annotations = json_find_member(zeroth, "annotations")) != NULL) {
if ((timezone = json_find_member(annotations, "timezone")) != NULL) {
JsonNode *tz;
// puts(json_stringify(timezone, NULL));
if ((tz = json_find_member(timezone, "name")) != NULL)
{
utstring_printf(tzname, "%s", tz->string_);
}
}
}
if ((ac = json_find_member(zeroth, "components")) != NULL) {
/*
* {
* "ISO_3166-1_alpha-2": "FR",
* "_type": "place",
* "city": "Sablonnières",
* "country": "France",
* "country_code": "fr",
* "county": "Seine-et-Marne",
* "place": "La Terre Noire",
* "political_union": "European Union",
* "postcode": "77510",
* "state": "Île-de-France"
* }
*/
JsonNode *j;
if ((j = json_find_member(ac, "country_code")) != NULL) {
if (j->tag == JSON_STRING) {
char *bp = j->string_;
int ch;
while (*bp) {
ch = (islower(*bp)) ? toupper(*bp) : *bp;
utstring_printf(cc, "%c", ch);
++bp;
}
}
}
if ((j = json_find_member(ac, "city")) != NULL) {
if (j->tag == JSON_STRING) {
utstring_printf(locality, "%s", j->string_);
}
}
}
}
json_delete(json);
return (1);
}
JsonNode *revgeo(struct udata *ud, double lat, double lon, UT_string *addr, UT_string *cc)
{
static UT_string *url;
static UT_string *cbuf; /* Buffer for curl GET */
static UT_string *locality = NULL;
static UT_string *tzname = NULL;
long http_code;
CURLcode res;
int rc;
JsonNode *geo;
time_t now;
geocoder geocoder;
if ((geo = json_mkobject()) == NULL) {
return (NULL);
}
if (lat == 0.0L && lon == 0.0L) {
utstring_printf(addr, "Unknown (%lf,%lf)", lat, lon);
utstring_printf(cc, "__");
return (geo);
}
utstring_renew(url);
utstring_renew(cbuf);
utstring_renew(locality);
utstring_renew(tzname);
if (!ud->geokey || !*ud->geokey) {
utstring_printf(addr, "Unknown (%lf,%lf)", lat, lon);
utstring_printf(cc, "__");
return (geo);
}
if (strncmp(ud->geokey, "opencage:", strlen("opencage:")) == 0) {
utstring_printf(url, OPENCAGE_URL, lat, lon, ud->geokey + strlen("opencage:"));
geocoder = OPENCAGE;
} else if (strncmp(ud->geokey, "revgeod:", strlen("revgeod:")) == 0) {
/* revgeod:localhost:8865 */
utstring_printf(url, REVGEOD_URL, ud->geokey + strlen("revgeod:"), lat, lon);
geocoder = REVGEOD;
} else {
utstring_printf(url, GOOGLE_URL, lat, lon, ud->geokey);
geocoder = GOOGLE;
}
// fprintf(stderr, "--------------- %s\n", UB(url));
curl_easy_setopt(curl, CURLOPT_URL, UB(url));
curl_easy_setopt(curl, CURLOPT_USERAGENT, "OwnTracks-Recorder/1.0");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, GEOCODE_TIMEOUT);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writemem);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)cbuf);
res = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (res != CURLE_OK || http_code != 200) {
utstring_printf(addr, "revgeo failed for (%lf,%lf): HTTP status_code==%ld", lat, lon, http_code);
utstring_printf(cc, "__");
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
json_delete(geo);
return (NULL);
}
switch (geocoder) {
case GOOGLE:
rc = goog_decode(cbuf, addr, cc, locality);
break;
case OPENCAGE:
rc = opencage_decode(cbuf, addr, cc, locality, tzname);
break;
case REVGEOD:
rc = revgeod_decode(cbuf, addr, cc, locality);
break;
}
if (!rc) {
json_delete(geo);
return (NULL);
}
// fprintf(stderr, "revgeo returns %d: %s\n", rc, UB(addr));
time(&now);
json_append_member(geo, "cc", json_mkstring(UB(cc)));
json_append_member(geo, "addr", json_mkstring(UB(addr)));
json_append_member(geo, "tst", json_mknumber((double)now));
if (utstring_len(locality) > 0) {
json_append_member(geo, "locality", json_mkstring(UB(locality)));
}
if (utstring_len(tzname) > 0) {
json_append_member(geo, "tzname", json_mkstring(UB(tzname)));
}
return (geo);
}
void revgeo_init()
{
curl = curl_easy_init();
}
void revgeo_free()
{
curl_easy_cleanup(curl);
curl = NULL;
}
#if 0
int main()
{
double lat = 52.25458, lon = 5.1494;
double clat = 51.197500, clon = 6.699179;
UT_string *location = NULL, *cc = NULL;
JsonNode *json;
char *js;
curl = curl_easy_init();
utstring_renew(location);
utstring_renew(cc);
if ((json = revgeo(NULL, lat, lon, location, cc)) != NULL) {
js = json_stringify(json, " ");
printf("%s\n", js);
free(js);
} else {
printf("Cannot get revgeo\n");
}
if ((json = revgeo(NULL, clat, clon, location, cc)) != NULL) {
js = json_stringify(json, " ");
printf("%s\n", js);
free(js);
} else {
printf("Cannot get revgeo\n");
}
curl_easy_cleanup(curl);
return (0);
}
#endif