/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* gweather-location.c - Location-handling code
*
* Copyright 2008, Red Hat, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see
* .
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include "gweather-location.h"
#include "gweather-timezone.h"
#include "gweather-parser.h"
#include "gweather-private.h"
/* This is the precision of coordinates in the database */
#define EPSILON 0.000001
/**
* SECTION:gweatherlocation
* @Title: GWeatherLocation
*
* A #GWeatherLocation represents a "location" of some type known to
* libgweather; anything from a single weather station to the entire
* world. See #GWeatherLocationLevel for information about how the
* hierarchy of locations works.
*/
/**
* GWeatherLocationLevel:
* @GWEATHER_LOCATION_WORLD: A location representing the entire world.
* @GWEATHER_LOCATION_REGION: A location representing a continent or
* other top-level region.
* @GWEATHER_LOCATION_COUNTRY: A location representing a "country" (or
* other geographic unit that has an ISO-3166 country code)
* @GWEATHER_LOCATION_ADM1: A location representing a "first-level
* administrative division"; ie, a state, province, or similar
* division.
* @GWEATHER_LOCATION_CITY: A location representing a city
* @GWEATHER_LOCATION_WEATHER_STATION: A location representing a
* weather station.
* @GWEATHER_LOCATION_DETACHED: A location that is detached from the
* database, for example because it was loaded from external storage
* and could not be fully recovered. The parent of this location is
* the nearest weather station.
* @GWEATHER_LOCATION_NAMED_TIMEZONE: A location representing a named
* or special timezone in the world, such as UTC
*
* The size/scope of a particular #GWeatherLocation.
*
* Locations form a hierarchy, with a %GWEATHER_LOCATION_WORLD
* location at the top, divided into regions or countries, and so on.
* Countries may or may not be divided into "adm1"s, and "adm1"s may
* or may not be divided into "adm2"s. A city will have at least one,
* and possibly several, weather stations inside it. Weather stations
* will never appear outside of cities.
*
* Building a database with gweather_location_get_world() will never
* create detached instances, but deserializing might.
**/
static int
sort_locations_by_name (gconstpointer a, gconstpointer b)
{
GWeatherLocation *loc_a = *(GWeatherLocation **)a;
GWeatherLocation *loc_b = *(GWeatherLocation **)b;
return g_utf8_collate (loc_a->local_sort_name, loc_b->local_sort_name);
}
static int
sort_locations_by_distance (gconstpointer a, gconstpointer b, gpointer user_data)
{
GWeatherLocation *loc_a = *(GWeatherLocation **)a;
GWeatherLocation *loc_b = *(GWeatherLocation **)b;
GWeatherLocation *city = (GWeatherLocation *)user_data;
double dist_a, dist_b;
dist_a = gweather_location_get_distance (loc_a, city);
dist_b = gweather_location_get_distance (loc_b, city);
if (dist_a < dist_b)
return -1;
else if (dist_a > dist_b)
return 1;
else
return 0;
}
static gboolean
parse_coordinates (const char *coordinates,
double *latitude, double *longitude)
{
char *p;
*latitude = g_ascii_strtod (coordinates, &p) * M_PI / 180.0;
if (p == (char *)coordinates)
return FALSE;
if (*p++ != ' ')
return FALSE;
*longitude = g_ascii_strtod (p, &p) * M_PI / 180.0;
return !*p;
}
static GWeatherLocation *
location_new (GWeatherLocationLevel level)
{
GWeatherLocation *loc;
loc = g_slice_new0 (GWeatherLocation);
loc->latitude = loc->longitude = DBL_MAX;
loc->level = level;
loc->ref_count = 1;
return loc;
}
static void
add_nearest_weather_station (GWeatherLocation *location)
{
GWeatherLocation **siblings;
GWeatherLocation *closest = NULL;
double min_distance = G_MAXDOUBLE;
guint i;
g_assert (location->parent);
g_assert (gweather_location_get_level (location) == GWEATHER_LOCATION_CITY);
if (location->children != NULL)
return;
siblings = location->parent->children;
for (i = 0; siblings[i] != NULL; i++) {
double distance;
if (siblings[i] == location)
continue;
/* Skip siblings without valid coordinates */
if (!siblings[i]->latlon_valid)
continue;
distance = gweather_location_get_distance (location, siblings[i]);
if (distance < min_distance)
closest = siblings[i];
}
if (!closest) {
g_critical ("Location '%s' has no valid airports attached", location->english_name);
return;
}
location->children = g_new0 (GWeatherLocation *, 2);
location->children[0] = g_memdup (closest, sizeof(GWeatherLocation));
}
static void
add_nearest_weather_stations (GWeatherLocation *location)
{
GWeatherLocation **children;
guint i;
/* For each city without a , add the nearest airport in the
* same country or state to it */
children = gweather_location_get_children (location);
for (i = 0; children[i] != NULL; i++) {
if (gweather_location_get_level (children[i]) == GWEATHER_LOCATION_CITY)
add_nearest_weather_station (children[i]);
else
add_nearest_weather_stations (children[i]);
}
}
static GWeatherLocation *
location_new_from_xml (GWeatherParser *parser, GWeatherLocationLevel level,
GWeatherLocation *parent)
{
GWeatherLocation *loc, *child;
GPtrArray *children = NULL;
const char *tagname;
char *value, *normalized;
int tagtype;
unsigned int i;
loc = location_new (level);
loc->parent = parent;
if (level == GWEATHER_LOCATION_WORLD) {
loc->metar_code_cache = g_hash_table_ref (parser->metar_code_cache);
loc->country_code_cache = g_hash_table_ref (parser->country_code_cache);
loc->timezone_cache = g_hash_table_ref (parser->timezone_cache);
}
children = g_ptr_array_new ();
if (xmlTextReaderRead (parser->xml) != 1)
goto error_out;
while ((tagtype = xmlTextReaderNodeType (parser->xml)) !=
XML_READER_TYPE_END_ELEMENT) {
if (tagtype != XML_READER_TYPE_ELEMENT) {
if (xmlTextReaderRead (parser->xml) != 1)
goto error_out;
continue;
}
tagname = (const char *) xmlTextReaderConstName (parser->xml);
if ((!strcmp (tagname, "name") || !strcmp (tagname, "_name")) && !loc->english_name) {
loc->msgctxt = _gweather_parser_get_msgctxt_value (parser);
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
loc->english_name = g_strdup (value);
if (loc->msgctxt) {
loc->local_name = g_strdup (g_dpgettext2 ("libgweather-locations",
(char*) loc->msgctxt, value));
} else {
loc->local_name = g_strdup (g_dgettext ("libgweather-locations", value));
}
normalized = g_utf8_normalize (loc->local_name, -1, G_NORMALIZE_ALL);
loc->local_sort_name = g_utf8_casefold (normalized, -1);
g_free (normalized);
normalized = g_utf8_normalize (loc->english_name, -1, G_NORMALIZE_ALL);
loc->english_sort_name = g_utf8_casefold (normalized, -1);
g_free (normalized);
xmlFree (value);
} else if (!strcmp (tagname, "iso-code") && !loc->country_code) {
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
loc->country_code = g_strdup (value);
xmlFree (value);
} else if (!strcmp (tagname, "tz-hint") && !loc->tz_hint) {
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
loc->tz_hint = g_strdup (value);
xmlFree (value);
} else if (!strcmp (tagname, "code") && !loc->station_code) {
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
loc->station_code = g_strdup (value);
xmlFree (value);
} else if (!strcmp (tagname, "coordinates") && !loc->latlon_valid) {
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
if (parse_coordinates (value, &loc->latitude, &loc->longitude))
loc->latlon_valid = TRUE;
else
g_warning ("Coordinates could not be parsed: '%s'", value);
xmlFree (value);
} else if (!strcmp (tagname, "zone") && !loc->forecast_zone) {
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
loc->forecast_zone = g_strdup (value);
xmlFree (value);
} else if (!strcmp (tagname, "radar") && !loc->radar) {
value = _gweather_parser_get_value (parser);
if (!value)
goto error_out;
loc->radar = g_strdup (value);
xmlFree (value);
} else if (!strcmp (tagname, "region")) {
child = location_new_from_xml (parser, GWEATHER_LOCATION_REGION, loc);
if (!child)
goto error_out;
g_ptr_array_add (children, child);
} else if (!strcmp (tagname, "country")) {
child = location_new_from_xml (parser, GWEATHER_LOCATION_COUNTRY, loc);
if (!child)
goto error_out;
g_ptr_array_add (children, child);
} else if (!strcmp (tagname, "state")) {
child = location_new_from_xml (parser, GWEATHER_LOCATION_ADM1, loc);
if (!child)
goto error_out;
g_ptr_array_add (children, child);
} else if (!strcmp (tagname, "city")) {
child = location_new_from_xml (parser, GWEATHER_LOCATION_CITY, loc);
if (!child)
goto error_out;
g_ptr_array_add (children, child);
} else if (!strcmp (tagname, "location")) {
child = location_new_from_xml (parser, GWEATHER_LOCATION_WEATHER_STATION, loc);
if (!child)
goto error_out;
g_ptr_array_add (children, child);
} else if (!strcmp (tagname, "named-timezone")) {
child = location_new_from_xml (parser, GWEATHER_LOCATION_NAMED_TIMEZONE, loc);
if (!child)
goto error_out;
g_ptr_array_add (children, child);
} else if (!strcmp (tagname, "timezones")) {
loc->zones = _gweather_timezones_parse_xml (parser);
if (!loc->zones)
goto error_out;
} else {
if (xmlTextReaderNext (parser->xml) != 1)
goto error_out;
}
}
if (xmlTextReaderRead (parser->xml) != 1 && parent)
goto error_out;
if (level == GWEATHER_LOCATION_WEATHER_STATION ||
level == GWEATHER_LOCATION_NAMED_TIMEZONE) {
/* Cache weather stations by METAR code */
GList *a, *b;
a = g_hash_table_lookup (parser->metar_code_cache, loc->station_code);
b = g_list_append (a, gweather_location_ref (loc));
if (b != a)
g_hash_table_replace (parser->metar_code_cache, loc->station_code, b);
}
if (level == GWEATHER_LOCATION_COUNTRY) {
if (loc->country_code) {
GWeatherLocation *existing;
existing = g_hash_table_lookup (parser->country_code_cache, loc->country_code);
if (existing)
g_warning ("A country with country code '%s' is already in the database",
loc->country_code);
g_hash_table_replace (parser->country_code_cache, loc->country_code,
gweather_location_ref (loc));
}
}
if (children->len) {
if (level == GWEATHER_LOCATION_CITY)
g_ptr_array_sort_with_data (children, sort_locations_by_distance, loc);
else
g_ptr_array_sort (children, sort_locations_by_name);
g_ptr_array_add (children, NULL);
loc->children = (GWeatherLocation **)g_ptr_array_free (children, FALSE);
} else
g_ptr_array_free (children, TRUE);
return loc;
error_out:
gweather_location_unref (loc);
for (i = 0; i < children->len; i++)
gweather_location_unref (children->pdata[i]);
g_ptr_array_free (children, TRUE);
return NULL;
}
static GWeatherLocation *global_world;
/**
* gweather_location_get_world:
*
* Obtains the shared #GWeatherLocation of type %GWEATHER_LOCATION_WORLD,
* representing a hierarchy containing all of the locations from
* Locations.xml.
*
* Return value: (allow-none) (transfer none): a %GWEATHER_LOCATION_WORLD
* location, or %NULL if Locations.xml could not be found or could not be parsed.
* The return value is owned by libgweather and should not be modified or freed.
**/
GWeatherLocation *
gweather_location_get_world (void)
{
GWeatherParser *parser;
if (!global_world) {
const char *locations_path;
locations_path = g_getenv ("LIBGWEATHER_LOCATIONS_PATH");
if (locations_path) {
parser = _gweather_parser_new_for_path (locations_path);
if (!parser) {
g_warning ("Failed to open '%s' as LIBGWEATHER_LOCATIONS_PATH",
locations_path);
parser = _gweather_parser_new ();
}
} else {
parser = _gweather_parser_new ();
}
if (!parser)
return NULL;
global_world = location_new_from_xml (parser, GWEATHER_LOCATION_WORLD, NULL);
if (!g_getenv ("LIBGWEATHER_LOCATIONS_NO_NEAREST"))
add_nearest_weather_stations (global_world);
_gweather_parser_free (parser);
}
return global_world;
}
/**
* gweather_location_ref:
* @loc: a #GWeatherLocation
*
* Adds 1 to @loc's reference count.
*
* Return value: @loc
**/
GWeatherLocation *
gweather_location_ref (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
loc->ref_count++;
return loc;
}
/**
* gweather_location_unref:
* @loc: a #GWeatherLocation
*
* Subtracts 1 from @loc's reference count, and frees it if the
* reference count reaches 0.
**/
void
gweather_location_unref (GWeatherLocation *loc)
{
int i;
g_return_if_fail (loc != NULL);
if (--loc->ref_count)
return;
g_return_if_fail (loc->level != GWEATHER_LOCATION_WORLD);
g_free (loc->english_name);
g_free (loc->local_name);
g_free (loc->msgctxt);
g_free (loc->local_sort_name);
g_free (loc->english_sort_name);
g_free (loc->country_code);
g_free (loc->tz_hint);
g_free (loc->station_code);
g_free (loc->forecast_zone);
g_free (loc->radar);
if (loc->children) {
for (i = 0; loc->children[i]; i++) {
loc->children[i]->parent = NULL;
gweather_location_unref (loc->children[i]);
}
g_free (loc->children);
}
if (loc->zones) {
for (i = 0; loc->zones[i]; i++)
gweather_timezone_unref (loc->zones[i]);
g_free (loc->zones);
}
if (loc->metar_code_cache)
g_hash_table_unref (loc->metar_code_cache);
if (loc->timezone_cache)
g_hash_table_unref (loc->timezone_cache);
if (loc->country_code_cache)
g_hash_table_unref (loc->country_code_cache);
g_slice_free (GWeatherLocation, loc);
}
GType
gweather_location_get_type (void)
{
static volatile gsize type_volatile = 0;
if (g_once_init_enter (&type_volatile)) {
GType type = g_boxed_type_register_static (
g_intern_static_string ("GWeatherLocation"),
(GBoxedCopyFunc) gweather_location_ref,
(GBoxedFreeFunc) gweather_location_unref);
g_once_init_leave (&type_volatile, type);
}
return type_volatile;
}
/**
* gweather_location_get_name:
* @loc: a #GWeatherLocation
*
* Gets @loc's name, localized into the current language.
*
* Return value: @loc's name
**/
const char *
gweather_location_get_name (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
return g_strdup (loc->local_name);
}
/**
* gweather_location_get_sort_name:
* @loc: a #GWeatherLocation
*
* Gets @loc's "sort name", which is the name after having
* g_utf8_normalize() (with %G_NORMALIZE_ALL) and g_utf8_casefold()
* called on it. You can use this to sort locations, or to comparing
* user input against a location name.
*
* Return value: @loc's sort name
**/
const char *
gweather_location_get_sort_name (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
return loc->local_sort_name;
}
/**
* gweather_location_get_level:
* @loc: a #GWeatherLocation
*
* Gets @loc's level, from %GWEATHER_LOCATION_WORLD, to
* %GWEATHER_LOCATION_WEATHER_STATION.
*
* Return value: @loc's level
**/
GWeatherLocationLevel
gweather_location_get_level (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, GWEATHER_LOCATION_WORLD);
return loc->level;
}
/**
* gweather_location_level_to_string:
* @level: a #GWeatherLocationLevel
*
* Returns the location level as a string, useful for debugging
* purposes.
*
* Return value: a string
**/
const char *
gweather_location_level_to_string (GWeatherLocationLevel level)
{
switch (level) {
case GWEATHER_LOCATION_WORLD:
return "world";
case GWEATHER_LOCATION_REGION:
return "region";
case GWEATHER_LOCATION_COUNTRY:
return "country";
case GWEATHER_LOCATION_ADM1:
return "adm1";
case GWEATHER_LOCATION_CITY:
return "city";
case GWEATHER_LOCATION_WEATHER_STATION:
return "station";
case GWEATHER_LOCATION_DETACHED:
return "detached";
case GWEATHER_LOCATION_NAMED_TIMEZONE:
return "named-timezone";
default:
g_assert_not_reached();
}
return NULL;
}
/**
* gweather_location_get_parent:
* @loc: a #GWeatherLocation
*
* Gets @loc's parent location.
*
* Return value: (transfer none) (allow-none): @loc's parent, or %NULL
* if @loc is a %GWEATHER_LOCATION_WORLD node.
**/
GWeatherLocation *
gweather_location_get_parent (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
return loc->parent;
}
/**
* gweather_location_get_children:
* @loc: a #GWeatherLocation
*
* Gets an array of @loc's children; this is owned by @loc and will
* not remain valid if @loc is freed.
*
* Return value: (transfer none) (array zero-terminated=1): @loc's
* children. (May be empty, but will not be %NULL.)
**/
GWeatherLocation **
gweather_location_get_children (GWeatherLocation *loc)
{
static GWeatherLocation *no_children = NULL;
g_return_val_if_fail (loc != NULL, NULL);
if (loc->children)
return loc->children;
else
return &no_children;
}
static void
foreach_city (GWeatherLocation *loc,
GFunc callback,
gpointer user_data,
const char *country_code,
GWeatherFilterFunc func,
gpointer user_data_func)
{
if (country_code) {
const char *loc_country_code = gweather_location_get_country(loc);
if (loc_country_code && (g_strcmp0 (loc_country_code, country_code) != 0))
return;
}
if (func) {
if (!func (loc, user_data_func))
return;
}
if (loc->level == GWEATHER_LOCATION_WEATHER_STATION) {
callback (loc, user_data);
} else if (loc->children) {
int i;
for (i = 0; loc->children[i]; i++)
foreach_city (loc->children[i], callback, user_data, country_code, func, user_data_func);
}
}
struct FindNearestCityData {
double latitude;
double longitude;
GWeatherLocation *location;
double distance;
};
struct ArgData {
double latitude;
double longitude;
GWeatherLocation *location;
GTask *task;
};
typedef struct ArgData ArgData;
static double
location_distance (double lat1, double long1,
double lat2, double long2)
{
/* average radius of the earth in km */
static const double radius = 6372.795;
return acos (cos (lat1) * cos (lat2) * cos (long1 - long2) +
sin (lat1) * sin (lat2)) * radius;
}
static void
find_nearest_city (GWeatherLocation *location,
gpointer user_data) {
struct FindNearestCityData *data = user_data;
double distance = location_distance (location->latitude, location->longitude,
data->latitude, data->longitude);
if (data->location == NULL || data->distance > distance) {
data->location = location;
data->distance = distance;
}
}
/**
* gweather_location_find_nearest_city:
* @loc: (allow-none): The parent location, which will be searched recursively
* @lat: Latitude, in degrees
* @lon: Longitude, in degrees
*
* Finds the nearest city to the passed latitude and
* longitude, among the descendants of @loc.
*
* @loc must be at most a %GWEATHER_LOCATION_ADM1 location.
* This restriction may be lifted in a future version.
*
* Note that this function does not check if (@lat, @lon) fall inside
* @loc, or are in the same region and timezone as the return value.
*
* Returns: (transfer full): the city closest to (@lat, @lon), in the
* region or administrative district of @loc.
*
* Since: 3.12
*/
GWeatherLocation *
gweather_location_find_nearest_city (GWeatherLocation *loc,
double lat,
double lon)
{
/* The data set really isn't too big. Don't concern ourselves
* with a proper nearest neighbors search. Instead, just do
* an O(n) search. */
struct FindNearestCityData data;
g_return_val_if_fail (loc == NULL || loc->level < GWEATHER_LOCATION_CITY, NULL);
if (loc == NULL)
loc = gweather_location_get_world ();
lat = lat * M_PI / 180.0;
lon = lon * M_PI / 180.0;
data.latitude = lat;
data.longitude = lon;
data.location = NULL;
data.distance = 0.0;
foreach_city (loc, (GFunc) find_nearest_city, &data, NULL, NULL, NULL);
return gweather_location_ref (data.location);
}
/**
* gweather_location_find_nearest_city_full:
* @loc: (allow-none): The parent location, which will be searched recursively
* @lat: Latitude, in degrees
* @lon: Longitude, in degrees
* @func: (scope notified) (allow-none): returns true to continue check for
* the location and false to filter the location out
* @user_data: for customization
* @destroy: to destroy user_data
*
* Finds the nearest city to the passed latitude and
* longitude, among the descendants of @loc.
*
* Supports the use of own filter function to filter out locations.
* Geocoding should be done on the application side if needed.
*
* @loc must be at most a %GWEATHER_LOCATION_ADM1 location.
* This restriction may be lifted in a future version.
*
* Returns: (transfer full): the city closest to (@lat, @lon), in the
* region or administrative district of @loc with validation of filter function.
*
* Since: 3.12
*/
GWeatherLocation *
gweather_location_find_nearest_city_full (GWeatherLocation *loc,
double lat,
double lon,
GWeatherFilterFunc func,
gpointer user_data,
GDestroyNotify destroy)
{
/* The data set really isn't too big. Don't concern ourselves
* with a proper nearest neighbors search. Instead, just do
* an O(n) search. */
struct FindNearestCityData data;
g_return_val_if_fail (loc == NULL || loc->level < GWEATHER_LOCATION_CITY ||
loc->level == GWEATHER_LOCATION_NAMED_TIMEZONE, NULL);
if (loc == NULL)
loc = gweather_location_get_world ();
lat = lat * M_PI / 180.0;
lon = lon * M_PI / 180.0;
data.latitude = lat;
data.longitude = lon;
data.location = NULL;
data.distance = 0.0;
foreach_city (loc, (GFunc) find_nearest_city, &data, NULL, func, user_data);
destroy (user_data);
return gweather_location_ref (data.location);
}
static void
_got_place (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
ArgData *info = (user_data);
GTask *task = info->task;
GeocodePlace *place;
GError *error = NULL;
const char *country_code;
struct FindNearestCityData data;
place = geocode_reverse_resolve_finish (GEOCODE_REVERSE (source_object), result, &error);
if (place == NULL) {
g_task_return_error (task, error);
g_slice_free (ArgData, info);
g_object_unref (task);
return;
}
country_code = geocode_place_get_country_code (place);
data.latitude = info->latitude * M_PI / 180.0;
data.longitude = info->longitude * M_PI / 180.0;
data.location = NULL;
data.distance = 0.0;
foreach_city (info->location, (GFunc) find_nearest_city, &data, country_code, NULL, NULL);
g_slice_free (ArgData, info);
if (data.location == NULL) {
g_task_return_pointer (task, NULL, NULL);
} else {
GWeatherLocation *location;
location = _gweather_location_new_detached(data.location, geocode_place_get_town (place), TRUE, data.latitude, data.longitude);
g_task_return_pointer (task, location, (GDestroyNotify)gweather_location_unref);
}
g_object_unref (task);
}
/**
* gweather_location_detect_nearest_city:
* @loc: (allow-none): The parent location, which will be searched recursively
* @lat: Latitude, in degrees
* @lon: Longitude, in degrees
* @cancellable: optional, NULL to ignore
* @callback: callback function for GAsyncReadyCallback argument for GAsyncResult
* @user_data: user data passed to @callback
*
* Initializes geocode reversing to find place for (@lat, @lon) coordinates. Calls the callback
* function passed by user when the result is ready.
*
* @loc must be at most a %GWEATHER_LOCATION_ADM1 location.
* This restriction may be lifted in a future version.
*
* Since: 3.12
*/
void
gweather_location_detect_nearest_city (GWeatherLocation *loc,
double lat,
double lon,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
ArgData *data;
GeocodeLocation *location;
GeocodeReverse *reverse;
GTask *task;
g_return_if_fail (loc == NULL || loc->level < GWEATHER_LOCATION_CITY ||
loc->level == GWEATHER_LOCATION_NAMED_TIMEZONE);
if (loc == NULL)
loc = gweather_location_get_world ();
location = geocode_location_new (lat, lon, GEOCODE_LOCATION_ACCURACY_CITY);
reverse = geocode_reverse_new_for_location (location);
task = g_task_new (NULL, cancellable, callback, user_data);
data = g_slice_new0 (ArgData);
data->latitude = lat;
data->longitude = lon;
data->location = loc;
data->task = task;
geocode_reverse_resolve_async (reverse, cancellable, _got_place, data);
}
/**
* gweather_location_detect_nearest_location_finish:
* @result:
* @error: Stores error if any occurs in retrieving the result
*
* Fetches the location from @result.
*
* Returns: (transfer full): Customized GWeatherLocation
*
* Since: 3.12
*/
GWeatherLocation *
gweather_location_detect_nearest_city_finish (GAsyncResult *result, GError **error)
{
GTask *task;
g_return_val_if_fail (g_task_is_valid (result,
NULL),
NULL);
task = G_TASK (result);
return g_task_propagate_pointer (task, error);
}
/**
* gweather_location_has_coords:
* @loc: a #GWeatherLocation
*
* Checks if @loc has valid latitude and longitude.
*
* Return value: %TRUE if @loc has valid latitude and longitude.
**/
gboolean
gweather_location_has_coords (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, FALSE);
return loc->latlon_valid;
}
/**
* gweather_location_get_coords:
* @loc: a #GWeatherLocation
* @latitude: (out): on return will contain @loc's latitude
* @longitude: (out): on return will contain @loc's longitude
*
* Gets @loc's coordinates; you must check
* gweather_location_has_coords() before calling this.
**/
void
gweather_location_get_coords (GWeatherLocation *loc,
double *latitude, double *longitude)
{
//g_return_if_fail (loc->latlon_valid);
g_return_if_fail (loc != NULL);
g_return_if_fail (latitude != NULL);
g_return_if_fail (longitude != NULL);
*latitude = loc->latitude / M_PI * 180.0;
*longitude = loc->longitude / M_PI * 180.0;
}
/**
* gweather_location_get_distance:
* @loc: a #GWeatherLocation
* @loc2: a second #GWeatherLocation
*
* Determines the distance in kilometers between @loc and @loc2.
*
* Return value: the distance between @loc and @loc2.
**/
double
gweather_location_get_distance (GWeatherLocation *loc, GWeatherLocation *loc2)
{
g_return_val_if_fail (loc != NULL, G_MAXDOUBLE);
g_return_val_if_fail (loc2 != NULL, G_MAXDOUBLE);
g_return_val_if_fail (loc->latlon_valid, G_MAXDOUBLE);
g_return_val_if_fail (loc2->latlon_valid, G_MAXDOUBLE);
return location_distance (loc->latitude, loc->longitude,
loc2->latitude, loc2->longitude);
}
/**
* gweather_location_get_country:
* @loc: a #GWeatherLocation
*
* Gets the ISO 3166 country code of @loc (or %NULL if @loc is a
* region- or world-level location)
*
* Return value: (allow-none): @loc's country code (or %NULL if @loc
* is a region- or world-level location)
**/
const char *
gweather_location_get_country (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
while (loc->parent && !loc->country_code)
loc = loc->parent;
return loc->country_code;
}
/**
* gweather_location_get_timezone:
* @loc: a #GWeatherLocation
*
* Gets the timezone associated with @loc, if known.
*
* The timezone is owned either by @loc or by one of its parents.
* FIXME.
*
* Return value: (transfer none) (allow-none): @loc's timezone, or
* %NULL
**/
GWeatherTimezone *
gweather_location_get_timezone (GWeatherLocation *loc)
{
const char *tz_hint;
int i;
g_return_val_if_fail (loc != NULL, NULL);
while (loc && !loc->tz_hint)
loc = loc->parent;
if (!loc)
return NULL;
tz_hint = loc->tz_hint;
while (loc) {
while (loc && !loc->zones)
loc = loc->parent;
if (!loc)
return NULL;
for (i = 0; loc->zones[i]; i++) {
if (!strcmp (tz_hint, gweather_timezone_get_tzid (loc->zones[i])))
return loc->zones[i];
}
loc = loc->parent;
}
return NULL;
}
/**
* gweather_location_get_timezone_str:
* @loc: a #GWeatherLocation
*
* Gets the timezone associated with @loc, if known, as a string.
*
* The timezone string is owned either by @loc or by one of its
* parents, do not free it.
*
* Return value: (transfer none) (allow-none): @loc's timezone as
* a string, or %NULL
**/
const char *
gweather_location_get_timezone_str (GWeatherLocation *loc)
{
const char *tz_hint;
g_return_val_if_fail (loc != NULL, NULL);
while (loc && !loc->tz_hint)
loc = loc->parent;
if (!loc)
return NULL;
tz_hint = loc->tz_hint;
return tz_hint;
}
static void
add_timezones (GWeatherLocation *loc, GPtrArray *zones)
{
int i;
if (loc->zones) {
for (i = 0; loc->zones[i]; i++)
g_ptr_array_add (zones, gweather_timezone_ref (loc->zones[i]));
}
if (loc->level < GWEATHER_LOCATION_COUNTRY && loc->children) {
for (i = 0; loc->children[i]; i++)
add_timezones (loc->children[i], zones);
}
}
/**
* gweather_location_get_timezones:
* @loc: a #GWeatherLocation
*
* Gets an array of all timezones associated with any location under
* @loc. You can use gweather_location_free_timezones() to free this
* array.
*
* Return value: (transfer full) (array zero-terminated=1): an array
* of timezones. May be empty but will not be %NULL.
**/
GWeatherTimezone **
gweather_location_get_timezones (GWeatherLocation *loc)
{
GPtrArray *zones;
g_return_val_if_fail (loc != NULL, NULL);
zones = g_ptr_array_new ();
add_timezones (loc, zones);
g_ptr_array_add (zones, NULL);
return (GWeatherTimezone **)g_ptr_array_free (zones, FALSE);
}
/**
* gweather_location_free_timezones:
* @loc: a #GWeatherLocation
* @zones: an array returned from gweather_location_get_timezones()
*
* Frees the array of timezones returned by
* gweather_location_get_timezones().
**/
void
gweather_location_free_timezones (GWeatherLocation *loc,
GWeatherTimezone **zones)
{
int i;
g_return_if_fail (loc != NULL);
g_return_if_fail (zones != NULL);
for (i = 0; zones[i]; i++)
gweather_timezone_unref (zones[i]);
g_free (zones);
}
/**
* gweather_location_get_code:
* @loc: a #GWeatherLocation
*
* Gets the METAR station code associated with a
* %GWEATHER_LOCATION_WEATHER_STATION location.
*
* Return value: (allow-none): @loc's METAR station code, or %NULL
**/
const char *
gweather_location_get_code (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
return loc->station_code;
}
/**
* gweather_location_get_city_name:
* @loc: a #GWeatherLocation
*
* For a %GWEATHER_LOCATION_CITY or %GWEATHER_LOCATION_DETACHED location,
* this is equivalent to gweather_location_get_name().
* For a %GWEATHER_LOCATION_WEATHER_STATION location, it is equivalent to
* calling gweather_location_get_name() on the location's parent. For
* other locations it will return %NULL.
*
* Return value: (allow-none): @loc's city name, or %NULL
**/
char *
gweather_location_get_city_name (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
if (loc->level == GWEATHER_LOCATION_CITY ||
loc->level == GWEATHER_LOCATION_DETACHED) {
return g_strdup (loc->local_name);
} else if (loc->level == GWEATHER_LOCATION_WEATHER_STATION &&
loc->parent &&
loc->parent->level == GWEATHER_LOCATION_CITY) {
return g_strdup (loc->parent->local_name);
} else
return NULL;
}
/**
* gweather_location_get_country_name:
* @loc: a #GWeatherLocation
*
* Gets the country name of loc.
* For a %GWEATHER_LOCATION_COUNTRY location, this is equivalent to
* gweather_location_get_name().
* For a %GWEATHER_LOCATION_REGION and GWEATHER_LOCATION_WORLD location it
* will return %NULL.
* For other locations it will find the parent %GWEATHER_LOCATION_COUNTRY
* and return its name.
*
* Return value: (allow-none): @loc's country name, or %NULL
**/
char *
gweather_location_get_country_name (GWeatherLocation *loc)
{
GWeatherLocation *country;
g_return_val_if_fail (loc != NULL, NULL);
country = loc;
while (country != NULL && country->level != GWEATHER_LOCATION_COUNTRY) {
country = country->parent;
}
return country != NULL ? g_strdup (country->local_name) : NULL;
}
void
_gweather_location_update_weather_location (GWeatherLocation *gloc,
WeatherLocation *loc)
{
const char *code = NULL, *zone = NULL, *radar = NULL, *tz_hint = NULL, *country = NULL;
gboolean latlon_valid = FALSE;
gdouble lat = DBL_MAX, lon = DBL_MAX;
GWeatherLocation *l;
if (gloc->level == GWEATHER_LOCATION_CITY && gloc->children)
l = gloc->children[0];
else
l = gloc;
while (l && (!code || !zone || !radar || !tz_hint || !latlon_valid || !country)) {
if (!code && l->station_code)
code = l->station_code;
if (!zone && l->forecast_zone)
zone = l->forecast_zone;
if (!radar && l->radar)
radar = l->radar;
if (!tz_hint && l->tz_hint)
tz_hint = l->tz_hint;
if (!country && l->country_code)
country = l->country_code;
if (!latlon_valid && l->latlon_valid) {
lat = l->latitude;
lon = l->longitude;
latlon_valid = TRUE;
}
l = l->parent;
}
loc->name = g_strdup (gloc->local_name),
loc->code = g_strdup (code);
loc->zone = g_strdup (zone);
loc->radar = g_strdup (radar);
loc->country_code = g_strdup (country);
loc->tz_hint = g_strdup (tz_hint);
loc->latlon_valid = latlon_valid;
loc->latitude = lat;
loc->longitude = lon;
}
/**
* gweather_location_find_by_station_code:
* @world: a #GWeatherLocation at the world level
* @station_code: a 4 letter METAR code
*
* Retrieves the weather station identifier by @station_code.
* Note that multiple instances of the same weather station can exist
* in the database, and this function will return any of them, so this
* not usually what you want.
*
* See gweather_location_deserialize() to recover a stored #GWeatherLocation.
*
* Returns: (transfer none): a weather station level #GWeatherLocation for @station_code,
* or %NULL if none exists in the database.
*/
GWeatherLocation *
gweather_location_find_by_station_code (GWeatherLocation *world,
const gchar *station_code)
{
GList *l;
l = g_hash_table_lookup (world->metar_code_cache, station_code);
return l ? l->data : NULL;
}
/**
* gweather_location_find_by_country_code:
* @world: a #GWeatherLocation at the world
* @country_code: a country code
*
* Retrieves the country identified by the specified ISO 3166 code,
* if present in the database.
*
* Returns: (transfer none): a country level #GWeatherLocation, or %NULL.
*/
GWeatherLocation *
gweather_location_find_by_country_code (GWeatherLocation *world,
const gchar *country_code)
{
return g_hash_table_lookup (world->country_code_cache, country_code);
}
/**
* gweather_location_equal:
* @one: a #GWeatherLocation
* @two: another #GWeatherLocation
*
* Compares two #GWeatherLocation and sees if they represent the same
* place.
* It is only legal to call this for cities, weather stations or
* detached locations.
* Note that this function only checks for geographical characteristics,
* such as coordinates and METAR code. It is still possible that the two
* locations belong to different worlds (in which case care must be
* taken when passing them GWeatherLocationEntry and GWeatherInfo), or
* if one is them is detached it could have a custom name.
*
* Returns: %TRUE if the two locations represent the same place as
* far as libgweather can tell, and %FALSE otherwise.
*/
gboolean
gweather_location_equal (GWeatherLocation *one,
GWeatherLocation *two)
{
int level;
if (one == two)
return TRUE;
if (one->level != two->level &&
one->level != GWEATHER_LOCATION_DETACHED &&
two->level != GWEATHER_LOCATION_DETACHED)
return FALSE;
level = one->level;
if (level == GWEATHER_LOCATION_DETACHED)
level = two->level;
if (level == GWEATHER_LOCATION_COUNTRY)
return g_strcmp0 (one->country_code, two->country_code);
if (level == GWEATHER_LOCATION_ADM1) {
if (g_strcmp0 (one->english_sort_name, two->english_sort_name) != 0)
return FALSE;
return one->parent && two->parent &&
gweather_location_equal (one->parent, two->parent);
}
if (g_strcmp0 (one->station_code, two->station_code) != 0)
return FALSE;
if (one->level != GWEATHER_LOCATION_DETACHED &&
two->level != GWEATHER_LOCATION_DETACHED &&
!gweather_location_equal (one->parent, two->parent))
return FALSE;
return ABS(one->latitude - two->latitude) < EPSILON &&
ABS(one->longitude - two->longitude) < EPSILON;
}
/* ------------------- serialization ------------------------------- */
#define FORMAT 2
static GVariant *
gweather_location_format_two_serialize (GWeatherLocation *location)
{
const char *name;
gboolean is_city;
GVariantBuilder latlon_builder;
GVariantBuilder parent_latlon_builder;
name = location->english_name;
/* Normalize location to be a weather station or detached */
if (location->level == GWEATHER_LOCATION_CITY) {
location = location->children[0];
is_city = TRUE;
} else {
is_city = FALSE;
}
g_variant_builder_init (&latlon_builder, G_VARIANT_TYPE ("a(dd)"));
if (location->latlon_valid)
g_variant_builder_add (&latlon_builder, "(dd)", location->latitude, location->longitude);
g_variant_builder_init (&parent_latlon_builder, G_VARIANT_TYPE ("a(dd)"));
if (location->parent && location->parent->latlon_valid)
g_variant_builder_add (&parent_latlon_builder, "(dd)", location->parent->latitude, location->parent->longitude);
return g_variant_new ("(ssba(dd)a(dd))",
name, location->station_code, is_city,
&latlon_builder, &parent_latlon_builder);
}
GWeatherLocation *
_gweather_location_new_detached (GWeatherLocation *nearest_station,
const char *name,
gboolean latlon_valid,
gdouble latitude,
gdouble longitude)
{
GWeatherLocation *self;
char *normalized;
self = g_slice_new0 (GWeatherLocation);
self->ref_count = 1;
self->level = GWEATHER_LOCATION_DETACHED;
if (name != NULL) {
self->english_name = g_strdup (name);
self->local_name = g_strdup (name);
normalized = g_utf8_normalize (name, -1, G_NORMALIZE_ALL);
self->english_sort_name = g_utf8_casefold (normalized, -1);
self->local_sort_name = g_strdup (self->english_sort_name);
g_free (normalized);
} else if (nearest_station) {
self->english_name = g_strdup (nearest_station->english_name);
self->local_name = g_strdup (nearest_station->local_name);
self->english_sort_name = g_strdup (nearest_station->english_sort_name);
self->local_sort_name = g_strdup (nearest_station->local_sort_name);
}
self->parent = nearest_station;
self->children = NULL;
if (nearest_station)
self->station_code = g_strdup (nearest_station->station_code);
g_assert (nearest_station || latlon_valid);
if (latlon_valid) {
self->latlon_valid = TRUE;
self->latitude = latitude;
self->longitude = longitude;
} else {
self->latlon_valid = nearest_station->latlon_valid;
self->latitude = nearest_station->latitude;
self->longitude = nearest_station->longitude;
}
return self;
}
static GWeatherLocation *
gweather_location_common_deserialize (GWeatherLocation *world,
const char *name,
const char *station_code,
gboolean is_city,
gboolean latlon_valid,
gdouble latitude,
gdouble longitude,
gboolean parent_latlon_valid,
gdouble parent_latitude,
gdouble parent_longitude)
{
GList *candidates, *l;
GWeatherLocation *found;
/* First find the list of candidate locations */
candidates = g_hash_table_lookup (world->metar_code_cache, station_code);
/* A station code beginning with @ indicates a named timezone entry, just
* return it directly
*/
if (station_code[0] == '@')
return gweather_location_ref (candidates->data);
/* If we don't have coordinates, fallback immediately to making up
* a location
*/
if (!latlon_valid)
return candidates ? _gweather_location_new_detached (candidates->data, name, FALSE, 0, 0) : NULL;
found = NULL;
/* First try weather stations directly. */
for (l = candidates; l; l = l->next) {
GWeatherLocation *ws, *city;
ws = l->data;
if (!ws->latlon_valid ||
ABS(ws->latitude - latitude) >= EPSILON ||
ABS(ws->longitude - longitude) >= EPSILON)
/* Not what we're looking for... */
continue;
/* If we can't check for the latitude and longitude
of the parent, we just assume we found what we needed
*/
if ((!parent_latlon_valid || !ws->parent || !ws->parent->latlon_valid) ||
(ABS(parent_latitude - ws->parent->latitude) < EPSILON &&
ABS(parent_longitude - ws->parent->longitude) < EPSILON)) {
/* Found! Now check which one we want (ws or city) and the name... */
if (is_city)
city = ws->parent;
else
city = ws;
if (city == NULL) {
/* Oops! There is no city for this weather station! */
continue;
}
if (name == NULL ||
g_strcmp0 (name, city->english_name) == 0 ||
g_strcmp0 (name, city->local_name) == 0)
found = gweather_location_ref (city);
else
found = _gweather_location_new_detached (ws, name, TRUE, latitude, longitude);
break;
}
}
if (found)
return found;
/* No weather station matches the serialized data, let's pick
one at random from the station code list */
if (candidates)
return _gweather_location_new_detached (candidates->data,
name, TRUE, latitude, longitude);
else
return NULL;
}
static GWeatherLocation *
gweather_location_format_one_deserialize (GWeatherLocation *world,
GVariant *variant)
{
const char *name;
const char *station_code;
gboolean is_city, latlon_valid, parent_latlon_valid;
gdouble latitude, longitude, parent_latitude, parent_longitude;
/* This one instead is a critical, because format is specified in
the containing variant */
g_return_val_if_fail (g_variant_is_of_type (variant,
G_VARIANT_TYPE ("(ssbm(dd)m(dd))")), NULL);
g_variant_get (variant, "(&s&sbm(dd)m(dd))", &name, &station_code, &is_city,
&latlon_valid, &latitude, &longitude,
&parent_latlon_valid, &parent_latitude, &parent_longitude);
return gweather_location_common_deserialize(world, name, station_code, is_city,
latlon_valid, latitude, longitude,
parent_latlon_valid, parent_latitude, parent_longitude);
}
static GWeatherLocation *
gweather_location_format_two_deserialize (GWeatherLocation *world,
GVariant *variant)
{
const char *name;
const char *station_code;
GVariant *latlon_variant;
GVariant *parent_latlon_variant;
gboolean is_city, latlon_valid, parent_latlon_valid;
gdouble latitude, longitude, parent_latitude, parent_longitude;
/* This one instead is a critical, because format is specified in
the containing variant */
g_return_val_if_fail (g_variant_is_of_type (variant,
G_VARIANT_TYPE ("(ssba(dd)a(dd))")), NULL);
g_variant_get (variant, "(&s&sb@a(dd)@a(dd))", &name, &station_code, &is_city,
&latlon_variant, &parent_latlon_variant);
if (g_variant_n_children (latlon_variant) > 0) {
latlon_valid = TRUE;
g_variant_get_child (latlon_variant, 0, "(dd)", &latitude, &longitude);
} else {
latlon_valid = FALSE;
latitude = 0;
longitude = 0;
}
if (g_variant_n_children (parent_latlon_variant) > 0) {
parent_latlon_valid = TRUE;
g_variant_get_child (parent_latlon_variant, 0, "(dd)", &parent_latitude, &parent_longitude);
} else {
parent_latlon_valid = FALSE;
parent_latitude = 0;
parent_longitude = 0;
}
return gweather_location_common_deserialize(world, name, station_code, is_city,
latlon_valid, latitude, longitude,
parent_latlon_valid, parent_latitude, parent_longitude);
}
/**
* gweather_location_serialize:
* @loc: a city, weather station or detached #GWeatherLocation
*
* Transforms a #GWeatherLocation into a #GVariant, in a way that
* calling gweather_location_deserialize() will hold an equivalent
* #GWeatherLocation.
* The resulting variant can then be stored into GSettings or on disk.
* This call is only valid for cities, weather stations and detached
* locations.
* The format of the resulting #GVariant is private to libgweather,
* and it is subject to change. You should use the "v" format in GSettings,
* to ensure maximum compatibility with future versions of the library.
*
* Returns: (transfer none): the serialization of @location.
*/
GVariant *
gweather_location_serialize (GWeatherLocation *loc)
{
g_return_val_if_fail (loc != NULL, NULL);
g_return_val_if_fail (loc->level >= GWEATHER_LOCATION_CITY, NULL);
return g_variant_new ("(uv)", FORMAT,
gweather_location_format_two_serialize (loc));
}
/**
* gweather_location_deserialize:
* @world: a world-level #GWeatherLocation
* @serialized: the #GVariant representing the #GWeatherLocation
*
* This call undoes the effect of gweather_location_serialize(), that
* is, it turns a #GVariant into a #GWeatherLocation. The conversion
* happens in the context of @world (i.e, for a city or weather station,
* the resulting location will be attached to a administrative division,
* country and region as appropriate).
*
* Returns: (transfer full): the deserialized location.
*/
GWeatherLocation *
gweather_location_deserialize (GWeatherLocation *world,
GVariant *serialized)
{
GVariant *v;
GWeatherLocation *loc;
int format;
g_return_val_if_fail (world != NULL, NULL);
g_return_val_if_fail (serialized != NULL, NULL);
/* This is not a critical error, because the serialization format
is not public, so apps can't check this before calling */
if (!g_variant_is_of_type (serialized, G_VARIANT_TYPE ("(uv)")))
return NULL;
g_variant_get (serialized, "(uv)", &format, &v);
if (format == 1)
loc = gweather_location_format_one_deserialize (world, v);
else if (format == 2)
loc = gweather_location_format_two_deserialize (world, v);
else
loc = NULL;
g_variant_unref (v);
return loc;
}
/**
* gweather_location_new_detached:
* @name: the user visible location name
* @icao: (nullable): the ICAO code of the location
* @latitude: the latitude of the location
* @longitude: the longitude of the location
*
* Construct a new location from the given data, supplementing
* any missing information from the static database.
*/
GWeatherLocation *
gweather_location_new_detached (const char *name,
const char *icao,
gdouble latitude,
gdouble longitude)
{
GWeatherLocation *world, *city;
g_return_val_if_fail (name != NULL, NULL);
if (*name == 0)
name = NULL;
world = gweather_location_get_world ();
if (icao != NULL) {
return gweather_location_common_deserialize (world, name,
icao, FALSE,
TRUE, latitude, longitude,
FALSE, 0, 0);
} else {
city = gweather_location_find_nearest_city (world, latitude, longitude);
latitude = DEGREES_TO_RADIANS (latitude);
longitude = DEGREES_TO_RADIANS (longitude);
return _gweather_location_new_detached (city, name,
TRUE, latitude, longitude);
}
}