/*
* 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.
*/
/**
* @exports Sector
*/
define([
'../geom/Angle',
'../error/ArgumentError',
'../geom/Location',
'../util/Logger',
'../geom/Vec3',
'../util/WWMath'
],
function (Angle,
ArgumentError,
Location,
Logger,
Vec3,
WWMath) {
"use strict";
/**
* Constructs a Sector from specified minimum and maximum latitudes and longitudes in degrees.
* @alias Sector
* @constructor
* @classdesc Represents a rectangular region in geographic coordinates in degrees.
* @param {Number} minLatitude The sector's minimum latitude in degrees.
* @param {Number} maxLatitude The sector's maximum latitude in degrees.
* @param {Number} minLongitude The sector's minimum longitude in degrees.
* @param {Number} maxLongitude The sector's maximum longitude in degrees.
*/
var Sector = function (minLatitude, maxLatitude, minLongitude, maxLongitude) {
/**
* This sector's minimum latitude in degrees.
* @type {Number}
*/
this.minLatitude = minLatitude;
/**
* This sector's maximum latitude in degrees.
* @type {Number}
*/
this.maxLatitude = maxLatitude;
/**
* This sector's minimum longitude in degrees.
* @type {Number}
*/
this.minLongitude = minLongitude;
/**
* This sector's maximum longitude in degrees.
* @type {Number}
*/
this.maxLongitude = maxLongitude;
};
/**
* A sector with minimum and maximum latitudes and minimum and maximum longitudes all zero.
* @constant
* @type {Sector}
*/
Sector.ZERO = new Sector(0, 0, 0, 0);
/**
* A sector that encompasses the full range of latitude ([-90, 90]) and longitude ([-180, 180]).
* @constant
* @type {Sector}
*/
Sector.FULL_SPHERE = new Sector(-90, 90, -180, 180);
/**
* Sets this sector's latitudes and longitudes to those of a specified sector.
* @param {Sector} sector The sector to copy.
* @returns {Sector} This sector, set to the values of the specified sector.
* @throws {ArgumentError} If the specified sector is null or undefined.
*/
Sector.prototype.copy = function (sector) {
if (!sector) {
throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "copy", "missingSector"));
}
this.minLatitude = sector.minLatitude;
this.maxLatitude = sector.maxLatitude;
this.minLongitude = sector.minLongitude;
this.maxLongitude = sector.maxLongitude;
return this;
};
/**
* Indicates whether this sector has width or height.
* @returns {Boolean} true if this sector's minimum and maximum latitudes or minimum and maximum
* longitudes do not differ, otherwise false.
*/
Sector.prototype.isEmpty = function () {
return this.minLatitude === this.maxLatitude && this.minLongitude === this.maxLongitude;
};
/**
* Returns the angle between this sector's minimum and maximum latitudes, in degrees.
* @returns {Number} The difference between this sector's minimum and maximum latitudes, in degrees.
*/
Sector.prototype.deltaLatitude = function () {
return this.maxLatitude - this.minLatitude;
};
/**
* Returns the angle between this sector's minimum and maximum longitudes, in degrees.
* @returns {Number} The difference between this sector's minimum and maximum longitudes, in degrees.
*/
Sector.prototype.deltaLongitude = function () {
return this.maxLongitude - this.minLongitude;
};
/**
* Returns the angle midway between this sector's minimum and maximum latitudes.
* @returns {Number} The mid-angle of this sector's minimum and maximum latitudes, in degrees.
*/
Sector.prototype.centroidLatitude = function () {
return 0.5 * (this.minLatitude + this.maxLatitude);
};
/**
* Returns the angle midway between this sector's minimum and maximum longitudes.
* @returns {Number} The mid-angle of this sector's minimum and maximum longitudes, in degrees.
*/
Sector.prototype.centroidLongitude = function () {
return 0.5 * (this.minLongitude + this.maxLongitude);
};
/**
* Computes the location of the angular center of this sector, which is the mid-angle of each of this sector's
* latitude and longitude dimensions.
* @param {Location} result A pre-allocated {@link Location} in which to return the computed centroid.
* @returns {Location} The specified result argument containing the computed centroid.
* @throws {ArgumentError} If the result argument is null or undefined.
*/
Sector.prototype.centroid = function (result) {
if (!result) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "centroid", "missingResult"));
}
result.latitude = this.centroidLatitude();
result.longitude = this.centroidLongitude();
return result;
};
/**
* Returns this sector's minimum latitude in radians.
* @returns {Number} This sector's minimum latitude in radians.
*/
Sector.prototype.minLatitudeRadians = function () {
return this.minLatitude * Angle.DEGREES_TO_RADIANS;
};
/**
* Returns this sector's maximum latitude in radians.
* @returns {Number} This sector's maximum latitude in radians.
*/
Sector.prototype.maxLatitudeRadians = function () {
return this.maxLatitude * Angle.DEGREES_TO_RADIANS;
};
/**
* Returns this sector's minimum longitude in radians.
* @returns {Number} This sector's minimum longitude in radians.
*/
Sector.prototype.minLongitudeRadians = function () {
return this.minLongitude * Angle.DEGREES_TO_RADIANS;
};
/**
* Returns this sector's maximum longitude in radians.
* @returns {Number} This sector's maximum longitude in radians.
*/
Sector.prototype.maxLongitudeRadians = function () {
return this.maxLongitude * Angle.DEGREES_TO_RADIANS;
};
/**
* Modifies this sector to encompass an array of specified locations.
* @param {Location[]} locations An array of locations. The array may be sparse.
* @returns {Sector} This sector, modified to encompass all locations in the specified array.
* @throws {ArgumentError} If the specified array is null, undefined or empty or has fewer than two locations.
*/
Sector.prototype.setToBoundingSector = function (locations) {
if (!locations || locations.length < 2) {
throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "setToBoundingSector",
"missingArray"));
}
var minLatitude = 90,
maxLatitude = -90,
minLongitude = 180,
maxLongitude = -180;
for (var idx = 0, len = locations.length; idx < len; idx += 1) {
var location = locations[idx];
if (!location) {
continue;
}
minLatitude = Math.min(minLatitude, location.latitude);
maxLatitude = Math.max(maxLatitude, location.latitude);
minLongitude = Math.min(minLongitude, location.longitude);
maxLongitude = Math.max(maxLongitude, location.longitude);
}
this.minLatitude = minLatitude;
this.maxLatitude = maxLatitude;
this.minLongitude = minLongitude;
this.maxLongitude = maxLongitude;
return this;
};
/**
* Computes bounding sectors from a list of locations that span the dateline.
* @param {Location[]} locations The locations to bound.
* @returns {Sector[]} Two sectors, one in the eastern hemisphere and one in the western hemisphere.
* Returns null if the computed bounding sector has zero width or height.
* @throws {ArgumentError} If the specified array is null, undefined or empty or the number of locations
* is less than 2.
*/
Sector.splitBoundingSectors = function (locations) {
if (!locations || locations.length < 2) {
throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "splitBoundingSectors",
"missingArray"));
}
var minLat = 90;
var minLon = 180;
var maxLat = -90;
var maxLon = -180;
var lastLocation = null;
for (var idx = 0, len = locations.length; idx < len; idx += 1) {
var location = locations[idx];
var lat = location.latitude;
if (lat < minLat) {
minLat = lat;
}
if (lat > maxLat) {
maxLat = lat;
}
var lon = location.longitude;
if (lon >= 0 && lon < minLon) {
minLon = lon;
}
if (lon <= 0 && lon > maxLon) {
maxLon = lon;
}
if (lastLocation != null) {
var lastLon = lastLocation.longitude;
if (WWMath.signum(lon) != WWMath.signum(lastLon)) {
if (Math.abs(lon - lastLon) < 180) {
// Crossing the zero longitude line too
maxLon = 0;
minLon = 0;
}
}
}
lastLocation = location;
}
if (minLat === maxLat && minLon === maxLon) {
return null;
}
return [
new Sector(minLat, maxLat, minLon, 180), // Sector on eastern hemisphere.
new Sector(minLat, maxLat, -180, maxLon) // Sector on western hemisphere.
];
};
/**
* Indicates whether this sector intersects a specified sector.
* This sector intersects the specified sector when each sector's boundaries either overlap with the specified
* sector or are adjacent to the specified sector.
* The sectors are assumed to have normalized angles (angles within the range [-90, 90] latitude and
* [-180, 180] longitude).
* @param {Sector} sector The sector to test intersection with. May be null or undefined, in which case this
* function returns false.
* @returns {Boolean} true if the specifies sector intersections this sector, otherwise false.
*/
Sector.prototype.intersects = function (sector) {
// Assumes normalized angles: [-90, 90], [-180, 180].
return sector
&& this.minLongitude <= sector.maxLongitude
&& this.maxLongitude >= sector.minLongitude
&& this.minLatitude <= sector.maxLatitude
&& this.maxLatitude >= sector.minLatitude;
};
/**
* Indicates whether this sector intersects a specified sector exclusive of the sector boundaries.
* This sector overlaps the specified sector when the union of the two sectors defines a non-empty sector.
* The sectors are assumed to have normalized angles (angles within the range [-90, 90] latitude and
* [-180, 180] longitude).
* @param {Sector} sector The sector to test overlap with. May be null or undefined, in which case this
* function returns false.
* @returns {Boolean} true if the specified sector overlaps this sector, otherwise false.
*/
Sector.prototype.overlaps = function (sector) {
// Assumes normalized angles: [-90, 90], [-180, 180].
return sector
&& this.minLongitude < sector.maxLongitude
&& this.maxLongitude > sector.minLongitude
&& this.minLatitude < sector.maxLatitude
&& this.maxLatitude > sector.minLatitude;
};
/**
* Indicates whether this sector fully contains a specified sector.
* This sector contains the specified sector when the specified sector's boundaries are completely contained
* within this sector's boundaries, or are equal to this sector's boundaries.
* The sectors are assumed to have normalized angles (angles within the range [-90, 90] latitude and
* [-180, 180] longitude).
* @param {Sector} sector The sector to test containment with. May be null or undefined, in which case this
* function returns false.
* @returns {Boolean} true if the specified sector contains this sector, otherwise false.
*/
Sector.prototype.contains = function (sector) {
// Assumes normalized angles: [-90, 90], [-180, 180].
return sector
&& this.minLatitude <= sector.minLatitude
&& this.maxLatitude >= sector.maxLatitude
&& this.minLongitude <= sector.minLongitude
&& this.maxLongitude >= sector.maxLongitude;
};
/**
* Indicates whether this sector contains a specified geographic location.
* @param {Number} latitude The location's latitude in degrees.
* @param {Number} longitude The location's longitude in degrees.
* @returns {Boolean} true if this sector contains the location, otherwise false.
*/
Sector.prototype.containsLocation = function (latitude, longitude) {
// Assumes normalized angles: [-90, 90], [-180, 180].
return this.minLatitude <= latitude
&& this.maxLatitude >= latitude
&& this.minLongitude <= longitude
&& this.maxLongitude >= longitude;
};
/**
* Sets this sector to the intersection of itself and a specified sector.
* @param {Sector} sector The sector to intersect with this one.
* @returns {Sector} This sector, set to its intersection with the specified sector.
* @throws {ArgumentError} If the specified sector is null or undefined.
*/
Sector.prototype.intersection = function (sector) {
if (!sector instanceof Sector) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "intersection", "missingSector"));
}
// Assumes normalized angles: [-180, 180], [-90, 90].
if (this.minLatitude < sector.minLatitude)
this.minLatitude = sector.minLatitude;
if (this.maxLatitude > sector.maxLatitude)
this.maxLatitude = sector.maxLatitude;
if (this.minLongitude < sector.minLongitude)
this.minLongitude = sector.minLongitude;
if (this.maxLongitude > sector.maxLongitude)
this.maxLongitude = sector.maxLongitude;
// If the sectors do not overlap in either latitude or longitude, then the result of the above logic results in
// the max being greater than the min. In this case, set the max to indicate that the sector is empty in
// that dimension.
if (this.maxLatitude < this.minLatitude)
this.maxLatitude = this.minLatitude;
if (this.maxLongitude < this.minLongitude)
this.maxLongitude = this.minLongitude;
return this;
};
/**
* Returns a list of the Lat/Lon coordinates of a Sector's corners.
*
* @returns {Array} an array of the four corner locations, in the order SW, SE, NE, NW
*/
Sector.prototype.getCorners = function () {
var corners = [];
corners.push(new Location(this.minLatitude, this.minLongitude));
corners.push(new Location(this.minLatitude, this.maxLongitude));
corners.push(new Location(this.maxLatitude, this.maxLongitude));
corners.push(new Location(this.maxLatitude, this.minLongitude));
return corners;
};
/**
* Returns an array of {@link Vec3} that bounds the specified sector on the surface of the specified
* {@link Globe}. The returned points enclose the globe's surface terrain in the sector,
* according to the specified vertical exaggeration, minimum elevation, and maximum elevation. If the minimum and
* maximum elevation are equal, this assumes a maximum elevation of 10 + the minimum.
*
* @param {Globe} globe the globe the extent relates to.
* @param {Number} verticalExaggeration the globe's vertical surface exaggeration.
*
* @returns {Vec3} a set of points that enclose the globe's surface on the specified sector. Can be turned into a {@link BoundingBox}
* with the setToVec3Points method.
*
* @throws {ArgumentError} if the globe is null.
*/
Sector.prototype.computeBoundingPoints = function (globe, verticalExaggeration) {
// TODO: Refactor this method back to computeBoundingBox.
// This method was originally computeBoundingBox and returned a BoundingBox. This created a circular dependency between
// Sector and BoundingBox that the Karma unit test suite doesn't appear to like. If we discover a way to make Karma handle this
// situation, we should refactor this method.
if (globe === null) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "computeBoundingBox", "missingGlobe"));
}
var minAndMaxElevations = globe.minAndMaxElevationsForSector(this);
// Compute the exaggerated minimum and maximum heights.
var minHeight = minAndMaxElevations[0] * verticalExaggeration;
var maxHeight = minAndMaxElevations[1] * verticalExaggeration;
if (minHeight === maxHeight)
maxHeight = minHeight + 10; // Ensure the top and bottom heights are not equal.
var points = [];
var corners = this.getCorners();
for (var i = 0; i < corners.length; i++) {
points.push(globe.computePointFromPosition(corners[i].latitude, corners[i].longitude, minHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(corners[i].latitude, corners[i].longitude, maxHeight, new Vec3(0, 0, 0)));
}
// A point at the centroid captures the maximum vertical dimension.
var centroid = this.centroid(new Location(0, 0));
points.push(globe.computePointFromPosition(centroid.latitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
// If the sector spans the equator, then the curvature of all four edges need to be taken into account. The
// extreme points along the top and bottom edges are located at their mid-points, and the extreme points along
// the left and right edges are on the equator. Add points with the longitude of the sector's centroid but with
// the sector's min and max latitude, and add points with the sector's min and max longitude but with latitude
// at the equator. See WWJINT-225.
if (this.minLatitude < 0 && this.maxLatitude > 0) {
points.push(globe.computePointFromPosition(this.minLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(this.maxLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(0, this.minLongitude, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(0, this.maxLongitude, maxHeight, new Vec3(0, 0, 0)));
}
// If the sector is located entirely in the southern hemisphere, then the curvature of its top edge needs to be
// taken into account. The extreme point along the top edge is located at its mid-point. Add a point with the
// longitude of the sector's centroid but with the sector's max latitude. See WWJINT-225.
else if (this.minLatitude < 0) {
points.push(globe.computePointFromPosition(this.maxLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
}
// If the sector is located entirely in the northern hemisphere, then the curvature of its bottom edge needs to
// be taken into account. The extreme point along the bottom edge is located at its mid-point. Add a point with
// the longitude of the sector's centroid but with the sector's min latitude. See WWJINT-225.
else {
points.push(globe.computePointFromPosition(this.minLatitude, centroid.longitude, maxHeight, new Vec3(0, 0, 0)));
}
// If the sector spans 360 degrees of longitude then is a band around the entire globe. (If one edge is a pole
// then the sector looks like a circle around the pole.) Add points at the min and max latitudes and longitudes
// 0, 180, 90, and -90 to capture full extent of the band.
if (this.deltaLongitude() >= 360) {
var minLat = this.minLatitude;
points.push(globe.computePointFromPosition(minLat, 0, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(minLat, 90, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(minLat, -90, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(minLat, 180, maxHeight, new Vec3(0, 0, 0)));
var maxLat = this.maxLatitude;
points.push(globe.computePointFromPosition(maxLat, 0, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(maxLat, 90, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(maxLat, -90, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(maxLat, 180, maxHeight, new Vec3(0, 0, 0)));
}
else if (this.deltaLongitude() > 180) {
// Need to compute more points to ensure the box encompasses the full sector.
var cLon = centroid.longitude;
var cLat = centroid.latitude;
// centroid latitude, longitude midway between min longitude and centroid longitude
var lon = (this.minLongitude + cLon) / 2;
points.push(globe.computePointFromPosition(cLat, lon, maxHeight, new Vec3(0, 0, 0)));
// centroid latitude, longitude midway between centroid longitude and max longitude
lon = (cLon + this.maxLongitude) / 2;
points.push(globe.computePointFromPosition(cLat, lon, maxHeight, new Vec3(0, 0, 0)));
// centroid latitude, longitude at min longitude and max longitude
points.push(globe.computePointFromPosition(cLat, this.minLongitude, maxHeight, new Vec3(0, 0, 0)));
points.push(globe.computePointFromPosition(cLat, this.maxLongitude, maxHeight, new Vec3(0, 0, 0)));
}
return points;
};
/**
* Sets this sector to the union of itself and a specified sector.
* @param {Sector} sector The sector to union with this one.
* @returns {Sector} This sector, set to its union with the specified sector.
* @throws {ArgumentError} if the specified sector is null or undefined.
*/
Sector.prototype.union = function (sector) {
if (!sector instanceof Sector) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Sector", "union", "missingSector"));
}
// Assumes normalized angles: [-180, 180], [-90, 90].
if (this.minLatitude > sector.minLatitude)
this.minLatitude = sector.minLatitude;
if (this.maxLatitude < sector.maxLatitude)
this.maxLatitude = sector.maxLatitude;
if (this.minLongitude > sector.minLongitude)
this.minLongitude = sector.minLongitude;
if (this.maxLongitude < sector.maxLongitude)
this.maxLongitude = sector.maxLongitude;
return this;
};
return Sector;
});