TL;DR
- Builtin command
Image LOCATE=googleearth
locates the image using Google Earth (works only for one image) - New Internal Command for enumerating gps tags in selected images
- passes them over to windows 10 mapps app or google earth
- ability to extract thumbnails from exif and using them in google earth instead of pins (still buggy yet)
- in Google Earth a click on the location will show the corresponding image in large
About
This script adds the new internal command ImageLocateAdvanced
referring to the built-in command Image LOCATE
, which is yet only able to locate one image using google earth (see here https://resource.dopus.com/t/image-locate-googleearth/37961/10), but this will be fixed in future versions.
But this script is not only for flattening this current flaw but also using thumbnails of those images as well as for adding support for the Windows Maps app which is shipped with Windows 10.
Usage
The command enumerates the selected images in the source path (not in subdirectories yet). Use one of the following commands into a button like this (this is the Google Earth default behaviour; adjust the path for the icon according to your installation path of Google Earth)
<?xml version="1.0"?>
<button backcol="none" display="both" label_pos="right" textcol="none">
<label>Locate in Google Earth</label>
<icon1>C:\Program Files\Google Earth\googleearth.exe,0</icon1>
<function type="normal">
<instruction>ImageLocateAdvanced Provider=GoogleEarth</instruction>
</function>
</button>
Google Earth
-
ImageLocateAdvanced Provider=GoogleEarth
This command will enumerate the selected images, and write the name of the image togehter with its gps position into an kml file. If the images are in a collection, the kml will be created in %TEMP%, otherwise in the same folder. Also the path of the image will be written to the kml, so that a click on the position in Google Earth will show the image in large (inside of GE).
For this google earth has to be installed or the kml file has to be associated with google earth in windows settings.
Default is that the script will try to extract the thumbnail from the exif header and put it into a subfolder, so that GE can use them instead of the default pins. The thumbnail approach is for performance improvements of GE (instead of using the original image).
You can also click on items (no matter if thumbnails have been created or not) so that the image becomes shown in large on the map.
Optional -
ImageLocateAdvanced Provider=GoogleEarth NoThumbnailExtraction
use to turn off default behaviour so that no thumbnails will be created or used.
-
ImageLocateAdvanced Provider=GoogleEarth ThumbnailScale=1.5
scaling of the thumbnailpins (please use the american representation of floats with a '.'), can only be used whenNoThumbnailExtraction
is not set.
Windows Maps App
-
ImageLocateAdvanced Provider=WindowsMaps
The maps app will start, open a new tab with the name of the current folder of DOpus and the count of images containing GPS tags and display those tags with the corresponding file name on the map and it does some kind of geocoding. Default map style forced by script is sattelite ("3d").
Optional
-
ImageLocateAdvanced Provider=WindowsMaps MapStyle=R
default is 3d; 3d for 3D satellite maps, A for Aerial and R for roads
Known Issues
The extraction of the thumbnails from the exif header does not allways work correctly since the exif header is not really parsed. Instead the script is looking for
the hex values of the start tag of the exif header (0xFF,0xE1) and if found looking for start (0xFF,0xD8) and end tag of the thumbnail (0xFF,0xD9) and copying the values in between as image.
There seems to be a maximium amount of locations or amount of chars that can be passed over to windows maps (the data is passed as an argument string). but this is not documented (or I just didnt found it).
For me there seemed to be a maximum somewhere around 900 so that they were not shown in the app. So right now the script cuts data larger > 900 items into several batches, not sure though if it works perfectly.
Do you have ideas, improvements or want to help?
Let me know, I could need some help with correct exif parsing and a prettier preview of the images, but im not so much into fancy web coding (which is needed in the CDATA part).
Installation:
To install the command, download the *.js.txt file below and drag it to Preferences / Toolbars / Scripts or copy code to "%USERPROFILE%\AppData\Roaming\GPSoftware\Directory Opus\Script AddIns" as "Command.Image.LocateAdvanced.js"
Download
- Version 1.01 (2021-03-29) - Mean typo fixed so that the command now is actually 'ImageLocateAdvanced' as inteded (in v1.0 it was ImageLocatedAdvanced) Command.Image.LocateAdvanced.js.txt (31.1 KB)
- Version 1.0 (2021-03-09) - Inital Version Command.Image.LocateAdvanced.js.txt (31.2 KB)
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.01";
initData.url = "https://resource.dopus.com/t/internal-command-sync-dopus-settings-to-git/38026";
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 = "ImageLocateAdvanced";
cmd.method = "ImageLocateAdvancedInit";
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 ImageLocateAdvancedInit(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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xmlWriter.WriteLine("<kml xmlns=\"http://earth.google.com/kml/2.0\">");
xmlWriter.WriteLine( "\t<Document>");
//handle files with gps information, write data into kml file / placemark nodes
var gpsProcessor = function(item, lat, lng, gpsCount)
{
xmlWriter.WriteLine("\t\t<Placemark>");
xmlWriter.WriteLine("\t\t\t<name>" + item.name + "</name>");
if(extractThumbnail)
if(ExtractExifThumbnail(item, kmlRoot, scriptCmdData)) //Extract thumbnail
xmlWriter.WriteLine("\t\t\t<Style><IconStyle><Icon><href>KML Thumbnails/" + item.name + "</href></Icon><scale>" + thumbnailScale + "</scale></IconStyle></Style>");
xmlWriter.WriteLine("\t\t\t<description><![CDATA[" + item.name + "<img src=\"" + item.name + "\" style=\"width: 400px; height: 400px;\" >]]></description>");
xmlWriter.WriteLine("\t\t\t<Point>");
xmlWriter.WriteLine("\t\t\t\t<coordinates>" + lng + "," + lat + "</coordinates>"); //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</Point>");
xmlWriter.WriteLine("\t\t</Placemark>");
};
var processResult = ProcessFiles(scriptCmdData, gpsProcessor, "Google Earth");
xmlWriter.WriteLine("\t</Document>");
xmlWriter.WriteLine("</kml>");
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(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 = "561a230e53dd37d7580a5d4e46c0e1c4"; DATE = "2021.03.29 - 13:08:46"