Search on geo-position
authorColin Clark <colin.clark@cclark.uk>
Thu, 11 May 2017 18:06:13 +0000 (19:06 +0100)
committerColin Clark <colin.clark@cclark.uk>
Thu, 11 May 2017 18:06:13 +0000 (19:06 +0100)
Additional search option to locate images within a distance of a
location. The search origin can be specified in a number of ways - see
the Help file.

doc/docbook/GuideImageSearchSearch.xml
doc/docbook/GuideReference.xml
doc/docbook/GuideReferenceDecodeLatLong.xml [new file with mode: 0644]
src/bar_gps.c
src/search.c

index 1071fab..f1719ea 100644 (file)
         </term>\r
         <listitem>The search will match if the file's associated keywords match all, match any, or exclude the entered keywords, depending on the method selected from the drop down menu. Keywords can be separated with a space, comma, or tab character.</listitem>\r
       </varlistentry>\r
+      <varlistentry>\r
+        <term>\r
+          <guilabel>Geocoded position</guilabel>\r
+        </term>\r
+        <listitem>\r
+          The search will match if the file's GPS position is less than or greater than the selected distance from the specified position, or is not geocoded, depending on the method selected from the drop down menu.\r
+          The search location can be specified by\r
+          <itemizedlist>\r
+            <listitem>\r
+              Type in a latitude/longitude in the format\r
+              <code>89.123 179.123</code>\r
+            </listitem>\r
+            <listitem>Drag-and-drop a geocoded image onto the search box</listitem>\r
+            <listitem>If Geeqie's map is displayed, a left-click on the map will store the latitude/longitude under the mouse cursor into the clipboard. It can then be pasted into the search box.</listitem>\r
+            <listitem>Copy-and-paste (in some circumstances drag-and-drop) the result of an Internet search.</listitem>\r
+          </itemizedlist>\r
+          <note>\r
+            In this last case, the result of a search may contain the latitude/longitude embedded in the URL. This may be automatically decoded with the help of an external file:-\r
+            <programlisting xml:space="preserve">~/.config/geeqie/geocode-parameters.awk</programlisting>\r
+            See\r
+            <link linkend="GuideReferenceDecodeLatLong">Decoding Latitude and Longitude</link>\r
+            for details on how to create this file.\r
+          </note>\r
+        </listitem>\r
+      </varlistentry>\r
     </variablelist>\r
     <para />\r
     <para />\r
index e12d928..72f05a3 100644 (file)
@@ -10,5 +10,6 @@
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="GuideReferenceLIRC.xml" />\r
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="GuideReferenceStandards.xml" />\r
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="GuideReferenceSupportedFormats.xml" />\r
+  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="GuideReferenceDecodeLatLong.xml" />\r
   <para />\r
 </chapter>
diff --git a/doc/docbook/GuideReferenceDecodeLatLong.xml b/doc/docbook/GuideReferenceDecodeLatLong.xml
new file mode 100644 (file)
index 0000000..83f31e5
--- /dev/null
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<section id="GuideReferenceDecodeLatLong">\r
+  <title id="titleGuideReferenceDecodeLatLong">Decoding Latitude and Longitude</title>\r
+  <para>This section is relevent to the search option "Search on geo-location".</para>\r
+  <para>\r
+    The result of some internet or other searches for placenames can contain a latitude and longitude embedded in a text string. For example an openstreetmap.org search can give a URL such as:\r
+    <para />\r
+    <code>https://www.openstreetmap.org/search?query=51.5542%2C-0.1816#map=12/51.5542/-0.1818</code>\r
+  </para>\r
+  <para>\r
+    If you paste such a string into the search box, the latitude/longitude can be automatically extracted and used as the origin of the search. To do this create the file\r
+    <para />\r
+    <code>~/.config/geeqie/geocode-parameters.awk</code>\r
+    <para />\r
+    and copy the following text into it:\r
+  </para>\r
+  <para>\r
+    <programlisting xml:space="preserve">\r
+# Store this file in:\r
+# ~/.config/geeqie/geocode-parameters.awk\r
+#\r
+# This file is used by the Search option "search on geo-position".\r
+# It is used to decode the results of internet or other searches\r
+# to extract a geo-position from a text string. \r
+# To include other searches, follow the examples below and\r
+# ensure the returned value is either in the format:\r
+# 89.123 179.123\r
+# or\r
+# Error: $0\r
+#\r
+\r
+function check_parameters(latitude, longitude)\r
+    {\r
+    # Ensure the parameters are numbers    \r
+    if ((latitude == (latitude+0)) &amp;&amp; (longitude == (longitude+0)))\r
+        {\r
+        if (latitude &gt;= -90 &amp;&amp; latitude &lt;= 90 &amp;&amp;\r
+                        longitude &gt;= -180 &amp;&amp; longitude &lt;= 180)\r
+            {\r
+            return latitude " " longitude\r
+            }\r
+        else\r
+            {\r
+            return "Error: " latitude " " longitude\r
+            }\r
+        }\r
+    else\r
+        {\r
+        return "Error: " latitude " " longitude\r
+        }\r
+    }\r
+\r
+# This awk file is accessed by the decode_geo_parameters() function\r
+# in search.c. The call is of the format:\r
+# echo "string_to_be_searched" | awk -f geocode-parameters.awk\r
+#\r
+# Search the input string for known formats.\r
+{\r
+if (index($0, "http://www.geonames.org/maps/google_"))\r
+    {\r
+    # This is a drag-and-drop or copy-paste from a geonames.org search\r
+    # in the format e.g.\r
+    # http://www.geonames.org/maps/google_51.513_-0.092.html\r
+    \r
+    gsub(/http:\/\/www.geonames.org\/maps\/google_/, "")\r
+    gsub(/.html/, "")\r
+    gsub(/_/, " ")\r
+    print check_parameters($1, $2)\r
+    }\r
+\r
+else if (index($0, "https://www.openstreetmap.org/search?query="))\r
+    {\r
+    # This is a copy-paste from an openstreetmap.org search\r
+    # in the format e.g.\r
+    # https://www.openstreetmap.org/search?query=51.4878%2C-0.1353#map=11/51.4880/-0.1356\r
+    \r
+    gsub(/https:\/\/www.openstreetmap.org\/search\?query=/, "")\r
+    gsub(/#map=.*/, "")\r
+    gsub(/%2C/, " ")\r
+    print check_parameters($1, $2)\r
+    }\r
+\r
+else if (index($0, "https://www.openstreetmap.org/#map="))\r
+    {\r
+    # This is a copy-paste from an openstreetmap.org search\r
+    # in the format e.g.\r
+    # https://www.openstreetmap.org/#map=5/18.271/16.084\r
+    \r
+    gsub(/https:\/\/www.openstreetmap.org\/#map=[^\/]*/,"")\r
+    gsub(/\//," ")\r
+    print check_parameters($1, $2)\r
+    }\r
+\r
+else if (index($0, "https://www.google.com/maps/"))\r
+    {\r
+    # This is a copy-paste from a google.com maps search\r
+    # in the format e.g.\r
+    # https://www.google.com/maps/place/London,+UK/@51.5283064,-0.3824815,10z/data=....\r
+    \r
+    gsub(/https:\/\/www.google.com\/maps.*@/,"")\r
+    sub(/,/," ")\r
+    gsub(/,.*/,"")\r
+    print check_parameters($1, $2)\r
+    }\r
+\r
+else if (index($0,".html"))\r
+    {\r
+    # This is an unknown html address\r
+    \r
+    print "Error: " $0\r
+    }\r
+\r
+else if (index($0,"http"))\r
+    {\r
+    # This is an unknown html address\r
+    \r
+    print "Error: " $0\r
+    }\r
+\r
+else if (index($0, ","))\r
+    {\r
+    # This is assumed to be a simple lat/long of the format:\r
+    # 89.123,179.123\r
+    \r
+    split($0, latlong, ",")\r
+    print check_parameters(latlong[1], latlong[2])\r
+    }\r
+\r
+else\r
+    {\r
+    # This is assumed to be a simple lat/long of the format:\r
+    # 89.123 179.123\r
+    \r
+    split($0, latlong, " ")\r
+    print check_parameters(latlong[1], latlong[2])\r
+    }\r
+}\r
+\r
+    </programlisting>\r
+  </para>\r
+</section>\r
index a112be6..d21b824 100644 (file)
@@ -680,6 +680,8 @@ static gboolean bar_pane_gps_map_keypress_cb(GtkWidget *widget, GdkEventButton *
 {
        PaneGPSData *pgd = data;
        GtkWidget *menu;
+       GtkClipboard *clipboard;
+       gchar *geo_coords;
 
        if (bevent->button == MOUSE_BUTTON_RIGHT)
                {
@@ -694,7 +696,17 @@ static gboolean bar_pane_gps_map_keypress_cb(GtkWidget *widget, GdkEventButton *
                }
        else if (bevent->button == MOUSE_BUTTON_LEFT)
                {
-               return FALSE;
+               clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
+               geo_coords = g_strdup_printf("%lf %lf",
+                                                       champlain_view_y_to_latitude(
+                                                               CHAMPLAIN_VIEW(pgd->gps_view),bevent->y),
+                                                       champlain_view_x_to_longitude(
+                                                               CHAMPLAIN_VIEW(pgd->gps_view),bevent->x));
+               gtk_clipboard_set_text(clipboard, geo_coords, -1);
+
+               g_free(geo_coords);
+
+               return TRUE;
                }
        else
                {
index a858ac7..4bc5334 100644 (file)
@@ -32,6 +32,7 @@
 #include "image-load.h"
 #include "img-view.h"
 #include "layout.h"
+#include "math.h"
 #include "menu.h"
 #include "metadata.h"
 #include "misc.h"
@@ -60,6 +61,7 @@
 #define SEARCH_BUFFER_MATCH_MISS 1
 #define SEARCH_BUFFER_FLUSH_SIZE 99
 
+#define GEOCODE_NAME "geocode-parameters.awk"
 
 typedef enum {
        SEARCH_MATCH_NONE,
@@ -170,6 +172,7 @@ struct _SearchData
        MatchType match_dimensions;
        MatchType match_keywords;
        MatchType match_comment;
+       MatchType match_gps;
 
        gboolean match_name_enable;
        gboolean match_size_enable;
@@ -199,6 +202,18 @@ struct _SearchData
        ThumbLoader *thumb_loader;
        gboolean thumb_enable;
        FileData *thumb_fd;
+
+       /* Used for lat/long coordinate search
+       */
+       gint search_gps;
+       gdouble search_lat, search_lon;
+       GtkWidget *entry_gps_coord;
+       GtkWidget *check_gps;
+       GtkWidget *spin_gps;
+       GtkWidget *units_gps;
+       GtkWidget *menu_gps;
+       gboolean match_gps_enable;
+
 };
 
 typedef struct _MatchFileData MatchFileData;
@@ -253,6 +268,12 @@ static const MatchList text_search_menu_comment[] = {
        { N_("miss"),           SEARCH_MATCH_NONE }
 };
 
+static const MatchList text_search_menu_gps[] = {
+       { N_("not geocoded"),   SEARCH_MATCH_NONE },
+       { N_("less than"),      SEARCH_MATCH_UNDER },
+       { N_("greater than"),   SEARCH_MATCH_OVER }
+};
+
 static GList *search_window_list = NULL;
 
 
@@ -1356,6 +1377,12 @@ static GtkTargetEntry result_drag_types[] = {
 };
 static gint n_result_drag_types = 2;
 
+static GtkTargetEntry result_drop_types[] = {
+       { "text/uri-list", 0, TARGET_URI_LIST },
+       { "text/plain", 0, TARGET_TEXT_PLAIN }
+};
+static gint n_result_drop_types = 2;
+
 static void search_dnd_data_set(GtkWidget *widget, GdkDragContext *context,
                                GtkSelectionData *selection_data, guint info,
                                guint time, gpointer data)
@@ -1402,6 +1429,83 @@ static void search_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointe
                }
 }
 
+#define BUFSIZE 128
+
+static gchar *decode_geo_parameters(const gchar *input_text)
+{
+       gchar *message;
+       gchar *path = g_build_filename(get_rc_dir(), GEOCODE_NAME, NULL);
+       gchar *cmd = g_strconcat("echo \'", input_text, "\'  | awk -f ", path, NULL);
+
+       if (g_file_test(path, G_FILE_TEST_EXISTS))
+               {
+               gchar buf[BUFSIZE];
+               FILE *fp;
+
+               if ((fp = popen(cmd, "r")) == NULL)
+                       {
+                       message = g_strconcat("Error: opening pipe\n", input_text, NULL);
+                       }
+               else
+                       {
+                       fgets(buf, BUFSIZE, fp);
+                       message = g_strconcat(buf, NULL);
+
+                       if(pclose(fp))
+                               {
+                               message = g_strconcat("Error: Command not found or exited with error status\n", input_text, NULL);
+                               }
+                       }
+               }
+       else
+               {
+               message = g_strconcat(input_text, NULL);
+               }
+
+       g_free(path);
+       g_free(cmd);
+       return message;
+}
+
+static void search_gps_dnd_received_cb(GtkWidget *pane, GdkDragContext *context,
+                                                                               gint x, gint y,
+                                                                               GtkSelectionData *selection_data, guint info,
+                                                                               guint time, gpointer data)
+{
+       SearchData *sd = data;
+       GList *list;
+       gdouble latitude, longitude;
+       FileData *fd;
+
+       if (info == TARGET_URI_LIST)
+               {
+               list = uri_filelist_from_gtk_selection_data(selection_data);
+
+               /* If more than one file, use only the first file in a list.
+               */
+               if (list != NULL)
+                       {
+                       fd = list->data;
+                       latitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLatitude", 1000);
+                       longitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLongitude", 1000);
+                       if (latitude != 1000 && longitude != 1000)
+                               {
+                               gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord),
+                                                       g_strdup_printf("%lf %lf", latitude, longitude));
+                               }
+                       else
+                               {
+                               gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord), "Image is not geocoded");
+                               }
+                       }
+               }
+
+       if (info == TARGET_TEXT_PLAIN)
+               {
+               gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord),"");
+               }
+}
+
 static void search_dnd_init(SearchData *sd)
 {
        gtk_drag_source_set(sd->result_view, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
@@ -1411,6 +1515,14 @@ static void search_dnd_init(SearchData *sd)
                         G_CALLBACK(search_dnd_data_set), sd);
        g_signal_connect(G_OBJECT(sd->result_view), "drag_begin",
                         G_CALLBACK(search_dnd_begin), sd);
+
+       gtk_drag_dest_set(GTK_WIDGET(sd->entry_gps_coord),
+                                        GTK_DEST_DEFAULT_ALL,
+                                         result_drop_types, n_result_drop_types,
+                                        GDK_ACTION_COPY);
+
+       g_signal_connect(G_OBJECT(sd->entry_gps_coord), "drag_data_received",
+                                       G_CALLBACK(search_gps_dnd_received_cb), sd);
 }
 
 /*
@@ -1890,8 +2002,63 @@ static gboolean search_file_next(SearchData *sd)
                        }
                }
 
-       if ((match || extra_only) &&
-           (sd->match_dimensions_enable || sd->match_similarity_enable))
+       if (match && sd->match_gps_enable)
+               {
+               /* Calculate the distance the image is from the specified origin.
+               * This is a standard algorithm. A simplified one may be faster.
+               */
+               #define RADIANS  0.0174532925
+               #define KM_EARTH_RADIUS 6371
+               #define MILES_EARTH_RADIUS 3959
+               #define NAUTICAL_MILES_EARTH_RADIUS 3440
+
+               gdouble latitude, longitude, range, conversion;
+
+               if (g_strcmp0(gtk_combo_box_text_get_active_text(
+                                               GTK_COMBO_BOX_TEXT(sd->units_gps)), _("km")) == 0)
+                       {
+                       conversion = KM_EARTH_RADIUS;
+                       }
+               else if (g_strcmp0(gtk_combo_box_text_get_active_text(
+                                               GTK_COMBO_BOX_TEXT(sd->units_gps)), _("miles")) == 0)
+                       {
+                       conversion = MILES_EARTH_RADIUS;
+                       }
+               else
+                       {
+                       conversion = NAUTICAL_MILES_EARTH_RADIUS;
+                       }
+
+               tested = TRUE;
+               match = FALSE;
+
+               latitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLatitude", 1000);
+               longitude = metadata_read_GPS_coord(fd, "Xmp.exif.GPSLongitude", 1000);
+               if (latitude != 1000 && longitude != 1000)
+                       {
+                       range = conversion * acos(sin(latitude * RADIANS) *
+                                               sin(sd->search_lat * RADIANS) + cos(latitude * RADIANS) *
+                                               cos(sd->search_lat * RADIANS) * cos((sd->search_lon -
+                                               longitude) * RADIANS));
+                       if (sd->match_gps == SEARCH_MATCH_UNDER)
+                               {
+                               if (sd->search_gps >= range)
+                                       match = TRUE;
+                               }
+                       else if (sd->match_gps == SEARCH_MATCH_OVER)
+                               {
+                               if (sd->search_gps < range)
+                                       match = TRUE;
+                               }
+                       }
+               else if (sd->match_gps == SEARCH_MATCH_NONE)
+                       {
+                       match = TRUE;
+                       }
+               }
+
+       if ((match || extra_only) && (sd->match_dimensions_enable ||
+                                                               sd ->match_similarity_enable))
                {
                tested = TRUE;
 
@@ -2122,6 +2289,7 @@ static void search_start_cb(GtkWidget *widget, gpointer data)
        SearchData *sd = data;
        GtkTreeViewColumn *column;
        gchar *path;
+       gchar *entry_text;
 
        if (sd->search_folder_list)
                {
@@ -2152,6 +2320,32 @@ static void search_start_cb(GtkWidget *widget, gpointer data)
                tab_completion_append_to_history(sd->entry_similarity, sd->search_similarity_path);
                }
 
+       /* Check the coordinate entry.
+       * If the result is not sensible, it should get blocked.
+       */
+       if (sd->match_gps_enable)
+               {
+               if (sd->match_gps != SEARCH_MATCH_NONE)
+                       {
+                       entry_text = decode_geo_parameters(gtk_entry_get_text(
+                                                                               GTK_ENTRY(sd->entry_gps_coord)));
+
+                       sd->search_lat = 1000;
+                       sd->search_lon = 1000;
+                       sscanf(entry_text," %lf  %lf ", &sd->search_lat, &sd->search_lon );
+                       if (!(entry_text != NULL && !g_strstr_len(entry_text, -1, "Error") &&
+                                               sd->search_lat >= -90 && sd->search_lat <= 90 &&
+                                               sd->search_lon >= -180 && sd->search_lon <= 180))
+                               {
+                               file_util_warning_dialog(_(
+                                               "Entry does not contain a valid lat/long value"),
+                                                       entry_text, GTK_STOCK_DIALOG_WARNING, sd->window);
+                               return;
+                               }
+                       g_free(entry_text);
+                       }
+               }
+
        string_list_free(sd->search_keyword_list);
        sd->search_keyword_list = keyword_list_pull(sd->entry_keywords);
 
@@ -2425,6 +2619,16 @@ static void menu_choice_spin_cb(GtkAdjustment *adjustment, gpointer data)
        *value = (gint)gtk_adjustment_get_value(adjustment);
 }
 
+static void menu_choice_gps_cb(GtkWidget *combo, gpointer data)
+{
+       SearchData *sd = data;
+
+       if (!menu_choice_get_match_type(combo, &sd->match_gps)) return;
+
+       menu_choice_set_visible(gtk_widget_get_parent(sd->spin_gps),
+                                       (sd->match_gps != SEARCH_MATCH_NONE));
+}
+
 static GtkWidget *menu_spin(GtkWidget *box, gdouble min, gdouble max, gint value,
                            GCallback func, gpointer data)
 {
@@ -2607,6 +2811,9 @@ void search_new(FileData *dir_fd, FileData *example_file)
 
        sd->search_similarity = 95;
 
+       sd->search_gps = 1;
+       sd->match_gps = SEARCH_MATCH_NONE;
+
        if (example_file)
                {
                sd->search_similarity_path = g_strdup(example_file->path);
@@ -2767,6 +2974,38 @@ void search_new(FileData *dir_fd, FileData *example_file)
        pref_checkbox_new_int(hbox, _("Match case"),
                              sd->search_comment_match_case, &sd->search_comment_match_case);
 
+       /* Search for images within a specified range of a lat/long coordinate
+       */
+       hbox = menu_choice(sd->box_search, &sd->check_gps, &sd->menu_gps,
+                          _("Image is"), &sd->match_gps_enable,
+                          text_search_menu_gps, sizeof(text_search_menu_gps) / sizeof(MatchList),
+                          G_CALLBACK(menu_choice_gps_cb), sd);
+
+       hbox2 = gtk_hbox_new(FALSE, PREF_PAD_SPACE);
+       gtk_box_pack_start(GTK_BOX(hbox), hbox2, FALSE, FALSE, 0);
+       sd->spin_gps = menu_spin(hbox2, 1, 9999, sd->search_gps,
+                                                                  G_CALLBACK(menu_choice_spin_cb), &sd->search_gps);
+
+       sd->units_gps = gtk_combo_box_text_new();
+       gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("km"));
+       gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("miles"));
+       gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(sd->units_gps), _("n.m."));
+       gtk_box_pack_start(GTK_BOX(hbox2), sd->units_gps, FALSE, FALSE, 0);
+       gtk_combo_box_set_active(GTK_COMBO_BOX(sd->units_gps), 0);
+       gtk_widget_set_tooltip_text(sd->units_gps, "kilometres, miles or nautical miles");
+       gtk_widget_show(sd->units_gps);
+
+       pref_label_new(hbox2, _("from"));
+
+       sd->entry_gps_coord = gtk_entry_new();
+       gtk_editable_set_editable(GTK_EDITABLE(sd->entry_gps_coord), TRUE);
+       gtk_widget_set_has_tooltip(sd->entry_gps_coord, TRUE);
+       gtk_widget_set_tooltip_text(sd->entry_gps_coord, _("Enter a coordinate in the form:\n89.123 179.456\nor drag-and-drop a geo-coded image\nor left-click on the map and paste\nor cut-and-paste or drag-and-drop\nan internet search URL\nSee the Help file"));
+       gtk_box_pack_start(GTK_BOX(hbox2), sd->entry_gps_coord, TRUE, TRUE, 0);
+       gtk_widget_set_sensitive(sd->entry_gps_coord, TRUE);
+
+       gtk_widget_show(sd->entry_gps_coord);
+
        /* Done the types of searches */
 
        scrolled = gtk_scrolled_window_new(NULL, NULL);