diff --git a/source/Ox/js/Geo.js b/source/Ox/js/Geo.js index 9133181b..5ebc6de3 100644 --- a/source/Ox/js/Geo.js +++ b/source/Ox/js/Geo.js @@ -17,6 +17,13 @@ }); } + 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}) @@ -24,6 +31,7 @@ > 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; }; @@ -31,6 +39,7 @@ /*@ 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: @@ -222,6 +231,221 @@ 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 + @*/ + 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 + return !ret; + }); + // Break if no outer part contains the inner part + return ret; + }); + 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}} + @*/ + Ox.intersectAreas = function(areas) { + // FIXME: handle the a corner case where + // two areas have two intersections + 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(Ox.sub(areas, 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(Ox.merge(part, parts)); + })); + ret = intersections.length == 0 ? null + : Ox.joinAreas(intersections); + } + if (ret === null) { + return false; + } 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.map(gaps, function(gap, i) { + return Ox.containsArea({ + sw: {lat: -90, lng: area.sw.lng}, + ne: {lat: 90, lng: area.ne.lng} + }, gap) ? i : null; + }); + } + 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; + return false; + } + }); + return ret; + } + Ox.sub(areas, 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; + }; + }()); //@ Ox.Line (undocumented)