Command: ImageLocateAdvanced

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 when NoThumbnailExtraction 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

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"

3 Likes

Thanks. I've been enjoying the previous button using the Windows Maps UWP. I don't mean to be dense, but assuming I've installed Google Earth, how would I go about initiating this mapping? Is there a button?

No worries, I should have stated that out in the documentation. In fact it got a lot easier than in the previous version so a button for the default Google Earth functionality now looks like this

<?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>

Adjust the path for the Google Earth executable path for the button icon according to your installation path, mine might be different, or use another icon. You can also use the parameters NoThumbnailExtraction and ThumbnailScale as stated out above.
Be aware that the command looks for images in the source tab and not in subdirs yet.
Nice to see that someone enjoys this functionality as I do :sunglasses:

1 Like

Thanks! Sorry for the delay, I've been busy :slight_smile:

FYI, there was a typo in the button xml.

ImageLocateAdvanced should be ImageLocatedAdvanced

Just in case anyone else wants to take advantage.

Thanks for noticing and letting me know, the typo was actually in the script method and command configuration instead of the buttons since it was inspired by the internal command Image LOCATE. I fixed this now in v 1.01.

1 Like