/**
* 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)
};
};