var Logging = true; // The OnInit function is called by Directory Opus to initialize the script add-in function OnInit(initData) { //uid added via script wizard (do not change after publishing this script) var uid = "F896BF3F-5CF2-4EBA-91C0-16E49B9D0808"; // Provide basic information about the script by initializing the properties of the ScriptInitData object initData.name = "Command.Image.ImageLocateAdvanced"; initData.desc = "Advanced functionality of locating images by gps exif information in google earth and windows 10 uwp maps"; initData.copyright = "(c) 2021 Felix Froemel"; initData.version = "1.0"; initData.url = "https://resource.dopus.com/t/command-imagelocateadvanced/38030"; initData.default_enable = true; var cfg = new ConfigHelper(initData); cfg.add("EnableLogging", true).des("Enable script output."); // Create a new ScriptCommand object and initialize it to add the command to Opus var cmd = initData.AddCommand(); cmd.name = "ImageLocatedAdvanced"; cmd.method = "ImageLocatedAdvancedInit"; cmd.desc = initData.desc; cmd.label = "Image Locate Advanced Command for Google Earth and Windows 10 Maps App"; cmd.template = "PROVIDER/K,NOTHUMBNAILEXTRACTION/S,THUMBNAILSCALE/K,MAPSTYLE/K"; } //Entry method for command call function ImageLocatedAdvancedInit(scriptCmdData) { InstallPolyfills(); Logging = Script.config["EnableLogging"]; var COMMAND_FAILED = true; var COMMAND_OK = false; var source = scriptCmdData.func.sourcetab; var selectedFiles = source.selected_files; if(selectedFiles == 0) { Log("No files selected"); return COMMAND_FAILED; } var args = scriptCmdData.func.args; var provider = args.provider; if(provider) { if(provider == "GoogleEarth") { var extractThumbnails = args.nothumbnailextraction ? false : true; var thumnailScale = args.thumbnailscale || 2; if(LocateGoogleEarth(scriptCmdData, extractThumbnails, thumnailScale)) return COMMAND_OK; return COMMAND_FAILED; } else if(provider == "WindowsMaps") { var mapStyle = args.mapstyle || "3d"; if(mapStyle.toUpperCase() != "3D" && mapStyle.toUpperCase() != "A" && mapStyle.toUpperCase() != "R") mapStyle = "3d"; if(LocateBingMaps(scriptCmdData, mapStyle)) return COMMAND_OK; return COMMAND_FAILED; } } else { Log("Supplied provider is unknown, use either 'GoogleEarth' or 'WindowsMaps'"); return COMMAND_FAILED; } } // Process files so that a kml file is created (either in current dir or temp if dir is collection) // if setting for thumbnails is enabled extract exif thumbnail from image, save to /KML Thumbnails (same folder as kml file) // open generated kml file, Google Earth either has to be installed or a file association for kml has to be manually created //https://developers.google.com/kml/documentation/kml_tut#basic_kml function LocateGoogleEarth(scriptCmdData, extractThumbnail, thumbnailScale) { var sourceTab = scriptCmdData.func.sourcetab; var kmlRoot = ""; if(String(sourceTab.path).startsWith("coll:")) kmlRoot = DOpus.FSUtil.resolve("/temp"); else kmlRoot = sourceTab.path; var kmlName = sourceTab.path.stem + " " + DOpus.Create().Date().Format("D#yyyy-MM-dd T#HH-mm-ss") + ".kml"; var kmlPath = kmlRoot + "\\" + kmlName; var fso = new ActiveXObject("Scripting.FileSystemObject"); var xmlWriter = fso.CreateTextFile(kmlPath, true); xmlWriter.WriteLine(""); xmlWriter.WriteLine(""); xmlWriter.WriteLine( "\t"); //handle files with gps information, write data into kml file / placemark nodes var gpsProcessor = function(item, lat, lng, gpsCount) { xmlWriter.WriteLine("\t\t"); xmlWriter.WriteLine("\t\t\t" + item.name + ""); if(extractThumbnail) if(ExtractExifThumbnail(item, kmlRoot, scriptCmdData)) //Extract thumbnail xmlWriter.WriteLine("\t\t\t"); xmlWriter.WriteLine("\t\t\t]]>"); xmlWriter.WriteLine("\t\t\t"); xmlWriter.WriteLine("\t\t\t\t" + lng + "," + lat + ""); //a bit weird, "A Point that specifies the position of the Placemark on the Earth's surface (longitude, latitude, and optional altitude)" xmlWriter.WriteLine("\t\t\t"); xmlWriter.WriteLine("\t\t"); }; var processResult = ProcessFiles(scriptCmdData, gpsProcessor, "Google Earth"); xmlWriter.WriteLine("\t"); xmlWriter.WriteLine(""); xmlWriter.Close(); if(processResult > 0) { //Open generated kml file //Google Earth either has to be installed or a file association for kml has to be manually created DOpus.Create.Command.RunCommand(kmlPath); return true; // Success } else if(processResult == -1) { /* Maybe just kml should be deleted or be controlled by settings Log("Deleting kml file + thumbfolder"); var cmd = DOpus.Create.Command(); cmd.SetSource(thumbFolderParentPath); cmd.AddLine("DELETE " + kmlName + "); cmd.AddLine("DELETE KML Thumbnails"); cmd.Run(); */ } return false; } // Process files so that a parameter string with name + (lat,lng) is passed to win10 uwp maps app function LocateBingMaps(scriptCmdData, mapStyle) { var sourceTab = scriptCmdData.func.sourcetab; var collectionPoints = ""; //collection=name.My%20Trip%20Stops~point.36.116584_-115.176753_Las%20Vegas~point.37.8268_-122.4798_Golden%20Gate%20Bridge var maxGpsCountPerTab = 900;//Might be more possible, max is between 900 - 1000, this is not documented; might also be a max char count var totalGpsCount = 0; var subGpsCount = 0;//if more than 900 tags var startMaps = function(gpsCount) { var collectionName = sourceTab.path.stem + " (" + gpsCount + " images with GPS tag)"; StartWindowsMapsApp(collectionName, collectionPoints, mapStyle); } //handle files with gps information var gpsProcessor = function(item, lat, lng, gpsCount) { totalGpsCount++; subGpsCount++; collectionPoints += "~point." + lat + "_" + lng + "_" + item.name; if(gpsCount % 900 == 0) { startMaps(subGpsCount); collectionPoints = ""; subGpsCount = 0; } }; var processResult = ProcessFiles(scriptCmdData, gpsProcessor, "Windows Maps"); if(processResult > 0) //count of gps tags { startMaps(subGpsCount); return true; // Success } return false; } // enumerate selected files, extract gps information and process files by callback // returns count of found gps tags function ProcessFiles(scriptCmdData, gpsProcessor, processTitle) { var sourceTab = scriptCmdData.func.sourcetab; var selectedFiles = sourceTab.selected_files; var progress = scriptCmdData.Func.Command.Progress; progress.abort = progress.pause = true; progress.Init(sourceTab, "ImageLocateAdvanced - " + processTitle); progress.SetStatus("0/" + selectedFiles.count + " files"); progress.SetFiles(selectedFiles.count); DOpus.Delay(200); progress.Show(); var fileCount = 0; var gpsCount = 0; var fsItems = new Enumerator(selectedFiles); while (!fsItems.atEnd()) { var abortState = progress.GetAbortState(); if (abortState == "a") { return -1; } else if (abortState == "p") { DOpus.Delay(500); continue; } var currentItem = fsItems.item(); progress.SetName(currentItem.name); if (!currentItem.is_dir) { progress.SetName(currentItem.name); var fileMeta = currentItem.metadata; if (fileMeta == "image") { var imageMeta = fileMeta.image; var lat = imageMeta["latitude"]; var lng = imageMeta["longitude"]; if(lat == "" || typeof(lat) === 'undefined' || lng == "" || typeof(lng) === 'undefined') { Log("Error with gps information " + currentItem.name); } else { gpsCount++; gpsProcessor(currentItem, lat, lng, gpsCount); } } } fsItems.moveNext(); progress.SetFilesProgress(fileCount++); progress.SetStatus(fileCount + "/" + selectedFiles.count + " files, GPS tags: " + gpsCount); } return gpsCount; } // Tries to extract the thumbnail from exif data and stores it into subfolder // returns true if successfull // Idea taken from https://www.bram.us/2016/02/18/read-exif-thumbnail-from-jpg-image-using-javascript/ // http://gvsoft.no-ip.org/exif/exif-explanation.html#ExifMarker function ExtractExifThumbnail(file, thumbFolderParentPath, scriptCmdData) { var sourceTab = scriptCmdData.func.sourcetab; var path = file.realpath; var thumbFolderName = "KML Thumbnails"; var thumbFolderPath = thumbFolderParentPath + "\\" + thumbFolderName; if(!DOpus.FSUtil.Exists(thumbFolderPath)) { var cmd = DOpus.Create.Command(); cmd.SetSource(thumbFolderParentPath); Log(thumbFolderPath); cmd.RunCommand("CreateFolder \"" + thumbFolderName + "\""); } if(DOpus.FSUtil.Exists(thumbFolderPath + "\\" + file.name)) { Log("Thumbnail " + file.name + " already exists"); return true; } var imageFile = file.Open(); var imageBlob = DOpus.Create.Blob(); if (imageFile.error != 0 || (imageFile.size.Compare(0) != 0 && imageFile.Read(imageBlob) <= 0)) { Log("ERROR reading source image " + file.path); imageFile.Close(); imageBlob.Free(); return; } var startExifTagBlob = DOpus.Create.Blob(0xFF,0xE1); var startThumbTagBlob = DOpus.Create.Blob(0xFF,0xD8); var endThumbTagBlob = DOpus.Create.Blob(0xFF,0xD9); var exifStart = imageBlob.Find(startExifTagBlob); if(exifStart != -1) { var thumbStart = imageBlob.Find(startThumbTagBlob, exifStart); if(thumbStart != -1) { var thumbEnd = imageBlob.Find(endThumbTagBlob, thumbStart); if(thumbEnd != -1) { var thumbBlob = DOpus.Create.Blob(); var size = thumbEnd - thumbStart; thumbBlob.CopyFrom(imageBlob, 0, thumbStart, size); //blob, to, from, size var thumbFile = DOpus.FSUtil.OpenFile(thumbFolderPath + "\\" + file.name, "wc", sourceTab); if (thumbFile.error != 0) { Log("ERROR opening thumb file " + thumbFolderPath + "\\" + file.name + ", probably already existing"); thumbFile.Close(); imageFile.Close(); startExifTagBlob.Free(); startThumbTagBlob.Free(); endThumbTagBlob.Free(); imageBlob.Free(); return false; } var writtenSize = thumbFile.Write(thumbBlob); if (thumbBlob.size.Compare(writtenSize) != 0) { Log("ERROR writing thumb file " + thumbFolderPath + "\\" + file.name + ", size = " + size + ", written size = " + writtenSize); thumbFile.Close(); imageFile.Close(); startExifTagBlob.Free(); startThumbTagBlob.Free(); endThumbTagBlob.Free(); imageBlob.Free(); return false; } else { Log("SUCCESS extracting exif thumb (EXIF start: " + exifStart + ", thumb start: " + thumbStart + ", thumb end: " + thumbEnd + ", size: " + size + ")"); thumbFile.Close(); imageFile.Close(); startExifTagBlob.Free(); startThumbTagBlob.Free(); endThumbTagBlob.Free(); imageBlob.Free(); return true; } } else { Log("ERROR End of thumb not found " + path + ", but exif: " + exifStart + " and thumb start: " + + thumbStart); imageFile.Close(); startExifTagBlob.Free(); startThumbTagBlob.Free(); endThumbTagBlob.Free(); imageBlob.Free(); return false; } } else { Log("ERROR Start of thumb not found " + path + ", but exif: " + exifStart); imageFile.Close(); startExifTagBlob.Free(); startThumbTagBlob.Free(); endThumbTagBlob.Free(); imageBlob.Free(); return false; } } else { Log("ERROR Start of EXIF not found " + path); imageFile.Close(); startExifTagBlob.Free(); startThumbTagBlob.Free(); endThumbTagBlob.Free(); imageBlob.Free(); return false; } return false; } //Start the maps uwp app (is a bit more complicated than opening a normal exe file) //Pass the arguments containing file name + location to maps function StartWindowsMapsApp(collectionName, collectionPoints, mapStyle) { //https://docs.microsoft.com/en-us/windows/uwp/launch-resume/launch-maps-app#bingmaps-param-reference var cmd = "shell:appsFolder\\Microsoft.WindowsMaps_8wekyb3d8bbwe!App bingmaps:?collection=name."; var params = new String(collectionName + collectionPoints + "&sty=" + mapStyle); //var params = new String(collectionName + collectionPoints + "&sty=3d"); //3d map style, "a"/"r"/"3d" cmd += params; //DOpus.NewCommand.RunCommand("Clipboard SET " + cmd); Log(cmd); Log("Starting Windows 10 UWP Maps App"); DOpus.NewCommand.RunCommand(cmd); } // Install string.startsWith, string.replaceAll function InstallPolyfills() { //https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position) { position = position || 0; return this.indexOf(searchString, position) === position; }; } /** * String.prototype.replaceAll() polyfill * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ * @author Chris Ferdinandi * @license MIT */ if (!String.prototype.replaceAll) { String.prototype.replaceAll = function(str, newStr) { // If a regex pattern if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') { return this.replace(str, newStr); } // If a string return this.replace(new RegExp(str, 'g'), newStr); }; } } function ConfigHelper(data){ //v1.2 var t=this; t.d=data; t.c=data.config; t.cd=DOpus.Create.Map(); t.add=function(name, val, des){ t.l={n:name,ln:name. toLowerCase()}; return t.val(val).des(des);} t.des=function(des){ if (!des) return t; if (t.cd.empty) t.d.config_desc=t.cd; t.cd(t.l.n)=des; return t;} t.val=function(val){ var l=t.l; if (l.v!==l.x&&typeof l.v=="object") l.v.push_back(val);else l.v=t.c[l.n]=val;return t;} t.trn=function(){return t.des(t("script.config."+t.l.ln));} } function Log(msg) { if(Logging) DOpus.Output("Image Locate Advanced: " + String(msg)); } /////////////////////////////////////////////////////////////////////////////// function OnAboutScript(data){ //v0.1 var cmd = DOpus.Create.Command(); if (!cmd.Commandlist('s').exists("ScriptWizard")){ if (DOpus.Dlg.Request("The 'ScriptWizard' add-in has not been found.\n\n"+ "Install 'ScriptWizard' from [resource.dopus.com].\nThe add-in enables this dialog and also offers "+ "easy updating of scripts and many more.","Yes, take me there!|Cancel", "No About.. ", data.window)) cmd.RunCommand('http://resource.dopus.com/viewtopic.php?f=35&t=23179');} else cmd.RunCommand('ScriptWizard ABOUT WIN='+data.window+' FILE="'+Script.File+'"'); } //MD5 = "8008a0c21348cae44de89c2f2d5d36ea"; DATE = "2021.03.09 - 20:58:47"