/**
* 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