diff --git a/contrib/geoToolbox.lua b/contrib/geoToolbox.lua
new file mode 100644
index 0000000..33b95ca
--- /dev/null
+++ b/contrib/geoToolbox.lua
@@ -0,0 +1,590 @@
+--[[
+ This file is part of darktable,
+ copyright (c) 2016 Tobias Jakobs
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+]]
+--[[
+
+USAGE
+* require this script from your main lua file
+ To do this add this line to the file .config/darktable/luarc:
+require "geoToolbox"
+
+* it creates a new geoToolbox lighttable module
+]]
+
+local dt = require "darktable"
+local df = require "lib/dtutils.file"
+local gettext = dt.gettext
+
+dt.configuration.check_version(...,{3,0,0},{4,0,0})
+
+-- Tell gettext where to find the .mo file translating messages for a particular domain
+gettext.bindtextdomain("geoToolbox",dt.configuration.config_dir.."/lua/")
+
+local function _(msgid)
+ return gettext.dgettext("geoToolbox", msgid)
+end
+
+--
+local labelDistance = dt.new_widget("label")
+labelDistance.label = "Distance:"
+
+local labelCopyGPSlat = dt.new_widget("check_button")
+{
+ label = "latitude:",
+ value = true
+}
+local labelCopyGPSlon = dt.new_widget("check_button")
+{
+ label = "longitude:",
+ value = true
+}
+local labelCopyGPSele = dt.new_widget("check_button")
+{
+ label = "elevation:",
+ value = true
+}
+--
+
+local function selectWithGPS()
+ local selection = {}
+ for _,image in ipairs(dt.database) do
+ if (image.longitude and image.latitude) then
+ table.insert(selection,image)
+ end
+ end
+ dt.gui.selection(selection)
+end
+
+local function selectWithoutGPS()
+ local selection = {}
+ for _,image in ipairs(dt.database) do
+ if (not image.longitude and not image.latitude) then
+ table.insert(selection,image)
+ end
+ end
+ dt.gui.selection(selection)
+end
+
+-- This is used for older images in the DB
+function isnan(x) return x ~= x end
+
+-- Function from:
+-- https://forums.coronalabs.com/topic/29019-convert-string-to-date/
+-- This looks much scarier than it really is. The goal is to turn that string
+-- into a Unix timestamp (number of seconds since Jan 1, 1970, the standard
+-- used by most systems). So we use the string.match() method to fetch the
+-- various date and time parts into their own variables: xyear, xmonth,etc.
+function makeTimeStamp(dateString)
+ local convertedTimestamp
+--dt.print_error(dateString)
+ if (dateString) then
+ local pattern = "(%d+)%:(%d+)%:(%d+) (%d+):(%d+):(%d+)"
+ local xyear, xmonth, xday, xhour, xminute, xseconds = dateString:match(pattern)
+--dt.print_error(xyear)
+ convertedTimestamp = os.time({
+ year = xyear,
+ month = xmonth,
+ day = xday,
+ hour = xhour,
+ min = xminute,
+ sec = xseconds})
+ else
+ convertedTimestamp = 0
+ end
+
+ return convertedTimestamp
+end
+
+local first_have_data = false
+local first_latitude = ''
+local first_longitude = ''
+local first_elevation = ''
+local first_image_date = 0
+
+local function getFirstCoordinate()
+ local sel_images = dt.gui.selection()
+
+ first_latitude = ''
+ first_longitude = ''
+ first_elevation = ''
+ first_image_date = 0
+
+ for _,image in ipairs(sel_images) do
+ if not image then
+ first_have_data = false
+ else
+ image_date = image.exif_datetime_taken
+ first_have_data = true
+ if (image.latitude) then
+ first_latitude = image.latitude
+ end
+ if (image.longitude) then
+ first_longitude = image.longitude
+ end
+ if (image.elevation) then
+ first_elevation = image.elevation
+ end
+ if (image.exif_datetime_taken) then
+ first_image_date = makeTimeStamp(image.exif_datetime_taken)
+--dt.print_error(image.exif_datetime_taken)
+--dt.print_error(first_image_date)
+ end
+ end
+ return
+ end
+end
+
+local second_have_data = false
+local second_latitude = ''
+local second_longitude = ''
+local second_elevation = ''
+local second_image_date = 0
+
+local function getSecondCoordinate()
+ local sel_images = dt.gui.selection()
+
+ second_latitude = ''
+ second_longitude = ''
+ second_elevation = ''
+ second_image_date = 0
+
+ for _,image in ipairs(sel_images) do
+ if not image then
+ second_have_data = false
+ else
+ image_date = image.exif_datetime_taken
+ second_have_data = true
+ if (image.latitude) then
+ second_latitude = image.latitude
+ end
+ if (image.longitude) then
+ second_longitude = image.longitude
+ end
+ if (image.elevation) then
+ second_elevation = image.elevation
+ end
+ if (image.exif_datetime_taken) then
+ second_image_date = makeTimeStamp(image.exif_datetime_taken)
+ end
+ end
+ return
+ end
+end
+
+local calcInBetweenSlider = dt.new_widget("slider")
+{
+ label = "Position between",
+ soft_min = 0, -- The soft minimum value for the slider, the slider can't go beyond this point
+ soft_max = 100, -- The soft maximum value for the slider, the slider can't go beyond this point
+ hard_min = -100, -- The hard minimum value for the slider, the user can't manually enter a value beyond this point
+ hard_max = 200, -- The hard maximum value for the slider, the user can't manually enter a value beyond this point
+ value = 50 -- The current value of the slider
+}
+
+--ToDo: this needs more love
+local function calcInBetween()
+ local sel_images = dt.gui.selection()
+ for _,image in ipairs(sel_images) do
+ if image then
+ image_date = makeTimeStamp(image.exif_datetime_taken)
+ if (first_have_data and second_have_data) then
+ local start_new = 0
+ local end_new = second_image_date - first_image_date
+ local image_date_new = image_date - first_image_date
+
+ local percent_in_between
+ if (end_new == 0) then
+ percent_in_between = 1
+ else
+ percent_in_between = image_date_new/end_new
+ end
+ calcInBetweenSlider.value = percent_in_between * 100
+dt.print_error(percent_in_between)
+ local in_between_latitude = first_latitude + (second_latitude - first_latitude) * percent_in_between
+ local in_between_longitude = first_longitude + (second_longitude - first_longitude) * percent_in_between
+
+ if (first_elevation and second_elevation) then
+ local in_between_elevation = first_elevation + (second_elevation - first_elevation) * percent_in_between
+
+dt.print_error(first_image_date)
+dt.print_error(second_image_date)
+dt.print_error(image_date)
+dt.print_error(first_elevation)
+dt.print_error(second_elevation)
+dt.print_error(in_between_elevation)
+
+ end
+ end
+ end
+ end
+end
+
+local copyGPS_have_data = false
+local copyGPS_latitude = ''
+local copyGPS_longitude = ''
+local copyGPS_elevation = ''
+
+local function copyGPS()
+ local sel_images = dt.gui.selection()
+
+ copyGPS_latitude = ''
+ copyGPS_longitude = ''
+ copyGPS_elevation = ''
+
+ for _,image in ipairs(sel_images) do
+ if not image then
+ copyGPS_have_data = false
+ else
+ copyGPS_have_data = true
+ if (image.latitude and labelCopyGPSlat.value) then
+ copyGPS_latitude = image.latitude
+ end
+ if (image.longitude and labelCopyGPSlon.value) then
+ copyGPS_longitude = image.longitude
+ end
+ if (image.elevation and labelCopyGPSele.value) then
+ copyGPS_elevation = image.elevation
+ end
+ end
+
+ labelCopyGPSlat.label = "latitude: " .. copyGPS_latitude
+ labelCopyGPSlon.label = "longitude: " ..copyGPS_longitude
+ labelCopyGPSele.label = "elevation: " .. copyGPS_elevation
+
+ return
+ end
+end
+
+local function pastGPS(image)
+ local sel_images = dt.gui.selection()
+
+ for _,image in ipairs(sel_images) do
+ if (labelCopyGPSlat.value) then
+ image.latitude = copyGPS_latitude
+ end
+ if (labelCopyGPSlon.value) then
+ image.longitude = copyGPS_longitude
+ end
+ if (labelCopyGPSele.value) then
+ image.elevation = copyGPS_elevation
+ end
+ end
+end
+
+local function openLocationInGnomeMaps()
+
+ if not df.check_if_bin_exists("gnome-maps") then
+ dt.print_error(_("gnome-maps not found"))
+ return
+ end
+
+ local sel_images = dt.gui.selection()
+
+ local lat1 = 0;
+ local lon1 = 0;
+ local i = 0;
+
+ -- Use the first image with geo information
+ for _,image in ipairs(sel_images) do
+ if ((image.longitude and image.latitude) and
+ (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data
+ ) then
+ lat1 = image.latitude;
+ lon1 = image.longitude;
+ break
+ end
+
+ local startCommand
+ startCommand = "gnome-maps \"geo:" .. lat1 .. "," .. lon1 .."\""
+ dt.print_error(startCommand)
+
+ if coroutine.yield("RUN_COMMAND", startCommand) then
+ dt.print(_("Command failed ..."))
+ end
+
+ end
+
+end
+
+
+-- I used code from here:
+-- http://stackoverflow.com/questions/27928/how-do-i-calculate-distance-between-two-latitude-longitude-points
+local function getDistance(lat1, lon1, ele1, lat2, lon2, ele2)
+
+ local earthRadius = 6371; -- Radius of the earth in km
+ local dLat = math.rad(lat2-lat1); -- deg2rad below
+ local dLon = math.rad(lon2-lon1);
+ local a =
+ math.sin(dLat/2) * math.sin(dLat/2) +
+ math.cos(math.rad(lat1)) * math.cos(math.rad(lat2)) *
+ math.sin(dLon/2) * math.sin(dLon/2)
+ ;
+ local angle = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a));
+ local distance = earthRadius * angle; -- Distance in km
+
+ -- Add the elevation to the calculation
+ local elevation = 0
+ elevation = math.abs(ele1 - ele2) / 1000; --in km
+
+ if (elevation > 0) then
+ distance = math.sqrt(math.pow(elevation,2) + math.pow(distance,2) )
+ end
+ return distance
+end
+
+local function calcDistance()
+
+ local sel_images = dt.gui.selection()
+
+ local lat1 = 0;
+ local lon1 = 0;
+ local lat2 = 0;
+ local lon2 = 0;
+ local ele1 = 0;
+ local ele2 = 0;
+ local i = 0;
+
+ local sel_images = dt.gui.selection()
+
+ for _,image in ipairs(sel_images) do
+ if ((image.longitude and image.latitude) and
+ (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data
+ ) then
+
+ i = i + 1;
+
+ if (i == 1) then
+ lat1 = image.latitude
+ lon1 = image.longitude
+ if (image.elevation) then
+ ele1 = image.elevation
+ else
+ ele1 = 0
+ end
+ end
+
+ lat2 = image.latitude
+ lon2 = image.longitude
+ if (image.elevation) then
+ ele2 = image.elevation
+ else
+ ele2 = 0;
+ end
+
+ end
+ end
+
+ local distance = getDistance(lat1, lon1, ele1, lat2, lon2, ele2)
+
+ if (distance < 1) then
+ distance = distance * 1000
+ distanceUnit = "m"
+ else
+ distanceUnit = "km"
+ end
+
+ return string.format("Distance: %.2f %s", distance, distanceUnit)
+end
+
+local function printCalcDistance()
+ dt.print(calcDistance())
+end
+
+local function toolboxCalcDistance()
+ labelDistance.label = calcDistance()
+end
+
+local altitude_file_chooser_button = dt.new_widget("file_chooser_button")
+ {
+ title = "export altitude CSV", -- The title of the window when choosing a file
+ value = "", -- The currently selected file
+ is_directory = true -- True if the file chooser button only allows directories to be selecte
+ }
+local altitude_filename = dt.new_widget("entry")
+ {
+ text = "altitude.csv",
+ placeholder = "altitude.csv",
+ editable = true,
+ tooltip = "Name of the exported file",
+ reset_callback = function(self) self.text = "text" end
+ }
+
+local function altitudeProfile()
+ dt.print("Start export")
+ local sel_images = dt.gui.selection()
+
+ local lat1 = 0;
+ local lon1 = 0;
+ local lat2 = 0;
+ local lon2 = 0;
+ local ele1 = 0;
+ local ele2 = 0;
+ local i = 0;
+ local csv_file = '';
+ csv_file = "km;m".."\n";
+
+ local distance = 0;
+ local distanceFromStart = 0;
+ local elevation = 0;
+ local elevationAdd = 0;
+
+ local sel_images = dt.gui.selection()
+ for _,image in ipairs(sel_images) do
+ if ((not isnan(image.longitude) and not isnan(image.latitude) and not isnan(image.elevation) and image.elevation) and
+ (image.longitude ~= 0 and image.latitude ~= 90) -- Sometimes the north-pole but most likely just wrong data
+ ) then
+
+ i = i + 1;
+
+ if (i == 1) then
+ lat1 = image.latitude
+ lon1 = image.longitude
+ ele1 = image.elevation
+ else
+ lat1 = lat2
+ lon1 = lon2
+ ele1 = ele2
+ end
+
+ lat2 = image.latitude
+ lon2 = image.longitude
+ ele2 = image.elevation
+
+ if (i == 1) then
+ distanceFromStart = 0
+ else
+ local distance = getDistance(lat1, lon1, ele1, lat2, lon2, ele2)
+ distanceFromStart = distanceFromStart + distance;
+ end
+
+ csv_file = csv_file .. distanceFromStart..";"..image.elevation.."\n";
+
+ end
+
+ end --for
+
+ local exportDirectory = altitude_file_chooser_button.value
+ local exportFilename = altitude_filename.text
+ if (exportFilename == '') then
+ exportFilename = altitude_filename.placeholder
+ end
+ file = io.open(exportDirectory.."/"..exportFilename, "w")
+ file:write(csv_file)
+ file:close()
+ dt.print("File created in "..exportDirectory)
+
+end
+
+
+local separator = dt.new_widget("separator"){}
+local separator2 = dt.new_widget("separator"){}
+local separator3 = dt.new_widget("separator"){}
+local separator4 = dt.new_widget("separator"){}
+
+dt.register_lib(
+ "geoToolbox", -- Module name
+ "geo toolbox", -- name
+ true, -- expandable
+ false, -- resetable
+ {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, -- containers
+ dt.new_widget("box")
+ {
+ orientation = "vertical",
+ dt.new_widget("button")
+ {
+ label = "select geo images",
+ tooltip = "Select all images with GPS information",
+ clicked_callback = selectWithGPS
+ },
+ dt.new_widget("button")
+ {
+ label = "select not geo images",
+ tooltip = "Select all images without GPS information",
+ clicked_callback = selectWithoutGPS
+ },
+ separator,--------------------------------------------------------
+ dt.new_widget("button")
+ {
+ label = "copy GPS data",
+ tooltip = "Copy the GPS data",
+ clicked_callback = copyGPS
+ },
+ labelCopyGPSlat,
+ labelCopyGPSlon,
+ labelCopyGPSele,
+ dt.new_widget("button")
+ {
+ label = "past GPS data",
+ tooltip = "Past the GPS data",
+ clicked_callback = pastGPS
+ },
+ separator2,--------------------------------------------------------
+--ToDo: This need a better UI
+--[[
+ dt.new_widget("button")
+ {
+ label = "get 1st coordinate",
+ tooltip = "Select first image and click this button",
+ clicked_callback = getFirstCoordinate
+ },
+ dt.new_widget("button")
+ {
+ label = "get 2nd coordinate",
+ tooltip = "Select second image and click this button",
+ clicked_callback = getSecondCoordinate
+ },
+ dt.new_widget("button")
+ {
+ label = "calc in between",
+ tooltip = "Select third image and click this button",
+ clicked_callback = calcInBetween
+ },
+ calcInBetweenSlider,
+ separator3,--------------------------------------------------------
+]]
+ dt.new_widget("button")
+ {
+ label = "Open in Gnome Maps",
+ tooltip = "Open Location in Gnome Maps",
+ clicked_callback = openLocationInGnomeMaps
+ },
+ separator4,--------------------------------------------------------
+ dt.new_widget("label"){label = "altitude CSV export"},
+ altitude_file_chooser_button,
+ altitude_filename,
+ dt.new_widget("button")
+ {
+ label = "export altitude CSV file",
+ tooltip = "create an altitude profile using the GPS data in the metadata",
+ clicked_callback = altitudeProfile
+ },
+ labelDistance
+ },
+ nil,-- view_enter
+ nil -- view_leave
+)
+
+
+-- Register
+dt.register_event("shortcut",printCalcDistance,_("Calculate the distance from latitude and longitude in km"))
+dt.register_event("mouse-over-image-changed",toolboxCalcDistance)
+
+dt.register_event("shortcut", selectWithGPS, _("Select all images with GPS information"))
+dt.register_event("shortcut", selectWithoutGPS, _("Select all images without GPS information"))
+
+-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
+-- kate: hl Lua;