From: Colin Clark Date: Wed, 25 Apr 2018 10:46:38 +0000 (+0100) Subject: Local time - UTC offset and daylight saving correction X-Git-Tag: v1.5~156 X-Git-Url: http://geeqie.org/cgi-bin/gitweb.cgi?p=geeqie.git;a=commitdiff_plain;h=cf43eb55c1491b033ac6773b1dc0571a4b69e47f Local time - UTC offset and daylight saving correction Use GPS lat/long to provide local time corrected for UTC offset and DST correction. The corrected time is available in formatted.localtime The selected timezone is available in formatted.timezone --- diff --git a/ZoneDetect.md b/ZoneDetect.md new file mode 100644 index 00000000..9b1ecc3f --- /dev/null +++ b/ZoneDetect.md @@ -0,0 +1,10 @@ +The detection of timezone is performed by + [ZoneDetect](https://github.com/BertoldVdb/ZoneDetect) + + timezone16.bin has a longitude resolution of 0.0055 degrees (~0.5km). + timezone21.bin has a longitude resolution of 0.00017 degrees (~20m). + + The safe zone result indicates how close you are to the nearest border (flat earth model using lat and lon as x and y), so you can know when to do a new lookup. If you don't need it you can ignore it, in this case you can pass a NULL parameter to save the (very small) calculation effort. + + + Note that the [underlying database](https://github.com/evansiroky/timezone-boundary-builder) will change from time-to-time. diff --git a/configure.in b/configure.in index c89ca352..102fbcd2 100644 --- a/configure.in +++ b/configure.in @@ -590,6 +590,7 @@ AC_CONFIG_FILES([ plugins/ufraw/Makefile plugins/import/Makefile plugins/geocode-parameters/Makefile + plugins/ZoneDetect/Makefile geeqie.spec ]) diff --git a/doc/docbook/GuideReference.xml b/doc/docbook/GuideReference.xml index 51aa16dc..9b45b3a6 100644 --- a/doc/docbook/GuideReference.xml +++ b/doc/docbook/GuideReference.xml @@ -11,6 +11,7 @@ + diff --git a/doc/docbook/GuideReferenceFileDates.xml b/doc/docbook/GuideReferenceFileDates.xml new file mode 100644 index 00000000..5373c25e --- /dev/null +++ b/doc/docbook/GuideReferenceFileDates.xml @@ -0,0 +1,19 @@ + +
+ File date types + Geeqie understands 4 types of file dates: + + + Standard date (mtime) - The date the file contents were last modified, or when the file was created if since unchanged. + + + Status changed (ctime) - The date the file status was last changed. This includes change of owner, change of read/write permission and change of contents. + + + Exif date original (Exif.Photo.DateTimeOriginal) - For images from a digital camera, this is the time the photo was taken. + + + Exif date digitized (Exif.Photo.DateTimeDigitized) - For scanned images, this the time the scan was made. For images from a digital camera, it may be identical to the above date. + + +
diff --git a/doc/docbook/GuideReferenceTags.xml b/doc/docbook/GuideReferenceTags.xml index c2b01677..42929b88 100644 --- a/doc/docbook/GuideReferenceTags.xml +++ b/doc/docbook/GuideReferenceTags.xml @@ -239,6 +239,66 @@ Latitude, Longitude + + + formatted.localtime + + + + Exif.GPSInfo.GPSLatitude + + Exif.GPSInfo.GPSLatitudeRef + + Exif.GPSInfo.GPSLongitude + + Exif.GPSInfo.GPSLongitudeRef + + Exif.GPSInfo.GPSDateStamp + + Exif.GPSInfo.GPSTimeStamp + + + + + Local time corrected for UTC offset, DST correction + + + Refer to + UTC and DST + section. + + + + + + + + formatted.timezone + + + + Exif.GPSInfo.GPSLatitude + + Exif.GPSInfo.GPSLatitudeRef + + Exif.GPSInfo.GPSLongitude + + Exif.GPSInfo.GPSLongitudeRef + + + + + Timezone indicated by lat/long + + + Refer to + UTC and DST + section. + + + + + @@ -324,6 +384,14 @@ GPS altitiude + + + formatted.timezone + + + Time zone + + Xmp.photoshop.Country @@ -440,6 +508,14 @@ Date + + + formatted.localtime + + + Local time + + formatted.ShutterSpeed diff --git a/doc/docbook/GuideReferenceUTC.xml b/doc/docbook/GuideReferenceUTC.xml new file mode 100644 index 00000000..ceb38639 --- /dev/null +++ b/doc/docbook/GuideReferenceUTC.xml @@ -0,0 +1,20 @@ + +
+ UTC and Daylight Saving Time (DST) + Geeqie can display the local time at which a photo was taken, adjusted for UTC offset and Daylight Saving Time. + + If the image exif data contains the required parameters (see + Pre-formatted tags + ), Geeqie will use the latitude and longitude to determine which timezone the image was taken in. + + The UTC offset and Daylight Saving Time Correction for that timezone, combined with the GPS UTC value, will then be used to compute the correct local time. + + This value may be displayed in either the Info Sidebar or Overlay Screen Display by using the parameter + formatted.localtime + + + The timezone for the image may also be displayed by using the parameter + formatted.timezone + . + +
diff --git a/doc/docbook/GuideSidebarsInfo.xml b/doc/docbook/GuideSidebarsInfo.xml index 0853b98e..33134151 100644 --- a/doc/docbook/GuideSidebarsInfo.xml +++ b/doc/docbook/GuideSidebarsInfo.xml @@ -250,6 +250,32 @@ formatted.GPSPosition Latitude, Longitude
+ + formatted.localtime + + Local time corrected for UTC offset, DST correction + + + Refer to + UTC and DST + section. + + + + + + formatted.timezone + + Timezone indicated by GPS lat/long values + + + Refer to + UTC and DST + section. + + + + file.size file size in bytes diff --git a/plugins/Makefile.am b/plugins/Makefile.am index 9d1aaca3..4b57bd0c 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -1,6 +1,6 @@ #FIXME enable or disable individual plugins from configure -SUBDIRS = rotate symlink ufraw import geocode-parameters +SUBDIRS = rotate symlink ufraw import geocode-parameters ZoneDetect qq_desktoptemplatedir = $(appdir) qq_desktoptemplate_DATA = template.desktop diff --git a/plugins/ZoneDetect/Makefile.am b/plugins/ZoneDetect/Makefile.am new file mode 100644 index 00000000..adead9ac --- /dev/null +++ b/plugins/ZoneDetect/Makefile.am @@ -0,0 +1 @@ +dist_gq_bin_SCRIPTS = timezone21.bin diff --git a/plugins/ZoneDetect/timezone21.bin b/plugins/ZoneDetect/timezone21.bin new file mode 100644 index 00000000..02b6645b Binary files /dev/null and b/plugins/ZoneDetect/timezone21.bin differ diff --git a/src/Makefile.am b/src/Makefile.am index 82478de5..6ea6ff34 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -264,7 +264,9 @@ geeqie_SOURCES = \ window.c \ window.h \ lua.c \ - glua.h + glua.h \ + zonedetect.c \ + zonedetect.h geeqie_LDADD = $(GTK_LIBS) $(GLIB_LIBS) $(INTLLIBS) $(JPEG_LIBS) $(TIFF_LIBS) $(LCMS_LIBS) $(EXIV2_LIBS) $(LIBCHAMPLAIN_LIBS) $(LIBCHAMPLAIN_GTK_LIBS) $(LUA_LIBS) $(CLUTTER_LIBS) $(CLUTTER_GTK_LIBS) $(FFMPEGTHUMBNAILER_LIBS) diff --git a/src/bar.c b/src/bar.c index 0eb8fdf2..8f8673c2 100644 --- a/src/bar.c +++ b/src/bar.c @@ -99,6 +99,7 @@ static const gchar default_config_exif[] = " " " " " " +" " " " " " " " @@ -138,6 +139,7 @@ static const gchar default_config_location[] = " " " " " " +" " " " " " " " diff --git a/src/exif-common.c b/src/exif-common.c index 2af25a56..67caac34 100644 --- a/src/exif-common.c +++ b/src/exif-common.c @@ -56,6 +56,7 @@ #include "ui_fileops.h" #include "cache.h" #include "jpeg_parser.h" +#include "zonedetect.h" static gdouble exif_rational_to_double(ExifRational *r, gint sign) @@ -607,6 +608,247 @@ static gchar *exif_build_formatted_GPSAltitude(ExifData *exif) return g_strdup_printf("%0.f m %s", alt, (ref==0)?_("Above Sea Level"):_("Below Sea Level")); } +/** + * @brief Extracts timezone from a ZoneDetect search structure + * @param results ZoneDetect search structure + * @returns Timezone in the form "Europe/London" + * + * Refer to https://github.com/BertoldVdb/ZoneDetect + * for structure details + */ +static gchar *zd_tz(ZoneDetectResult* results) +{ + gchar *timezone = NULL; + gchar *timezone_pre = NULL; + gchar *timezone_id = NULL; + unsigned int index = 0; + + if (!results) + { + return NULL; + } + + while(results[index].lookupResult != ZD_LOOKUP_END) + { + if(results[index].data) + { + for(unsigned int i=0; i 0) + { + tmp = g_locale_to_utf8(buf, buflen, NULL, NULL, &error); + if (error) + { + log_printf("Error converting locale strftime to UTF-8: %s\n", error->message); + g_error_free(error); + } + else + { + g_free(text_date_time); + text_date_time = g_strdup(tmp); + } + } + g_free(tmp); + } + putenv(time_zone_org); + + g_free(time_zone); + g_free(time_zone_org); + } + else + { + log_printf("Error: Init of timezone database %s failed\n", zd_path); + } + ZDCloseDatabase(cd); + g_free(zd_path); + } + + g_free(text_latitude); + g_free(text_longitude); + g_free(text_latitude_ref); + g_free(text_longitude_ref); + g_free(text_date); + g_free(text_time); + + return text_date_time; +} + +/** + * @brief Gets timezone from GPS lat/long + * @param exif + * @returns Timezone string in the form "Europe/London" + * + * + */ +static gchar *exif_build_formatted_timezone(ExifData *exif) +{ + gfloat latitude; + gfloat longitude; + gchar *text_latitude; + gchar *text_longitude; + gchar *text_latitude_ref; + gchar *text_longitude_ref; + gchar *lat_deg; + gchar *lat_min; + gchar *lon_deg; + gchar *lon_min; + gchar *time_zone = NULL; + gchar *zd_path; + ZoneDetect *cd; + ZoneDetectResult *results; + + text_latitude = exif_get_data_as_text(exif, "Exif.GPSInfo.GPSLatitude"); + text_longitude = exif_get_data_as_text(exif, "Exif.GPSInfo.GPSLongitude"); + text_latitude_ref = exif_get_data_as_text(exif, "Exif.GPSInfo.GPSLatitudeRef"); + text_longitude_ref = exif_get_data_as_text(exif, "Exif.GPSInfo.GPSLongitudeRef"); + + if (text_latitude && text_longitude && text_latitude_ref && + text_longitude_ref) + { + lat_deg = strtok(text_latitude, "deg'"); + lat_min = strtok(NULL, "deg'"); + latitude = atof(lat_deg) + atof(lat_min) / 60; + if (g_strcmp0(text_latitude_ref, "South") == 0) + { + latitude = -latitude; + } + lon_deg = strtok(text_longitude, "deg'"); + lon_min = strtok(NULL, "deg'"); + longitude = atof(lon_deg) + atof(lon_min) / 60; + if (g_strcmp0(text_longitude_ref, "West") == 0) + { + longitude = -longitude; + } + zd_path = g_build_filename(GQ_BIN_DIR, TIMEZONE_DATABASE, NULL); + cd = ZDOpenDatabase(zd_path); + if (cd) + { + results = ZDLookup(cd, latitude, longitude, NULL); + time_zone = zd_tz(results); + ZDFreeResults(results); + } + else + { + log_printf("Error: Init of timezone database %s failed\n", zd_path); + } + ZDCloseDatabase(cd); + g_free(zd_path); + } + + g_free(text_latitude); + g_free(text_longitude); + g_free(text_latitude_ref); + g_free(text_longitude_ref); + + return time_zone; +} /* List of custom formatted pseudo-exif tags */ #define EXIF_FORMATTED_TAG(name, label) { EXIF_FORMATTED()#name, label, exif_build_formatted##_##name } @@ -627,6 +869,8 @@ ExifFormattedText ExifFormattedList[] = { EXIF_FORMATTED_TAG(ColorProfile, N_("Color profile")), EXIF_FORMATTED_TAG(GPSPosition, N_("GPS position")), EXIF_FORMATTED_TAG(GPSAltitude, N_("GPS altitude")), + EXIF_FORMATTED_TAG(localtime, N_("Local time")), + EXIF_FORMATTED_TAG(timezone, N_("Time zone")), {"file.size", N_("File size"), NULL}, {"file.date", N_("File date"), NULL}, {"file.mode", N_("File mode"), NULL}, diff --git a/src/main.h b/src/main.h index 2465622a..120f528f 100644 --- a/src/main.h +++ b/src/main.h @@ -123,6 +123,8 @@ #include "options.h" #define DESKTOP_FILE_TEMPLATE GQ_APP_DIR "/template.desktop" + +#define TIMEZONE_DATABASE "timezone21.bin" /* *---------------------------------------------------------------------------- * main.c diff --git a/src/preferences.c b/src/preferences.c index 3df77c11..d242f0fc 100644 --- a/src/preferences.c +++ b/src/preferences.c @@ -46,6 +46,7 @@ #include "ui_tabcomp.h" #include "ui_utildlg.h" #include "window.h" +#include "zonedetect.h" #include @@ -2739,26 +2740,24 @@ void show_about_window(LayoutWindow *lw) gchar *comment; gint i_authors = 0; gchar *path; + GString *copyright; + gchar *zd_path; + ZoneDetect *cd; FILE *fp = NULL; #define LINE_LENGTH 1000 gchar line[LINE_LENGTH]; -#if !GTK_CHECK_VERSION(3,0,0) - GString *copyright; - copyright = g_string_new(NULL); - path = g_build_filename(GQ_HELPDIR, "COPYING", NULL); - fp = fopen(path, "r"); - if (fp) + copyright = g_string_append(copyright, "This program comes with absolutely no warranty.\nGNU General Public License, version 2 or later.\nSee https://www.gnu.org/licenses/old-licenses/gpl-2.0.html\n\n"); + + zd_path = g_build_filename(GQ_BIN_DIR, TIMEZONE_DATABASE, NULL); + cd = ZDOpenDatabase(zd_path); + if (cd) { - while(fgets(line, LINE_LENGTH, fp)) - { - copyright = g_string_append(copyright, line); - } - fclose(fp); + copyright = g_string_append(copyright, ZDGetNotice(cd)); } - g_free(path); -#endif + ZDCloseDatabase(cd); + g_free(zd_path); authors[0] = NULL; path = g_build_filename(GQ_HELPDIR, "AUTHORS", NULL); @@ -2794,16 +2793,12 @@ void show_about_window(LayoutWindow *lw) "comments", comment, "authors", authors, "translator-credits", _("translator-credits"), -#if GTK_CHECK_VERSION(3,0,0) - "license-type", GTK_LICENSE_GPL_2_0, -#else - "license", copyright->str, -#endif + "wrap-license", TRUE, + "license", copyright->str, NULL); -#if !GTK_CHECK_VERSION(3,0,0) g_string_free(copyright, TRUE); -#endif + gint n = 0; while(n < i_authors) { diff --git a/src/zonedetect.c b/src/zonedetect.c new file mode 100644 index 00000000..74bb8f4e --- /dev/null +++ b/src/zonedetect.c @@ -0,0 +1,597 @@ +/* + * Copyright (c) 2018, Bertold Van den Bergh (vandenbergh@bertold.org) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the author nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include + +#include "zonedetect.h" + +struct ZoneDetectOpaque { + int fd; + uint32_t length; + uint8_t* mapping; + + uint8_t tableType; + uint8_t version; + uint8_t precision; + uint8_t numFields; + + char* notice; + char** fieldNames; + + uint32_t bboxOffset; + uint32_t metadataOffset; + uint32_t dataOffset; +}; + +static int32_t ZDFloatToFixedPoint(float input, float scale, unsigned int precision) { + float inputScaled = input / scale; + return inputScaled * (float)(1 << (precision-1)); +} + +static unsigned int ZDDecodeVariableLengthUnsigned(ZoneDetect* library, uint32_t* index, uint32_t* result) { + uint32_t value = 0; + unsigned int i=0, shift = 0; + + if(*index >= library->length) { + return 0; + } + + uint8_t* buffer = library->mapping + *index; + uint8_t* bufferEnd = library->mapping + library->length - 1; + + while(1) { + value |= (buffer[i] & 0x7F) << shift; + shift += 7; + + if(!(buffer[i] & 0x80)) { + break; + } + + i++; + if(buffer + i > bufferEnd) { + return 0; + } + } + + i++; + *result = value; + *index += i; + return i; +} + +static unsigned int ZDDecodeVariableLengthSigned(ZoneDetect* library, uint32_t* index, int32_t* result) { + uint32_t value = 0; + unsigned int retVal = ZDDecodeVariableLengthUnsigned(library, index, &value); + *result = (value & 1)?-(value/2):(value/2); + return retVal; +} + +static char* ZDParseString(ZoneDetect* library, uint32_t* index) { + uint32_t strLength; + if(!ZDDecodeVariableLengthUnsigned(library, index, &strLength)) { + return NULL; + } + + uint32_t strOffset = *index; + unsigned int remoteStr = 0; + if(strLength >= 256) { + strOffset = library->metadataOffset + strLength - 256; + remoteStr = 1; + + if(!ZDDecodeVariableLengthUnsigned(library, &strOffset, &strLength)) { + return NULL; + } + + if(strLength > 256) { + return NULL; + } + } + + char* str = malloc(strLength + 1); + + if(str) { + unsigned int i; + for(i=0; imapping[strOffset+i] ^ 0x80; + } + str[strLength] = 0; + } + + if(!remoteStr) { + *index += strLength; + } + + return str; +} + +static int ZDParseHeader(ZoneDetect* library) { + if(library->length < 7) { + return -1; + } + + if(memcmp(library->mapping, "PLB", 3)) { + return -1; + } + + library->tableType = library->mapping[3]; + library->version = library->mapping[4]; + library->precision = library->mapping[5]; + library->numFields = library->mapping[6]; + + if(library->version != 0) { + return -1; + } + + uint32_t index = 7; + + library->fieldNames = malloc(library->numFields * sizeof(char*)); + unsigned int i; + for(i=0; inumFields; i++) { + library->fieldNames[i] = ZDParseString(library, &index); + } + + library->notice = ZDParseString(library, &index); + if(!library->notice) { + return -1; + } + + uint32_t tmp; + /* Read section sizes */ + /* By memset: library->bboxOffset = 0 */ + + if(!ZDDecodeVariableLengthUnsigned(library, &index, &tmp)) return -1; + library->metadataOffset = tmp + library->bboxOffset; + + if(!ZDDecodeVariableLengthUnsigned(library, &index, &tmp))return -1; + library->dataOffset = tmp + library->metadataOffset; + + if(!ZDDecodeVariableLengthUnsigned(library, &index, &tmp)) return -1; + + /* Add header size to everything */ + library->bboxOffset += index; + library->metadataOffset += index; + library->dataOffset += index; + + /* Verify file length */ + if(tmp + library->dataOffset != library->length) { + return -2; + } + + return 0; +} + +static int ZDPointInBox(int32_t xl, int32_t x, int32_t xr, int32_t yl, int32_t y, int32_t yr) { + if((xl <= x && x <= xr) || (xr <= x && x <= xl)) { + if((yl <= y && y <= yr) || (yr <= y && y <= yl)) { + return 1; + } + } + + return 0; +} + +static ZDLookupResult ZDPointInPolygon(ZoneDetect* library, uint32_t polygonIndex, int32_t latFixedPoint, int32_t lonFixedPoint, uint64_t* distanceSqrMin) { + uint32_t numVertices; + int32_t pointLat = 0, pointLon = 0, diffLat = 0, diffLon = 0, firstLat = 0, firstLon = 0, prevLat = 0, prevLon = 0; + lonFixedPoint -= 3; + + /* Read number of vertices */ + if(!ZDDecodeVariableLengthUnsigned(library, &polygonIndex, &numVertices)) return ZD_LOOKUP_PARSE_ERROR; + if(numVertices > 1000000) return ZD_LOOKUP_PARSE_ERROR; + + int prevQuadrant = 0, winding = 0; + + uint32_t i; + for(i=0; i<=numVertices; i++) { + if(i=latFixedPoint) { + if(pointLon>=lonFixedPoint) { + quadrant = 0; + } else { + quadrant = 1; + } + } else { + if(pointLon>=lonFixedPoint) { + quadrant = 3; + } else { + quadrant = 2; + } + } + + if(i>0) { + int windingNeedCompare = 0, lineIsStraight = 0; + float a = 0, b = 0; + + /* Calculate winding number */ + if(quadrant == prevQuadrant) { + /* Do nothing */ + } else if(quadrant == (prevQuadrant + 1) % 4) { + winding ++; + } else if((quadrant + 1) % 4 == prevQuadrant) { + winding --; + } else { + windingNeedCompare = 1; + } + + /* Avoid horizontal and vertical lines */ + if((pointLon == prevLon || pointLat == prevLat)) { + lineIsStraight = 1; + } + + /* Calculate the parameters of y=ax+b if needed */ + if(!lineIsStraight && (distanceSqrMin || windingNeedCompare)) { + a = ((float)pointLat - (float)prevLat)/((float)pointLon - prevLon); + b = (float)pointLat - a*(float)pointLon; + } + + /* Jumped two quadrants. */ + if(windingNeedCompare) { + if(lineIsStraight) { + if(distanceSqrMin) *distanceSqrMin=0; + return ZD_LOOKUP_ON_BORDER_SEGMENT; + } + + /* Check if the target is on the border */ + int32_t intersectLon = ((float)latFixedPoint - b)/a; + if(intersectLon == lonFixedPoint) { + if(distanceSqrMin) *distanceSqrMin=0; + return ZD_LOOKUP_ON_BORDER_SEGMENT; + } + + /* Ok, it's not. In which direction did we go round the target? */ + int sign = (intersectLon < lonFixedPoint)?2:-2; + if(quadrant == 2 || quadrant == 3) { + winding += sign; + } else { + winding -= sign; + } + } + + /* Calculate closest point on line (if needed) */ + if(distanceSqrMin) { + float closestLon, closestLat; + if(!lineIsStraight) { + closestLon=((float)lonFixedPoint+a*(float)latFixedPoint-a*b)/(a*a+1); + closestLat=(a*((float)lonFixedPoint+a*(float)latFixedPoint)+b)/(a*a+1); + } else { + if(pointLon == prevLon) { + closestLon=pointLon; + closestLat=latFixedPoint; + } else { + closestLon=lonFixedPoint; + closestLat=pointLat; + } + } + + int closestInBox = ZDPointInBox(pointLon, closestLon, prevLon, pointLat, closestLat, prevLat); + + int64_t diffLat, diffLon; + if(closestInBox) { + /* Calculate squared distance to segment. */ + diffLat = closestLat - latFixedPoint; + diffLon = (closestLon - lonFixedPoint); + } else { + /* + * Calculate squared distance to vertices + * It is enough to check the current point since the polygon is closed. + */ + diffLat = pointLat - latFixedPoint; + diffLon = (pointLon - lonFixedPoint); + } + + /* Note: lon has half scale */ + uint64_t distanceSqr = diffLat*diffLat + diffLon*diffLon*4; + if(distanceSqr < *distanceSqrMin) *distanceSqrMin = distanceSqr; + } + } + + prevQuadrant = quadrant; + prevLat = pointLat; + prevLon = pointLon; + } + + if(winding == -4) { + return ZD_LOOKUP_IN_ZONE; + } else if(winding == 4) { + return ZD_LOOKUP_IN_EXCLUDED_ZONE; + } else if(winding == 0) { + return ZD_LOOKUP_NOT_IN_ZONE; + } + + /* Should not happen */ + if(distanceSqrMin) *distanceSqrMin=0; + return ZD_LOOKUP_ON_BORDER_SEGMENT; +} + +void ZDCloseDatabase(ZoneDetect* library) { + if(library) { + if(library->fieldNames) { + unsigned int i; + for(i=0; inumFields; i++) { + if(library->fieldNames[i]) { + free(library->fieldNames[i]); + } + } + free(library->fieldNames); + } + if(library->notice) { + free(library->notice); + } + if(library->mapping) { + munmap(library->mapping, library->length); + } + if(library->fd >= 0) { + close(library->fd); + } + free(library); + } +} + +ZoneDetect* ZDOpenDatabase(const char* path) { + ZoneDetect* library = (ZoneDetect*)malloc(sizeof(*library)); + + if(library) { + memset(library, 0, sizeof(*library)); + + library->fd = open(path, O_RDONLY | O_CLOEXEC); + if(library->fd < 0) { + goto fail; + } + + library->length = lseek(library->fd, 0, SEEK_END); + if(library->length <= 0) { + goto fail; + } + lseek(library->fd, 0, SEEK_SET); + + library->mapping = mmap(NULL, library->length, PROT_READ, MAP_PRIVATE | MAP_FILE, library->fd, 0); + if(!library->mapping) { + goto fail; + } + + /* Parse the header */ + if(ZDParseHeader(library)) { + goto fail; + } + } + + return library; + +fail: + ZDCloseDatabase(library); + return NULL; +} + +ZoneDetectResult* ZDLookup(ZoneDetect* library, float lat, float lon, float* safezone) { + int32_t latFixedPoint = ZDFloatToFixedPoint(lat, 90, library->precision); + int32_t lonFixedPoint = ZDFloatToFixedPoint(lon, 180, library->precision); + unsigned int numResults = 0; + uint64_t distanceSqrMin=-1; + + /* Iterate over all polygons */ + uint32_t bboxIndex = library->bboxOffset; + int32_t metadataIndex = 0; + int32_t polygonIndex = 0; + + ZoneDetectResult* results = malloc(sizeof(ZoneDetectResult)); + if(!results) { + return NULL; + } + + while(bboxIndex < library->metadataOffset) { + int32_t minLat, minLon, maxLat, maxLon, metadataIndexDelta; + uint32_t polygonIndexDelta; + if(!ZDDecodeVariableLengthSigned(library, &bboxIndex, &minLat)) break; + if(!ZDDecodeVariableLengthSigned(library, &bboxIndex, &minLon)) break; + if(!ZDDecodeVariableLengthSigned(library, &bboxIndex, &maxLat)) break; + if(!ZDDecodeVariableLengthSigned(library, &bboxIndex, &maxLon)) break; + if(!ZDDecodeVariableLengthSigned(library, &bboxIndex, &metadataIndexDelta)) break; + if(!ZDDecodeVariableLengthUnsigned(library, &bboxIndex, &polygonIndexDelta)) break; + + metadataIndex+=metadataIndexDelta; + polygonIndex+=polygonIndexDelta; + + if(latFixedPoint >= minLat) { + if(latFixedPoint <= maxLat && + lonFixedPoint >= minLon && + lonFixedPoint <= maxLon) { + + /* Indices valid? */ + if(library->metadataOffset + metadataIndex >= library->dataOffset) continue; + if(library->dataOffset + polygonIndex >= library->length) continue; + + ZDLookupResult lookupResult = ZDPointInPolygon(library, library->dataOffset + polygonIndex, latFixedPoint, lonFixedPoint, (safezone)?&distanceSqrMin:NULL); + if(lookupResult == ZD_LOOKUP_PARSE_ERROR) { + break; + } else if(lookupResult != ZD_LOOKUP_NOT_IN_ZONE) { + ZoneDetectResult* newResults = realloc(results, sizeof(ZoneDetectResult) * (numResults+2)); + + if(newResults) { + results = newResults; + results[numResults].metaId = metadataIndex; + results[numResults].numFields = library->numFields; + results[numResults].fieldNames = library->fieldNames; + results[numResults].lookupResult = lookupResult; + + numResults++; + } else { + break; + } + } + } + } else { + /* The data is sorted along minLat */ + break; + } + } + + /* Clean up results */ + unsigned int i, j; + for(i=0; imetadataOffset + results[i].metaId; + results[i].data = malloc(library->numFields * sizeof(char*)); + if(results[i].data) { + for(j=0; jnumFields; j++) { + results[i].data[j] = ZDParseString(library, &tmpIndex); + } + } + } + + /* Write end marker */ + results[numResults].lookupResult = ZD_LOOKUP_END; + results[numResults].numFields = 0; + results[numResults].fieldNames = NULL; + results[numResults].data = NULL; + + if(safezone) { + *safezone = sqrtf(distanceSqrMin) * 90 / (float)(1 << (library->precision-1)); + } + + return results; +} + +void ZDFreeResults(ZoneDetectResult* results) { + unsigned int index = 0; + + if(!results) { + return; + } + + while(results[index].lookupResult != ZD_LOOKUP_END) { + if(results[index].data) { + unsigned int i; + for(i=0; inotice; +} + +uint8_t ZDGetTableType(ZoneDetect* library) { + return library->tableType; +} + +const char* ZDLookupResultToString(ZDLookupResult result) { + switch(result) { + case ZD_LOOKUP_IGNORE: + return "Ignore"; + case ZD_LOOKUP_END: + return "End"; + case ZD_LOOKUP_PARSE_ERROR: + return "Parsing error"; + case ZD_LOOKUP_NOT_IN_ZONE: + return "Not in zone"; + case ZD_LOOKUP_IN_ZONE: + return "In zone"; + case ZD_LOOKUP_IN_EXCLUDED_ZONE: + return "In excluded zone"; + case ZD_LOOKUP_ON_BORDER_VERTEX: + return "Target point is border vertex"; + case ZD_LOOKUP_ON_BORDER_SEGMENT: + return "Target point is on border"; + } + + return "Unknown"; +} + diff --git a/src/zonedetect.h b/src/zonedetect.h new file mode 100644 index 00000000..4be53672 --- /dev/null +++ b/src/zonedetect.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2018, Bertold Van den Bergh (vandenbergh@bertold.org) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the author nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#ifndef _ZONEDETECT_H_ +#define _ZONEDETECT_H_ + +typedef enum { + ZD_LOOKUP_IGNORE = -3, + ZD_LOOKUP_END = -2, + ZD_LOOKUP_PARSE_ERROR = -1, + ZD_LOOKUP_NOT_IN_ZONE = 0, + ZD_LOOKUP_IN_ZONE = 1, + ZD_LOOKUP_IN_EXCLUDED_ZONE = 2, + ZD_LOOKUP_ON_BORDER_VERTEX = 3, + ZD_LOOKUP_ON_BORDER_SEGMENT = 4 +} ZDLookupResult; + +typedef struct { + ZDLookupResult lookupResult; + + uint32_t metaId; + uint8_t numFields; + char** fieldNames; + char** data; +} ZoneDetectResult; + +struct ZoneDetectOpaque; +typedef struct ZoneDetectOpaque ZoneDetect; + +#ifdef __cplusplus +extern "C" { +#endif + +ZoneDetect* ZDOpenDatabase(const char* path); +void ZDCloseDatabase(ZoneDetect* library); + +ZoneDetectResult* ZDLookup(ZoneDetect* library, float lat, float lon, float* safezone); +void ZDFreeResults(ZoneDetectResult* results); + +const char* ZDGetNotice(ZoneDetect* library); +uint8_t ZDGetTableType(ZoneDetect* library); +const char* ZDLookupResultToString(ZDLookupResult result); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/web/help/GuideIndex.html b/web/help/GuideIndex.html index 703f6c88..af022c80 100644 --- a/web/help/GuideIndex.html +++ b/web/help/GuideIndex.html @@ -648,10 +648,13 @@ dd.answer div.label { float: left; } 13.10. Additional pixbuf loaders
  • -13.11. Decoding Latitude and Longitude +13.11. UTC and Daylight Saving Time (DST)
  • -13.12. Standards +13.12. Decoding Latitude and Longitude +
  • +
  • +13.13. Standards
  • diff --git a/web/help/GuideReference.html b/web/help/GuideReference.html index 8f00d85d..a2537db7 100644 --- a/web/help/GuideReference.html +++ b/web/help/GuideReference.html @@ -440,6 +440,7 @@ dd.answer div.label { float: left; }
  • XMP, Exif and IPTC
  • Supported File Formats
  • Additional pixbuf loaders
  • +
  • UTC and Daylight Saving Time (DST)
  • Decoding Latitude and Longitude
  • Standards
  • @@ -487,10 +488,13 @@ dd.answer div.label { float: left; } 13.10. Additional pixbuf loaders
  • -13.11. Decoding Latitude and Longitude +13.11. UTC and Daylight Saving Time (DST)
  • -13.12. Standards +13.12. Decoding Latitude and Longitude +
  • +
  • +13.13. Standards
  • diff --git a/web/help/GuideReferenceCommandLine.html b/web/help/GuideReferenceCommandLine.html index 32a576c9..c811ebeb 100644 --- a/web/help/GuideReferenceCommandLine.html +++ b/web/help/GuideReferenceCommandLine.html @@ -441,6 +441,7 @@ dd.answer div.label { float: left; }
  • XMP, Exif and IPTC
  • Supported File Formats
  • Additional pixbuf loaders
  • +
  • UTC and Daylight Saving Time (DST)
  • Decoding Latitude and Longitude
  • Standards
  • diff --git a/web/help/GuideReferenceConfig.html b/web/help/GuideReferenceConfig.html index 67f0f41b..a4f36652 100644 --- a/web/help/GuideReferenceConfig.html +++ b/web/help/GuideReferenceConfig.html @@ -441,6 +441,7 @@ dd.answer div.label { float: left; }
  • XMP, Exif and IPTC
  • Supported File Formats
  • Additional pixbuf loaders
  • +
  • UTC and Daylight Saving Time (DST)
  • Decoding Latitude and Longitude
  • Standards
  • diff --git a/web/help/GuideReferenceDecodeLatLong.html b/web/help/GuideReferenceDecodeLatLong.html index 695f0794..4f6e13f6 100644 --- a/web/help/GuideReferenceDecodeLatLong.html +++ b/web/help/GuideReferenceDecodeLatLong.html @@ -3,7 +3,7 @@ Decoding Latitude and Longitude - + + + + + +
    +

    UTC and Daylight Saving Time (DST)

    + +

    Geeqie can display the local time at which a photo was taken, adjusted for UTC offset and Daylight Saving Time.

    +

    + If the image exif data contains the required parameters (see + Pre-formatted tags + ), Geeqie will use the latitude and longitude to determine which timezone the image was taken in. +

    + The UTC offset and Daylight Saving Time Correction for that timezone, combined with the GPS UTC value, will then be used to compute the correct local time. +

    + This value may be displayed in either the Info Sidebar or Overlay Screen Display by using the parameter + formatted.localtime +

    +

    + The timezone for the image may also be displayed by using the parameter + formatted.timezone + . +

    +
    + + + diff --git a/web/help/GuideReferenceXmpExif.html b/web/help/GuideReferenceXmpExif.html index cf7c88f7..95d67ca5 100644 --- a/web/help/GuideReferenceXmpExif.html +++ b/web/help/GuideReferenceXmpExif.html @@ -441,6 +441,7 @@ dd.answer div.label { float: left; }
  • XMP, Exif and IPTC
  • Supported File Formats
  • Additional pixbuf loaders
  • +
  • UTC and Daylight Saving Time (DST)
  • Decoding Latitude and Longitude
  • Standards
  • @@ -687,9 +688,54 @@ dd.answer div.label { float: left; } - + formatted.GPSPosition + + + Exif.GPSInfo.GPSLatitude +

    + Exif.GPSInfo.GPSLatitudeRef +

    + Exif.GPSInfo.GPSLongitude +

    + Exif.GPSInfo.GPSLongitudeRef +
    + + + Latitude, Longitude + + + + + formatted.localtime + + + + Exif.GPSInfo.GPSLatitude +

    + Exif.GPSInfo.GPSLatitudeRef +

    + Exif.GPSInfo.GPSLongitude +

    + Exif.GPSInfo.GPSLongitudeRef +

    + Exif.GPSInfo.GPSDateStamp +

    + Exif.GPSInfo.GPSTimeStamp +
    + + + + Local time corrected for UTC offset, DST correction + 1 + + + + + + formatted.timezone + Exif.GPSInfo.GPSLatitude @@ -702,7 +748,10 @@ dd.answer div.label { float: left; } - Latitude, Longitude + + Timezone indicated by lat/long + 2 + @@ -787,6 +836,14 @@ dd.answer div.label { float: left; } + + formatted.timezone + + + Time zone + + + Xmp.photoshop.Country @@ -794,7 +851,7 @@ dd.answer div.label { float: left; } Country - +

    Xmp.iptc.CountryCode

    @@ -804,7 +861,7 @@ dd.answer div.label { float: left; } Country Code - + Xmp.photoshop.State @@ -812,7 +869,7 @@ dd.answer div.label { float: left; } State - + Xmp.photoshop.City @@ -820,7 +877,7 @@ dd.answer div.label { float: left; } City - + Xmp.iptc.Location @@ -899,6 +956,14 @@ dd.answer div.label { float: left; } + + formatted.localtime + + + Local time + + + formatted.ShutterSpeed @@ -906,7 +971,7 @@ dd.answer div.label { float: left; } Shutter speed - + formatted.Aperture @@ -914,7 +979,7 @@ dd.answer div.label { float: left; } Aperture - + formatted.ExposureBias @@ -922,7 +987,7 @@ dd.answer div.label { float: left; } Exposure bias - + formatted.ISOSpeedRating @@ -930,7 +995,7 @@ dd.answer div.label { float: left; } ISO sensitivity - + formatted.FocalLength @@ -938,7 +1003,7 @@ dd.answer div.label { float: left; } Focal length - + formatted.FocalLength35mmFilm @@ -946,7 +1011,7 @@ dd.answer div.label { float: left; } Focal length 35mm - + formatted.SubjectDistance @@ -954,7 +1019,7 @@ dd.answer div.label { float: left; } Subject distance - + formatted.Flash @@ -962,7 +1027,7 @@ dd.answer div.label { float: left; } Flash - + formatted.Resolution @@ -970,7 +1035,7 @@ dd.answer div.label { float: left; } Resolution - + formatted.ColorProfile @@ -978,7 +1043,7 @@ dd.answer div.label { float: left; } Color profile - + Exif.Photo.ExposureProgram @@ -986,7 +1051,7 @@ dd.answer div.label { float: left; } Exposure Program - + Exif.Photo.MeteringMode @@ -994,7 +1059,7 @@ dd.answer div.label { float: left; } Metering Mode - + Exif.Photo.LightSource @@ -1200,6 +1265,24 @@ dd.answer div.label { float: left; } +
    +
    +1 +

    + Refer to + UTC and DST + section. +

    +
    +
    +2 +

    + Refer to + UTC and DST + section. +

    +
    +