/*                                                              
@@@BUILDINFO@@@ MeasurementScaleMarker.jsx 1.0.0.8                                                             
*/

/**
 * Build a measurement scale marker for the current document.
 * The document must have a measurement scale.
 */

// enable double clicking from the Macintosh Finder or the Windows Explorer
#target photoshop
// in case we double clicked the file
app.bringToFront();

// on localized builds we pull the $$$/Strings from a .dat file, see documentation for more details
$.localize = true;

// global for our result, send a 'cancel' back to Photoshop to not get a histor entry and to not get a recorded action
var returnResult = 'OK';

// Only run if we're in a supported version of Photoshop
// Note, removing this will not make the script work, as it depends on
// core application code, it just makes it fail better.
if (!app.featureEnabled("Measurement")) {
	if (app.playbackDisplayDialogs != DialogModes.NO)
		{
		Window.alert (localize ("$$$/JavaScripts/MeasurementScaleMarker/CommandNotAvaiable=The Measurement Scale Marker feature is not available in this version of Photoshop."));
		returnResult = 'cancel';
		}
	else
		throw( new Error(localize("$$$/JavaScripts/MeasurementScaleMarker/CommandNotAvaiable=The Measurement Scale Marker feature is not available in this version of Photoshop.")));
}

else {
	// Suspend the history for this script, so that we show it as a single, atomic
	// operation that can be undone/redone in one click, and only shows a single
	// entry in the History palette.
	try {
		app.activeDocument.suspendHistory(localize("$$$/JavaScripts/MeasurementScaleMarker/HistoryPaletteLabel=Place Measurement Scale Marker"), "makeScaleMarker();");
	} 
	catch(e) {
		if ( confirm( localize( "$$$/JavaScripts/MeasurementScaleMarker/Sorry=Sorry, something major happened and I can't continue! Would you like to see more info?" ) ) )
			alert( e + " : " + e.line );
		returnResult = 'cancel';
	}
}

// if we pass back 'cancel' to Photoshop we will not get a history entry or a recorded event in the actions palette
returnResult;

/** The main function */
function makeScaleMarker() {
	try {
		keyLength = app.stringIDToTypeID("measurementScaleMarkerLength");
		keyDisplayText = app.stringIDToTypeID("measurementScaleMarkerDisplayText");
		keyTextPositionBottom = app.stringIDToTypeID("measurementScaleMarkerTextPositionBottom");
		keyColor = app.stringIDToTypeID("measurementScaleMarkerColor");
		keyLocationBottom = app.stringIDToTypeID("measurementScaleMarkerLocationBottom");
		keyLocationLeft = app.stringIDToTypeID("measurementScaleMarkerLocationLeft");
		keyDontRecord = app.stringIDToTypeID("dontRecord");

		scriptGUID = "359FE506-CB49-4d05-9646-B26C99789E1F";
		settings = new Object();

		deleteExistingMarkers = false;

		getScaleValues();
		setupInitialValues();

		if (checkForExistingScaleMarkers() && doDialog()) {
		
			// Make sure we are valid (particularly for scripting)
			if (settings.scaleMarkerLength <= 0.0)
				throw new Error (localize("$$$/JavaScripts/MeasurementScaleMarker/Alert/InvalidLength=Invalid length entered - only positive real numbers are allowed."));
				
			// Clear any document selections so we don't try to do any operations
			// in the selection
			app.activeDocument.selection.deselect();

			deleteExistingScaleMarkers();
			determineLocations();

			// Determine the actual scale marker length we'll use, based on the
			// incremental number of logical units to use, using the logical length
			// of the measurement scale.  This ensures that we will always have a
			// visible, and useful scale marker length.  
			scaleMarkerActualLength = settings.scaleMarkerLength * logicalLength;

			var	layerGroup = createLayerGroup();
			var textLayer = createText(layerGroup);
			var graphicLayer = createGraphic(layerGroup);

			// Select the layer group so user can easily move the whole thing 
			// around after, and so that the last layer we just created isn't the 
			// selected layer.
			app.activeDocument.activeLayer = layerGroup;

			// Stuff the parameters we used back into an ActionDescriptor so that we
			// can return them in case this script is being recorded.  Then set this
			// as the playbackParameters.
			var params = new ActionDescriptor();
			params.putDouble(keyLength, settings.scaleMarkerLength);
			params.putBoolean(keyDisplayText, settings.displayText);
			params.putBoolean(keyTextPositionBottom, settings.textPositionBottom);
			params.putString(keyColor, settings.markerColor.rgb.hexValue);
			// We do not pass back the location when recording a script, because it
			// is dependent on the size of the document and thus a fixed point will
			// not be useful in a recorded script.  User may explicitly pick a
			// location though when writing a script that places a scale marker.  Or
			// if you do want to record the location, you can enable the next two lines.
			app.playbackParameters = params;

			// Save off any settings the user made for the next time
			var d = objectToDescriptor(settings, settingsObjectToDescriptorValues);
			app.putCustomOptions(scriptGUID, d);

			return;
		}
	} 
	// Lot's of things can go wrong
	// Give a generic alert and see if they want the details
	catch( e ) {
		if ( confirm( localize( "$$$/JavaScripts/MeasurementScaleMarker/Sorry=Sorry, something major happened and I can't continue! Would you like to see more info?" ) ) ) {
			alert( e + " : " + e.line );
		}
		returnResult = 'cancel';
	}

	// If we're here, we either got an error, or the user cancelled
	// set the global variable and then return it so suspendHistory gets undone
	returnResult = 'cancel';
	
	return returnResult;
	
}

/**
 * Get our scale values.  Values are put into global vars.
 */
function getScaleValues() {
	var scale = app.activeDocument.measurementScale
	pixelLength = scale.pixelLength;
	logicalLength = scale.logicalLength;
	logicalUnits = scale.logicalUnits;
}

/**
 * Set up the initial values for various user settings.  These are defined as
 * globals.
 */
function setupInitialValues() {
	var doc = app.activeDocument;
	// Define common strings
	layerGroupName = localize("$$$/JavaScripts/MeasurementScaleMarker/LayerSetName=Measurement Scale Marker");
	dialogTitle = localize("$$$/JavaScripts/MeasurementScaleMarker/Dialogs/Title=Measurement Scale Marker");

	// Define the font to use - this will be localized appropriately.  Note that
	// the font is the same on Mac and Win for English, but not necessarily for
	// other languages.
	if ( "Windows" == File.fs )
		textFontName = localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Windows=Times New Roman");
	else
		textFontName = localize("$$$/JavaScripts/MeasurementScaleMarker/TextFont/Mac=Times New Roman");

	// Setup our standard/default values - most recalculated later
	settings.scaleMarkerLength = 1;
	settings.displayText = true;

	settings.markerColor = new SolidColor;
	settings.markerColor.model = ColorModel.RGB;
	settings.markerColor.rgb.red = 0;
	settings.markerColor.rgb.green = 0;
	settings.markerColor.rgb.blue = 0;

	scaleFactor = doc.resolution / 72;
	if (scaleFactor >= 2) {
		scaleFactor /= 2; // Scale the graphics by half
		textFontSize = UnitValue(8, "pt"); // Scale the text by 30%
	}
	else {
		textFontSize = UnitValue(10, "pt");
	}
	settings.textPositionBottom = true;
	textLocation = [new UnitValue(0, "px"), new UnitValue(0, "px")];
	textFontSize.baseUnit = UnitValue(1/doc.resolution, "in");
	textGraphicMargin = 5 * scaleFactor;	// Number of pixels horizontally between text and graphic
	graphicLeft = 0;
	graphicTop = 0;
	graphicHeight = 4 * scaleFactor;

	// Determine location base point that we calculate all other values from;
	// this is the bottom left point at which we position the scale marker
	var docWidth = doc.width;
	var docHeight = doc.height;
	docWidth.convert("px");
	docHeight.convert("px");
	baseBottom = Math.floor(docHeight.value * 0.95);
	baseLeft = Math.floor(docWidth.value * 0.05);

	// Look for last used params via Photoshop registry (getCustomOptions will 
	// throw if none exist) - set these up first in case parameters passed in
	// are incomplete.
	try {
		var d = app.getCustomOptions(scriptGUID);
		settings = descriptorToObject(d, settingsObjectFromDescriptorValues);
	}
	catch(e) {
		// if we don't have any options, or they're corrupted, continue with defaults
	}

	// Usa any parameters we had passed in (being driven by a script)
	var numberOfProvidedParams = 0;
	if (playbackParameters.count > 0) { 
		if (playbackParameters.hasKey(keyLength)) {
			settings.scaleMarkerLength = playbackParameters.getDouble(keyLength);
			++numberOfProvidedParams;
		}
		if (playbackParameters.hasKey(keyDisplayText)) {
			settings.displayText = playbackParameters.getBoolean(keyDisplayText);
			++numberOfProvidedParams;
		}
		if (playbackParameters.hasKey(keyTextPositionBottom)) {
			settings.textPositionBottom = playbackParameters.getBoolean(keyTextPositionBottom);
			++numberOfProvidedParams;
		}
		if (playbackParameters.hasKey(keyColor)) {
			settings.markerColor.rgb.hexValue = playbackParameters.getString(keyColor);
			++numberOfProvidedParams;
		}
		if (4 == numberOfProvidedParams) {
			// We have all the parameters that the dialog would allow adjusting,
			// so no need to show dialog
			if (app.playbackDisplayDialogs == DialogModes.ALL) {
				app.playbackDisplayDialogs = DialogModes.Error;
			}
		}

		// The location is only specified in script, we do not pass this back
		// when recording.
		if (playbackParameters.hasKey(keyLocationBottom)) {
			baseBottom = playbackParameters.getInteger(keyLocationBottom);
		}
		if (playbackParameters.hasKey(keyLocationLeft)) {
			baseLeft = playbackParameters.getInteger(keyLocationLeft);
		}
	}
}

/**
 * See if there is already a scale marker in the document and
 * what to do about it if so.
 * 
 * @return True if ok to proceed, false if not.
 */
function checkForExistingScaleMarkers() {
	var result = true;

	if (app.playbackDisplayDialogs != DialogModes.NO) {
		// Look for our named layer
		try {
			var layerSets = app.activeDocument.layerSets;
			var existingLayerSet = layerSets.getByName(layerGroupName);
			if (existingLayerSet) {
				var existingMarkerDialogDefinition =
					"dialog { text: '" + dialogTitle + "', orientation:'column', alignChildren:'fill', \
						alertText: StaticText { text:'$$$/JavaScripts/MeasurementScaleMarker/Alert/ExistingMarker=There are existing measurement scale marker(s).  Would you like to remove or keep these, when placing the new marker?^n^nNote: selecting Remove applies to locked scale marker layers as well.', properties:{multiline:true} }, \
						buttonGroup: Group { orientation:'row', alignment:'center', \
							removeButton: Button { text:'$$$/JavaScripts/MeasurementScaleMarker/Dialog/Remove=Remove', properties:{name:'ok'} }, \
							keepButton: Button { text:'$$$/JavaScripts/MeasurementScaleMarker/Dialog/Keep=Keep', properties:{name:'keep'} }, \
							cancelButton: Button { text:'$$$/JavaScripts/MeasurementScaleMarker/Dialog/Cancel=Cancel', properties:{name:'cancel'} } \
						} \
					}";
				var existingMarkerDialog = new Window(existingMarkerDialogDefinition);

				existingMarkerDialog.buttonGroup.keepButton.onClick = function() {
					existingMarkerDialog.close();
				}

				buttonPressed = existingMarkerDialog.show();
				if (1 == buttonPressed) {	// Remove/OK button
					deleteExistingMarkers = true;
				}
				else if (2 == buttonPressed) {	// Cancel
					// Abort out of the script altogether
					result = false;
				}
			}
		} 
		catch (e) {
			// No existing layer set
		}
	}

	return result;
}

function deleteExistingScaleMarkers() {
	if (deleteExistingMarkers) {
		try {
			var layerSets = app.activeDocument.layerSets;
			var existingLayerSet = layerSets.getByName(layerGroupName);
			do {
				existingLayerSet.allLocked = false;
				existingLayerSet.remove();   						
			} while (existingLayerSet = layerSets.getByName(layerGroupName));
		} 
		catch (e) { 
			// No more markers left to remove 
		}
	}
}

function validateLengthTextEdit (lengthEdit) {
	if (0 == lengthEdit.text.length) {
		alert(localize("$$$/JavaScripts/MeasurementScaleMarker/Alert/NoLength=No length entered - please enter a length, or cancel."));
		return false;
	}

	// Validate the value they've entered - make sure it's a number
	if ((/[^\d\.]+/.test(lengthEdit.text)) || (!/\d+/.test(lengthEdit.text)) || (/\.+.*\.+/.test(lengthEdit.text))) {
		alert(localize("$$$/JavaScripts/MeasurementScaleMarker/Alert/InvalidLength=Invalid length entered - only positive real numbers are allowed."));
		return false;
	}
	
	// Validate the value they've entered - make sure it is greater than zero
	if (lengthEdit.text <= 0.0) {
		alert(localize("$$$/JavaScripts/MeasurementScaleMarker/Alert/InvalidLength=Invalid length entered - only positive real numbers are allowed."));
		return false;
	}
	
	// See if the value entered is too big for the image/canvas
	var usableDocWidth = app.activeDocument.width.as("px") - (baseLeft * 2);	// account for padding on each side
	var pixelsPerLogicalUnit = pixelLength / logicalLength;
	var maxMarkerLength = Math.floor(usableDocWidth / pixelsPerLogicalUnit);
	if (maxMarkerLength < 1)
		maxMarkerLength = 1;

	if (lengthEdit.text > maxMarkerLength) {
		alert(localize("$$$/JavaScripts/MeasurementScaleMarker/Alert/LengthTooLong=The length you entered is too long for this document, it has been reset to the maximum length for this document."));
		lengthEdit.text = maxMarkerLength;
		return false;
	}

	return true;
}

/**
 * Show our dialog(s).  If we're running interactively, then the
 * options dialog will be shown.  If running scripted with no
 * dialogs, don't show the dialog.
 * 
 * @return The result of the dialog.
 */
function doDialog() {
	if ((app.playbackDisplayDialogs == DialogModes.NO) || (app.playbackDisplayDialogs == DialogModes.ERROR))
		return true;

	// Determine marker color radio button setting - anything other than white
	// will be inferred to be black - we shouldn't have anything other than
	// black or white here for the dialog, that can only be used via the
	// scripting interface.
	var textColorBlack = true;
	if ((settings.markerColor.rgb.red == 255) &&
		(settings.markerColor.rgb.green == 255) &&
		(settings.markerColor.rgb.blue == 255)) {
		textColorBlack = false;
	}

	var scaleMarkerDialogDefinition = 
		"dialog { text: '" + dialogTitle + "', orientation:'row', alignChildren:'fill', \
			settingsControlsGroup: Group { orientation:'column', alignChildren:'left', \
				lengthGroup: Group { orientation:'row', \
					lengthLabel: StaticText { text:'$$$/JavaScripts/MeasurementScaleMarker/LogicalUnitsLength=Length:' }, \
					lengthEdit: EditText { text:'" + settings.scaleMarkerLength + "', preferredSize:[80, 20] }, \
					lengthDescription: StaticText { text:\"x " + logicalLength + " " + logicalUnits + "\" } \
				}, \
				annotationsGroup: Group { orientation:'column', alignChildren:'left', \
					displayTextCB: Checkbox { text:'$$$/JavaScripts/MeasurementScaleMarker/DisplayText=Display Te&xt', value:" + settings.displayText.toString() + " }, \
					choicesGroup: Group { orientation:'row', alignChildren:'top', \
						textPositionPanel: Panel { text:'$$$/JavaScripts/MeasurementScaleMarker/TextPositionTitle=Text Position', orientation:'column', alignChildren:'left', \
							textBottomRB: RadioButton { text:'$$$/JavaScripts/MeasurementScaleMarker/TextPositionTitle/Bottom=Bottom', value:" + settings.textPositionBottom.toString() + " }, \
							textTopRB: RadioButton { text:'$$$/JavaScripts/MeasurementScaleMarker/TextPositionTitle/Top=Top', value:" + (!settings.textPositionBottom).toString() + " } \
						}, \
						markerColorPanel: Panel { text:'$$$/JavaScripts/MeasurementScaleMarker/ColorGroupTitle=Color', orientation:'column', alignChildren:'left', \
							colorBlackRB: RadioButton { text:'$$$/JavaScripts/MeasurementScaleMarker/Color/Black=Black', value:" + textColorBlack.toString() + " }, \
							colorWhiteRB: RadioButton { text:'$$$/JavaScripts/MeasurementScaleMarker/Color/White=White', value:" + (!textColorBlack).toString() + " } \
						} \
					} \
				} \
			}, \
			okCancelGroup: Group { orientation:'column',  alignChildren:'fill',\
				ok: Button { text:'$$$/JavaScripts/MeasurementScaleMarker/Dialog/OK=OK', properties:{name:'ok'} }, \
				cancel: Button { text:'$$$/JavaScripts/MeasurementScaleMarker/Dialog/Cancel=Cancel', properties:{name:'cancel'} } \
			} \
		}";


	var scaleMarkerDialog = new Window(scaleMarkerDialogDefinition);

	// Modify dialog behaviors
	scaleMarkerDialog.settingsControlsGroup.annotationsGroup.choicesGroup.textPositionPanel.enabled = scaleMarkerDialog.settingsControlsGroup.annotationsGroup.displayTextCB.value;
	
	scaleMarkerDialog.settingsControlsGroup.annotationsGroup.displayTextCB.onClick = function() {
		scaleMarkerDialog.settingsControlsGroup.annotationsGroup.choicesGroup.textPositionPanel.enabled = scaleMarkerDialog.settingsControlsGroup.annotationsGroup.displayTextCB.value;
	}

	scaleMarkerDialog.okCancelGroup.ok.onClick = function() {
		if (!validateLengthTextEdit (scaleMarkerDialog.settingsControlsGroup.lengthGroup.lengthEdit))
			scaleMarkerDialog.settingsControlsGroup.lengthGroup.lengthEdit.active = true;
		else
			scaleMarkerDialog.close (1);
	}

	scaleMarkerDialog.settingsControlsGroup.lengthGroup.lengthEdit.active = true;

	// Show the dialog
	if (1 == scaleMarkerDialog.show()) {  // OK button
		settings.scaleMarkerLength = scaleMarkerDialog.settingsControlsGroup.lengthGroup.lengthEdit.text;
		settings.displayText = scaleMarkerDialog.settingsControlsGroup.annotationsGroup.displayTextCB.value;

		if (scaleMarkerDialog.settingsControlsGroup.annotationsGroup.choicesGroup.textPositionPanel.textBottomRB.value) {
			settings.textPositionBottom = true;
		} 
		else {
			settings.textPositionBottom = false;
		}

		if (scaleMarkerDialog.settingsControlsGroup.annotationsGroup.choicesGroup.markerColorPanel.colorBlackRB.value) {
			settings.markerColor.rgb.hexValue = "000000";
		}
		else {
			settings.markerColor.rgb.hexValue = "FFFFFF";
		}

		return true;
	}
	else {   // 0: window close button; 2: Cancel button
		return false;
	}
}

/**
 * Create a layer group to contain all the layers used in the scale marker.
 *
 * @return Reference to the layer group.
 */
function createLayerGroup() {
	var	layerGroup = app.activeDocument.layerSets.add();
	layerGroup.name = layerGroupName;

	return layerGroup;
}

/**
 * Determine the locations for the text and graphic portions of
 * the scale marker.  Values stored in global vars.
 */
function determineLocations() {
	graphicLeft = baseLeft;
	graphicTop = baseBottom - graphicHeight;

	if (settings.displayText && settings.textPositionBottom) {
		textLocation = [new UnitValue(baseLeft, "px"), new UnitValue(baseBottom, "px")];
		var textFontSizeInPx = textFontSize.as("px");
		graphicTop -= (textFontSizeInPx + textGraphicMargin);
	}
	else if (settings.displayText && !settings.textPositionBottom) {
		textLocation = [new UnitValue(baseLeft, "px"), new UnitValue(baseBottom - (graphicHeight + textGraphicMargin), "px")];
	}
}

/**
 * This function creates the text for the scale marker
 * 
 * @layerGroup The layergroup to create/put this text layer in.
 */
function createText(layerGroup) {
	if (settings.displayText) {
		// Create a new text layer in the specified layer group
		var	textLayer = layerGroup.artLayers.add();
		textLayer.kind = LayerKind.TEXT;

		// Create the text object
		var scaleMarkerText = textLayer.textItem;
		scaleMarkerText.position = textLocation;
		scaleMarkerText.color = settings.markerColor;
		scaleMarkerText.size = textFontSize;

		// Find the real font to use - if it doesn't get found, then the current
		// font will get used instead
		for (var i = 1; i < app.fonts.length; i++) {
			var aFont = app.fonts[i];
			if (aFont.name == textFontName) {
				scaleMarkerText.font = aFont.postScriptName;
			}
		}

		// Finally, set the contents/text
		scaleMarkerText.contents = scaleMarkerActualLength + " " + logicalUnits;

		return textLayer;
	}
}

/**
 * Utility function to make a point we use in our marker path:
 * paths only take points, so we build a point using a pixel
 * based location and then convert it to point units.
 */
function makePoint(x, y) {
	var aPoint = new PathPointInfo;
	aPoint.kind = PointKind.CORNERPOINT;

	var docResolution = app.activeDocument.resolution;
	var pixelLoc = UnitValue(x, "px");
	pixelLoc.baseUnit = UnitValue(1/docResolution, "in");
	var xPoint = pixelLoc.as("pt");
	pixelLoc = UnitValue(y, "px");
	pixelLoc.baseUnit = UnitValue(1/docResolution, "in");
	var yPoint = pixelLoc.as("pt");
	aPoint.anchor = Array(xPoint, yPoint);

	aPoint.leftDirection = aPoint.anchor
	aPoint.rightDirection = aPoint.anchor;

	return aPoint;
}

/**
 * Create the scale marker as a filled path on its own layer.
 * 
 * @layerGroup The layergroup to create/put this text layer in.
 */
function createGraphic(layerGroup) {
	// Create a new layer in the layer group
	var	pathLayer = layerGroup.artLayers.add();
	pathLayer.kind = LayerKind.NORMAL;
	pathLayer.name = localize("$$$/JavaScripts/MeasurementScaleMarker/GraphicLayerName=Marker Graphic");

	// Save the current preferences, and switch to display no dialogs
	var startDisplayDialogs = app.displayDialogs;
	app.displayDialogs = DialogModes.NO;

	var pixelsPerLogicalUnit = pixelLength / logicalLength;

	var graphicRight = graphicLeft + (scaleMarkerActualLength * pixelsPerLogicalUnit);
	if (graphicRight <= graphicLeft + 1)
		graphicRight = graphicLeft + 1;
	var graphicBottom = graphicTop + graphicHeight;

	// Figure out all of our points/bounding box
	var points = new Array();
	points[0] = makePoint(graphicRight, graphicBottom);
	points[1] = makePoint(graphicLeft, graphicBottom);
	points[2] = makePoint(graphicLeft, graphicTop);
	points[3] = makePoint(graphicRight, graphicTop);

	// Create our path by supplying path segments that are lines made up of the
	// points defined above
	var aSubPath = new SubPathInfo();
	aSubPath.operation = ShapeOperation.SHAPEADD;
	aSubPath.closed = true;
	aSubPath.entireSubPath = points;

	// Now add the path to the document and set the color to draw with
	var myPathItem = app.activeDocument.pathItems.add(localize("$$$/JavaScripts/MeasurementScaleMarker/PathName=Measurement Scale Marker Path"), new Array(aSubPath));
	// There is a bug in fillPath - it uses the foreground color instead of the
	// color passed in, so temporarily set the foreground color, draw, then set
	// it back when we're done.
	var origForeground = app.foregroundColor;
	app.foregroundColor = settings.markerColor;
	myPathItem.fillPath(settings.markerColor, ColorBlendMode.NORMAL, 100, false, 0.0, true, false);
	app.foregroundColor = origForeground;
	myPathItem.remove();

	// Reset the application preferences
	displayDialogs = startDisplayDialogs;

	return pathLayer;
}

/**
 * Function: objectToDescriptor
 * Usage: create an ActionDescriptor from a JavaScript Object
 * Input: JavaScript Object (o)
 *        Pre process converter (f)
 * Return: ActionDescriptor
 * NOTE: Only boolean, string, and number are supported, use a pre processor
 *       to convert (f) other types to one of these forms.
 */
function objectToDescriptor (o, f) {
	if (undefined != f) {
		o = f(o);
	}
	var d = new ActionDescriptor;
	var l = o.reflect.properties.length;
	for (var i = 0; i < l; i++ ) {
		var k = o.reflect.properties[i].toString();
		if (k == "__proto__" || k == "__count__" || k == "__class__" || k == "reflect")
			continue;
		var v = o[ k ];
		k = app.stringIDToTypeID(k);
		switch ( typeof(v) ) {
			case "boolean":
				d.putBoolean(k, v);
				break;
			case "string":
				d.putString(k, v);
				break;
			case "number":
				d.putDouble(k, v);
				break;
			default:
				throw( new Error("Unsupported type in objectToDescriptor " + typeof(v) ) );
		}
	}
    return d;
}


/**
 * Function: descriptorToObject
 * Usage: create a JavaScript Object from an ActionDescriptor
 * Input: ActionDescriptor
 * Return: JavaScript Object (o)
 *         Post process converter (f)
 * NOTE: Only boolean, string, and number are supported, use a post processor
 *       to convert (f) other types to one of these forms.
 */
function descriptorToObject (d, f) {
	var o = new Object();
	var l = d.count;
	for (var i = 0; i < l; i++ ) {
		var k = d.getKey(i);
		var t = d.getType(k);
		strk = app.typeIDToStringID(k);
		switch (t) {
			case DescValueType.BOOLEANTYPE:
				o[strk] = d.getBoolean(k);
				break;
			case DescValueType.STRINGTYPE:
				o[strk] = d.getString(k);
				break;
			case DescValueType.DOUBLETYPE:
				o[strk] = d.getDouble(k);
				break;
			case DescValueType.INTEGERTYPE:
			case DescValueType.ALIASTYPE:
			case DescValueType.CLASSTYPE:
			case DescValueType.ENUMERATEDTYPE:
			case DescValueType.LISTTYPE:
			case DescValueType.OBJECTTYPE:
			case DescValueType.RAWTYPE:
			case DescValueType.REFERENCETYPE:
			case DescValueType.UNITDOUBLE:
			default:
				throw( new Error("Unsupported type in descriptorToObject " + t ) );
		}
	}
	if (undefined != f) {
		o = f(o);
	}
    return o;
}

/**
 * Convert settings values to strings to remember for the next
 * time.
 * @return JavaScript Object with objects converted to strings
 *         for storage
 */
function settingsObjectToDescriptorValues(o) {
	o.scaleMarkerLength = o.scaleMarkerLength.toString();
	o.displayText = o.displayText.toString();
	o.markerColor = o.markerColor.rgb.hexValue;
	o.textPositionBottom = o.textPositionBottom.toString();
	return o;
}

/**
 * Convert strings from storage to settings JavaScript object
 * @return JavaScript settings object
 */
function settingsObjectFromDescriptorValues(o) {
	o.scaleMarkerLength = eval(o.scaleMarkerLength);
	o.displayText = eval(o.displayText);
	var markerColorHexValue = o.markerColor;
	o.markerColor = new SolidColor;
	o.markerColor.model = ColorModel.RGB;
	o.markerColor.rgb.hexValue = markerColorHexValue;
	o.textPositionBottom = eval(o.textPositionBottom);
	return o;
}

