Source: shapes/SurfaceShape.js

/*
 * 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 SurfaceShape
 */
define([
        '../error/AbstractError',
        '../geom/Angle',
        '../error/ArgumentError',
        '../geom/BoundingBox',
        '../geom/Location',
        '../util/Logger',
        '../cache/MemoryCache',
        '../error/NotYetImplementedError',
        '../pick/PickedObject',
        '../util/PolygonSplitter',
        '../render/Renderable',
        '../geom/Sector',
        '../shapes/ShapeAttributes',
        '../error/UnsupportedOperationError',
        '../util/WWMath'
    ],
    function (AbstractError,
              Angle,
              ArgumentError,
              BoundingBox,
              Location,
              Logger,
              MemoryCache,
              NotYetImplementedError,
              PickedObject,
              PolygonSplitter,
              Renderable,
              Sector,
              ShapeAttributes,
              UnsupportedOperationError,
              WWMath) {
        "use strict";

        /**
         * Constructs a surface shape with an optionally specified bundle of default attributes.
         * @alias SurfaceShape
         * @constructor
         * @augments Renderable
         * @abstract
         * @classdesc Represents a surface shape. This is an abstract base class and is meant to be instantiated
         * only by subclasses.
         * <p>
         * Surface shapes other than SurfacePolyline {@link SurfacePolyline} have an interior and an outline and utilize
         * the corresponding attributes in their associated ShapeAttributes {@link ShapeAttributes}. They do not
         * utilize image-related attributes.
         *
         * @param {ShapeAttributes} attributes The attributes to apply to this shape. May be null, in which case
         * attributes must be set directly before the shape is drawn.
         */
        var SurfaceShape = function (attributes) {

            Renderable.call(this);

            // All these are documented with their property accessors below.
            this._displayName = "Surface Shape";
            this._attributes = attributes ? attributes : new ShapeAttributes(null);
            this._highlightAttributes = null;
            this._highlighted = false;
            this._enabled = true;
            this._pathType = WorldWind.GREAT_CIRCLE;
            this._maximumNumEdgeIntervals = SurfaceShape.DEFAULT_NUM_EDGE_INTERVALS;
            this._polarThrottle = SurfaceShape.DEFAULT_POLAR_THROTTLE;
            this._sector = null;

            /**
             * Indicates the object to return as the owner of this shape when picked.
             * @type {Object}
             * @default null
             */
            this.pickDelegate = null;

            /*
             * The bounding sectors for this tile, which may be needed for crossing the dateline.
             * @type {Sector[]}
             * @protected
             */
            this._sectors = [];

            /*
             * The raw collection of locations defining this shape and are explicitly specified by the client of this class.
             * @type {Location[]}
             * @protected
             */
            this._locations = null;

            /*
             * Boundaries that are either the user specified locations or locations that are algorithmically generated.
             * @type {Location[]}
             * @protected
             */
            this._boundaries = null;

            /*
             * The collection of locations that describes a closed curve which can be filled.
             * @type {Location[][]}
             * @protected
             */
            this._interiorGeometry = null;

            /*
             * The collection of locations that describe the outline of the shape.
             * @type {Location[][]}
             * @protected
             */
            this._outlineGeometry = null;

            /*
             * Internal use only.
             * Inhibit the filling of the interior. This is to be used ONLY by polylines.
             * @type {Boolean}
             * @protected
             */
            this._isInteriorInhibited = false;

            /*
             * Indicates whether this object's state key is invalid. Subclasses must set this value to true when their
             * attributes change. The state key will be automatically computed the next time it's requested. This flag
             * will be set to false when that occurs.
             * @type {Boolean}
             * @protected
             */
            this.stateKeyInvalid = true;

            // Internal use only. Intentionally not documented.
            this._attributesStateKey = null;

            // Internal use only. Intentionally not documented.
            this.boundariesArePrepared = false;

            // Internal use only. Intentionally not documented.
            this.layer = null;

            // Internal use only. Intentionally not documented.
            this.pickColor = null;

            //the split contours returned from polygon splitter
            this.contours = [];
            this.containsPole = false;
            this.crossesAntiMeridian = false;

            /**
             * Indicates how long to use terrain-specific shape data before regenerating it, in milliseconds. A value
             * of zero specifies that shape data should be regenerated every frame. While this causes the shape to
             * adapt more frequently to the terrain, it decreases performance.
             * @type {Number}
             * @default 2000 (milliseconds)
             */
            this.expirationInterval = 2000;

            // Internal use only. Intentionally not documented.
            // Holds the per-globe data
            this.shapeDataCache = new MemoryCache(3, 2);

            // Internal use only. Intentionally not documented.
            // The shape-data-cache data that is for the currently active globe.
            this.currentData = null;
        };

        SurfaceShape.prototype = Object.create(Renderable.prototype);

        Object.defineProperties(SurfaceShape.prototype, {
            stateKey: {
                /**
                 * A hash key of the total visible external state of the surface shape.
                 * @memberof SurfaceShape.prototype
                 * @type {String}
                 */
                get: function () {
                    // If we don't have a state key for the shape attributes, consider this state key to be invalid.
                    if (!this._attributesStateKey) {
                        // Update the state key for the appropriate attributes for future
                        if (this._highlighted) {
                            if (!!this._highlightAttributes) {
                                this._attributesStateKey = this._highlightAttributes.stateKey;
                            }
                        } else {
                            if (!!this._attributes) {
                                this._attributesStateKey = this._attributes.stateKey;
                            }
                        }

                        // If we now actually have a state key for the attributes, it was previously invalid.
                        if (!!this._attributesStateKey) {
                            this.stateKeyInvalid = true;
                        }
                    } else {
                        // Detect a change in the appropriate attributes.
                        var currentAttributesStateKey = null;

                        if (this._highlighted) {
                            // If there are highlight attributes associated with this shape, ...
                            if (!!this._highlightAttributes) {
                                currentAttributesStateKey = this._highlightAttributes.stateKey;
                            }
                        } else {
                            if (!!this._attributes) {
                                currentAttributesStateKey = this._attributes.stateKey;
                            }
                        }

                        // If the attributes state key changed, ...
                        if (currentAttributesStateKey != this._attributesStateKey) {
                            this._attributesStateKey = currentAttributesStateKey;
                            this.stateKeyInvalid = true;
                        }
                    }

                    if (this.stateKeyInvalid) {
                        this._stateKey = this.computeStateKey();
                    }

                    return this._stateKey;
                }
            },

            /**
             * The shape's display name and label text.
             * @memberof SurfaceShape.prototype
             * @type {String}
             * @default Surface Shape
             */
            displayName: {
                get: function () {
                    return this._displayName;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this._displayName = value;
                }
            },

            /**
             * The shape's attributes. If null and this shape is not highlighted, this shape is not drawn.
             * @memberof SurfaceShape.prototype
             * @type {ShapeAttributes}
             * @default see [ShapeAttributes]{@link ShapeAttributes}
             */
            attributes: {
                get: function () {
                    return this._attributes;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this._attributes = value;
                    this._attributesStateKey = value.stateKey;
                }
            },

            /**
             * The attributes used when this shape's highlighted flag is true. If null and the
             * highlighted flag is true, this shape's normal attributes are used. If they, too, are null, this
             * shape is not drawn.
             * @memberof SurfaceShape.prototype
             * @type {ShapeAttributes}
             * @default null
             */
            highlightAttributes: {
                get: function () {
                    return this._highlightAttributes;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this._highlightAttributes = value;
                }
            },

            /**
             * Indicates whether this shape displays with its highlight attributes rather than its normal attributes.
             * @memberof SurfaceShape.prototype
             * @type {Boolean}
             * @default false
             */
            highlighted: {
                get: function () {
                    return this._highlighted;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this._highlighted = value;
                }
            },

            /**
             * Indicates whether this shape is drawn.
             * @memberof SurfaceShape.prototype
             * @type {Boolean}
             * @default true
             */
            enabled: {
                get: function () {
                    return this._enabled;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this._enabled = value;
                }
            },

            /**
             * The path type to used to interpolate between locations on this shape. Recognized values are:
             * <ul>
             * <li>WorldWind.GREAT_CIRCLE</li>
             * <li>WorldWind.RHUMB_LINE</li>
             * <li>WorldWind.LINEAR</li>
             * </ul>
             * @memberof SurfaceShape.prototype
             * @type {String}
             * @default WorldWind.GREAT_CIRCLE
             */
            pathType: {
                get: function () {
                    return this._pathType;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this.resetBoundaries();
                    this._pathType = value;
                }
            },

            /**
             * The maximum number of intervals an edge will be broken into. This is the number of intervals that an
             * edge that spans to opposite side of the globe would be broken into. This is strictly an upper bound
             * and the number of edge intervals may be lower if this resolution is not needed.
             * @memberof SurfaceShape.prototype
             * @type {Number}
             * @default SurfaceShape.DEFAULT_NUM_EDGE_INTERVALS
             */
            maximumNumEdgeIntervals: {
                get: function () {
                    return this._maximumNumEdgeIntervals;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this.resetBoundaries();
                    this._maximumNumEdgeIntervals = value;
                }
            },

            /**
             * A dimensionless number that controls throttling of edge traversal near the poles where edges need to be
             * sampled more closely together.
             * A value of 0 indicates that no polar throttling is to be performed.
             * @memberof SurfaceShape.prototype
             * @type {Number}
             * @default SurfaceShape.DEFAULT_POLAR_THROTTLE
             */
            polarThrottle: {
                get: function () {
                    return this._polarThrottle;
                },
                set: function (value) {
                    this.stateKeyInvalid = true;
                    this.resetBoundaries();
                    this._polarThrottle = value;
                }
            },

            /**
             * Defines the extent of the shape in latitude and longitude.
             * This sector only has valid data once the boundary is defined. Prior to this, it is null.
             * @memberof SurfaceShape.prototype
             * @type {Sector}
             */
            sector: {
                get: function () {
                    return this._sector;
                }
            }
        });

        SurfaceShape.staticStateKey = function (shape) {
            shape.stateKeyInvalid = false;

            if (shape.highlighted) {
                if (!shape._highlightAttributes) {
                    if (!shape._attributes) {
                        shape._attributesStateKey = null;
                    } else {
                        shape._attributesStateKey = shape._attributes.stateKey;
                    }
                } else {
                    shape._attributesStateKey = shape._highlightAttributes.stateKey;
                }
            } else {
                if (!shape._attributes) {
                    shape._attributesStateKey = null;
                } else {
                    shape._attributesStateKey = shape._attributes.stateKey;
                }
            }

            return "dn " + shape.displayName +
                " at " + (!shape._attributesStateKey ? "null" : shape._attributesStateKey) +
                " hi " + shape.highlighted +
                " en " + shape.enabled +
                " pt " + shape.pathType +
                " ne " + shape.maximumNumEdgeIntervals +
                " po " + shape.polarThrottle +
                " se " + "[" +
                shape.sector.minLatitude + "," +
                shape.sector.maxLatitude + "," +
                shape.sector.minLongitude + "," +
                shape.sector.maxLongitude +
                "]";
        };

        SurfaceShape.prototype.computeStateKey = function () {
            return SurfaceShape.staticStateKey(this);
        };

        /**
         * Returns this shape's area in square meters.
         * @param {Globe} globe The globe on which to compute the area.
         * @param {Boolean} terrainConformant If true, the returned area is that of the terrain,
         * including its hillsides and other undulations. If false, the returned area is the shape's
         * projected area.
         */
        SurfaceShape.prototype.area = function (globe, terrainConformant) {
            throw new NotYetImplementedError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceShape", "area", "notYetImplemented"));
        };

        // Internal function. Intentionally not documented.
        SurfaceShape.prototype.computeBoundaries = function (globe) {
            // This method is in the base class and should be overridden if the boundaries are generated.

            throw new AbstractError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "SurfaceShape", "computeBoundaries", "abstractInvocation"));
        };

        // Internal. Intentionally not documented.
        SurfaceShape.prototype.intersectsFrustum = function (dc) {
            if (this.currentData && this.currentData.extent) {
                if (dc.pickingMode) {
                    return this.currentData.extent.intersectsFrustum(dc.pickFrustum);
                } else {
                    return this.currentData.extent.intersectsFrustum(dc.navigatorState.frustumInModelCoordinates);
                }
            } else {
                return true;
            }
        };

        /**
         * Indicates whether a specified shape data object is current. Subclasses may override this method to add
         * criteria indicating whether the shape data object is current, but must also call this method on this base
         * class. Applications do not call this method.
         * @param {DrawContext} dc The current draw context.
         * @param {Object} shapeData The object to validate.
         * @returns {Boolean} true if the object is current, otherwise false.
         * @protected
         */
        SurfaceShape.prototype.isShapeDataCurrent = function (dc, shapeData) {
            return shapeData.verticalExaggeration === dc.verticalExaggeration
                && shapeData.expiryTime > Date.now();
        };

        /**
         * Creates a new shape data object for the current globe state. Subclasses may override this method to
         * modify the shape data object that this method creates, but must also call this method on this base class.
         * Applications do not call this method.
         * @returns {Object} The shape data object.
         * @protected
         */
        SurfaceShape.prototype.createShapeDataObject = function () {
            return {};
        };

        // Intentionally not documented.
        SurfaceShape.prototype.resetExpiration = function (shapeData) {
            // The random addition in the line below prevents all shapes from regenerating during the same frame.
            shapeData.expiryTime = Date.now() + this.expirationInterval + 1e3 * Math.random();
        };

        // Internal. Intentionally not documented.
        SurfaceShape.prototype.establishCurrentData = function (dc) {
            this.currentData = this.shapeDataCache.entryForKey(dc.globeStateKey);
            if (!this.currentData) {
                this.currentData = this.createShapeDataObject();
                this.resetExpiration(this.currentData);
                this.shapeDataCache.putEntry(dc.globeStateKey, this.currentData, 1);
            }

            this.currentData.isExpired = !this.isShapeDataCurrent(dc, this.currentData);
        };

        // Internal function. Intentionally not documented.
        SurfaceShape.prototype.render = function (dc) {
            if (!this.enabled) {
                return;
            }

            this.layer = dc.currentLayer;

            this.prepareBoundaries(dc);

            this.establishCurrentData(dc);

            if (this.currentData.isExpired || !this.currentData.extent) {
                this.computeExtent(dc);
                this.currentData.verticalExaggeration = dc.verticalExaggeration;
                this.resetExpiration(this.currentData);
            }

            // Use the last computed extent to see if this shape is out of view.
            if (this.currentData && this.currentData.extent && !this.intersectsFrustum(dc)) {
                return;
            }

            dc.surfaceShapeTileBuilder.insertSurfaceShape(this);
        };

        // Internal function. Intentionally not documented.
        SurfaceShape.prototype.interpolateLocations = function (locations) {
            var first = locations[0],
                next = first,
                prev,
                isNextFirst = true,
                isPrevFirst = true,// Don't care initially, this will get set in first iteration.
                countFirst = 0,
                isInterpolated = true,
                idx, len;

            this._locations = [first];

            for (idx = 1, len = locations.length; idx < len; idx += 1) {
                // Advance to next location, retaining previous location.
                prev = next;
                isPrevFirst = isNextFirst;

                next = locations[idx];

                // Detect whether the next location and the first location are the same.
                isNextFirst = next.latitude == first.latitude && next.longitude == first.longitude;

                // Inhibit interpolation if either endpoint if the first location,
                // except for the first segement which will be the actual first location or that location
                // as the polygon closes the first time.
                // All subsequent encounters of the first location are used to connected secondary domains with the
                // primary domain in multiply-connected geometry (an outer ring with multiple inner rings).
                isInterpolated = true;
                if (isNextFirst || isPrevFirst) {
                    countFirst += 1;

                    if (countFirst > 2) {
                        isInterpolated = false;
                    }
                }

                if (isInterpolated) {
                    this.interpolateEdge(prev, next, this._locations);
                }

                this._locations.push(next);

                prev = next;
            }

            // Force the closing of the border.
            if (!this._isInteriorInhibited) {
                // Avoid duplication if the first endpoint was already emitted.
                if (prev.latitude != first.latitude || prev.longitude != first.longitude) {
                    this.interpolateEdge(prev, first, this._locations);
                    this._locations.push(first);
                }
            }
        };

        // Internal function. Intentionally not documented.
        SurfaceShape.prototype.interpolateEdge = function (start, end, locations) {
            var distanceRadians = Location.greatCircleDistance(start, end),
                steps = Math.round(this._maximumNumEdgeIntervals * distanceRadians / Math.PI),
                dt,
                location;

            if (steps > 0) {
                dt = 1 / steps;
                location = start;

                for (var t = this.throttledStep(dt, location); t < 1; t += this.throttledStep(dt, location)) {
                    location = new Location(0, 0);
                    Location.interpolateAlongPath(this._pathType, t, start, end, location);

                    //florin: ensure correct longitude sign and decimal error for anti-meridian
                    if (start.longitude === 180 && end.longitude === 180) {
                        location.longitude = 180;
                    }
                    else if (start.longitude === -180 && end.longitude === -180) {
                        location.longitude = -180;
                    }

                    locations.push(location);
                }
            }
        };

        // Internal function. Intentionally not documented.
        // Return a throttled step size when near the poles.
        SurfaceShape.prototype.throttledStep = function (dt, location) {
            var cosLat = Math.cos(location.latitude * Angle.DEGREES_TO_RADIANS);
            cosLat *= cosLat; // Square cos to emphasize poles and de-emphasize equator.

            // Remap polarThrottle:
            //  0 .. INF => 0 .. 1
            // This acts as a weight between no throttle and fill throttle.
            var weight = this._polarThrottle / (1 + this._polarThrottle);

            return dt * ((1 - weight) + weight * cosLat);
        };

        // Internal function. Intentionally not documented.
        SurfaceShape.prototype.prepareBoundaries = function (dc) {
            if (this.boundariesArePrepared) {
                return;
            }

            this.computeBoundaries(dc);

            var newBoundaries = this.formatBoundaries();
            this.normalizeAngles(newBoundaries);
            newBoundaries = this.interpolateBoundaries(newBoundaries);

            var contoursInfo = [];
            var doesCross = PolygonSplitter.splitContours(newBoundaries, contoursInfo);
            this.contours = contoursInfo;
            this.crossesAntiMeridian = doesCross;

            this.prepareGeometry(dc, contoursInfo);

            this.prepareSectors();

            this.boundariesArePrepared = true;
        };

        //Internal. Formats the boundaries of a surface shape to be a multi dimensional array
        SurfaceShape.prototype.formatBoundaries = function () {
            var boundaries = [];
            if (!this._boundaries.length) {
                return boundaries;
            }
            if (this._boundaries[0].latitude != null) {
                //not multi dim array
                boundaries.push(this._boundaries);
            }
            else {
                boundaries = this._boundaries;
            }
            return boundaries;
        };

        // Internal. Resets boundaries for SurfaceShape recomputing.
        SurfaceShape.prototype.resetBoundaries = function () {
            this.boundariesArePrepared = false;
            this.shapeDataCache.clear(false);
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.normalizeAngles = function (boundaries) {
            for (var i = 0, len = boundaries.length; i < len; i++) {
                var polygon = boundaries[i];
                for (var j = 0, lenP = polygon.length; j < lenP; j++) {
                    var point = polygon[j];
                    if (point.longitude < -180 || point.longitude > 180) {
                        point.longitude = Angle.normalizedDegreesLongitude(point.longitude);
                    }
                    if (point.latitude < -90 || point.latitude > 90) {
                        point.latitude = Angle.normalizedDegreesLatitude(point.latitude);
                    }
                }
            }
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.interpolateBoundaries = function (boundaries) {
            var newBoundaries = [];
            for (var i = 0, len = boundaries.length; i < len; i++) {
                var contour = boundaries[i];
                this.interpolateLocations(contour);
                newBoundaries.push(this._locations.slice());
                this._locations.length = 0;
            }
            return newBoundaries;
        };

        /**
         * Computes the bounding sectors for the shape. There will be more than one if the shape crosses the date line,
         * but does not enclose a pole.
         *
         * @param {DrawContext} dc The drawing context containing a globe.
         *
         * @return {Sector[]}  Bounding sectors for the shape.
         */
        SurfaceShape.prototype.computeSectors = function (dc) {
            // Return a previously computed value if it already exists.
            if (this._sectors && this._sectors.length > 0) {
                return this._sectors;
            }

            this.prepareBoundaries(dc);

            return this._sectors;
        };

        /**
         * Computes the extent for the shape based on its sectors.
         *
         * @param {DrawContext} dc The drawing context containing a globe.
         *
         * @return {BoundingBox} The extent for the shape.
         */
        SurfaceShape.prototype.computeExtent = function (dc) {

            if (!this._sectors || this._sectors.length === 0) {
                return null;
            }

            if (!this.currentData) {
                return null;
            }

            if (!this.currentData.extent) {
                this.currentData.extent = new BoundingBox();
            }


            var boxPoints;
            // This surface shape does not cross the international dateline, and therefore has a single bounding sector.
            // Return the box which contains that sector.
            if (this._sectors.length === 1) {
                boxPoints = this._sectors[0].computeBoundingPoints(dc.globe, dc.verticalExaggeration);
                this.currentData.extent.setToVec3Points(boxPoints);
            }
            // This surface crosses the international dateline, and its bounding sectors are split along the dateline.
            // Return a box which contains the corners of the boxes bounding each sector.
            else {
                var boxCorners = [];

                for (var i = 0; i < this._sectors.length; i++) {
                    boxPoints = this._sectors[i].computeBoundingPoints(dc.globe, dc.verticalExaggeration);
                    var box = new BoundingBox();
                    box.setToVec3Points(boxPoints);
                    var corners = box.getCorners();
                    for (var j = 0; j < corners.length; j++) {
                        boxCorners.push(corners[j]);
                    }
                }
                this.currentData.extent.setToVec3Points(boxCorners);
            }

            return this.currentData.extent;

        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.prepareSectors = function () {
            this.determineSectors();
            if (this.crossesAntiMeridian) {
                this.sectorsOverAntiMeridian();
            }
            else {
                this.sectorsNotOverAntiMeridian();
            }
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.determineSectors = function () {
            for (var i = 0, len = this.contours.length; i < len; i++) {
                var contour = this.contours[i];
                var polygons = contour.polygons;
                contour.sectors = [];
                for (var j = 0, lenP = polygons.length; j < lenP; j++) {
                    var polygon = polygons[j];
                    var sector = new Sector(0, 0, 0, 0);
                    sector.setToBoundingSector(polygon);
                    if (this._pathType === WorldWind.GREAT_CIRCLE) {
                        var extremes = Location.greatCircleArcExtremeLocations(polygon);
                        var minLatitude = Math.min(sector.minLatitude, extremes[0].latitude);
                        var maxLatitude = Math.max(sector.maxLatitude, extremes[1].latitude);
                        sector.minLatitude = minLatitude;
                        sector.maxLatitude = maxLatitude;
                    }
                    contour.sectors.push(sector);
                }
            }
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.sectorsOverAntiMeridian = function () {
            var eastSector = new Sector(90, -90, 180, -180); //positive
            var westSector = new Sector(90, -90, 180, -180); //negative
            for (var i = 0, len = this.contours.length; i < len; i++) {
                var sectors = this.contours[i].sectors;
                for (var j = 0, lenS = sectors.length; j < lenS; j++) {
                    var sector = sectors[j];
                    if (sector.minLongitude < 0 && sector.maxLongitude > 0) {
                        westSector.union(sector);
                        eastSector.union(sector);
                    }
                    else if (sector.minLongitude < 0) {
                        westSector.union(sector);
                    }
                    else {
                        eastSector.union(sector);
                    }
                }
            }
            var minLatitude = Math.min(eastSector.minLatitude, westSector.minLatitude);
            var maxLatitude = Math.max(eastSector.maxLatitude, eastSector.maxLatitude);
            this._sector = new Sector(minLatitude, maxLatitude, -180, 180);
            this._sectors = [eastSector, westSector];
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.sectorsNotOverAntiMeridian = function () {
            this._sector = new Sector(90, -90, 180, -180);
            for (var i = 0, len = this.contours.length; i < len; i++) {
                var sectors = this.contours[i].sectors;
                for (var j = 0, lenS = sectors.length; j < lenS; j++) {
                    this._sector.union(sectors[j]);
                }
            }
            this._sectors = [this._sector];
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.prepareGeometry = function (dc, contours) {
            var interiorPolygons = [];
            var outlinePolygons = [];

            for (var i = 0, len = contours.length; i < len; i++) {
                var contour = contours[i];
                var poleIndex = contour.poleIndex;

                for (var j = 0, lenC = contour.polygons.length; j < lenC; j++) {
                    var polygon = contour.polygons[j];
                    var iMap = contour.iMap[j];
                    interiorPolygons.push(polygon);

                    if (contour.pole !== Location.poles.NONE && lenC > 1) {
                        //split with pole
                        if (j === poleIndex) {
                            this.outlineForPole(polygon, iMap, outlinePolygons);
                        }
                        else {
                            this.outlineForSplit(polygon, iMap, outlinePolygons);
                        }
                    }
                    else if (contour.pole !== Location.poles.NONE && lenC === 1) {
                        //only pole
                        this.outlineForPole(polygon, iMap, outlinePolygons);
                    }
                    else if (contour.pole === Location.poles.NONE && lenC > 1) {
                        //only split
                        this.outlineForSplit(polygon, iMap, outlinePolygons);
                    }
                    else if (contour.pole === Location.poles.NONE && lenC === 1) {
                        //no pole, no split
                        outlinePolygons.push(polygon);
                    }
                }
            }

            this._interiorGeometry = interiorPolygons;
            this._outlineGeometry = outlinePolygons;
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.outlineForPole = function (polygon, iMap, outlinePolygons) {
            this.containsPole = true;
            var outlinePolygon = [];
            var pCount = 0;
            for (var k = 0, lenP = polygon.length; k < lenP; k++) {
                var point = polygon[k];
                var intersection = iMap.get(k);
                if (intersection && intersection.forPole) {
                    pCount++;
                    if (pCount % 2 === 1) {
                        outlinePolygon.push(point);
                        outlinePolygons.push(outlinePolygon);
                        outlinePolygon = [];
                    }
                }
                if (pCount % 2 === 0) {
                    outlinePolygon.push(point);
                }
            }
            if (outlinePolygon.length) {
                outlinePolygons.push(outlinePolygon);
            }
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.outlineForSplit = function (polygon, iMap, outlinePolygons) {
            var outlinePolygon = [];
            var iCount = 0;
            for (var k = 0, lenP = polygon.length; k < lenP; k++) {
                var point = polygon[k];
                var intersection = iMap.get(k);
                if (intersection && !intersection.forPole) {
                    iCount++;
                    if (iCount % 2 === 0) {
                        outlinePolygon.push(point);
                        outlinePolygons.push(outlinePolygon);
                        outlinePolygon = [];
                    }
                }
                if (iCount % 2 === 1) {
                    outlinePolygon.push(point);
                }
            }
        };

        // Internal use only. Intentionally not documented.
        SurfaceShape.prototype.resetPickColor = function () {
            this.pickColor = null;
        };

        /**
         * Internal use only.
         * Render the shape onto the texture map of the tile.
         * @param {DrawContext} dc The draw context to render onto.
         * @param {CanvasRenderingContext2D} ctx2D The rendering context for SVG.
         * @param {Number} xScale The multiplicative scale factor in the horizontal direction.
         * @param {Number} yScale The multiplicative scale factor in the vertical direction.
         * @param {Number} dx The additive offset in the horizontal direction.
         * @param {Number} dy The additive offset in the vertical direction.
         */
        SurfaceShape.prototype.renderToTexture = function (dc, ctx2D, xScale, yScale, dx, dy) {
            var attributes = (this._highlighted ? (this._highlightAttributes || this._attributes) : this._attributes);
            var drawInterior = (!this._isInteriorInhibited && attributes.drawInterior);
            var drawOutline = (attributes.drawOutline && attributes.outlineWidth > 0);

            if (!drawInterior && !drawOutline) {
                return;
            }

            if (dc.pickingMode && !this.pickColor) {
                this.pickColor = dc.uniquePickColor();
            }

            if (dc.pickingMode) {
                var pickColor = this.pickColor.toHexString();
            }

            if (this.crossesAntiMeridian || this.containsPole) {
                if (drawInterior) {
                    this.draw(this._interiorGeometry, ctx2D, xScale, yScale, dx, dy);
                    ctx2D.fillStyle = dc.pickingMode ? pickColor : attributes.interiorColor.toRGBAString();
                    ctx2D.fill();
                }
                if (drawOutline) {
                    this.draw(this._outlineGeometry, ctx2D, xScale, yScale, dx, dy);
                    ctx2D.lineWidth = attributes.outlineWidth;
                    ctx2D.strokeStyle = dc.pickingMode ? pickColor : attributes.outlineColor.toRGBAString();
                    ctx2D.stroke();
                }
            }
            else {
                this.draw(this._interiorGeometry, ctx2D, xScale, yScale, dx, dy);
                if (drawInterior) {
                    ctx2D.fillStyle = dc.pickingMode ? pickColor : attributes.interiorColor.toRGBAString();
                    ctx2D.fill();
                }
                if (drawOutline) {
                    ctx2D.lineWidth = attributes.outlineWidth;
                    ctx2D.strokeStyle = dc.pickingMode ? pickColor : attributes.outlineColor.toRGBAString();
                    ctx2D.stroke();
                }
            }

            if (dc.pickingMode) {
                var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
                    null, this.layer, false);
                dc.resolvePick(po);
            }
        };

        SurfaceShape.prototype.draw = function (contours, ctx2D, xScale, yScale, dx, dy) {
            ctx2D.beginPath();
            for (var i = 0, len = contours.length; i < len; i++) {
                var contour = contours[i];
                var point = contour[0];
                var x = point.longitude * xScale + dx;
                var y = point.latitude * yScale + dy;
                ctx2D.moveTo(x, y);
                for (var j = 1, lenC = contour.length; j < lenC; j++) {
                    point = contour[j];
                    x = point.longitude * xScale + dx;
                    y = point.latitude * yScale + dy;
                    ctx2D.lineTo(x, y);
                }
            }
        };

        /**
         * Default value for the maximum number of edge intervals. This results in a maximum error of 480 m for an arc
         * that spans the entire globe.
         *
         * Other values for this parameter have the associated errors below:
         * Intervals        Maximum error (meters)
         *      2           1280253.5
         *      4           448124.5
         *      8           120837.6
         *      16          30628.3
         *      32          7677.9
         *      64          1920.6
         *      128         480.2
         *      256         120.0
         *      512         30.0
         *      1024        7.5
         *      2048        1.8
         * The errors cited above are upper bounds and the actual error may be lower.
         * @type {Number}
         */
        SurfaceShape.DEFAULT_NUM_EDGE_INTERVALS = 128;

        /**
         * The defualt value for the polar throttle, which slows edge traversal near the poles.
         * @type {Number}
         */
        SurfaceShape.DEFAULT_POLAR_THROTTLE = 10;

        return SurfaceShape;
    }
);