GpsGate Server JavaScript API

UI  1.0.0

GpsGate Server JavaScript API > UI > settings.js (source view)
Search:
 
Filters
/**
 * Copyright Franson Technology AB, Sweden, 2009
 * http://gpsgate.com, http://franson.com
 *
 * author Fredrik Blomqvist
 *
 */

/** @namespace */
var Franson = Franson || {};
/** @namespace */
Franson.Settings = Franson.Settings || {};


// merge in methods (Not overwrite), since the core (subdomains) should
// already have been injected on the server-side.
MochiKit.Base.setdefault(Franson.Settings,
{
	// subdomains
	site: {},
	application: {},
	user: {},

	init: function()
	{
		// transform the injected settings array structure to
		// a map structure. (todo: should change the injected format)
		var remapServerSettings = method(this, function(domain)
		{
			var tmp = {};
			var settings = this[domain];

			// save a "backup"... (test)
			this['_' + domain] = settings;

			for (var i = 0; i < settings.length; ++i)
			{
				var setting = this[domain][i];
				tmp[setting.key] = setting.value;
				// todo: should store type also
			//	{
			//
			//		'type': setting.type,
			//		'value': setting.value
			//	};
			}
			this[domain] = tmp;
		});

		remapServerSettings('site');
		remapServerSettings('application');
		remapServerSettings('user');

		// todo: make use of the .type property ('System.Int32' etc) of the settings to
		// choose the correct 'Set' method (Set*SettingInt(), Set*SettingDouble(), Set*Setting())

		return MochiKit.Async.succeed();
	}

	// todo: implement something like this that keeps track of multiple
	// asynchronous requests and also sets the setting to default if not found
	// domain = 'user', 'application', 'site'
,	sync: function(domain, settingsKeyDefault)
	{
		// todo: ...
		// return Deferred
	}

/*	// hmm, place these inside the subdomains instead? (or let these dig in all domains? ..)
	get: function(key)
	{
		// ..
	},

	set: function(key, value)
	{
		//..
	}
*/

});


/**
 * validate if obj implements the Interface necessary
 * for saving state. typically UI controls.
 * (ok name?)
 * note: also requirement that the "blob" from _saveState contains an .id property.
 * @method isStatefulLike
 * @param {object} obj
 * @return {boolean}
 */
Franson.Settings.isStatefulLike = function(obj)
{
	return (
		typeof(obj) != 'undefined' && obj != null &&
		typeof(obj.id) != 'undefined' && // enforce number or string?
		typeof(obj._saveState) == 'function' &&
		typeof(obj._restoreState) == 'function'
 	);
};


/**
 * State poller, brute force, doesn't depend on signals or similar. assumes state extraction is cheap.
 * intended for stuff that are not explicit settings, typically UI state etc.
 * states are treated as black-box JSON blobs.
 * todo: stubbed, WS-methods not set yet
 * todo: hmm, generalize this a bit. Prototype has a TimedObserver that is similar for example
 * todo: could be "fancy" and support different poll frequencty for different components etc.
 * ok name?
 * @singleton (ok?)
 */
Franson.Settings.StateManager =
{
	/**
	 * interval at which to scan registered components for changes
	 * @private
	 * @type {integer} ms
	 */
	_updateIterval: 10000, // 10s

	/**
	 * @private
	 * @type IntervalHandler
	 */
	_ih: null, // interval handler

	/**
	 * @private
	 * @type Map[id->IStateFul]
	 */
	_components: {},

	/**
	 * todo: could take am (optional) comparer also?
	 * todo: or optionally enable explicit signal attaching? (i.e remove obj from poll-loop only save when signalled)
	 */
	registerComponent: function(obj)
	{
		if (!Franson.Settings.isStatefulLike(obj))
		{
			logError('Trying to add an object that does not implement IStateful to StateManager');
			return;
		}
		if (typeof(this._components[obj.id]) != 'undefined')
		{
			logWarning('duplicate object(id) inserted in Settings.StateManager. overwrite');
		}

		this._components[obj.id] = {
			obj: obj,
			state: obj._saveState(),
			dirty: true
			// store clock also? (could allow scattered polling to reduce load?)
		};
	},

	/**
	 *
	 */
	unregisterComponent: function(obj)
	{
		// warn if not found here also?
		delete this._components[obj.id];
	},

	/**
	 *
	 */
	loadStates: function()
	{
		var d = GpsGate.Server.Directory.GetControlStates(Franson_Session.UserId);
		d.addCallback(method(this, function(result)
		{
			for (var id in result)
			{
				var state = eval('(' + result[id] + ')'); // try-catch here?

				var v = this._components[state.id];
				if (typeof(v) != 'undefined' && v.obj != null)
				{
					try
					{
						v.state = state;
						v.obj._restoreState(state);
						v.dirty = false;
					}
					catch (e)
					{
						// todo: hmm, should add this to some kind of retry-queue. might just have been too early..
						// though note that we don't delete it
						logWarning('Failed restoring state:', e);
					}
				}
				else
				{
					logWarning('Matching state object not found:', state.id);
				}
			}
			return result;
		}));
		return d;
	},

	/**
	 * starts the "thread" that keeps the
	 * components' state in sync with server
	 */
	start: function()
	{
		this.stop();

		this._ih = setInterval(
			method(this, function()
			{
				var changed = this._pollStates();

				if (changed)
				{
					// debug
					logDebug('detected changed states');
					for (var id in this._components)
					{
						if (this._components[id].dirty)
						{
							var jsonState = MochiKit.Base.serializeJSON(this._components[id].state);
							logDebug(jsonState);
						}
					}

					this._saveChanges();
				}
			}),
			this._updateIterval
		);
	},

	// todo: or allow this to be public (to not have to use polling)
	_pollStates: function()
	{
		var changed = false;
		for (var id in this._components)
		{
			var v = this._components[id];

			try
			{
				var currentState = v.obj._saveState();
			}
			catch (e)
			{
				logWarning('Failed to get State from component, deleting.', e.message); // or do this after N:th attempt?
				delete this._components[id];
			}

			if (!Franson.Util.equalJSON(v.state, currentState))
			{
				v.dirty = true;
				v.state = currentState;
				changed = true;
			}
			// observe that we don't set dirty = false here, only in _saveChanges (or after initial load)
		}

		return changed;
	},

	_saveChanges: function()
	{
		var changedObjects = {};
		for (var id in this._components)
		{
			if (this._components[id].dirty)
				changedObjects[id] = this._components[id].state;
		}

		var d = GpsGate.Server.Directory.SetControlStates(Franson_Session.UserId, changedObjects);
		d.addCallbacks(
			method(this, function(result)
			{
				// we assume no state changes have occurred during the actual save operation.. (should need a lock or second flag..)
				for (var id in changedObjects)
				{
					this._components[id].dirty = false;
				}
				// todo: fire signal? (to allow UI to indicate "auto saved" or similar)
				return result;
			}),
			function(error)
			{
				logDebug('State save NOT ok!', error);
				// not ok..
				// todo: fire signal?
				return error;
			}
		);
		return d;
	},

	/**
	 * stops the "thread"
	 */
	stop: function()
	{
		if (this._ih != null)
		{
			clearInterval(this._ih);
			this._ih = null;
		}
	},

	/**
	 * @return {integer} ms
	 */
	getInterval: function()
	{
		return this._updateInterval;
	},

	/**
	 * @param {integer} interval ms
	 */
	setInterval: function(interval)
	{
		this._updateInterval = interval;
	},

	clear: function()
	{
		this._components = {};
	}

};


//-------------------------------------------------------------------------------------------


/**
 * DEPRECATED. Should use new units.js instead
 * @module Units
 *
 */

/**
 * namespace
 * @class Franson.Units
 * @static
 */
Franson.Units = Franson.Units || {};

/**
 * @method distanceSIToLocal
 */
Franson.Units.distanceSIToLocal = function(disSI)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	var factorMin = 1;
	var unitMin = localize('VT_UNIT_SHORT_METER');

	if (measurementUnits == 'en-US')
	{
		factorMin = 1.0936133;
		unitMin = localize('VT_UNIT_SHORT_YARD');
	}
	else if (measurementUnits == 'nautic')
	{
		factorMin = 0.000539; // m to nmi
		unitMin = localize('VT_UNIT_SHORT_NAUTICAL_MILE');
	}
	else
	{
		// Meter: do nothing
	}

	return {
		value: disSI * factorMin,
		unit: unitMin
	};
};

/**
 * @method distanceLocalToSI
 */
Franson.Units.distanceLocalToSI = function(distLocal)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	var factorMin = 1;
	var unitMin = localize('VT_UNIT_SHORT_METER');

	if (measurementUnits == 'en-US')
	{
		factorMin = 1.0936133;
		unitMin = localize('VT_UNIT_SHORT_YARD');
	}
	else if (measurementUnits == 'nautic')
	{
		factorMin = 0.000539; // m to nmi
		unitMin = localize('VT_UNIT_SHORT_NAUTICAL_MILE');
	}
	else
	{
		// Meter: do nothing
	}

	return {
		value: distLocal / factorMin,
		unit: unitMin
	};
};

/**
 * @method getDistanceString
 * @param {number} [distanceMeter=0]
 * @return {string}
 */
Franson.Units.getDistanceString = function(distanceMeter)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	distanceMeter = distanceMeter || 0;

	var factorMaj = 1;
	var factorMin = 1;
	var unitMaj = localize('VT_UNIT_SHORT_KILOMETER');
	var unitMin = localize('VT_UNIT_SHORT_METER');

	if (measurementUnits == 'en-US')
	{
		factorMaj = 1.60934424;
		factorMin = 1.0936133;

		unitMaj = localize('VT_UNIT_SHORT_MILE');
		unitMin = localize('VT_UNIT_SHORT_YARD');
	}
	else if (measurementUnits == 'nautic')
	{
		factorMaj = 1.852;     //0.539956; // Km to nmi
		factorMin = 0.000539; // m to nmi

		unitMaj = localize('VT_UNIT_SHORT_NAUTICAL_MILE');
		unitMin = localize('VT_UNIT_SHORT_NAUTICAL_MILE');
	}

	// todo: move this kind of stuff to a Franson.Units package?
	if (distanceMeter > 999)
	{
		return Franson.Util.roundToMaxFixed((distanceMeter / 1000 / factorMaj), 1) + ' ' + unitMaj;// + 'Km ' + Franson.Util.roundToMaxFixed((distanceMeter / 1000 / 1.60934424), 3) + 'mi';
	}

	return Math.round(distanceMeter * factorMin) + ' ' + unitMin; // + 'm ' + Math.round(distanceMeter * 1.0936133) + 'yd';
};


/**
 * @method getSpeedString
 * @param {number} groundSpeedMs speed in m/s
 */
Franson.Units.getSpeedString = function(groundSpeedMs)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	var factor = 3.6;
	var unit = localize('VT_UNIT_SHORT_KILOMETERS_PER_HOUR');

	if (measurementUnits == 'en-US')
	{
		factor = 2.24;
		unit = localize('VT_UNIT_SHORT_MILES_PER_HOUR');
	}
	else if (measurementUnits == 'nautic')
	{
		factor = 1.94384449;
		unit = localize('VT_UNIT_SHORT_KNOTS');
	}

	var groundSpeed = groundSpeedMs * factor;

	// todo: should create a real object to not have to bind toString
	var ret = {
		'value': groundSpeed,
		'unit': unit,

		'toString': method(ret, function()
		{
			return format('{0:.1f} {1}', this.value, this.unit);
		})
	};

	return ret;
};


/**
 * @method getSpeedInMs
 * @param {number} speed speed in current units
 */
Franson.Units.getSpeedInMs = function(speed)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	var factor = 0.2778; // Km/h

	if (measurementUnits == 'en-US')
	{
		factor = 0.447;
	}
	else if (measurementUnits == 'nautic')
	{
		factor = 0.514444444;
	}

	var ms = speed * factor;

	return { 'value': ms, 'factor': factor };
};

/**
 * @method getAltInMeters
 * @param {number} alt altitude in current units
 */
Franson.Units.getAltInMeters = function(alt)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	var factor = 1;

	if (measurementUnits == 'en-US' || measurementUnits == 'nautic')
	{
		factor = 0.3048; // feet
	}

	var meters = alt * factor;

	return { 'value': meters, 'factor': factor };
};

/**
 * @method getAltString
 * @param {number} meter
 * @return {value, unit}
 */
Franson.Units.getAltString = function(meter)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	var factor = 1;
	var unit = localize('VT_UNIT_SHORT_METER');

	if (measurementUnits == 'en-US' || measurementUnits == 'nautic')
	{
		unit = localize('VT_UNIT_FEET');
		factor = 3.2808399; // feet
	}

	var altitude = meter * factor;

	// todo: should create a real object to not have to bind toString
	var ret = {
		'value': altitude,
		'unit': unit,

		'toString': method(ret, function()
		{
			return format('{0:.1f} {1}', this.value, this.unit); // todo: store a format-string like this for each unit? (include nr useful decimals)
		})
	};
	return ret;
};


/**
 * todo: move to a units package
 * @method formatLatAsDMS
 * @param {number} lat decimal degrees latitude
 * @return {string}
 */
Franson.Units.formatLatAsDMS = function(lat)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	if (measurementUnits != 'nautic')
		return MochiKit.Format.roundToFixed(lat, 5);

	var DMS = Franson.Geo.toDegMinSec(lat);

	var ns = DMS[0] < 0 ? localize('VT_UNIT_SHORT_SOUTH') : localize('VT_UNIT_SHORT_NORTH');

	return Math.abs(DMS[0]) + String.fromCharCode(176) + ' ' + DMS[1] + '\' ' + MochiKit.Format.roundToFixed(DMS[2], 3) + '\'\' ' + ns;
};

/**
 * todo: move to a units package
 * @method formatLngAsDMS
 * @param {number} lng longitude, decimal degrees.
 * @return {string}
 */
Franson.Units.formatLngAsDMS = function(lng)
{
	var measurementUnits = Franson.Settings.user['measurement_units'] || '';

	if (measurementUnits != 'nautic')
		return MochiKit.Format.roundToFixed(lng, 5);

	var DMS = Franson.Geo.toDegMinSec(lng);

	var ew = DMS[0] < 0 ? localize('VT_UNIT_SHORT_WEST') : localize('VT_UNIT_SHORT_EAST');

	return Math.abs(DMS[0]) + String.fromCharCode(176) + ' ' + DMS[1] + '\' ' + MochiKit.Format.roundToFixed(DMS[2], 3) + '\'\' ' + ew;
};


/**
 * conveniece for formatLatAsDSM & formatLngAsDMS
 * @method formatLatLngAsDMS
 * @return {(lat: string, lng: string)}
 */
Franson.Units.formatLatLngAsDMS = function(latlng)
{
	return {
		lat: this.formatLatAsDMS(latlng.lat),
		lng: this.formatLngAsDMS(latlng.lng)
	};
};

Copyright © 2009 Franson Technology AB, Sweden. All rights reserved.