示例#1
0
文件: pgtips.py 项目: CiderMan/PGTips
    def __init__(
        self,
        parent,
        id=-1,
        title=_DEFAULT_STATUS_TEXT,
        pos=wx.DefaultPosition,
        size=(800, 500),
        style=wx.DEFAULT_FRAME_STYLE,
    ):
        wx.Frame.__init__(self, parent, id, title, pos, size, style)

        self._statusBar = CustomStatusBar(self)

        self.SetStatusBar(self._statusBar)

        self._optionsDialog = OptionsDialog(self, optFile="pgtips.opt")

        self._closing = False

        self._exiftoolChecked = ""

        self._mgr = aui.AuiManager(
            self,
            aui.AUI_MGR_ALLOW_FLOATING
            | aui.AUI_MGR_TRANSPARENT_DRAG
            | aui.AUI_MGR_VENETIAN_BLINDS_HINT
            | aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE
            | aui.AUI_MGR_LIVE_RESIZE
            | aui.AUI_MGR_SMOOTH_DOCKING
            | aui.AUI_MGR_ANIMATE_FRAMES,
        )

        # Sort out the menu
        menuDef = [
            (
                "&File",
                [
                    ("Load GPS Files...\tCtrl-G", self.OnLoadGpsFiles),
                    ("Load image Files...\tCtrl-L", self.OnLoadImages),
                    ("", None),
                    ("Import files\tCtrl-I", self.OnImport),
                    ("Export files\tCtrl-E", self.OnExport),
                    ("", None),
                    ("Exit\tCtrl-Q", self.OnClose),
                ],
            ),
            ("&Geotag", [("Geotag all images\tCtrl-T", self.OnGeotagAll)]),
            ("&Tools", [("Toggle full screen mode\tF11", self.OnToggleFullScreen), ("Options...", self.OnOptions)]),
        ]

        def create_menu(definition):
            menu = wx.Menu()
            for name, content in definition:
                if content is None:
                    menu.AppendSeparator()
                elif isinstance(content, list):
                    m = create_menu(content)
                    menu.AppendMenu(-1, name, m)
                else:
                    item = menu.Append(-1, name)
                    self.Bind(wx.EVT_MENU, content, item)
            return menu

        mb = wx.MenuBar()
        for title, menu in menuDef:
            mb.Append(create_menu(menu), title)

        self.SetMenuBar(mb)

        self._fullscreen = True
        # self.Maximise(True)
        # Removed as is problematic on Windows. Workaround for this at the bottom of the file
        # self.ShowFullScreen(self._fullscreen, wx.FULLSCREEN_NOBORDER | wx.FULLSCREEN_NOCAPTION)

        # create several text controls
        source = slippy.CloudmadeTileSource("742a64a1ef7540f18e5092560b84a67b")
        tileCache = slippy.SlippyCache(source)  # , proxy = "http://80.254.147.83:8080")

        self._slipMap = slippy.SlippyPanel(self, -1, cache=tileCache, size=(300, 300))
        self._tracks = []
        self._markers = {}

        self._gpxTree = wx.TreeCtrl(
            self, -1, wx.DefaultPosition, wx.Size(-1, -1), wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS | wx.TR_LINES_AT_ROOT
        )
        self._gpxRoot = self._gpxTree.AddRoot("GPS Files")

        self._images = ImageListCtrlPanel(self)

        # add the panes to the manager
        self._mgr.AddPane(self._slipMap, wx.TOP, "Mapping pane")
        # self._mgr.AddPane(text1, wx.TOP, 'Preview pane')
        self._mgr.AddPane(self._gpxTree, wx.TOP, "GPS fle pane")
        self._mgr.AddPane(self._images, wx.CENTER)

        # tell the manager to 'commit' all the changes just made
        self._mgr.Update()

        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self._gpxTree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnNewTrack)

        # Create the worker thread
        self._working = False
        self._workerQueue = Queue.Queue()
        self._workerThread = Thread(target=self._WorkerThread)
        self._workerThread.start()

        self._importing = False
        self._importQueue = Queue.Queue()
        self._importThread = Thread(target=self._ImportThread)
        self._importThread.start()

        self._checkQueue = Queue.Queue()

        self._images.add_image_select_notify(self.OnImageSelected)
        self._images.add_image_deselect_notify(self.OnImageDeselected)

        self._PulseProgress()
示例#2
0
文件: pgtips.py 项目: CiderMan/PGTips
class MyFrame(wx.Frame):
    def __init__(
        self,
        parent,
        id=-1,
        title=_DEFAULT_STATUS_TEXT,
        pos=wx.DefaultPosition,
        size=(800, 500),
        style=wx.DEFAULT_FRAME_STYLE,
    ):
        wx.Frame.__init__(self, parent, id, title, pos, size, style)

        self._statusBar = CustomStatusBar(self)

        self.SetStatusBar(self._statusBar)

        self._optionsDialog = OptionsDialog(self, optFile="pgtips.opt")

        self._closing = False

        self._exiftoolChecked = ""

        self._mgr = aui.AuiManager(
            self,
            aui.AUI_MGR_ALLOW_FLOATING
            | aui.AUI_MGR_TRANSPARENT_DRAG
            | aui.AUI_MGR_VENETIAN_BLINDS_HINT
            | aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE
            | aui.AUI_MGR_LIVE_RESIZE
            | aui.AUI_MGR_SMOOTH_DOCKING
            | aui.AUI_MGR_ANIMATE_FRAMES,
        )

        # Sort out the menu
        menuDef = [
            (
                "&File",
                [
                    ("Load GPS Files...\tCtrl-G", self.OnLoadGpsFiles),
                    ("Load image Files...\tCtrl-L", self.OnLoadImages),
                    ("", None),
                    ("Import files\tCtrl-I", self.OnImport),
                    ("Export files\tCtrl-E", self.OnExport),
                    ("", None),
                    ("Exit\tCtrl-Q", self.OnClose),
                ],
            ),
            ("&Geotag", [("Geotag all images\tCtrl-T", self.OnGeotagAll)]),
            ("&Tools", [("Toggle full screen mode\tF11", self.OnToggleFullScreen), ("Options...", self.OnOptions)]),
        ]

        def create_menu(definition):
            menu = wx.Menu()
            for name, content in definition:
                if content is None:
                    menu.AppendSeparator()
                elif isinstance(content, list):
                    m = create_menu(content)
                    menu.AppendMenu(-1, name, m)
                else:
                    item = menu.Append(-1, name)
                    self.Bind(wx.EVT_MENU, content, item)
            return menu

        mb = wx.MenuBar()
        for title, menu in menuDef:
            mb.Append(create_menu(menu), title)

        self.SetMenuBar(mb)

        self._fullscreen = True
        # self.Maximise(True)
        # Removed as is problematic on Windows. Workaround for this at the bottom of the file
        # self.ShowFullScreen(self._fullscreen, wx.FULLSCREEN_NOBORDER | wx.FULLSCREEN_NOCAPTION)

        # create several text controls
        source = slippy.CloudmadeTileSource("742a64a1ef7540f18e5092560b84a67b")
        tileCache = slippy.SlippyCache(source)  # , proxy = "http://80.254.147.83:8080")

        self._slipMap = slippy.SlippyPanel(self, -1, cache=tileCache, size=(300, 300))
        self._tracks = []
        self._markers = {}

        self._gpxTree = wx.TreeCtrl(
            self, -1, wx.DefaultPosition, wx.Size(-1, -1), wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS | wx.TR_LINES_AT_ROOT
        )
        self._gpxRoot = self._gpxTree.AddRoot("GPS Files")

        self._images = ImageListCtrlPanel(self)

        # add the panes to the manager
        self._mgr.AddPane(self._slipMap, wx.TOP, "Mapping pane")
        # self._mgr.AddPane(text1, wx.TOP, 'Preview pane')
        self._mgr.AddPane(self._gpxTree, wx.TOP, "GPS fle pane")
        self._mgr.AddPane(self._images, wx.CENTER)

        # tell the manager to 'commit' all the changes just made
        self._mgr.Update()

        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self._gpxTree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnNewTrack)

        # Create the worker thread
        self._working = False
        self._workerQueue = Queue.Queue()
        self._workerThread = Thread(target=self._WorkerThread)
        self._workerThread.start()

        self._importing = False
        self._importQueue = Queue.Queue()
        self._importThread = Thread(target=self._ImportThread)
        self._importThread.start()

        self._checkQueue = Queue.Queue()

        self._images.add_image_select_notify(self.OnImageSelected)
        self._images.add_image_deselect_notify(self.OnImageDeselected)

        self._PulseProgress()

    # This method is the one called in the worker thread and dispatches
    # the tasks that are passed to it
    def _WorkerThread(self):
        while not self._closing:
            fn, args, kwargs = self._workerQueue.get()
            self._working = True
            while self._working:
                if fn is not None:
                    fn(*args, **kwargs)
                try:
                    fn, args, kwargs = self._workerQueue.get_nowait()
                except Queue.Empty:
                    self._working = False

    # Method to pass work to the worker thread using the same pattern as
    # wx.CallAfter()
    def _do_work(self, fn, *args, **kwargs):
        self._workerQueue.put((fn, args, kwargs))

    def _do_check(self, fn, *args, **kwargs):
        self._checkQueue.put((fn, args, kwargs))

    # This method polls whether the working thread is busy and pulses the
    # status bar progress indicator while that it true
    def _PulseProgress(self):
        while not self._closing:
            try:
                fn, args, kwargs = self._checkQueue.get_nowait()
                fn(*args, **kwargs)
            except Queue.Empty:
                break

        if self._working or self._importing:
            self._statusBar.progress.Pulse()
            wx.CallLater(100, self._PulseProgress)
        else:
            self._statusBar.SetStatusText(_DEFAULT_STATUS_TEXT)
            self._statusBar.progress.SetValue(0)
            wx.CallLater(500, self._PulseProgress)

    def _exiftool_check(self):
        exiftool = "exiftool"
        path = self._optionsDialog.options["ExiftoolPath"]
        if path != "":
            exiftool = os.path.join(path, exiftool)
        if self._exiftoolChecked != exiftool:
            error = False
            try:
                p = Popen([exiftool, "-ver"], stdin=PIPE, stdout=PIPE, stderr=STDOUT)
                stdout, stderr = p.communicate()
            except OSError:
                error = True
            if error:
                response = wx.MessageDialog(
                    self,
                    "PGTips is uses EXIFtool to read the metadata in your photographs\n"
                    "and set the geotags in them. However, it has not been possible\n"
                    "to run exiftool. Please ensure that you have EXIFtool on this PC\n"
                    "and that the configuration (e.g. path in Tools->Options) is correct",
                    "Configuration problem",
                    style=wx.OK | wx.ICON_ERROR,
                ).ShowModal()
                return False
            self._exiftoolChecked = exiftool

        return True

    ################################################################################
    ####
    #### Load GPS files
    ####
    ################################################################################

    def _add_gps_file(self, gpsFile):
        text = os.path.basename(gpsFile.get_filename())
        item, cookie = self._gpxTree.GetFirstChild(self._gpxRoot)
        n = 0
        while item.IsOk() and text <= self._gpxTree.GetItemText(item):
            item, cookie = self._gpxTree.GetNextChild(self._gpxRoot, cookie)
            n += 1
        if item.IsOk():
            item = self._gpxTree.InsertItemBefore(self._gpxRoot, n, text)
        else:
            item = self._gpxTree.AppendItem(self._gpxRoot, text)
        wrappedGpsFile = GpsFileCache(gpsFile)
        # Note that the PyData is:
        # 1: The object for the whole file
        # 2: The indices of the sub-tracks to which this element corresponds
        # 3: A placeholder for the cached track array
        # 4: A placeholder for the cached region
        self._gpxTree.SetPyData(item, (wrappedGpsFile, range(len(gpsFile))))
        for n, t in enumerate(gpsFile):
            subitem = self._gpxTree.AppendItem(item, "%s - %s" % (str(t[0][0]), str(t[-1][0])))
            self._gpxTree.SetPyData(subitem, (wrappedGpsFile, [n]))

        # TODO: Now check whether there are any images to geotag

    def _load_gps_files_work(self, path):
        for t in gen_tracks_from_files(path):
            wx.CallAfter(self._add_gps_file, t)

    def OnLoadGpsFiles(self, event):
        dlg = wx.DirDialog(self, style=wx.DD_DIR_MUST_EXIST, defaultPath="/home/steve/GPSTracks/")
        if dlg.ShowModal() == wx.ID_OK:
            self._do_work(self._load_gps_files_work, dlg.GetPath())

    # End the "load GPS files" operation

    ################################################################################
    ####
    #### Load images
    ####
    ################################################################################

    def _add_image(self, img):
        if img["FileType"] == "JPG" and img["Orientation"] != "1":
            print img.get_filename(), "is not orientation 1"

        # TODO: Check whether this image is geotagged by any of the GPS files
        self._images.add_image(img)

    def _load_file_work(self, path):
        wx.CallAfter(self._statusBar.SetStatusText, "Loading files from " + path)
        include = self._optionsDialog.options["ImageExtensions"]
        if len(include) == 0:
            include = None
        for i in gen_images_from_files(path, include=include, exiftool=self._exiftoolChecked):
            wx.CallAfter(self._statusBar.SetStatusText, "Loading " + i["FileName"])
            wx.CallAfter(self._add_image, i)

    def OnLoadImages(self, event):
        if not self._exiftool_check():
            return
        dlg = wx.DirDialog(self, style=wx.DD_DIR_MUST_EXIST, defaultPath="/home/steve/Master/")
        if dlg.ShowModal() == wx.ID_OK:
            self._do_work(self._load_file_work, dlg.GetPath())

    # End the "load images" operation

    ################################################################################
    ####
    #### Import files
    ####
    ################################################################################

    # This function is run as a seperate thread to automatically batch the files
    # to exiftool to improve performance
    def _ImportThread(self):
        while not self._closing:
            files = []
            try:
                filename = self._importQueue.get_nowait()
            except Queue.Empty:
                self._importing = False
                filename = self._importQueue.get()
            self._importing = True
            if filename is not None:
                files.append(filename)
                try:
                    timeout = 0.5
                    while True:
                        filename = self._importQueue.get(block=timeout > 0, timeout=timeout)
                        if filename is not None:
                            files.append(filename)
                            timeout -= 0.1
                        else:
                            break
                except Queue.Empty:
                    for i in gen_images_from_files(files, exiftool=self._exiftoolChecked):
                        wx.CallAfter(self._statusBar.SetStatusText, "Loading " + i["FileName"])
                        wx.CallAfter(self._add_image, i)

    def _copy_file_work(self, f, dirpath, workingDir, ext, process, useJpegtran, jpegexiforient, jpegtran):
        wx.CallAfter(self._statusBar.SetStatusText, "Importing " + f)
        srcFile = os.path.join(dirpath, f)
        destFile = os.path.join(workingDir, f)
        if os.path.exists(destFile):
            # User must have OK'd this so delete the destination file then copy the new one into place
            os.remove(destFile)
        shutil.copy(srcFile, workingDir)
        if process and ext in [".jpg", ".jpeg"] and useJpegtran:
            # losslessly rotate
            p = Popen([jpegexiforient, "-n", destFile], stdin=PIPE, stdout=PIPE, stderr=PIPE)
            stdout, stderr = p.communicate()
            try:
                orient = int(stdout)
            except ValueError:
                # No orientation flag? Set orient to the value that prevents any further work
                orient = 1
            if orient > 1:
                wx.CallAfter(self._statusBar.SetStatusText, "Importing %s (losslessly rotating)" % f)
                print destFile, "- using option:", _jpegtranOptions[orient]
                tmpFile = destFile + ".pgtips~.jpg"
                p = Popen(
                    [jpegtran, "-copy", "all"] + _jpegtranOptions[orient] + ["-outfile", tmpFile, destFile],
                    stdin=PIPE,
                    stdout=PIPE,
                    stderr=PIPE,
                )
                stdout, stderr = p.communicate()
                assert p.returncode == 0, "jpegtran failed: " + stderr

                p = Popen([jpegexiforient, "-1", tmpFile], stdin=PIPE, stdout=PIPE, stderr=PIPE)
                stdout, stderr = p.communicate()
                assert p.returncode == 0, "jpegexiforient failed: " + stderr

                os.remove(destFile)
                os.rename(tmpFile, destFile)

        if process:
            # Now that the file is where it needs to be, load the file
            # TODO: Could optimize by batching files together?
            if 1:
                self._importQueue.put(destFile)
            else:
                wx.CallAfter(self._statusBar.SetStatusText, "Loading " + f)
                for i in gen_images_from_files(destFile, exiftool=self._exiftoolChecked):
                    wx.CallAfter(self._add_image, i)

    def _copy_file_overwrite_check(self, f, dirpath, workingDir, ext, process, useJpegtran, jpegexiforient, jpegtran):
        if (
            wx.MessageBox("%s already exists in %s; overwrite?" % (f, workingDir), "File exists", wx.YES_NO, self)
            == wx.YES
        ):
            self._do_work(
                self._copy_file_work, f, dirpath, workingDir, ext, process, useJpegtran, jpegexiforient, jpegtran
            )

    def _import_files_work(self, fromDir, workingDir, useJpegtran, jpegexiforient, jpegtran, emptyWorkingDir):
        if emptyWorkingDir:
            wx.CallAfter(self._statusBar.SetStatusText, "Deleting files in " + workingDir)
            files = os.listdir(workingDir)
            for f in files:
                print "Deleting", os.path.join(workingDir, f)
                os.remove(os.path.join(workingDir, f))

        other = self._optionsDialog.options["OtherExtensions"]
        image = self._optionsDialog.options["ImageExtensions"]

        wx.CallAfter(self._statusBar.SetStatusText, "Searching for files to import...")

        # Walk and copy files to working dir
        for dirpath, dirnames, filenames in os.walk(fromDir):
            for f in filenames:
                ext = os.path.splitext(f)[1].lower()
                copy = False
                process = False
                if ext in other or len(other) == 0:
                    copy = True
                if ext in image or len(image) == 0:
                    copy = True
                    process = True
                if copy:
                    destFile = os.path.join(workingDir, f)
                    if os.path.exists(destFile):
                        self._do_check(
                            self._copy_file_overwrite_check,
                            f,
                            dirpath,
                            workingDir,
                            ext,
                            process,
                            useJpegtran,
                            jpegexiforient,
                            jpegtran,
                        )
                    else:
                        self._do_work(
                            self._copy_file_work,
                            f,
                            dirpath,
                            workingDir,
                            ext,
                            process,
                            useJpegtran,
                            jpegexiforient,
                            jpegtran,
                        )

    def OnImport(self, event):
        if not self._exiftool_check():
            return
        useJpegtran = self._optionsDialog.options["JpegtranEnabled"]
        jpegtran = "jpegtran"
        jpegexiforient = "jpegexiforient"
        if useJpegtran:
            error = False
            path = self._optionsDialog.options["JpegtranPath"]
            if path != "":
                jpegtran = os.path.join(path, jpegtran)
                jpegexiforient = os.path.join(path, jpegexiforient)
            try:
                p = Popen([jpegtran, "-v"], stdin=PIPE, stdout=PIPE, stderr=STDOUT)
                stdout, stderr = p.communicate()
            except OSError:
                error = True
            try:
                p = Popen([jpegexiforient, "--help"], stdin=PIPE, stdout=PIPE, stderr=STDOUT)
                stdout, stderr = p.communicate()
            except OSError:
                error = True
            if error:
                response = wx.MessageDialog(
                    self,
                    "PGTips is configured to use jpegtran but it was not able to\n"
                    "execute jpegtran and/or jpegexiforient. This means that it\n"
                    "will not be possible to automatically rotate any JPEG files\n"
                    "during import\n\n"
                    "Do you want to continue importing?",
                    "Configuration problem",
                    style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
                ).ShowModal()
                if response == wx.ID_NO:
                    return
                else:
                    useJpegtran = False

        dlg = wx.DirDialog(self, style=wx.DD_DIR_MUST_EXIST, defaultPath="/home/steve/Master/")
        if dlg.ShowModal() == wx.ID_OK:
            workingDir = self._optionsDialog.options["WorkingDir"]
            emptyWorkingDir = False
            if not os.path.isdir(workingDir):
                try:
                    os.makedirs(workingDir)
                except OSError:
                    wx.MessageBox("Unable to create " + workingDir, "Error", style=wx.ICON_ERROR)
                    return
            else:
                files = os.listdir(workingDir)
                if len(files) > 0:
                    response = wx.MessageDialog(
                        self,
                        "The working directory is not empty.\n\n"
                        "Do you want to delete the current contents before importing?",
                        "Working directory not empty",
                        style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
                    ).ShowModal()
                    if response == wx.ID_YES:
                        emptyWorkingDir = True

            self._do_work(
                self._import_files_work,
                dlg.GetPath(),
                workingDir,
                useJpegtran,
                jpegexiforient,
                jpegtran,
                emptyWorkingDir,
            )

    # End the "import files" operation

    ################################################################################
    ####
    #### Export files
    ####
    ################################################################################

    def _export_file_overwrite(self, f, exportDir, srcFile):
        dest = os.path.join(exportDir, f)
        wx.CallAfter(self._statusBar.SetStatusText, "Removing " + dest)
        os.remove(dest)

        wx.CallAfter(self._statusBar.SetStatusText, "Exporting " + f)
        shutil.move(srcFile, exportDir)

    def _export_file_overwrite_check(self, f, exportDir, srcFile):
        if (
            wx.MessageBox("%s already exists in %s; overwrite?" % (f, exportDir), "File exists", wx.YES_NO, self)
            == wx.YES
        ):
            self._do_work(self._export_file_overwrite, f, exportDir, srcFile)

    def _export_file_work(self, dirpath, f):
        images = self._optionsDialog.options["ImageExtensions"]
        other = self._optionsDialog.options["OtherExtensions"]

        ext = os.path.splitext(f)[1].lower()
        srcFile = os.path.abspath(os.path.join(dirpath, f))
        img = None
        exportDatetime = None
        if ext in images or len(images) == 0:
            for img in self._images.iter_images():
                if os.path.abspath(img.get_filename()) == srcFile:
                    wx.CallAfter(self._images.rm_image, img)
                    break
            else:
                img = list(gen_images_from_files(srcFile, exiftool=self._exiftoolChecked))[0]
            exportDatetime = img.dateTime
        if exportDatetime is None and (ext in other or len(other) == 0):
            otherFile = list(gen_images_from_files(srcFile, exiftool=self._exiftoolChecked))[0]
            exportDatetime = otherFile.dateTime
            print f, exportDatetime
            assert exportDatetime is not None
            # exportDatetime = datetime.datetime.fromtimestamp(os.stat(srcFile).st_ctime)
        if exportDatetime is not None:
            exportDir = os.path.join(
                self._optionsDialog.options["FilingDir"],
                exportDatetime.strftime(self._optionsDialog.options["FilingStruct"]),
            )
            if not os.path.isdir(exportDir):
                os.makedirs(exportDir)
            if os.path.exists(os.path.join(exportDir, f)):
                self._do_check(self._export_file_overwrite_check, f, exportDir, srcFile)
            else:
                wx.CallAfter(self._statusBar.SetStatusText, "Exporting " + f)
                shutil.move(srcFile, exportDir)

    def OnExport(self, event):
        fromDir = self._optionsDialog.options["WorkingDir"]

        # Walk and copy files to working dir
        for dirpath, dirnames, filenames in os.walk(fromDir):
            for f in filenames:
                self._do_work(self._export_file_work, dirpath, f)

    # End the "export files" operation

    ################################################################################
    ####
    #### Geotag all files
    ####
    ################################################################################

    def _geotag_work(self, img):
        wx.CallAfter(self._statusBar.SetStatusText, "Geotagging " + img["FileName"])
        geotag = img.geotag

        item, cookie = self._gpxTree.GetFirstChild(self._gpxRoot)
        while item.IsOk():
            tracks, n = self._gpxTree.GetPyData(item)
            geotag = tracks.gpsFile.match_time(img.dateTime)
            if geotag is not None:
                break
            item, cookie = self._gpxTree.GetNextChild(self._gpxRoot, cookie)

        print img["FileName"], "taken at", geotag
        if geotag is not None:
            img.set_geotag(geotag)
            wx.CallAfter(self._images.update_image, img)
            img.save_changes()

    def OnGeotagAll(self, event):
        print "Geotag All"
        for img in self._images.iter_images():
            self._do_work(self._geotag_work, img)

    # End the "geotag all files" operation

    def OnOptions(self, event):
        self._optionsDialog.ShowModal()

    def OnNewTrack(self, event):
        if not self._closing:
            for t in self._tracks:
                self._slipMap.RemoveTrack(t)
            self._tracks = []
            item = self._gpxTree.GetSelection()
            wrappedGpsFile, ns = self._gpxTree.GetPyData(item)
            tracks = []
            for n in ns:
                t = wrappedGpsFile.get_track(n)
                tracks.append(t)
            self._tracks += tracks
            region = reduce(lambda x, y: x + y.get_region(), tracks, slippy.Region())
            map(self._slipMap.AddTrack, tracks)
            self._slipMap.ShowRegion(region)

    def OnImageSelected(self, img):
        print "Selected", img.get_filename()
        geotag = img.geotag
        if geotag is not None:
            m = slippy.Marker(img, geotag[0], geotag[1], movable=False)
            try:
                self._slipMap.RemoveMarker(self._markers[img])
            except KeyError:
                pass
            self._markers[img] = m
            self._slipMap.AddMarker(m)
            self._slipMap.CentreMap(geotag[0], geotag[1])

    def OnImageDeselected(self, img):
        print "Deselected", img.get_filename()
        try:
            self._slipMap.RemoveMarker(self._markers[img])
            del self._markers[img]
        except KeyError:
            pass

    def OnToggleFullScreen(self, event):
        self._fullscreen = not self._fullscreen
        self.ShowFullScreen(self._fullscreen, wx.FULLSCREEN_NOBORDER | wx.FULLSCREEN_NOCAPTION)

    def OnClose(self, event):
        self._closing = True
        # Kill the worker thread
        self._workerQueue.put((None, None, None))
        if self._workerThread.is_alive():
            self._workerThread.join()
        # Kill the import thread
        self._importQueue.put(None)
        if self._importThread.is_alive():
            self._importThread.join()
        # deinitialize the frame manager
        self._mgr.UnInit()
        # delete the frame
        self.Destroy()