'use strict'; (function() { // fixme: make all this work with different types of "points" // i.e. {lat, lng}, [lat, lng] function deg(point) { return Ox.map(point, function(val) { return Ox.mod(Ox.deg(val) + 180, 360) - 180; }); } function rad(point) { return Ox.map(point, function(val) { return Ox.rad(val); }); } function splitArea(area) { return Ox.crossesDateline(area.sw, area.ne) ? [ {sw: area.sw, ne: {lat: area.ne.lat, lng: 180}}, {sw: {lat: area.sw.lat, lng: -180}, ne: area.ne} ] : [area]; } /*@ Ox.crossesDateline Returns true if a given line crosses the dateline > Ox.crossesDateline({lat: 0, lng: -90}, {lat: 0, lng: 90}) false > Ox.crossesDateline({lat: 0, lng: 90}, {lat: 0, lng: -90}) true @*/ // FIXME: argument should be {w: ..., e: ...} Ox.crossesDateline = function(pointA, pointB) { return pointA.lng > pointB.lng; }; /*@ Ox.getArea Returns the area in square meters of a given rectancle @*/ // FIXME: argument should be {sw: ..., ne: ...} Ox.getArea = function(pointA, pointB) { /* area of a ring between two latitudes: 2 * PI * r^2 * abs(sin(latA) - sin(latB)) see http://mathforum.org/library/drmath/view/63767.html => 2 * Math.PI * Math.pow(Ox.EARTH_RADIUS, 2) * Math.abs(Math.sin(Ox.rad(latA)) - Math.sin(Ox.rad(latB))) * Math.abs(Ox.rad(lngA) - Ox.rad(lngB)) / (2 * Math.PI) */ if (Ox.crossesDateline(pointA, pointB)) { pointB.lng += 360; } pointA = rad(pointA); pointB = rad(pointB); return Math.pow(Ox.EARTH_RADIUS, 2) * Math.abs(Math.sin(pointA.lat) - Math.sin(pointB.lat)) * Math.abs(pointA.lng - pointB.lng); }; /*@ Ox.getBearing Returns the bearing from one point to another > Ox.getBearing({lat: -45, lng: 0}, {lat: 45, lng: 0}) 0 > Ox.getBearing({lat: 0, lng: -90}, {lat: 0, lng: 90}) 90 @*/ Ox.getBearing = function(pointA, pointB) { var pointA = rad(pointA), pointB = rad(pointB), x = Math.cos(pointA.lat) * Math.sin(pointB.lat) - Math.sin(pointA.lat) * Math.cos(pointB.lat) * Math.cos(pointB.lng - pointA.lng), y = Math.sin(pointB.lng - pointA.lng) * Math.cos(pointB.lat); return (Ox.deg(Math.atan2(y, x)) + 360) % 360; }; /*@ Ox.getCenter Returns the center of a recangle on a spehre > Ox.getCenter({lat: -45, lng: -90}, {lat: 45, lng: 90}) {lat: 0, lng: 0} @*/ Ox.getCenter = function(pointA, pointB) { var pointA = rad(pointA), pointB = rad(pointB), x = Math.cos(pointB.lat) * Math.cos(pointB.lng - pointA.lng), y = Math.cos(pointB.lat) * Math.sin(pointB.lng - pointA.lng), d = Math.sqrt( Math.pow(Math.cos(pointA.lat) + x, 2) + Math.pow(y, 2) ), lat = Math.atan2(Math.sin(pointA.lat) + Math.sin(pointB.lat), d), lng = pointA.lng + Math.atan2(y, Math.cos(pointA.lat) + x); return deg({lat: lat, lng: lng}); }; /*@ Ox.getCircle Returns points on a circle around a given point (center, radius, precision) -> Points center Center point ({lat, lng}) radius Radius in meters precision Precision (the circle will have 2^precision segments) @*/ Ox.getCircle = function(center, radius, precision) { return Ox.range( 0, 360, 360 / Math.pow(2, precision) ).map(function(bearing) { return Ox.getPoint(center, radius, bearing); }); }; /*@ Ox.getDegreesPerMeter Returns degrees per meter at a given latitude > 360 / Ox.getDegreesPerMeter(0) Ox.EARTH_CIRCUMFERENCE @*/ Ox.getDegreesPerMeter = function(lat) { return 360 / Ox.EARTH_CIRCUMFERENCE / Math.cos(lat * Math.PI / 180); }; /*@ Ox.getDistance Returns the distance in meters between two points > Ox.getDistance({lat: -45, lng: -90}, {lat: 45, lng: 90}) * 2 Ox.EARTH_CIRCUMFERENCE @*/ Ox.getDistance = function(pointA, pointB) { var pointA = rad(pointA), pointB = rad(pointB); return Math.acos( Math.sin(pointA.lat) * Math.sin(pointB.lat) + Math.cos(pointA.lat) * Math.cos(pointB.lat) * Math.cos(pointB.lng - pointA.lng) ) * Ox.EARTH_RADIUS; }; /*@ Ox.getLatLngByXY Returns lat/lng for a given x/y on a 1x1 mercator projection > Ox.getLatLngByXY({x: 0.5, y: 0.5}) {lat: 0, lng: 0} @*/ Ox.getLatLngByXY = function(xy) { function getVal(val) { return (val - 0.5) * 2 * Math.PI; } return { lat: -Ox.deg(Math.atan(Ox.sinh(getVal(xy.y)))), lng: Ox.deg(getVal(xy.x)) }; }; /*@ Ox.getLine Returns points on a line between two points (pointA, pointB, precision) -> Points pointA Start point ({lat, lng}) pointB End point ({lat, lng}) precision Precision (the line will have 2^precision segments) @*/ Ox.getLine = function(pointA, pointB, precision) { var line = [pointA, pointB], points; while (precision > 0) { points = [line[0]]; Ox.loop(line.length - 1, function(i) { points.push( Ox.getCenter(line[i], line[i + 1]), line[i + 1] ); }); line = points; precision--; } return line; }; /*@ Ox.getMetersPerDegree Returns meters per degree at a given latitude > Ox.getMetersPerDegree(0) * 360 Ox.EARTH_CIRCUMFERENCE @*/ Ox.getMetersPerDegree = function(lat) { return Math.cos(lat * Math.PI / 180) * Ox.EARTH_CIRCUMFERENCE / 360; }; /*@ Ox.getPoint Returns a point at a given distance/bearing from a given point > Ox.getPoint({lat: -45, lng: 0}, Ox.EARTH_CIRCUMFERENCE / 4, 0) {lat: 45, lng: 0} @*/ Ox.getPoint = function(point, distance, bearing) { var pointB = {}; point = rad(point); distance /= Ox.EARTH_RADIUS; bearing = Ox.rad(bearing); pointB.lat = Math.asin( Math.sin(point.lat) * Math.cos(distance) + Math.cos(point.lat) * Math.sin(distance) * Math.cos(bearing) ); pointB.lng = point.lng + Math.atan2( Math.sin(bearing) * Math.sin(distance) * Math.cos(point.lat), Math.cos(distance) - Math.sin(point.lat) * Math.sin(pointB.lat) ); return deg(pointB); }; /*@ Ox.getXYByLatLng Returns x/y on a 1x1 mercator projection for a given lat/lng > Ox.getXYByLatLng({lat: 0, lng: 0}) {x: 0.5, y: 0.5} @*/ Ox.getXYByLatLng = function(latlng) { function getVal(val) { return (val / (2 * Math.PI) + 0.5) } return { x: getVal(Ox.rad(latlng.lng)), y: getVal(Ox.asinh(Math.tan(Ox.rad(-latlng.lat)))) }; }; /*@ Ox.isPolar Returns true if a given point is outside the bounds of a mercator projection > Ox.isPolar({lat: 90, lng: 0}) true @*/ Ox.isPolar = function(point) { return point.lat < Ox.MIN_LATITUDE || point.lat > Ox.MAX_LATITUDE; }; /*@ Ox.containsArea Returns true if an area contains another area > Ox.containsArea(Ox.test.areas[0], Ox.test.areas[1]) false > Ox.containsArea(Ox.test.areas[2], Ox.test.areas[3]) true @*/ // FIXME: Shouldn't this be rewritten as a test // if the intersection is equal to the inner area? Ox.containsArea = function(areaA, areaB) { // If an area crosses the dateline, // we split it into two parts, // west and east of the dateline var areas = [areaA, areaB].map(splitArea), ret; function contains(areaA, areaB) { return areaA.sw.lat <= areaB.sw.lat && areaA.sw.lng <= areaB.sw.lng && areaA.ne.lat >= areaB.ne.lat && areaA.ne.lng >= areaB.ne.lng; } // For each part of the inner area, test if it // is contained in any part of the outer area Ox.forEach(areas[1], function(area1) { Ox.forEach(areas[0], function(area0) { ret = contains(area0, area1); // Break if the outer part contains the inner part ret && Ox.break(); }); // Break if no outer part contains the inner part !ret && Ox.break(); }); return ret; }; /*@ Ox.intersectAreas Returns the intersection of two areas, or null > Ox.intersectAreas([Ox.test.areas[0], Ox.test.areas[1]]) {sw: {lat: 0, lng: 0}, ne: {lat: 0, lng: 0}} > Ox.intersectAreas([Ox.test.areas[2], Ox.test.areas[3]]) {sw: {lat: 25, lng: -155}, ne: {lat: 30, lng: -150}} @*/ // FIXME: handle the a corner case where // two areas have two intersections Ox.intersectAreas = function(areas) { var intersections, ret; // If an area crosses the dateline, // we split it into two parts, // west and east of the dateline areas = areas.map(splitArea); ret = areas[0]; function intersect(areaA, areaB) { return areaA.sw.lat > areaB.ne.lat || areaA.sw.lng > areaB.ne.lng || areaA.ne.lat < areaB.sw.lat || areaA.ne.lng < areaB.sw.lng ? null : { sw: { lat: Math.max(areaA.sw.lat, areaB.sw.lat), lng: Math.max(areaA.sw.lng, areaB.sw.lng) }, ne: { lat: Math.min(areaA.ne.lat, areaB.ne.lat), lng: Math.min(areaA.ne.lng, areaB.ne.lng) } }; } Ox.forEach(areas.slice(1), function(parts) { if (ret.length == 1 && parts.length == 1) { ret = intersect(ret[0], parts[0]); } else { // intersect each part of the intersection // with all parts of the next area intersections = Ox.compact(ret.map(function(part) { return Ox.intersectAreas(parts.concat(part)); })); ret = intersections.length == 0 ? null : Ox.joinAreas(intersections); } if (ret === null) { Ox.break(); } else { ret = splitArea(ret); } }); return ret ? Ox.joinAreas(ret) : null; }; /*@ Ox.joinAreas Joins an array of areas > Ox.joinAreas(Ox.test.areas) {sw: {lat: -30, lng: 150}, ne: {lat: 30, lng: -150}} @*/ Ox.joinAreas = function(areas) { // While the combined latitude is trivial (min to max), the combined longitude // spans from the eastern to the western edge of the largest gap between areas var ret = areas[0], gaps = [{ sw: {lat: -90, lng: ret.ne.lng}, ne: {lat: 90, lng: ret.sw.lng} }]; function containsGaps(area) { return Ox.getIndices(gaps, function(gap) { return Ox.containsArea({ sw: {lat: -90, lng: area.sw.lng}, ne: {lat: 90, lng: area.ne.lng} }, gap); }); } function intersectsWithGaps(area) { var ret = {}; gaps.forEach(function(gap, i) { var intersection = Ox.intersectAreas([area, gap]); if (intersection) { ret[i] = intersection; } }); return ret; } function isContainedInGap(area) { var ret = -1; Ox.forEach(gaps, function(gap, i) { if (Ox.containsArea(gap, area)) { ret = i; Ox.break(); } }); return ret; } areas.slice(1).forEach(function(area) { var index, indices, intersections; if (area.sw.lat < ret.sw.lat) { ret.sw.lat = area.sw.lat; } if (area.ne.lat > ret.ne.lat) { ret.ne.lat = area.ne.lat; } // If the area is contained in a gap, split the gap in two index = isContainedInGap(area); if (index > -1) { gaps.push({ sw: gaps[index].sw, ne: {lat: 90, lng: area.sw.lng} }); gaps.push({ sw: {lat: -90, lng: area.ne.lng}, ne: gaps[index].ne }); gaps.splice(index, 1); } else { // If the area contains gaps, remove them indices = containsGaps(area); Ox.reverse(indices).forEach(function(index) { gaps.splice(index, 1); }); // If the area intersects with gaps, shrink them intersections = intersectsWithGaps(area); Ox.forEach(intersections, function(intersection, index) { gaps[index] = { sw: { lat: -90, lng: gaps[index].sw.lng == intersection.sw.lng ? intersection.ne.lng : gaps[index].sw.lng }, ne: { lat: 90, lng: gaps[index].ne.lng == intersection.ne.lng ? intersection.sw.lng : gaps[index].ne.lng } }; }); } }); if (gaps.length == 0) { ret.sw.lng = -180; ret.ne.lng = 180; } else { gaps.sort(function(a, b) { return ( b.ne.lng + (Ox.crossesDateline(b.sw, b.ne) ? 360 : 0) - b.sw.lng ) - ( a.ne.lng + (Ox.crossesDateline(a.sw, a.ne) ? 360 : 0) - a.sw.lng ); }); ret.sw.lng = gaps[0].ne.lng; ret.ne.lng = gaps[0].sw.lng; } return ret; }; }());