/*
* Copyright 2015-2017 WorldWind Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
define([
'./HashMap',
'../geom/Location',
'../geom/Position',
'./WWMath'
],
function (HashMap,
Location,
Position,
WWMath) {
'use strict';
/**
* Splits polygons that cross the anti-meridian and/or contain a pole.
* @exports PolygonSplitter
*/
var PolygonSplitter = {
//Internal
//Keeps track of the index of added points so that no point is duplicated
addedIndex: -1,
//Internal
//The index where pole insertion began
poleIndexOffset: -1,
/**
* Splits an array of polygons that cross the anti-meridian or contain a pole.
*
* @param {Array} contours an array of arrays of Locations or Positions
* Each array entry defines one of this polygon's boundaries.
* @param {Array} resultContours an empty array to put the result of the split. Each element will have the
* shape of PolygonSplitter.formatContourOutput
* @returns {Boolean} true if one of the boundaries crosses the anti-meridian otherwise false
* */
splitContours: function (contours, resultContours) {
var doesCross = false;
for (var i = 0, len = contours.length; i < len; i++) {
var contourInfo = this.splitContour(contours[i]);
if (contourInfo.polygons.length > 1) {
doesCross = true;
}
resultContours.push(contourInfo);
}
return doesCross;
},
/**
* Splits a polygon that cross the anti-meridian or contain a pole.
*
* @param {Location[] | Position[]} points an array of Locations or Positions that define a polygon
* @returns {Object} @see PolygonSplitter.formatContourOutput
* */
splitContour: function (points) {
var iMap = new HashMap();
var newPoints = [];
var intersections = [];
var polygons = [];
var iMaps = [];
var poleIndex = -1;
var pole = this.findIntersectionAndPole(points, newPoints, intersections, iMap);
if (intersections.length === 0) {
polygons.push(newPoints);
iMaps.push(iMap);
return this.formatContourOutput(polygons, pole, poleIndex, iMaps);
}
if (intersections.length > 2) {
intersections.sort(function (a, b) {
return b.latitude - a.latitude;
});
}
if (pole !== Location.poles.NONE) {
newPoints = this.handleOnePole(newPoints, intersections, iMap, pole);
iMap = this.reindexIntersections(intersections, iMap, this.poleIndexOffset);
}
if (intersections.length === 0) {
polygons.push(newPoints);
iMaps.push(iMap);
poleIndex = 0;
return this.formatContourOutput(polygons, pole, poleIndex, iMaps);
}
this.linkIntersections(intersections, iMap);
poleIndex = this.makePolygons(newPoints, intersections, iMap, polygons, iMaps);
return this.formatContourOutput(polygons, pole, poleIndex, iMaps);
},
/**
* Internal. Applications should not call this method.
* Finds the intersections with the anti-meridian and if the polygon contains one of the poles.
* A new polygon is constructed with the intersections and pole points and stored in newPoints
*
* @param {Location[] | Position[]} points An array of Locations or Positions that define a polygon
* @param {Location[] | Position[]} newPoints An empty array where to store the resulting polygon with intersections
* @param {Array} intersections An empty array where to store the intersection latitude and index
* @param {HashMap} iMap A hashMap to store intersection data
* The key is the index in the newPoints array and value is PolygonSplitter.makeIntersectionEntry
* @returns {Number} The pole number @see Location.poles
* */
findIntersectionAndPole: function (points, newPoints, intersections, iMap) {
var containsPole = false;
var minLatitude = 90.0;
var maxLatitude = -90.0;
this.addedIndex = -1;
for (var i = 0, lenC = points.length; i < lenC; i++) {
var pt1 = points[i];
var pt2 = points[(i + 1) % lenC];
minLatitude = Math.min(minLatitude, pt1.latitude);
maxLatitude = Math.max(maxLatitude, pt1.latitude);
var doesCross = Location.locationsCrossDateLine([pt1, pt2]);
if (doesCross) {
containsPole = !containsPole;
var iLatitude = Location.meridianIntersection(pt1, pt2, 180);
if (iLatitude === null) {
iLatitude = (pt1.latitude + pt2.latitude) / 2;
}
var iLongitude = WWMath.signum(pt1.longitude) * 180 || 180;
var iLoc1 = this.createPoint(iLatitude, iLongitude, pt1.altitude);
var iLoc2 = this.createPoint(iLatitude, -iLongitude, pt2.altitude);
this.safeAdd(newPoints, pt1, i, lenC);
var index = newPoints.length;
iMap.set(index, this.makeIntersectionEntry(index));
iMap.set(index + 1, this.makeIntersectionEntry(index + 1));
intersections.push({
indexEnd: index,
indexStart: index + 1,
latitude: iLatitude
});
newPoints.push(iLoc1);
newPoints.push(iLoc2);
this.safeAdd(newPoints, pt2, i + 1, lenC);
}
else {
this.safeAdd(newPoints, pt1, i, lenC);
this.safeAdd(newPoints, pt2, i + 1, lenC);
}
}
var pole = Location.poles.NONE;
if (containsPole) {
pole = this.determinePole(minLatitude, maxLatitude);
}
return pole;
},
/**
* Internal. Applications should not call this method.
* Determine which pole is enclosed. If the shape is entirely in one hemisphere, then assume that it encloses
* the pole in that hemisphere. Otherwise, assume that it encloses the pole that is closest to the shape's
* extreme latitude.
* @param {Number} minLatitude The minimum latitude of a polygon that contains a pole
* @param {Number} maxLatitude The maximum latitude of a polygon that contains a pole
* @returns {Number} The pole number @see Location.poles
* */
determinePole: function (minLatitude, maxLatitude) {
var pole;
if (minLatitude > 0) {
pole = Location.poles.NORTH; // Entirely in Northern Hemisphere.
}
else if (maxLatitude < 0) {
pole = Location.poles.SOUTH; // Entirely in Southern Hemisphere.
}
else if (Math.abs(maxLatitude) >= Math.abs(minLatitude)) {
pole = Location.poles.NORTH; // Spans equator, but more north than south.
}
else {
pole = Location.poles.SOUTH; // Spans equator, but more south than north.
}
return pole;
},
/**
* Internal. Applications should not call this method.
* Creates a new array of points containing the two pole locations on both sides of the anti-meridian
*
* @param {Location[] | Position[]} points
* @param {Array} intersections
* @param {HashMap} iMap
* @param {Number} pole
* @return {Object} an object containing the new points and a new reIndexed iMap
* */
handleOnePole: function (points, intersections, iMap, pole) {
var pointsClone;
if (pole === Location.poles.NORTH) {
var intersection = intersections.shift();
var poleLat = 90;
}
else if (pole === Location.poles.SOUTH) {
intersection = intersections.pop();
poleLat = -90;
}
var iEnd = iMap.get(intersection.indexEnd);
var iStart = iMap.get(intersection.indexStart);
iEnd.forPole = true;
iStart.forPole = true;
this.poleIndexOffset = intersection.indexStart;
pointsClone = points.slice(0, intersection.indexEnd + 1);
var polePoint1 = this.createPoint(poleLat, points[iEnd.index].longitude, points[iEnd.index].altitude);
var polePoint2 = this.createPoint(poleLat, points[iStart.index].longitude, points[iStart.index].altitude);
pointsClone.push(polePoint1, polePoint2);
pointsClone = pointsClone.concat(points.slice(this.poleIndexOffset));
return pointsClone;
},
/**
* Internal. Applications should not call this method.
* Links adjacents pairs of intersection by index
* @param {Array} intersections
* @param {HashMap} iMap
* */
linkIntersections: function (intersections, iMap) {
for (var i = 0; i < intersections.length - 1; i += 2) {
var i0 = intersections[i];
var i1 = intersections[i + 1];
var iEnd0 = iMap.get(i0.indexEnd);
var iStart0 = iMap.get(i0.indexStart);
var iEnd1 = iMap.get(i1.indexEnd);
var iStart1 = iMap.get(i1.indexStart);
iEnd0.linkTo = i1.indexStart;
iStart0.linkTo = i1.indexEnd;
iEnd1.linkTo = i0.indexStart;
iStart1.linkTo = i0.indexEnd;
}
},
/**
* Internal. Applications should not call this method.
* ReIndexes the intersections due to the poles being added to the array of points
* @param {Array} intersections
* @param {HashMap} iMap
* @param {Number} indexOffset the index from which to start reIndexing
* @returns {HashMap} a new hash map with the correct indices
* */
reindexIntersections: function (intersections, iMap, indexOffset) {
iMap = HashMap.reIndex(iMap, indexOffset, 2);
for (var i = 0, len = intersections.length; i < len; i++) {
if (intersections[i].indexEnd >= indexOffset) {
intersections[i].indexEnd += 2;
}
if (intersections[i].indexStart >= indexOffset) {
intersections[i].indexStart += 2;
}
}
return iMap;
},
/**
* Internal. Applications should not call this method.
* @param {Location[] | Position[]} points
* @param {Array} intersections
* @param {HashMap} iMap
* @param {Array} polygons an empty array to store the resulting polygons
* @param {HashMap[]} iMaps an empty array to store the resulting hasp maps for each polygon
* @returns {Number} the pole number @see Location.poles
* */
makePolygons: function (points, intersections, iMap, polygons, iMaps) {
var poleIndex = -1;
for (var i = 0; i < intersections.length - 1; i += 2) {
var i0 = intersections[i];
var i1 = intersections[i + 1];
var start = i0.indexStart;
var end = i1.indexEnd;
var polygon = [];
var polygonHashMap = new HashMap();
var containsPole = this.makePolygon(start, end, points, iMap, polygon, polygonHashMap);
if (polygon.length) {
polygons.push(polygon);
iMaps.push(polygonHashMap);
if (containsPole) {
poleIndex = polygons.length - 1;
}
}
start = i1.indexStart;
end = i0.indexEnd;
polygon = [];
polygonHashMap = new HashMap();
containsPole = this.makePolygon(start, end, points, iMap, polygon, polygonHashMap);
if (polygon.length) {
polygons.push(polygon);
iMaps.push(polygonHashMap);
if (containsPole) {
poleIndex = polygons.length - 1;
}
}
}
return poleIndex;
},
/**
* Internal. Applications should not call this method.
* Paths from a start intersection index to an end intersection index and makes a polygon and a hashMap
* with the intersection indices
* @param {Number} start the index of a start type intersection
* @param {Number} end the index of an end type intersection
* @param {Location[] | Position[]} points
* @param {HashMap} iMap
* @param {Location[] | Position[]} resultPolygon an empty array to store the resulting polygon
* @param {HashMap} polygonHashMap a hash map to record the indices of the intersections in the polygon
* @returns {Boolean} true if the polygon contains a pole
* */
makePolygon: function (start, end, points, iMap, resultPolygon, polygonHashMap) {
var pass = false;
var len = points.length;
var containsPole = false;
if (end < start) {
end += len;
}
for (var i = start; i <= end; i++) {
var idx = i % len;
var pt = points[idx];
var intersection = iMap.get(idx);
if (intersection) {
if (intersection.visited) {
break;
}
resultPolygon.push(pt);
polygonHashMap.set(resultPolygon.length - 1, intersection);
if (intersection.forPole) {
containsPole = true;
}
else {
if (pass) {
i = intersection.linkTo - 1;
if (i + 1 === start) {
break;
}
}
pass = !pass;
intersection.visited = true;
}
}
else {
resultPolygon.push(pt);
}
}
return containsPole;
},
/**
* Internal. Applications should not call this method.
* Adds an element to an array preventing duplication
* @param {Location[] | Position[]} points
* @param {Location | Position} point
* @param {Number} index The index of the Point from the source array
* @param {Number} len The length of the source array
* */
safeAdd: function (points, point, index, len) {
if (this.addedIndex < index && this.addedIndex < len - 1) {
points.push(point);
this.addedIndex = index;
}
},
/**
* Internal. Applications should not call this method.
* Creates a Location or a Position
* @param {Number} latitude
* @param {Number} longitude
* @param {Number} altitude
* @returns Location | Position
* */
createPoint: function (latitude, longitude, altitude) {
if (altitude == null) {
return new Location(latitude, longitude);
}
return new Position(latitude, longitude, altitude);
},
/**
* Internal. Applications should not call this method.
* @param {Array} polygons an array of arrays of Locations or Positions
* @param {Number} pole the pole number @see Location.poles
* @param {Number} poleIndex the index of the polygon containing the pole
* @param {HashMap[]} iMaps an array of hash maps for each polygon
* */
formatContourOutput: function (polygons, pole, poleIndex, iMaps) {
return {
polygons: polygons,
pole: pole,
poleIndex: poleIndex,
iMap: iMaps
};
},
/**
* Internal. Applications should not call this method.
* @param {Number} index the index of the intersection in the array of points
* */
makeIntersectionEntry: function (index) {
if (index == null) {
index = -1;
}
return {
visited: false,
forPole: false,
index: index,
linkTo: -1
}
}
};
return PolygonSplitter;
});