changeset 2324:3709d9c3862f search-lat-long

Search on geographical position Additional search function: within specified range of a geographical position
author Colin Clark <cclark@mcb.net>
date Sun, 08 May 2016 19:10:39 +0100
parents b18b437258f0
children
files doc/docbook/GuideImageSearchSearch.xml src/search.c
diffstat 2 files changed, 296 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/doc/docbook/GuideImageSearchSearch.xml	Fri May 06 17:09:18 2016 +0100
+++ b/doc/docbook/GuideImageSearchSearch.xml	Sun May 08 19:10:39 2016 +0100
@@ -52,20 +52,20 @@
       Each search parameter can be enabled or disabled with the check box to it's left. For a file to be a match, all enabled parameters must be true.
     </para>
     <variablelist><varlistentry><term>
-        File name
-      </term><listitem><para/></listitem></varlistentry></variablelist>
+    <emphasis role="strong">File name</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
     <para>
       The search will match if the entered text appears within the file name, or if the text exactly matches the file name, depending on the method selected from the drop down menu. The text comparison can be made to be case sensitive by enabling the Match case checkbox.
     </para>
     <variablelist><varlistentry><term>
-        File size
-      </term><listitem><para/></listitem></varlistentry></variablelist>
+    <emphasis role="strong">File size</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
     <para>
       The search will match if the file size on disk is equal to, less than, greater than, or between the entered value, depending on the method selected from the drop down menu. The between test is inclusive, for example a file of size 10 will match if the size parameters are between 10 and 15.
     </para>
     <variablelist><varlistentry><term>
-        File date
-      </term><listitem><para/></listitem></varlistentry></variablelist>
+    <emphasis role="strong">File date</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
     <para>
       The search will match if the file modification time on disk is equal to, before, after, or between the entered date, depending on the method selected from the drop down menu. The between test is inclusive, for example a file with date of 10/04/2003 will match if the date parameters are between 10/04/2003 and 12/31/2003.
     </para>
@@ -73,8 +73,8 @@
       For convenience, the button with the down arrow displays a pop up calendar to enter the date.
     </para>
     <variablelist><varlistentry><term>
-        Image dimensions
-      </term><listitem><para/></listitem></varlistentry></variablelist>
+    <emphasis role="strong">Image dimensions</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
     <para>
       The search will match if the image dimensions are equal to, less than, greater than, or between the entered values, depending on the method selected from the drop down menu. The between test is inclusive.
     </para>
@@ -82,18 +82,41 @@
       The image dimensions test is simple, both width and height must be within the allowed values for a match.
     </para>
     <variablelist><varlistentry><term>
-        Image content
-      </term><listitem><para/></listitem></varlistentry></variablelist>
+    <emphasis role="strong">Image content</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
     <para>
       The search will match if the image contents are similar within the percentage value, inclusive. This uses the same test and data that is used to determine image similarity when <link linkend="GuideImageSearchFindingDuplicates">Finding Duplicates</link>. The entry is for entering the path for the image to use in this test.
     </para>
     <variablelist><varlistentry><term>
-        Keywords
-      </term><listitem><para/></listitem></varlistentry></variablelist>
+    <emphasis role="strong">Keywords</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
     <para>
       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.
     </para>
-    <para/></section><section id="Resultslist"><title>
+    <variablelist><varlistentry><term>
+    <emphasis role="strong">Comment</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
+    <para>
+      The search will match if the file's associated Comment contains, or does not contain, the entered text, depending on the method selected from the drop down menu.
+    </para>
+    <variablelist><varlistentry><term>
+    <emphasis role="strong">Location</emphasis>
+    </term><listitem><para/></listitem></varlistentry></variablelist>
+    <para>
+      The search will match if the file's GPS location is within a range of a geographical point.
+    </para><para>Via a drop-down box, the range may be specified in kilometres, miles or nautical miles.
+    </para><para>The centre point of the search may be defined by:
+    </para>
+    <itemizedlist spacing="compact"><listitem><para>Typing in a latitude/longitude or by cut-and-paste from an internet search.
+    </para><para>The format must be of the form:
+    </para><para>N 12° 34' 56.78" W  12° 34' 56.78" or
+    </para><para>N 12 34 56.78 W 12 34 56.78
+    </para></listitem><listitem><para>Drag and drop a geocoded image onto the text box.
+    </para></listitem><listitem><para>Drag and drop a hyperlink place-name from a geonames.org search.
+    </para><para>Currently only searches from geonames.org are supported.
+    </para><para>A button which calls up a geonames.org search is provided as a convenience.
+    </para></listitem></itemizedlist>
+    </section><section id="Resultslist"><title>
       Results list
     </title>
     <para>
--- a/src/search.c	Fri May 06 17:09:18 2016 +0100
+++ b/src/search.c	Sun May 08 19:10:39 2016 +0100
@@ -190,6 +190,16 @@
 	ThumbLoader *thumb_loader;
 	gboolean thumb_enable;
 	FileData *thumb_fd;
+
+	/* Used for lat/lon coordinate search
+	 */
+	gint search_gps;
+	gdouble search_lat, search_lon;
+	GtkWidget *label_gps_drop, *entry_gps_coord;
+	GtkWidget *check_gps;
+	GtkWidget *spin_gps;
+	GtkWidget *units_gps;
+	gboolean match_gps_enable;
 };
 
 typedef struct _MatchFileData MatchFileData;
@@ -1386,6 +1396,112 @@
 		}
 }
 
+/* Convert lat/lon from a signed double to a string of
+ * the format N 12° 34' 56.78"
+ */
+GString *search_gps_convert_coordinate(gdouble coordinate, const char *key)
+{
+	gint deg, min;
+	gdouble sec;
+	GString *message;
+	gchar *ref;
+
+	if (g_strcmp0(key, "Longitude") == 0)
+		if (coordinate < 0)
+			ref = "W";
+		else
+			ref = "E";
+	else if (g_strcmp0(key, "Latitude") == 0)
+		{
+		if (coordinate < 0)
+			{
+			ref = "S";
+			}
+		else
+			ref = "N";
+		}
+
+	if (coordinate < 0)
+		coordinate = -coordinate;
+	deg = coordinate;
+	min = (coordinate * 60) - (deg * 60);
+	sec = (coordinate * 3600) - ((deg * 3600) + (min * 60));
+
+	message = g_string_new("");
+	g_string_append_printf(message, "%s %i° %i\' %.2lf\"", ref, deg, min, sec);
+
+	return message;
+}
+
+/* Get the coordinates from the result of a drag-and-drop, and copy it into the
+ * user editable box. It will either be a string from a geonames.org search,
+ * or a file list from a drag from the geeqie layout window.
+ */
+GString *search_gps_extract_coordinates(GtkSelectionData *selection_data, gpointer data)
+{
+	gchar **latlon;
+	GString *message = g_string_new("");
+	gchar *str_ptr;
+	gdouble latitude, longitude;
+	GList *list;
+	FileData *fd;
+	SearchData *sd = data;
+	gchar **array = NULL;
+
+	array = gtk_selection_data_get_uris(selection_data);
+	if (array != NULL)
+		{
+		str_ptr = g_strstr_len(array[0], -1, "http://www.geonames.org/maps/google_");
+		if (str_ptr != NULL)
+			{
+			latlon = g_strsplit(g_strndup(str_ptr + 36, g_strrstr(str_ptr + 36, ".html") - str_ptr + 36), "_", 0);
+			latitude = g_ascii_strtod(latlon[0], NULL);
+			g_string_append(message, search_gps_convert_coordinate(latitude, "Latitude")->str);
+			g_string_append(message, "  ");
+
+			longitude = g_ascii_strtod(latlon[1], NULL);
+			g_string_append(message, search_gps_convert_coordinate(longitude, "Longitude")->str);
+
+			gtk_button_set_label(GTK_BUTTON(sd->label_gps_drop), "geonames.org");
+
+			g_strfreev(latlon);
+			}
+		else
+			{
+			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)
+					{
+					g_string_append(message, search_gps_convert_coordinate(latitude, "Latitude")->str);
+					g_string_append(message, "  ");
+					g_string_append(message, search_gps_convert_coordinate(longitude, "Longitude")->str);
+					}
+				}
+			}
+		g_strfreev (array);
+		}
+
+	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;
+
+	gtk_entry_set_text(GTK_ENTRY(sd->entry_gps_coord), search_gps_extract_coordinates(selection_data, sd)->str);
+}
+
 static void search_dnd_init(SearchData *sd)
 {
 	gtk_drag_source_set(sd->result_view, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK,
@@ -1395,6 +1511,18 @@
 			 G_CALLBACK(search_dnd_data_set), sd);
 	g_signal_connect(G_OBJECT(sd->result_view), "drag_begin",
 			 G_CALLBACK(search_dnd_begin), sd);
+#if 0
+	g_signal_connect(G_OBJECT(sd->result_view), "drag_end",
+			 G_CALLBACK(search_dnd_end), sd);
+#endif
+
+	gtk_drag_dest_set(GTK_WIDGET(sd->entry_gps_coord),
+			  GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP,
+			   result_drag_types, n_result_drag_types,
+			  GDK_ACTION_COPY);
+
+	g_signal_connect(G_OBJECT(sd->entry_gps_coord), "drag_data_received",
+			 G_CALLBACK(search_gps_dnd_received_cb), sd);
 }
 
 /*
@@ -1863,6 +1991,46 @@
 			}
 		}
 
+	/* 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(sd->units_gps)), _("km")) == 0)
+		{
+		conversion = KM_EARTH_RADIUS;
+		}
+	else if (g_strcmp0(gtk_combo_box_text_get_active_text(GTK_COMBO_BOX(sd->units_gps)), _("miles")) == 0)
+		{
+		conversion = MILES_EARTH_RADIUS;
+		}
+	else
+		{
+		conversion = NAUTICAL_MILES_EARTH_RADIUS;
+		}
+
+	if (match && sd->match_gps_enable && sd->search_lat && sd->search_lon)
+		{
+		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->search_gps >= range)
+			 	match = TRUE;
+			}
+		}
+
 	if ((match || extra_only) &&
 	    (sd->match_dimensions_enable || sd->match_similarity_enable))
 		{
@@ -2095,6 +2263,10 @@
 	SearchData *sd = data;
 	GtkTreeViewColumn *column;
 	gchar *path;
+	gdouble deg_lat, min_lat, sec_lat, deg_lon, min_lon, sec_lon, latitude, longitude;
+	gchar north_south[35];
+	gchar east_west[35];
+	gchar *entry_text;
 
 	if (sd->search_folder_list)
 		{
@@ -2124,6 +2296,54 @@
 			}
 		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)
+		{
+		entry_text = g_strdup(gtk_entry_get_text(GTK_ENTRY(sd->entry_gps_coord)));
+		g_strstrip(entry_text);
+		if (entry_text != NULL && strlen(entry_text) <= 37)
+			{
+			g_strcanon(entry_text, "NSEW0123456789.", ' ');
+			sscanf(entry_text,"%s %lf %lf %lf %s %lf %lf  %lf",
+					north_south, &deg_lat, &min_lat, &sec_lat,
+					east_west, &deg_lon, &min_lon, &sec_lon);
+
+			latitude = deg_lat + min_lat / 60 + sec_lat / 3600;
+			longitude = deg_lon + min_lon / 60 + sec_lon / 3600;
+
+			if ((g_strcmp0(north_south, "N") == 0 || g_strcmp0(north_south, "S") == 0) &&
+					(g_strcmp0(east_west, "E") == 0 || g_strcmp0(east_west, "W") == 0) &&
+					(latitude <= 90.0) && (latitude >= -90.0) &&
+					(longitude <= 180.0) && (longitude >= -180.0))
+				{
+				if (g_strcmp0(north_south, "S") == 0)
+					latitude = -latitude;
+				if (g_strcmp0(east_west, "W") == 0)
+					longitude = -longitude;
+				sd->search_lat = latitude;
+				sd->search_lon = longitude;
+				}
+			else
+				{
+				file_util_warning_dialog(_("Entry does not contain a valid lat/lon value"),
+							 _("Please enter a coordinate in the form:N \n12° 34' 56.78\" W  12° 34' 56.78\" \n or\n 12 34 56.78 W  12 34 56.78 \n or drag-and-drop"),
+							 GTK_STOCK_DIALOG_WARNING, sd->window);
+				return;
+				}
+			}
+		else
+			{
+			file_util_warning_dialog(_("Entry does not contain a valid lat/lon value"),
+						 _("Please enter a coordinate in the form:N \n12° 34' 56.78\" W  12° 34' 56.78\" \n or\nN 12 34 56.78 W 12 34 56.78 \\n or drag-and-drop"),
+						 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);
@@ -2579,6 +2799,7 @@
 	sd->match_name_enable = TRUE;
 
 	sd->search_similarity = 95;
+	sd->search_gps = 1;
 
 	if (example_file)
 		{
@@ -2738,6 +2959,45 @@
 	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/lon coordinate
+	 */
+	hbox = menu_choice(sd->box_search, &sd->check_gps, NULL,
+					   _("Image is within"), &sd->match_gps_enable,
+					   NULL, 0, NULL, sd);
+	sd->spin_gps = menu_spin(hbox, 1, 9999, sd->search_gps,
+					G_CALLBACK(menu_choice_spin_cb), &sd->search_gps);
+	gtk_drag_dest_unset(sd->spin_gps);
+
+	sd->units_gps = gtk_combo_box_text_new();
+	gtk_combo_box_text_append_text(GTK_COMBO_BOX(sd->units_gps), _("km"));
+	gtk_combo_box_text_append_text(GTK_COMBO_BOX(sd->units_gps), _("miles"));
+	gtk_combo_box_text_append_text(GTK_COMBO_BOX(sd->units_gps), _("n.m."));
+	gtk_box_pack_start(GTK_BOX(hbox), 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(hbox, _("of"));
+
+	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:\nN 12° 34' 56.78\" W  12° 34' 56.78\" \n or\nN 12 34 56.78 W  12 34 56.78 \n or drag-and-drop"));
+	gtk_box_pack_start(GTK_BOX(hbox), sd->entry_gps_coord, TRUE, TRUE, 0);
+	gtk_widget_set_sensitive(sd->entry_gps_coord, TRUE);
+	gtk_drag_dest_unset(sd->entry_gps_coord);
+
+	sd->label_gps_drop = gtk_link_button_new_with_label("http://www.geonames.org", _("geonames.org"));
+	gtk_widget_set_has_tooltip(sd->label_gps_drop, TRUE);
+	gtk_widget_set_tooltip_text(sd->label_gps_drop, _("Drag a placename from a www.geonames.org search, \n or drag a geocoded thumbnail onto the adjoining box.\n\n Click to start a www.geonames.org search."));
+	gtk_box_pack_start(GTK_BOX(hbox), sd->label_gps_drop, TRUE, TRUE, 0);
+	gtk_widget_set_sensitive(sd->label_gps_drop, FALSE);
+
+	g_signal_connect(G_OBJECT(sd->check_gps), "toggled",
+						G_CALLBACK(menu_choice_check_cb), sd->label_gps_drop);
+	gtk_widget_show(sd->label_gps_drop);
+	gtk_widget_show(sd->entry_gps_coord);
+
 	/* Done the types of searches */
 
 	scrolled = gtk_scrolled_window_new(NULL, NULL);