/// <summary>
        /// If there are missing folders, search for possible matches and raise a dialog box asking the user to locate them
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="fileDatabase"></param>
        /// <param name="missingFolders"></param>
        /// <returns>whether any folder are actually missing </returns>
        private static bool?CorrectForMissingFolders(Window owner, FileDatabase fileDatabase, List <string> missingRelativePaths)
        {
            // Abort if the arguments for null
            if (null == fileDatabase)
            {
                return(null);
            }
            if (null == missingRelativePaths)
            {
                return(null);
            }

            // We know that at least one or more folders are missing.
            // For each missing folder path, try to find all folders with the same name under the root folder.
            Dictionary <string, List <string> > matchingFolderNames = Util.FilesFolders.TryGetMissingFolders(fileDatabase.FolderPath, missingRelativePaths);
            Dictionary <string, string>         finalFileLocations;

            // We want to show the normal cursor when we display dialog boxes, so save the current cursor so we can store it.
            Cursor cursor = Mouse.OverrideCursor;

            bool?result;

            if (matchingFolderNames != null)
            {
                Mouse.OverrideCursor = null;
                // Present a dialog box that asks the user to locate the missing folders. It will show possible locations for each folder (if any).
                // The user can then confirm correct locations, manually set the locaton of those folders, or cancel altogether.
                MissingFoldersLocateAllFolders dialog = new MissingFoldersLocateAllFolders(owner, fileDatabase.FolderPath, matchingFolderNames);
                result = dialog.ShowDialog();

                if (result == true)
                {
                    // Get the updated folder locations and update the database
                    finalFileLocations = dialog.FinalFolderLocations;
                    foreach (string key in finalFileLocations.Keys)
                    {
                        ColumnTuple           columnToUpdate          = new ColumnTuple(Constant.DatabaseColumn.RelativePath, finalFileLocations[key]);
                        ColumnTuplesWithWhere columnToUpdateWithWhere = new ColumnTuplesWithWhere(columnToUpdate, key);
                        fileDatabase.UpdateFiles(columnToUpdateWithWhere);
                    }
                    Mouse.OverrideCursor = cursor;
                    return(true);
                }
            }
            Mouse.OverrideCursor = cursor;
            return(null); // Operaton aborted
        }
Exemplo n.º 2
0
        //Try to find a missing image
        private async void MenuItemEditFindMissingImage_Click(object sender, RoutedEventArgs e)
        {
            // Don't do anything if the image actually exists. This should not fire, as this menu item is only enabled
            // if there is a current image that doesn't exist. But just in case...
            if (null == this.DataHandler?.ImageCache?.Current || File.Exists(FilesFolders.GetFullPath(this.DataHandler.FileDatabase.FolderPath, this.DataHandler?.ImageCache?.Current)))
            {
                return;
            }

            string   folderPath   = this.DataHandler.FileDatabase.FolderPath;
            ImageRow currentImage = this.DataHandler?.ImageCache?.Current;

            // Search for - and return as list of relative path / filename tuples -  all folders under the root folder for files with the same name as the fileName.
            List <Tuple <string, string> > matchingRelativePathFileNameList = Util.FilesFolders.SearchForFoldersContainingFileName(folderPath, currentImage.File);

            // Remove any of the tuples that are spoken for i.e., that are already associated with a row in the database
            for (int i = matchingRelativePathFileNameList.Count - 1; i >= 0; i--)
            {
                if (this.DataHandler.FileDatabase.ExistsRelativePathAndFileInDataTable(matchingRelativePathFileNameList[i].Item1, matchingRelativePathFileNameList[i].Item2))
                {
                    // We only want matching files that are not already assigned to another datafield in the database
                    matchingRelativePathFileNameList.RemoveAt(i);
                }
            }

            // If there are no remaining tuples, it means no potential matches were found.
            // Display a message saying so and abort.
            if (matchingRelativePathFileNameList.Count == 0)
            {
                Dialogs.MissingFileSearchNoMatchesFoundDialog(this, currentImage.File);
                return;
            }

            // Now retrieve a list of all filenames located in the same folder (i.e., that have the same relative path) as the missing file.
            List <string> otherMissingFiles = this.DataHandler.FileDatabase.SelectFileNamesWithRelativePathFromDatabase(currentImage.RelativePath);

            // Remove the current missing file from the list, as well as any file that exists i.e., that is not missing.
            for (int i = otherMissingFiles.Count - 1; i >= 0; i--)
            {
                if (String.Equals(otherMissingFiles[i], currentImage.File) || File.Exists(Path.Combine(folderPath, currentImage.RelativePath, otherMissingFiles[i])))
                {
                    otherMissingFiles.RemoveAt(i);
                }
            }

            // For those that are left (if any), see if other files in the returned locations are in each path. Get their count, save them, and pass the count as a parameter e.g., a Dict with matching files, etc.
            // Or perhapse even better, a list of file names for each path Dict<string, List<string>>
            // As we did above, go through the other missing files and remove those that are spoken for i.e., that are already associated with a row in the database.
            // What remains will be a list of  root paths, each with a list of  missing (unassociated) files that could be candidates for locating
            Dictionary <string, List <string> > otherMissingFileCandidates = new Dictionary <string, List <string> >();

            foreach (Tuple <string, string> matchingPath in matchingRelativePathFileNameList)
            {
                List <string> orphanMissingFiles = new List <string>();
                foreach (string otherMissingFile in otherMissingFiles)
                {
                    // Its a potential candidate if its not already referenced but it exists in that relative path folder
                    if (false == this.DataHandler.FileDatabase.ExistsRelativePathAndFileInDataTable(matchingPath.Item1, otherMissingFile) &&
                        File.Exists(FilesFolders.GetFullPath(FolderPath, matchingPath.Item1, otherMissingFile)))
                    {
                        orphanMissingFiles.Add(otherMissingFile);
                    }
                }
                otherMissingFileCandidates.Add(matchingPath.Item1, orphanMissingFiles);
            }

            Dialog.MissingImageLocateRelativePaths dialog = new Dialog.MissingImageLocateRelativePaths(this, this.DataHandler.FileDatabase, currentImage.RelativePath, currentImage.File, otherMissingFileCandidates);

            bool?result = dialog.ShowDialog();

            if (result == true)
            {
                Tuple <string, string> locatedMissingFile = dialog.LocatedMissingFile;
                if (String.IsNullOrEmpty(locatedMissingFile.Item2))
                {
                    return;
                }

                // Update the original missing file
                List <ColumnTuplesWithWhere> columnTuplesWithWhereList = new List <ColumnTuplesWithWhere>();
                ColumnTuplesWithWhere        columnTuplesWithWhere     = new ColumnTuplesWithWhere();
                columnTuplesWithWhere.Columns.Add(new ColumnTuple(Constant.DatabaseColumn.RelativePath, locatedMissingFile.Item1)); // The new relative path
                columnTuplesWithWhere.SetWhere(currentImage.RelativePath, currentImage.File);                                       // Where the original relative path/file terms are met
                columnTuplesWithWhereList.Add(columnTuplesWithWhere);

                // Update the other missing files in the database, if any
                foreach (string otherMissingFileName in otherMissingFileCandidates[locatedMissingFile.Item1])
                {
                    columnTuplesWithWhere = new ColumnTuplesWithWhere();
                    columnTuplesWithWhere.Columns.Add(new ColumnTuple(Constant.DatabaseColumn.RelativePath, locatedMissingFile.Item1)); // The new value
                    columnTuplesWithWhere.SetWhere(currentImage.RelativePath, otherMissingFileName);                                    // Where the original relative path/file terms are met
                    columnTuplesWithWhereList.Add(columnTuplesWithWhere);
                }

                // Now update the database
                this.DataHandler.FileDatabase.UpdateFiles(columnTuplesWithWhereList);
                await this.FilesSelectAndShowAsync().ConfigureAwait(true);
            }
        }
Exemplo n.º 3
0
        // Populate the database with the metadata for the selected note field
        private async Task <ObservableCollection <Tuple <string, string, string> > > PopulateAsync(MetadataToolEnum metadataToolSelected)
        {
            // This list will hold key / value pairs that will be bound to the datagrid feedback,
            // which is the way to make those pairs appear in the data grid during background worker progress updates
            ObservableCollection <Tuple <string, string, string> > feedbackData = new ObservableCollection <Tuple <string, string, string> >();

            // if there are no metadata / label pairs, we are done.
            if (this.MetadataGrid.SelectedMetadata.Count == 0)
            {
                // Catch the case where there are no selected pairs, at least for now.
                feedbackData.Clear();
                feedbackData.Add(new Tuple <string, string, string>("Nothing was selected", "", "No changes were made"));
                return(feedbackData);
            }

            return(await Task.Run(() =>
            {
                // For each row in the database, get the image filename and try to extract the chosen metadata value.
                // If we can't decide if we want to leave the data field alone or to clear it depending on the state of the isClearIfNoMetadata (set via the checkbox)
                // Report progress as needed.

                // This tuple list will hold the id, key and value that we will want to update in the database
                List <ColumnTuplesWithWhere> imagesToUpdate = new List <ColumnTuplesWithWhere>();
                TimeZoneInfo imageSetTimeZone = DateTimeHandler.GetNeutralTimeZone();
                int percentDone = 0;

                double totalImages = this.FileDatabase.CountAllCurrentlySelectedFiles;
                Dictionary <string, ImageMetadata> metadata = new Dictionary <string, ImageMetadata>();
                string[] tags = this.MetadataGrid.SelectedTags;// Only needed by ExifTool, but cheap to get
                for (int imageIndex = 0; imageIndex < totalImages; ++imageIndex)
                {
                    // Provide feedback if the operation was cancelled during the database update
                    if (Token.IsCancellationRequested == true)
                    {
                        feedbackData.Clear();
                        feedbackData.Add(new Tuple <string, string, string>("Cancelled", "", "No changes were made"));
                        return feedbackData;
                    }

                    ImageRow image = this.FileDatabase.FileTable[imageIndex];

                    if (metadataToolSelected == MetadataToolEnum.MetadataExtractor)
                    {
                        // MetadataExtractor specific code
                        metadata = ImageMetadataDictionary.LoadMetadata(image.GetFilePath(this.FileDatabase.FolderPath));
                    }
                    else // if metadataToolSelected == MetadataToolEnum.ExifTool
                    {
                        // ExifTool specific code - note that we transform results into the same dictionary structure used by the MetadataExtractor
                        // Unlike MetadataExtractor, ExifTool returns TagName instad of Directory.TagName (I think - but does that mean it would break on duplicate values?
                        metadata.Clear();
                        Dictionary <string, string> exifData = this.MetadataGrid.ExifToolManager.FetchExifFrom(image.GetFilePath(this.FileDatabase.FolderPath), tags);
                        foreach (string tag in tags)
                        {
                            if (exifData.ContainsKey(tag))
                            {
                                metadata.Add(tag, new Timelapse.Util.ImageMetadata(String.Empty, tag, exifData[tag]));
                            }
                        }
                    }
                    // At this point, the metadata Key should be the tag name, rather than Directory.TagName
                    // (see ImageMetadataDiction.LoadDictionary to change it back so the key is the directory.name. I think Exif never returns the directory name, so thats ok too.

                    if (this.ReadyToRefresh())
                    {
                        percentDone = Convert.ToInt32(imageIndex / totalImages * 100.0);
                        this.Progress.Report(new ProgressBarArguments(percentDone, String.Format("{0}/{1} images. Processing {2}", imageIndex, totalImages, image.File), true, false));
                        Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime);  // Allows the UI thread to update every now and then
                    }

                    string dataLabelToUpdate = "";

                    foreach (KeyValuePair <string, string> kvp in this.MetadataGrid.SelectedMetadata)
                    {
                        string metadataTag = kvp.Key;
                        dataLabelToUpdate = kvp.Value;

                        if (false == metadata.ContainsKey(metadataTag))
                        {
                            // This just skips this metadata as it was not found in the file's metadata
                            // However, we still need to supply feedback and (if the user has asked for that option) to clear the data field
                            if (this.clearIfNoMetadata)
                            {
                                List <ColumnTuple> clearField = new List <ColumnTuple>()
                                {
                                    new ColumnTuple(dataLabelToUpdate, String.Empty)
                                };
                                imagesToUpdate.Add(new ColumnTuplesWithWhere(clearField, image.ID));
                                feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, "No metadata found - data field cleared"));
                            }
                            else
                            {
                                feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, "No metadata found - data field unchanged"));
                            }
                            continue;
                        }
                        string metadataValue = metadata[metadataTag].Value;
                        ColumnTuplesWithWhere imageUpdate;
                        if (this.useDateMetadataOnly)
                        {
                            if (DateTimeHandler.TryParseMetadataDateTaken(metadataValue, imageSetTimeZone, out DateTimeOffset metadataDateTime))
                            {
                                image.SetDateTimeOffset(metadataDateTime);

                                imageUpdate = image.GetDateTimeColumnTuples();
                                feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, metadataValue));
                            }
                            else
                            {
                                feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, String.Format("Data field unchanged - '{0}' is not a valid date/time.", metadataValue)));
                                continue;
                            }
                        }
                        else
                        {
                            imageUpdate = new ColumnTuplesWithWhere(new List <ColumnTuple>()
                            {
                                new ColumnTuple(dataLabelToUpdate, metadataValue)
                            }, image.ID);
                            feedbackData.Add(new Tuple <string, string, string>(image.File, metadataTag, metadataValue));
                        }
                        imagesToUpdate.Add(imageUpdate);
                    }
                }
                this.isAnyDataUpdated = true;
                this.Progress.Report(new ProgressBarArguments(100, String.Format("Writing metadata for {0} files. Please wait...", totalImages), false, true));
                Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime);  // Allows the UI thread to update every now and then
                this.FileDatabase.UpdateFiles(imagesToUpdate);
                return feedbackData;
            }, this.Token).ConfigureAwait(true));
        }
Exemplo n.º 4
0
        // Reads all the data from the old ImageData.xml files into the imageData structure from the XML file in the filepath.
        // Note that we need to know the code controls,as we have to associate any points read in with a particular counter control
        public static Tuple <int, int> Read(string filePath, FileDatabase imageDatabase)
        {
            // XML Preparation - follows CA3075  pattern for loading
            XmlDocument xmlDoc = new XmlDocument()
            {
                XmlResolver = null
            };

            System.IO.StringReader sreader = new System.IO.StringReader(File.ReadAllText(filePath));
            XmlReader reader = XmlReader.Create(sreader, new XmlReaderSettings()
            {
                XmlResolver = null
            });

            xmlDoc.Load(reader);

            // Import the old log (if any)
            XmlNodeList logNodes = xmlDoc.SelectNodes(Constant.ImageXml.Images + Constant.ImageXml.Slash + Constant.DatabaseColumn.Log);

            if (logNodes.Count > 0)
            {
                XmlNode logNode = logNodes[0];
                imageDatabase.ImageSet.Log = logNode.InnerText;
                imageDatabase.UpdateSyncImageSetToDatabase();
            }

            // Create three lists, each one representing the datalabels (in order found in the template) of notes, counters and choices
            // We will use these to find the matching ones in the xml data table.
            List <string> noteControlNames    = new List <string>();
            List <string> counterControlNames = new List <string>();
            List <string> choiceControlNames  = new List <string>();

            foreach (ControlRow control in imageDatabase.Controls)
            {
                // Note that code should be modified to  deal with flag controls
                switch (control.Type)
                {
                case Constant.Control.Counter:
                    counterControlNames.Add(control.DataLabel);
                    break;

                case Constant.Control.FixedChoice:
                    choiceControlNames.Add(control.DataLabel);
                    break;

                case Constant.Control.Note:
                    noteControlNames.Add(control.DataLabel);
                    break;

                default:

                    break;
                }
            }

            XmlNodeList  nodeList         = xmlDoc.SelectNodes(Constant.ImageXml.Images + Constant.ImageXml.Slash + Constant.DatabaseColumn.Image);
            int          imageID          = 0;
            TimeZoneInfo imageSetTimeZone = imageDatabase.ImageSet.GetSystemTimeZone();
            List <ColumnTuplesWithWhere> imagesToUpdate  = new List <ColumnTuplesWithWhere>();
            List <ColumnTuplesWithWhere> markersToUpdate = new List <ColumnTuplesWithWhere>();
            List <Tuple <string, List <ColumnTuple> > > fileNamesMarkersList = new List <Tuple <string, List <ColumnTuple> > >();

            int successCounter = 0;
            int skippedCounter = 0;

            foreach (XmlNode node in nodeList)
            {
                imageID++;

                // We ignore:
                // - Folder and Relative path, as the new template will have the correct values
                // - ImageQuality, as the new Timelapse version probably has a better determination of it
                // - DeleteFlag, as the old-style xml templates didn't have them
                // - Flags as the old-style xml templates didn't have them

                List <ColumnTuple> columnsToUpdate = new List <ColumnTuple>(); // Populate the data
                // File Field - We use the file name as a key into a particular database row. We don't change the database field as it is our key.
                string imageFileName = node[Constant.ImageXml.File].InnerText;

                // If the Folder Path differs from where we had previously loaded it,
                // warn the user that the new path will be substituted in its place
                // This gets the folderName in the Xml file, but we still ahve to get the folderName as it currently exists.
                // string folderName = node[Constant.ImageXml.Folder].InnerText;

                // Date - We use the original date, as the analyst may have adjusted them
                string date = node[Constant.ImageXml.Date].InnerText;
                columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.Date, date));

                // Date - We use the original time, although its almost certainly identical
                string time = node[Constant.ImageXml.Time].InnerText;
                columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.Time, time));

                // DateTime
                if (DateTimeHandler.TryParseLegacyDateTime(date, time, imageSetTimeZone, out DateTimeOffset dateTime))
                {
                    columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.DateTime, dateTime.UtcDateTime));
                    columnsToUpdate.Add(new ColumnTuple(Constant.DatabaseColumn.UtcOffset, dateTime.Offset));
                }

                // Notes: Iterate through
                int         innerNodeIndex = 0;
                XmlNodeList innerNodeList  = node.SelectNodes(Constant.Control.Note);
                foreach (XmlNode innerNode in innerNodeList)
                {
                    // System.Diagnostics.Debug.Print("Note: " + noteControlNames[innerNodeIndex] + " | " + innerNode.InnerText);
                    columnsToUpdate.Add(new ColumnTuple(noteControlNames[innerNodeIndex++], innerNode.InnerText));
                }

                // Choices: Iterate through
                innerNodeIndex = 0;
                innerNodeList  = node.SelectNodes(Constant.Control.FixedChoice);
                foreach (XmlNode innerNode in innerNodeList)
                {
                    // System.Diagnostics.Debug.Print("Choice: " + choiceControlNames[innerNodeIndex] + " | " + innerNode.InnerText);
                    columnsToUpdate.Add(new ColumnTuple(choiceControlNames[innerNodeIndex++], innerNode.InnerText));
                }

                // Counters: Iterate through
                List <ColumnTuple> counterCoordinates = new List <ColumnTuple>();
                innerNodeIndex = 0;
                innerNodeList  = node.SelectNodes(Constant.Control.Counter);
                foreach (XmlNode innerNode in innerNodeList)
                {
                    // Add the value of each counter to the dataline
                    XmlNodeList dataNode = innerNode.SelectNodes(Constant.DatabaseColumn.Data);
                    // System.Diagnostics.Debug.Print("Counter: " + counterControlNames[innerNodeIndex] + " | " + dataNode[0].InnerText);
                    columnsToUpdate.Add(new ColumnTuple(counterControlNames[innerNodeIndex], dataNode[0].InnerText));

                    // For each counter, find the points associated with it and compose them together as x1,y1|x2,y2|...|xn,yn
                    XmlNodeList pointNodeList = innerNode.SelectNodes(Constant.DatabaseColumn.Point);
                    string      countercoord  = String.Empty;
                    foreach (XmlNode pointNode in pointNodeList)
                    {
                        String x = pointNode.SelectSingleNode(Constant.DatabaseColumn.X).InnerText;
                        if (x.Length > 5)
                        {
                            x = x.Substring(0, 5);
                        }
                        String y = pointNode.SelectSingleNode(Constant.DatabaseColumn.Y).InnerText;
                        if (y.Length > 5)
                        {
                            y = y.Substring(0, 5);
                        }
                        countercoord += x + "," + y + "|";
                    }

                    // Remove the last "|" from the point list
                    if (!String.IsNullOrEmpty(countercoord))
                    {
                        countercoord = countercoord.Remove(countercoord.Length - 1); // Remove the last "|"
                    }

                    // Countercoords will have a list of points (possibly empty) with each list entry representing a control
                    counterCoordinates.Add(new ColumnTuple(counterControlNames[innerNodeIndex], countercoord));
                    innerNodeIndex++;
                }

                // add this image's updates to the update lists
                ColumnTuplesWithWhere imageToUpdate = new ColumnTuplesWithWhere(columnsToUpdate);
                // Since Timelapse1 didn't have relative paths, we only need to set Where using the image filename
                // imageToUpdate.SetWhere(currentFolderName, null, imageFileName); //<- replaced by the simpler SetWhere form below
                if (File.Exists(Path.Combine(Path.GetDirectoryName(filePath), imageFileName)))
                {
                    imageToUpdate.SetWhere(imageFileName);
                    imagesToUpdate.Add(imageToUpdate);
                    ColumnTuple ColumnTupleFileName = new ColumnTuple(Constant.DatabaseColumn.File, imageFileName);

                    // We have to do the markers later, as we need to get the ID of the matching filename from the data table,
                    // and use that to set the markers.
                    Tuple <string, List <ColumnTuple> > filenameMarkerTuple = new Tuple <string, List <ColumnTuple> >(imageFileName, counterCoordinates);
                    fileNamesMarkersList.Add(filenameMarkerTuple);
                    successCounter++;
                }
                else
                {
                    skippedCounter++;
                }
            }

            // batch update the data table
            imageDatabase.UpdateFiles(imagesToUpdate);

            // Now that we have updated the data table, we can update the markers.
            // We retrieve the ID of the filename associated with the markers from the data table,
            // and use that to set the correct row in the marker table.
            foreach (Tuple <string, List <ColumnTuple> > tuple in fileNamesMarkersList)
            {
                long id = imageDatabase.GetIDFromDataTableByFileName(tuple.Item1);
                ColumnTuplesWithWhere markerToUpdate = new ColumnTuplesWithWhere(tuple.Item2, id);
                markersToUpdate.Add(markerToUpdate);
                imageDatabase.UpdateMarkers(markersToUpdate);
            }

            if (reader != null)
            {
                reader.Dispose();
            }
            return(new Tuple <int, int>(successCounter, skippedCounter));
        }
Exemplo n.º 5
0
        // Populate the database with the metadata for the selected note field
        private async Task <ObservableCollection <KeyValuePair <string, string> > > PopulateAsync(bool?metadataExtractorRBIsChecked)
        {
            // This list will hold key / value pairs that will be bound to the datagrid feedback,
            // which is the way to make those pairs appear in the data grid during background worker progress updates
            ObservableCollection <KeyValuePair <string, string> > keyValueList = new ObservableCollection <KeyValuePair <string, string> >();

            return(await Task.Run(() =>
            {
                // For each row in the database, get the image filename and try to extract the chosen metadata value.
                // If we can't decide if we want to leave the data field alone or to clear it depending on the state of the isClearIfNoMetadata (set via the checkbox)
                // Report progress as needed.
                // This tuple list will hold the id, key and value that we will want to update in the database
                string dataLabelToUpdate = this.dataLabelByLabel[this.dataFieldLabel];
                List <ColumnTuplesWithWhere> imagesToUpdate = new List <ColumnTuplesWithWhere>();
                TimeZoneInfo imageSetTimeZone = this.fileDatabase.ImageSet.GetSystemTimeZone();
                int percentDone = 0;

                double totalImages = this.fileDatabase.CountAllCurrentlySelectedFiles;
                Dictionary <string, ImageMetadata> metadata = new Dictionary <string, ImageMetadata>();
                for (int imageIndex = 0; imageIndex < totalImages; ++imageIndex)
                {
                    // Provide feedback if the operation was cancelled during the database update
                    if (Token.IsCancellationRequested == true)
                    {
                        keyValueList.Clear();
                        keyValueList.Add(new KeyValuePair <string, string>("Cancelled", "No changes were made"));
                        return keyValueList;
                    }

                    ImageRow image = this.fileDatabase.FileTable[imageIndex];
                    if (metadataExtractorRBIsChecked == true)
                    {   // MetadataExtractor specific code
                        metadata = ImageMetadataDictionary.LoadMetadata(image.GetFilePath(this.fileDatabase.FolderPath));
                    }
                    else
                    {
                        // ExifTool specific code - note that we transform results into the same dictionary structure used by the MetadataExtractor
                        string[] tags = { this.metadataFieldName };
                        metadata.Clear();
                        Dictionary <string, string> exifData = this.exifTool.FetchExifFrom(image.GetFilePath(this.fileDatabase.FolderPath), tags);
                        if (exifData.ContainsKey(tags[0]))
                        {
                            metadata.Add(tags[0], new Timelapse.Util.ImageMetadata(String.Empty, tags[0], exifData[tags[0]]));
                        }
                    }

                    if (this.ReadyToRefresh())
                    {
                        percentDone = Convert.ToInt32(imageIndex / totalImages * 100.0);
                        this.Progress.Report(new ProgressBarArguments(percentDone, String.Format("{0}/{1} images. Processing {2}", imageIndex, totalImages, image.File), true, false));
                        Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime);  // Allows the UI thread to update every now and then
                    }

                    if (metadata.ContainsKey(this.metadataFieldName) == false)
                    {
                        if (this.clearIfNoMetadata)
                        {
                            // Clear the data field if there is no metadata...
                            if (dataLabelToUpdate == Constant.DatabaseColumn.DateTime)
                            {
                                image.SetDateTimeOffsetFromFileInfo(this.fileDatabase.FolderPath);
                                imagesToUpdate.Add(image.GetDateTimeColumnTuples());
                                keyValueList.Add(new KeyValuePair <string, string>(image.File, "No metadata found - date/time reread from file"));
                            }
                            else
                            {
                                List <ColumnTuple> clearField = new List <ColumnTuple>()
                                {
                                    new ColumnTuple(this.dataLabelByLabel[this.dataFieldLabel], String.Empty)
                                };
                                imagesToUpdate.Add(new ColumnTuplesWithWhere(clearField, image.ID));
                                keyValueList.Add(new KeyValuePair <string, string>(image.File, "No metadata found - data field is cleared"));
                            }
                        }
                        else
                        {
                            keyValueList.Add(new KeyValuePair <string, string>(image.File, "No metadata found - data field remains unaltered"));
                        }

                        continue;
                    }

                    string metadataValue = metadata[this.metadataFieldName].Value;
                    ColumnTuplesWithWhere imageUpdate;
                    if (dataLabelToUpdate == Constant.DatabaseColumn.DateTime)
                    {
                        if (DateTimeHandler.TryParseMetadataDateTaken(metadataValue, imageSetTimeZone, out DateTimeOffset metadataDateTime))
                        {
                            image.SetDateTimeOffset(metadataDateTime);
                            imageUpdate = image.GetDateTimeColumnTuples();
                            keyValueList.Add(new KeyValuePair <string, string>(image.File, metadataValue));
                        }
                        else
                        {
                            keyValueList.Add(new KeyValuePair <string, string>(image.File, String.Format("'{0}' - data field remains unaltered - not a valid date/time.", metadataValue)));
                            continue;
                        }
                    }
                    else
                    {
                        imageUpdate = new ColumnTuplesWithWhere(new List <ColumnTuple>()
                        {
                            new ColumnTuple(dataLabelToUpdate, metadataValue)
                        }, image.ID);
                        keyValueList.Add(new KeyValuePair <string, string>(image.File, metadataValue));
                    }
                    imagesToUpdate.Add(imageUpdate);
                }
                this.IsAnyDataUpdated = true;
                this.Progress.Report(new ProgressBarArguments(100, String.Format("Writing metadata for {0} files. Please wait...", totalImages), false, true));
                Thread.Sleep(Constant.ThrottleValues.RenderingBackoffTime);  // Allows the UI thread to update every now and then
                this.fileDatabase.UpdateFiles(imagesToUpdate);
                return keyValueList;
            }, this.Token).ConfigureAwait(true));
        }