GpsGate Server JavaScript API

Event  1.0.0

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

var Franson = Franson || {};
Franson.Geo = Franson.Geo || {}; // namespace. documented below

/**
 * Similar interface as Google's <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds">GLatLngBounds</a>
 * either initialize with no params or two.
 * todo: logic for when to normalize data?
 * todo: hmm, base this on a "generic" bounds?
 * todo: overload constructor to be able to take latlngs as input?
 * todo: datum wrap...
 * @param {LatLng} [sw] South West	// todo: hmm, since we now use extend() to add these it doesn't matter if they actually are sw or ne, could be any coords.
 * @param {LatLng} [ne] North East
 * @class Franson.Geo.Bounds
 * @constructor
 */
Franson.Geo.Bounds = function(sw, ne)
{
	/**
	 * ul
	 * @type LatLng
	 * @private
	 */
	this._min = null;
	/**
	 * lr
	 * @type LatLng
	 * @private
	 */
	this._max = null;

	if (arguments.length == 2)
	{
		this.extend(sw);
		this.extend(ne);
	}
};


// methods
Franson.Geo.Bounds.prototype =
{
	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.extend">GLatLngBounds.extend()</a>
	 * todo: allow iterable as input? + take other bounds as input? (other method?)
	 * @method extend
	 * @param {LatLng} latlng
	 * @return {self}
	 * @chainable
	 */
	extend: function(latlng)
	{
		if (this.isEmpty())
		{
			this._min = this._max = latlng;
		}
		else
		{
			this._min = {
				lat: Math.min(this._min.lat, latlng.lat),
				lng: Math.min(this._min.lng, latlng.lng)
			};
			this._max = {
				lat: Math.max(this._max.lat, latlng.lat),
				lng: Math.max(this._max.lng, latlng.lng)
			}
		}
		return this; // enable chaining. ok? (GLatLngBounds doesn't do this)
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.containsLatLng">GLatLngBounds.containsLatLng()</a>
	 * @method containsLatLng
	 * @param {LatLng} latlng
	 * @return {boolean}
	 */
	containsLatLng: function(latlng)
	{
		// todo: decide on convention for >, >= etc? (open, closed, half-open?) (provide flag?)
		return (
			!this.isEmpty() &&
			// half-open. ok?
			latlng.lat >= this._min.lat &&
			latlng.lng >= this._min.lng &&
			latlng.lat < this._max.lat &&
			latlng.lng < this._max.lng
		);
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.containsBounds">GLatLngBounds.containsBounds()</a>
	 * @method containsBounds
	 * @param {Franson.Geometry.Bounds} bounds
	 * @return {boolean}
	 */
	containsBounds: function(bounds)
	{
		return (
			!this.isEmpty() &&
			(bounds._min.lat >= this._min.lat) && (bounds._max.lat < this._max.lat) &&
			(bounds._min.lng >= this._min.lng) && (bounds._max.lng < this._max.lng)
		);
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.intersects">GLatLngBounds.intersects()</a>
	 * see also <a href="Franson.Geo.html#method_intersects">Franson.Geo.intersects()</a> for a tri-state version of this.
	 * @method intersects
	 * @param {Franson.Geometry.Bounds} bounds
	 * @return {boolean}
	 */
	intersects: function(bounds)
	{
		return (
			!this.isEmpty() &&
			(this._min.lat < bounds._max.lat) && (this._max.lat >= bounds._min.lat) &&
			(this._min.lng < bounds._max.lng) && (this._max.lng >= bounds._min.lng)
		);
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.isEmpty">GLatLngBounds.isEmpty()</a>
	 * @method isEmpty
	 * @return {boolean}
	 */
	isEmpty: function()
	{
		return this._min == null || this._max == null;
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.getCenter">GLatLngBounds.getCenter()</a>
	 * @method getCenter
	 * @return {LatLng}
	 */
	getCenter: function()
	{
		return {
			lat: (this._max.lat + this._min.lat) / 2,
			lng: (this._max.lng + this._min.lng) / 2
		};
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.toSpan">GLatLngBounds.toSpan()</a>
	 * @metod toSpan
	 * @return {LatLng}
	 */
	toSpan: function()
	{
		return {
			lat: this._max.lat - this._min.lat,
			lng: this._max.lng - this._min.lng
		};
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.getSouthWest">GLatLngBounds.getSouthWest()</a>
	 * @method getSouthWest
	 * @return {LatLng}
	 */
	getSouthWest: function()
	{
		return { lat: this._max.lat, lng: this._min.lng };
	},

	/**
	 * see <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLngBounds.getNorthEast">GLatLngBounds.getNorthEast()</a>
	 * @method getNorthEast
	 * @return {LatLng}
	 */
	getNorthEast: function()
	{
		return { lat: this._min.lat, lng: this._max.lng };
	}

	// getNorthWest, getSouthEast?

}; // Franson.Geo.Bounds.prototype


/**
 * namespace
 * @class Franson.Geo
 * @static
 */
MochiKit.Base.update(Franson.Geo,
{
	/**
	 * factory
	 * @method createBounds
	 * @param {Iterable[LatLng]} latlngs
	 * @return {Franson.Geo.Bounds}
	 */
	createBounds: function(latlngs) // calcBounds?
	{
		var bounds = new Franson.Geo.Bounds();
		forEach(latlngs, method(bounds, bounds.extend));
		return bounds;
	},

	/**
	 * factory
	 * todo: name..? union?
	 * @method createBoundsHull
	 * @param {Iterable[Franson.Geo.Bounds]} bounds
	 * @return {Franson.Geo.Bounds}
	 */
	createBoundsHull: function(bounds)
	{
		var hull = new Franson.Geo.Bounds();
		forEach(bounds, function(b)
		{
			hull.extend(b.getSouthWest());
			hull.extend(b.getNorthEast());
		});
		return hull;
	},

	/**
	 * factory
	 * @method createBoundsFromCenterSpan
	 * @param {LatLng} center
	 * @param {LatLng} span
	 * @return {Franson.Geo.Bounds}
	 */
	createBoundsFromCenterSpan: function(center, span)
	{
		return new Franson.Geo.Bounds(
			// SW
			{
				lat: center.lat - span.lat / 2,
				lng: center.lng - span.lng / 2
			},
			// NE
			{
				lat: center.lat + span.lat / 2,
				lng: center.lng + span.lng / 2
			}
		);
	},

	/**
	 * note: also overloaded as: <code>scaleBounds(bounds, scale)</code>
	 * @method scaleBounds
	 * @param {Bounds} bounds
	 * @param {number} scaleLat
	 * @param {number} [scaleLng=scaleLat]
	 * @return {Bounds}
	 */
	scaleBounds: function(bounds, scaleLat, scaleLng)
	{
		// overload on uniform scale case ( scaleGeoBounds(scale, bounds); )
		if (arguments.length == 2)
		{
			bounds = arguments[0];
			scaleLat = scaleLng = arguments[1];
		}

		var span = bounds.toSpan();

		return Franson.Geo.createBoundsFromCenterSpan(bounds.getCenter(), { lat: scaleLat * span.lat, lng: scaleLng * span.lng });
	},


	/**
	 * tri-state intersection test
	 * @method intersects
	 * @param {Franson.Geo.Bounds} baseLatLngBounds
	 * @param {Franson.Geo.Bounds} testLatLngBounds
	 * @return {integer} [ 0: outside (no intersection), 1: inside (testLatLngBounds inside baseLatLngBounds), 2: overlaps ] (todo: expose as named variable? (enum))
	 */
	intersects: function(baseGeoBounds, testGeoBounds)
	{
		// todo: this is possible to implement more efficiently
		if (baseGeoBounds.containsBounds(testGeoBounds))
		{
			return 1; // inside
		}
		else
		if (baseGeoBounds.intersects(testGeoBounds))
		{
			return 2; // overlaps
		}
		return 0; // outside
	},

	/**
	 * @method geoBoundsToPixelBounds
	 * @param {..} projection
	 * @param {Franson.Geo.Bounds} bounds
	 * @return {x, y, w, h} // todo: use Franson.Geometry.Bounds
	 */
	geoBoundsToPixelBounds: function(projection, bounds) // geoBoundsToDivPixelBounds..?
	{
		var sw = bounds.getSouthWest();
		var ne = bounds.getNorthEast();

		var ul = projection.fromLatLngToDivPixel({ lat: ne.lat, lng: sw.lng }); // min
		var lr = projection.fromLatLngToDivPixel({ lat: sw.lat, lng: ne.lng }); // max

		return {
			x: ul.x,
			y: lr.y, //ul.y,
			w: lr.x - ul.x,
			h: ul.y - lr.y //	h: lr.y - ul.y
		};
	},

	/**
	 * @method pixelBoundsToGeoBounds
	 * @param {Projection} projection
	 * @param {(x,y,w,h)} bounds
	 * @return {Franson.Geo.Bounds}
	 */
	pixelBoundsToGeoBounds: function(projection, bounds)
	{
	//	var ul = projection.fromDivPixelToLatLng(bounds.min());
	//	var lr = projection.fromDivPixelToLatLng(bounds.max());

		// support literals
		var ul = projection.fromDivPixelToLatLng({ x: bounds.x, y: bounds.y });
		var lr = projection.fromDivPixelToLatLng({ x: bounds.x + bounds.w, y: bounds.y + bounds.h });

		return new Franson.Geo.Bounds(ul, lr);
	},

	/**
	 * @method distanceBetween
	 * @param {LatLng} pos0
	 * @param {LatLng} pos1
 	 * @return {number} meters
  	 */
	distanceBetween: function(pos0, pos1)
	{
		return Franson.Geo.distanceRadians(pos0, pos1) * 6366710;
	},

	/**
	 * Distance between two position objects in radians.
	 * Great Circle Distance Calculations are used.
	 * see http://williams.best.vwh.net/avform.htm#GCF
	 * @method distanceRadians
	 * @param {latlng} pos0
	 * @param {latlng} pos1
	 * @return {number} distance in radians
	 */
	distanceRadians: function(pos0, pos1)
	{
		// Method 1
		// d = acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(lon1 - lon2))

		// Method 2
		// d = 2*asin(sqrt((sin((lat1 - lat2)/2))^2 + cos(lat1)*cos(lat2)*(sin((lon1 - lon2)/2))^2))

		var e1 = Franson.Math.degToRad(pos0.lng);
		var e2 = Franson.Math.degToRad(pos1.lng);

		var n1 = Franson.Math.degToRad(pos0.lat);
		var n2 = Franson.Math.degToRad(pos1.lat);

		var sin_lat = Math.sin((n1 - n2) / 2);
		var sin_lon = Math.sin((e1 - e2) / 2);

		var distance = 2 * Math.asin(
			Math.sqrt(sin_lat*sin_lat + Math.cos(n1) * Math.cos(n2) * sin_lon*sin_lon)
		);

		return distance;
	},

	normalizeLongitude: function(longitude)
	{
		if (longitude > 180 || longitude < -180)
		{
			// longitude MOD 360
			var iLong = Math.floor(longitude / 360); // float->int cast
			longitude -= iLong * 360;
		}

		if (longitude > 180)
		{
			longitude -= 360;
		}
		else if (longitude < -180)
		{
			longitude += 360;
		}

		return longitude;
	},

	/**
	 * http://williams.best.vwh.net/avform.htm#GCF
	 * @method moveLatLng
	 * @param {LatLng} pos
	 * @param {number} distance (meters)
	 * @param {number} bearing (degrees)
	 * @return {LatLng} 'pos' moved by 'distance' in the 'bearing' direction
	 */
	moveLatLng: function(pos, distance, bearing)
	{
		distance /= 6366710;

		// From here on distance is in radians

		// lat =asin(sin(lat1)*cos(d)+cos(lat1)*sin(d)*cos(tc))
		// dlon=atan2(sin(tc)*sin(d)*cos(lat1),cos(d)-sin(lat1)*sin(lat))
		// lon=mod( lon1-dlon +pi,2*pi )-pi

		var e1 = Franson.Math.degToRad(pos.lng);
		var n1 = Franson.Math.degToRad(pos.lat);

		bearing = Franson.Math.degToRad(bearing);

		var sin_n1 = Math.sin(n1); var cos_n1 = Math.cos(n1);
		var sin_distance = Math.sin(distance); var cos_distance = Math.cos(distance);

		var newLat = Math.asin(sin_n1 * cos_distance + cos_n1 * sin_distance * Math.cos(bearing));

		var deltaLon = Math.atan2(
			Math.sin(bearing) * sin_distance * cos_n1,
			cos_distance - sin_n1 * Math.sin(newLat)
		);

		var longitude = Franson.Geo.normalizeLongitude(Franson.Math.radToDeg(e1 + deltaLon));
		var latitude = Franson.Math.radToDeg(newLat);

		return { lng: longitude, lat: latitude };
	},

	// currently not used
	getPixelRadiusEstimate: function(projection, latlng, radius)
	{
		var nlatlng = Franson.Geo.moveLatLng(latlng, radius, 0);

	//	var p0 = projection.fromLatLngToContainerPixel(latlng);
	//	var p1 = projection.fromLatLngToContainerPixel(nlatlng);
		var p0 = projection.fromLatLngToDivPixel(latlng);
		var p1 = projection.fromLatLngToDivPixel(nlatlng);

		return Franson.Vec2.length(Franson.Vec2.sub(p1, p0));	// todo: perhaps create a Vec2.distance(a, b)?
	},

	/// ----ok to put these here?-----

	// todo: rename

	/**
	 * @method getPixelCenterRadius
	 * @param {..} projection
	 * @param {LatLng} latlng
	 * @param {number} r meters
	 * @return {(x,y,r)}
	 */
	getPixelCenterRadius: function(projection, latlng, r)
	{
		// generate position r meters north(bearing=0) from center
		var radiusLatLng = Franson.Geo.moveLatLng(latlng, r, 0);

	//	var pixelCenter = projection.fromLatLngToContainerPixel(latlng);
	//	var pixelRadiusPos = projection.fromLatLngToContainerPixel(radiusLatLng);
		var pixelCenter = projection.fromLatLngToDivPixel(latlng);
		var pixelRadiusPos = projection.fromLatLngToDivPixel(radiusLatLng);

		var pixelR = Franson.Vec2.length(Franson.Vec2.sub(pixelRadiusPos, pixelCenter));

		return {
			x: pixelCenter.x, y: pixelCenter.y,
			r: pixelR == 0 ? 1 : pixelR // To avoid IE freak when 0 rad (todo: should patch low-level dojox.vml graphics code instead..)
		};
	},

	/**
	 * todo: move to geoxfrm?
	 * @method getGeoCenterRadius
	 * @param {(x,y)} pos
	 * @param {number} r pixels
	 * @return { (lat: number, lng: number, r: number) } r in meters
	 */
	getGeoCenterRadius: function(projection, pos, r)
	{
	//	var center = projection.fromContainerPixelToLatLng(pos);
		var center = projection.fromDivPixelToLatLng(pos);

		var pixelRadiusPos = Franson.Vec2.add(pos, { x: 0, y: -r });
	//	var radiusPos = projection.fromContainerPixelToLatLng(pixelRadiusPos);
		var radiusPos = projection.fromDivPixelToLatLng(pixelRadiusPos);
		var geoR = Franson.Geo.distanceBetween(center, radiusPos);

		return {
			lat: center.lat, lng: center.lng,
			r: geoR
		};
	},


	/**
	 * see also <a href="#method_radiansToSeconds">radiansToSeconds()</a>
	 * @method secondsToRadians
	 * @param {number} seconds
	 * @return {float} radians
	 */
	secondsToRadians: function(seconds)
	{
		return Franson.Math.degToRad(seconds) / 3600;
	},

	/**
	 * see also <a href="#method_secondsToRadians">secondsToRadians()</a>
	 * @method radiansToSeconds
	 * @param {number} radians
	 * @return {float} seconds
	 */
	radiansToSeconds: function(radians)
	{
		return Franson.Math.radToDeg(radians * 3600);
	},


	/**
	 * see getCompassRoseSegment
	 * @method getCompassOctant
	 * @param {number} heading (degrees). Clockwise. 0 = N, 90 = E etc
	 * @return {string} ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']  (todo: hmm, or just return an int?)
	 */
	getCompassOctant: function(heading)
	{
	//	var oct = Math.floor(8 * (heading + 360/(2*8))/360) % 8; // direction octants are defined as +-360/16 degrees around each axis and diagonal
	//	if (oct < 0) oct += 8; // force positive (wraparound)

		var oct = Franson.Geo.getCompassRoseSegment(heading, 8);

		// CW from N
		return [ 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW' ][oct];
	},

	/**
	 * ok name?
	 * @method getCompassHextant
	 * @param {number} heading (degrees)
	 * @return {string} ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ]
	 */
	getCompassHextant: function(heading)
	{
	//	var seg = Math.floor(16 * (heading + 360/(2*16))/360) % 16; // direction hextants are defined as +-360/32 degrees around each axis and diagonal
	//	if (seg < 0) seg += 16; // force positive (wraparound)

		var seg = Franson.Geo.getCompassRoseSegment(heading, 16);

		// CW from N
		return ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ][seg];
	},


	/**
	 * see getCompassOctant
	 * see getCompassHextant
	 * @method getCompassRoseSegment
	 * @param {number} heading (degrees). Clockwise, 0 = N, 90 = E etc.
	 * @param {integer} numSegments
	 * @return {integer}
	 */
	getCompassRoseSegment: function(heading, numSegments)
	{
		// assert(numSegments % 4 == 0) // ok?
		var seg = Math.floor(numSegments * (heading + 360/(2*numSegments))/360) % numSegments;
		if (seg < 0) seg += numSegments; // force positive (wraparound)
		return seg;
	},

	/**
	 * @method toDegMinSec
	 * @param {number} val degrees (typically latitude or longitude, no bounds checks though)
	 * @return {number[3]} [degrees, minutes, seconds]
	 */
	toDegMinSec: function(val)
	{
		var deg = parseInt('' + val, 10);

		var dMin = (val % 1) * 60;
		var min = Math.abs(parseInt('' + dMin, 10));

		var sec = Math.abs((dMin % 1) * 60);

		return [deg, min, sec];
	},

	// todo: degMinSecToDecimal(deg, min, sec)

	/**
	 * @method reverseGeocode
	 * @param {LatLng[]} positions
	 * @return {Deferred(Location[])}
	 */
	reverseGeocode: function(positions)
	{
		return GpsGate.Server.Geocoder.ReverseGeocode(positions);
	}

}); // Franson.Geo

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