/*
* e-contact-map.c
*
* This program 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 of the License, or (at your option) version 3.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with the program; if not, see
*
* Copyright (C) 2011 Dan Vratil
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#ifdef WITH_CONTACT_MAPS
#include "e-contact-map.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "e-util/e-util.h"
#define E_CONTACT_MAP_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE \
((obj), E_TYPE_CONTACT_MAP, EContactMapPrivate))
typedef struct _AsyncContext AsyncContext;
struct _EContactMapPrivate {
GHashTable *markers; /* Hash table contact-name -> marker */
ChamplainMarkerLayer *marker_layer;
};
struct _AsyncContext {
EContactMap *map;
ClutterActor *marker;
};
enum {
CONTACT_ADDED,
CONTACT_REMOVED,
GEOCODING_STARTED,
GEOCODING_FAILED,
LAST_SIGNAL
};
static gint signals[LAST_SIGNAL] = {0};
G_DEFINE_TYPE (EContactMap, e_contact_map, GTK_CHAMPLAIN_TYPE_EMBED)
static void
async_context_free (AsyncContext *async_context)
{
g_clear_object (&async_context->map);
g_slice_free (AsyncContext, async_context);
}
static ClutterActor *
texture_new_from_pixbuf (GdkPixbuf *pixbuf,
GError **error)
{
ClutterActor *texture = NULL;
const guchar *data;
gboolean has_alpha, success;
gint width, height, rowstride;
ClutterTextureFlags flags = 0;
data = gdk_pixbuf_get_pixels (pixbuf);
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
has_alpha = gdk_pixbuf_get_has_alpha (pixbuf);
rowstride = gdk_pixbuf_get_rowstride (pixbuf);
texture = clutter_texture_new ();
success = clutter_texture_set_from_rgb_data (
CLUTTER_TEXTURE (texture),
data, has_alpha, width, height, rowstride,
(has_alpha ? 4: 3), flags, NULL);
if (!success) {
clutter_actor_destroy (CLUTTER_ACTOR (texture));
texture = NULL;
}
return texture;
}
static ClutterActor *
contact_map_photo_to_texture (EContactPhoto *photo)
{
ClutterActor *texture = NULL;
GdkPixbuf *pixbuf = NULL;
if (photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
GdkPixbufLoader *loader = gdk_pixbuf_loader_new ();
gdk_pixbuf_loader_write (
loader, photo->data.inlined.data,
photo->data.inlined.length, NULL);
gdk_pixbuf_loader_close (loader, NULL);
pixbuf = gdk_pixbuf_loader_get_pixbuf (loader);
if (pixbuf != NULL)
g_object_ref (pixbuf);
g_object_unref (loader);
} else if (photo->type == E_CONTACT_PHOTO_TYPE_URI) {
pixbuf = gdk_pixbuf_new_from_file (photo->data.uri, NULL);
}
if (pixbuf != NULL) {
texture = texture_new_from_pixbuf (pixbuf, NULL);
g_object_unref (pixbuf);
}
return texture;
}
static void
contact_map_address_resolved_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GHashTable *resolved = NULL;
gpointer marker_ptr;
AsyncContext *async_context = user_data;
ChamplainMarkerLayer *marker_layer;
ChamplainMarker *marker;
GeocodePlace *place;
GeocodeLocation *location;
GList *search_results;
const gchar *name;
GError *local_error = NULL;
marker = CHAMPLAIN_MARKER (async_context->marker);
marker_layer = async_context->map->priv->marker_layer;
name = champlain_label_get_text (CHAMPLAIN_LABEL (marker));
/* If the marker_layer does not exist anymore, the map has
* probably been destroyed before this callback was launched.
* It's not a failure, just silently clean up what was left
* behind and pretend nothing happened. */
if (!CHAMPLAIN_IS_MARKER_LAYER (marker_layer))
goto exit;
search_results = geocode_forward_search_finish (
GEOCODE_FORWARD (source_object), result, &local_error);
/* Sanity check. */
g_warn_if_fail (
((search_results != NULL) && (local_error == NULL)) ||
((search_results == NULL) && (local_error != NULL)));
/* Keep quiet if the search just came up empty. */
if (g_error_matches (local_error, GEOCODE_ERROR, GEOCODE_ERROR_NO_MATCHES)) {
g_clear_error (&local_error);
/* Leave a breadcrumb on the console for any other errors. */
} else if (local_error != NULL) {
g_warning ("%s: %s", G_STRFUNC, local_error->message);
g_clear_error (&local_error);
}
if (search_results == NULL) {
g_signal_emit (
async_context->map,
signals[GEOCODING_FAILED], 0, name);
goto exit;
}
place = GEOCODE_PLACE (search_results->data);
location = geocode_place_get_location (place);
/* Move the marker to resolved position. */
champlain_location_set_location (
CHAMPLAIN_LOCATION (marker),
geocode_location_get_latitude (location),
geocode_location_get_longitude (location));
champlain_marker_layer_add_marker (marker_layer, marker);
champlain_marker_set_selected (marker, FALSE);
/* Do not unref the list elements. */
g_list_free (search_results);
/* Store the marker in the hash table, using its label as key. */
marker_ptr = g_hash_table_lookup (
async_context->map->priv->markers, name);
if (marker_ptr != NULL) {
g_hash_table_remove (async_context->map->priv->markers, name);
champlain_marker_layer_remove_marker (marker_layer, marker_ptr);
}
g_hash_table_insert (
async_context->map->priv->markers,
g_strdup (name), marker);
g_signal_emit (
async_context->map,
signals[CONTACT_ADDED], 0, marker);
exit:
async_context_free (async_context);
if (resolved != NULL)
g_hash_table_unref (resolved);
}
static void
add_attr (GHashTable *hash_table,
const gchar *key,
const gchar *string)
{
GValue *value;
value = g_new0 (GValue, 1);
g_value_init (value, G_TYPE_STRING);
g_value_set_static_string (value, string);
g_hash_table_insert (hash_table, g_strdup (key), value);
}
static GHashTable *
address_to_xep (EContactAddress *address)
{
GHashTable *hash_table;
hash_table = g_hash_table_new_full (
(GHashFunc) g_str_hash,
(GEqualFunc) g_str_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) g_free);
add_attr (hash_table, "postalcode", address->code);
add_attr (hash_table, "country", address->country);
add_attr (hash_table, "region", address->region);
add_attr (hash_table, "locality", address->locality);
add_attr (hash_table, "street", address->street);
return hash_table;
}
static void
contact_map_finalize (GObject *object)
{
EContactMapPrivate *priv;
priv = E_CONTACT_MAP_GET_PRIVATE (object);
g_hash_table_destroy (priv->markers);
/* Chain up to parent's finalize() method. */
G_OBJECT_CLASS (e_contact_map_parent_class)->finalize (object);
}
static void
e_contact_map_class_init (EContactMapClass *class)
{
GObjectClass *object_class;
g_type_class_add_private (class, sizeof (EContactMapPrivate));
object_class = G_OBJECT_CLASS (class);
object_class->finalize = contact_map_finalize;
signals[CONTACT_ADDED] = g_signal_new (
"contact-added",
G_TYPE_FROM_CLASS (class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (EContactMapClass, contact_added),
NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1, G_TYPE_OBJECT);
signals[CONTACT_REMOVED] = g_signal_new (
"contact-removed",
G_TYPE_FROM_CLASS (class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (EContactMapClass, contact_removed),
NULL, NULL,
g_cclosure_marshal_VOID__STRING,
G_TYPE_NONE, 1, G_TYPE_STRING);
signals[GEOCODING_STARTED] = g_signal_new (
"geocoding-started",
G_TYPE_FROM_CLASS (class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (EContactMapClass, geocoding_started),
NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1, G_TYPE_OBJECT);
signals[GEOCODING_FAILED] = g_signal_new (
"geocoding-failed",
G_TYPE_FROM_CLASS (class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (EContactMapClass, geocoding_failed),
NULL, NULL,
g_cclosure_marshal_VOID__STRING,
G_TYPE_NONE, 1, G_TYPE_STRING);
}
static void
e_contact_map_init (EContactMap *map)
{
ChamplainMarkerLayer *layer;
ChamplainView *view;
map->priv = E_CONTACT_MAP_GET_PRIVATE (map);
map->priv->markers = g_hash_table_new_full (
(GHashFunc) g_str_hash,
(GEqualFunc) g_str_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) NULL);
view = gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (map));
/* This feature is somehow broken sometimes, so disable it for now */
champlain_view_set_zoom_on_double_click (view, FALSE);
layer = champlain_marker_layer_new_full (CHAMPLAIN_SELECTION_SINGLE);
champlain_view_add_layer (view, CHAMPLAIN_LAYER (layer));
map->priv->marker_layer = layer;
}
GtkWidget *
e_contact_map_new (void)
{
return g_object_new (E_TYPE_CONTACT_MAP, NULL);
}
void
e_contact_map_add_contact (EContactMap *map,
EContact *contact)
{
EContactAddress *address;
EContactPhoto *photo;
const gchar *contact_name;
const gchar *contact_uid;
g_return_if_fail (E_IS_CONTACT_MAP (map));
g_return_if_fail (E_IS_CONTACT (contact));
photo = e_contact_get (contact, E_CONTACT_PHOTO);
contact_name = e_contact_get_const (contact, E_CONTACT_FILE_AS);
contact_uid = e_contact_get_const (contact, E_CONTACT_UID);
address = e_contact_get (contact, E_CONTACT_ADDRESS_HOME);
if (address != NULL) {
gchar *name;
name = g_strdup_printf (
"%s (%s)", contact_name, _("Home"));
e_contact_map_add_marker (
map, name, contact_uid, address, photo);
g_free (name);
e_contact_address_free (address);
}
address = e_contact_get (contact, E_CONTACT_ADDRESS_WORK);
if (address != NULL) {
gchar *name;
name = g_strdup_printf (
"%s (%s)", contact_name, _("Work"));
e_contact_map_add_marker (
map, name, contact_uid, address, photo);
g_free (name);
e_contact_address_free (address);
}
if (photo != NULL)
e_contact_photo_free (photo);
}
void
e_contact_map_add_marker (EContactMap *map,
const gchar *name,
const gchar *contact_uid,
EContactAddress *address,
EContactPhoto *photo)
{
ClutterActor *marker;
GHashTable *hash_table;
GeocodeForward *geocoder;
AsyncContext *async_context;
g_return_if_fail (E_IS_CONTACT_MAP (map));
g_return_if_fail (name != NULL);
g_return_if_fail (contact_uid != NULL);
g_return_if_fail (address != NULL);
marker = champlain_label_new ();
champlain_label_set_text (CHAMPLAIN_LABEL (marker), name);
if (photo != NULL) {
champlain_label_set_image (
CHAMPLAIN_LABEL (marker),
contact_map_photo_to_texture (photo));
}
/* Stash the contact UID for EContactMapWindow. */
g_object_set_data_full (
G_OBJECT (marker), "contact-uid",
g_strdup (contact_uid),
(GDestroyNotify) g_free);
hash_table = address_to_xep (address);
geocoder = geocode_forward_new_for_params (hash_table);
g_hash_table_destroy (hash_table);
async_context = g_slice_new0 (AsyncContext);
async_context->map = g_object_ref (map);
async_context->marker = marker;
geocode_forward_search_async (
geocoder, NULL,
contact_map_address_resolved_cb,
async_context);
g_object_unref (geocoder);
g_signal_emit (map, signals[GEOCODING_STARTED], 0, marker);
}
/**
* The \name parameter must match the label of the
* marker (for example "John Smith (work)")
*/
void
e_contact_map_remove_contact (EContactMap *map,
const gchar *name)
{
ChamplainMarker *marker;
g_return_if_fail (E_IS_CONTACT_MAP (map));
g_return_if_fail (name != NULL);
marker = g_hash_table_lookup (map->priv->markers, name);
champlain_marker_layer_remove_marker (map->priv->marker_layer, marker);
g_hash_table_remove (map->priv->markers, name);
g_signal_emit (map, signals[CONTACT_REMOVED], 0, name);
}
void
e_contact_map_zoom_on_marker (EContactMap *map,
ClutterActor *marker)
{
ChamplainView *view;
gdouble lat, lng;
g_return_if_fail (E_IS_CONTACT_MAP (map));
g_return_if_fail (CLUTTER_IS_ACTOR (marker));
lat = champlain_location_get_latitude (CHAMPLAIN_LOCATION (marker));
lng = champlain_location_get_longitude (CHAMPLAIN_LOCATION (marker));
view = gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (map));
champlain_view_center_on (view, lat, lng);
champlain_view_set_zoom_level (view, 15);
}
ChamplainView *
e_contact_map_get_view (EContactMap *map)
{
g_return_val_if_fail (E_IS_CONTACT_MAP (map), NULL);
return gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (map));
}
#endif /* WITH_CONTACT_MAPS */