From 7a68bc70e745058623525e937f3f176fd63b5c04 Mon Sep 17 00:00:00 2001 From: William Wall Date: Mon, 20 May 2019 11:10:19 -0600 Subject: [PATCH] feat: add polar polygon support Adds support for polar polygons by splitting them by the meridian plane for display/query in 2D. Additionally, queries for OGC servers such as GeoServer are now split over the antimeridian in the event that the remote server does not allow coordinates outside of the longitudes [-180, 180]. resolves #540, #579 --- config/settings.json | 26 +++ externs/jsts.externs.js | 30 +++ package.json | 2 +- src/os/geo/geo2.js | 40 ++++ src/os/geo/jsts.js | 200 ++++++++++++++++++ src/os/geom/geometryfield.js | 4 +- src/os/mixin/geometrymixin.js | 15 ++ src/os/query/areamanager.js | 15 -- src/os/query/baseareamanager.js | 15 +- src/os/source/vectorsource.js | 7 +- .../ol/interaction/dragcircleinteraction.js | 2 + .../ol/interaction/drawpolygoninteraction.js | 15 +- src/plugin/ogc/query/ogcspatialformatter.js | 19 +- test/os/geo/jsts.test.js | 107 ++++++++++ 14 files changed, 451 insertions(+), 46 deletions(-) create mode 100644 test/os/geo/jsts.test.js diff --git a/config/settings.json b/config/settings.json index ceaf42bda..dc96652e3 100644 --- a/config/settings.json +++ b/config/settings.json @@ -52,6 +52,32 @@ "methods": ["GET", "POST", "PUT", "DELETE"], "encode": false }, + "projections": [ + { + "code": "EPSG:3031", + "extent": [ + -12369000, + -12369000, + 12369000, + 12369000 + ], + "isGlobal": false, + "proj4": "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs", + "title": "Antarctic Polar Stereographic" + }, + { + "code": "EPSG:3413", + "extent": [ + -12369000, + -12369000, + 12369000, + 12369000 + ], + "isGlobal": false, + "proj4": "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs", + "title": "NSIDC Sea Ice Polar Stereographic North" + } + ], "providers": { "basemap": { "type": "basemap", diff --git a/externs/jsts.externs.js b/externs/jsts.externs.js index cc8375308..925ed3ca9 100644 --- a/externs/jsts.externs.js +++ b/externs/jsts.externs.js @@ -193,6 +193,12 @@ jsts.geom.Geometry.prototype.getCoordinate = function() {}; jsts.geom.Geometry.prototype.getCoordinates = function() {}; +/** + * @return {jsts.geom.Point} + */ +jsts.geom.Geometry.prototype.getInteriorPoint = function() {}; + + /** * @return {number} */ @@ -340,6 +346,12 @@ jsts.geom.GeometryFactory.prototype.createMultiPolygon = function(polygons) {}; jsts.geom.GeometryFactory.prototype.createGeometryCollection = function(geometries) {}; +/** + * @param {jsts.Collection} geom + * @return {Array} + */ +jsts.geom.GeometryFactory.toPolygonArray = function(geom) {}; + /** * @param {Array} geometries @@ -454,6 +466,11 @@ jsts.geom.Polygon.prototype.getInteriorRingN = function(n) {}; jsts.geom.Polygon.prototype.getNumInteriorRing = function() {}; +/** + * @return {jsts.geom.LineString} + */ +jsts.geom.Polygon.prototype.getBoundary = function() {}; + /** * @param {Array} geometries @@ -560,3 +577,16 @@ jsts.operation.polygonize.Polygonizer.prototype.add = function(g) {}; * @return {jsts.Collection} */ jsts.operation.polygonize.Polygonizer.prototype.getPolygons = function() {}; + + +/** + * @type {Object} + */ +jsts.LineStringExtracter = {}; + + +/** + * @param {jsts.geom.Geometry} geom + * @return {jsts.Collection} + */ +jsts.LineStringExtracter.getLines = function(geom) {}; diff --git a/package.json b/package.json index 56688244f..c1cb22410 100644 --- a/package.json +++ b/package.json @@ -276,7 +276,7 @@ "jquery": "3.3.1", "js-polyfills": "^0.1.42", "jschardet": "^1.6.0", - "jsts": "^1.5.0", + "jsts": "^2.0.4", "modernizr": "~3.3.x", "moment": "~2.20.1", "navigator.sendbeacon": "~0.0.x", diff --git a/src/os/geo/geo2.js b/src/os/geo/geo2.js index d6af0a80b..36b52d020 100644 --- a/src/os/geo/geo2.js +++ b/src/os/geo/geo2.js @@ -173,3 +173,43 @@ os.geo2.normalizeGeometryCoordinates = function(geometry, opt_to, opt_proj) { return false; }; + +/** + * Check if a polygon caps one of the poles + * + * @param {ol.geom.Polygon} polygon + * @return {boolean} + */ +os.geo2.isPolarPolygon = function(polygon) { + var proj = /** @type {ol.ProjectionLike} */ (polygon.get(os.geom.GeometryField.PROJECTION) || os.map.PROJECTION); + var rings = polygon.getCoordinates(); + return rings.length ? os.geo2.isPolarRing(rings[0], proj) : false; +}; + + +/** + * Check if a polygon caps one of the poles + * + * @param {Array>} ring The polygon's exterior ring + * @param {ol.ProjectionLike=} opt_proj + * @return {boolean} if the polygon caps a pole + */ +os.geo2.isPolarRing = function(ring, opt_proj) { + opt_proj = ol.proj.get(opt_proj) || os.map.PROJECTION; + var extent = opt_proj.getExtent(); + var width = extent[2] - extent[0]; + var total = 0; + if (ring) { + for (var i = 1, n = ring.length; i < n; i++) { + var dx = ring[i][0] - ring[i - 1][0]; + if (dx > extent[2]) { + dx -= width; + } else if (dx < extent[0]) { + dx += width; + } + total += dx; + } + } + + return Math.abs(total) > 1; +}; diff --git a/src/os/geo/jsts.js b/src/os/geo/jsts.js index 4a7f5c7ee..c788a05a1 100644 --- a/src/os/geo/jsts.js +++ b/src/os/geo/jsts.js @@ -522,6 +522,198 @@ os.geo.jsts.toPolygon = function(geometry) { }; +/** + * @param {jsts.geom.Geometry} geometry + * @return {Array} + * @private + */ +os.geo.jsts.polygonize_ = function(geometry) { + var lines = jsts.LineStringExtracter.getLines(geometry); + var polygonizer = new jsts.operation.polygonize.Polygonizer(); + polygonizer.add(/** @type {jsts.Collection} */ (lines)); + var polys = polygonizer.getPolygons(); + return jsts.geom.GeometryFactory.toPolygonArray(polys); +}; + + +/** + * @param {ol.geom.Polygon|ol.geom.MultiPolygon} polygon + * @param {ol.geom.LineString} line + * @return {ol.geom.Polygon|ol.geom.MultiPolygon} + */ +os.geo.jsts.splitPolygonByLine = function(polygon, line) { + if (polygon && line) { + var olp = os.geo.jsts.OLParser.getInstance(); + var jstsPolygon = olp.read(polygon); + var jstsLine = olp.read(line); + + if (jstsPolygon && jstsLine) { + var nodedLinework = jstsPolygon.getBoundary().union(jstsLine); + var polys = os.geo.jsts.polygonize_(nodedLinework); + + // only keep polygons which are inside the input + polys = polys.filter(function(poly) { + return jstsPolygon.contains(poly.getInteriorPoint()); + }); + + polys = /** @type {Array} */ (polys.map(olp.write.bind(olp))); + + var rings = []; + for (var i = 0, n = polys.length; i < n; i++) { + rings = rings.concat(polys[i].getCoordinates()); + } + + if (polys.length > 1) { + var multi = new ol.geom.MultiPolygon([]); + multi.setPolygons(polys); + return multi; + } else if (polys.length > 0) { + return polys[0]; + } + } + } + + return polygon; +}; + + +/** + * @param {Array} polygons + * @private + */ +os.geo.jsts.flattenPolys_ = function(polygons) { + // do not cache length here as it is changing + for (var i = 0; i < polygons.length; i++) { + var item = polygons[i]; + if (item.getType() === ol.geom.GeometryType.MULTI_POLYGON) { + var args = [i, 1].concat(/** @type {ol.geom.MultiPolygon} */ (item).getPolygons()); + Array.prototype.splice.apply(polygons, args); + } + } +}; + + +/** + * @param {ol.geom.Polygon} polygon + * @return {boolean} + */ +os.geo.jsts.needsSplit = function(polygon) { + var coords = polygon.getCoordinates()[0]; + var proj = ol.proj.get(/** @type {string} */ (polygon.get(os.geom.GeometryField.PROJECTION)) || + os.map.PROJECTION); + + if (os.geo2.isPolarRing(coords, proj)) { + return true; + } + + var extent = proj.getExtent(); + var halfWidth = (extent[2] - extent[0]) / 2; + + for (var i = 1, n = coords.length; i < n; i++) { + if (Math.abs(coords[i][0] - coords[i - 1][0]) > halfWidth) { + return true; + } + } + + return false; +}; + + +proj4.defs('EPSG:3031', + '+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); +proj4.defs('EPSG:3413', + '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + + +/** + * @param {ol.geom.Polygon|ol.geom.MultiPolygon} polygon + * @return {ol.geom.Polygon|ol.geom.MultiPolygon} + */ +os.geo.jsts.splitPolarPolygon = function(polygon) { + if (polygon.getType() === ol.geom.GeometryType.MULTI_POLYGON) { + var multi = /** @type {ol.geom.MultiPolygon} */ (polygon); + var polygons = multi.getPolygons(); + polygons = polygons.map(os.geo.jsts.splitPolarPolygon); + os.geo.jsts.flattenPolys_(polygons); + multi.setPolygons(polygons); + return multi; + } else if (os.geo.jsts.needsSplit(/** @type {ol.geom.Polygon} */ (polygon))) { + var north = ol.extent.getCenter(polygon.getExtent())[1] >= 0; + polygon.toLonLat(); + var pLonLat = ol.proj.get(os.proj.EPSG4326); + var pPolar = ol.proj.get(north ? 'EPSG:3413' : 'EPSG:3031'); + var pFinal = os.map.PROJECTION; + + polygon.transform(pLonLat, pPolar); + var meridian = new ol.geom.LineString([[0, 0], [0, north ? 90 : -90], [180, 0]]); + meridian.transform(pLonLat, pPolar); + + var result = os.geo.jsts.splitPolygonByLine(polygon, meridian); + result.transform(pPolar, pLonLat); + + os.geo2.normalizeGeometryCoordinates(result, 0, os.proj.EPSG4326); + + if (result.getType() === ol.geom.GeometryType.MULTI_POLYGON) { + var polys = result.getCoordinates(); + + // The pole point needs go "straight up" a EPSG:4326 projection to the pole, which really + // means that we need two pole points, with one matching the longitude of the previous non-pole + // point and one matching the longitude of the next non-pole point. + for (var p = 0, pp = polys.length; p < pp; p++) { + var rings = polys[p]; + for (var r = 0, rr = rings.length; r < rr; r++) { + var ring = rings[r]; + + // not caching ring length because we are changing it + var sumLon = 0; + var numLons = 0; + for (var i = 0; i < ring.length; i++) { + var lon = ring[i][0]; + + if (Math.abs(Math.abs(lon) - 180) > 1E-12) { + sumLon += lon; + numLons++; + } + + if (Math.abs(Math.abs(ring[i][1]) - 90) < 1E-12) { + var idx = goog.math.modulo(i - 1, ring.length); + var before = ring[i].slice(); + before[0] = ring[idx][0]; + + idx = goog.math.modulo(i + 1, ring.length); + var after = ring[i].slice(); + after[0] = ring[idx][0]; + + ring.splice(i, 1, before, after); + i++; + } + } + + // Normalization puts all antimeridian coordinates at -180. For eastern + // hemisphere polygons, we need those to be 180 + if (sumLon / numLons >= 0) { + var n = ring.length; + for (i = 0; i < n; i++) { + if (Math.abs(Math.abs(ring[i][0]) - 180) < 1E-12) { + ring[i][0] = 180; + } + } + } + } + } + + result.setCoordinates(polys); + result.transform(pLonLat, pFinal); + result.set(os.geom.GeometryField.NORMALIZED, true); + result.set(os.geom.GeometryField.POLE, north); + } + return result; + } + + return polygon; +}; + + /** * Adds the target geometry to the source geometry. * @@ -1132,6 +1324,14 @@ os.geo.jsts.validate = function(geometry, opt_quiet, opt_undefinedIfInvalid) { return undefined; } + var pole = geometry.get(os.geom.GeometryField.POLE); + if (pole != null) { + // If a geometry was split over the pole by os.geo.jsts.splitPolarPolygon then each of the polygons in the + // MultiPolygon are valid. The MultiPolygon is technically not valid because they touch on a segment (the + // segment(s) making up the meridian/antimeridian). + return geometry; + } + var geomType = geometry.getType(); if (geomType == ol.geom.GeometryType.POLYGON || geomType == ol.geom.GeometryType.MULTI_POLYGON) { try { diff --git a/src/os/geom/geometryfield.js b/src/os/geom/geometryfield.js index bfde54449..81432d175 100644 --- a/src/os/geom/geometryfield.js +++ b/src/os/geom/geometryfield.js @@ -7,5 +7,7 @@ goog.provide('os.geom.GeometryField'); */ os.geom.GeometryField = { DIRTY: '_dirty', - NORMALIZED: '_normalized' + NORMALIZED: '_normalized', + POLE: '_pole', + PROJECTION: '_projection' }; diff --git a/src/os/mixin/geometrymixin.js b/src/os/mixin/geometrymixin.js index 07abf9b9d..ba910d8a3 100644 --- a/src/os/mixin/geometrymixin.js +++ b/src/os/mixin/geometrymixin.js @@ -145,7 +145,22 @@ ol.geom.Geometry.prototype.toLonLat = function() { }; + (function() { + var oldTransform = ol.geom.Geometry.prototype.transform; + + /** + * @param {ol.ProjectionLike} pFrom + * @param {ol.ProjectionLike} pTo + * @return {ol.geom.Geometry} + */ + ol.geom.Geometry.prototype.transform = function(pFrom, pTo) { + var to = ol.proj.get(pTo); + this.set(os.geom.GeometryField.PROJECTION, to.getCode()); + return oldTransform.call(this, pFrom, to); + }; + + /** * Openlayers' implementation does not actually clone the underlying geometries * @return {!ol.geom.GeometryCollection} The clone diff --git a/src/os/query/areamanager.js b/src/os/query/areamanager.js index 5b29ee21b..fcf30c89e 100644 --- a/src/os/query/areamanager.js +++ b/src/os/query/areamanager.js @@ -357,21 +357,6 @@ os.query.AreaManager.prototype.addInternal = function(feature, opt_bulk) { }; -/** - * @inheritDoc - */ -os.query.AreaManager.prototype.normalizeGeometry = function(feature) { - // TODO: I'd prefer to have the normalizeGeometryCoordinate method take an optional projection - // rather than converting to/from EPSG:4326 - var geom = feature.getGeometry(); - - if (!geom.get(os.geom.GeometryField.NORMALIZED)) { - os.geo2.normalizeGeometryCoordinates(geom); - feature.setGeometry(geom); - } -}; - - /** * Converts all color types to a standard rgba string. * diff --git a/src/os/query/baseareamanager.js b/src/os/query/baseareamanager.js index 14d2bfc51..0f48c9da6 100644 --- a/src/os/query/baseareamanager.js +++ b/src/os/query/baseareamanager.js @@ -249,6 +249,10 @@ os.query.BaseAreaManager.prototype.isValidFeature = function(feature) { var isValid = false; var geomType = geometry.getType(); if (geomType == ol.geom.GeometryType.POLYGON || geomType == ol.geom.GeometryType.MULTI_POLYGON) { + geometry = os.geo.jsts.splitPolarPolygon( + /** @type {ol.geom.Polygon|ol.geom.MultiPolygon} */ (geometry)); + os.interpolate.interpolateGeom(geometry); + var validated = os.geo.jsts.validate(geometry); var validatedOriginal = null; @@ -274,22 +278,13 @@ os.query.BaseAreaManager.prototype.isValidFeature = function(feature) { } if (isValid) { - this.normalizeGeometry(feature); + os.geo2.normalizeGeometryCoordinates(feature.getGeometry()); } return isValid; }; -/** - * @param {ol.Feature} feature - * @protected - */ -os.query.BaseAreaManager.prototype.normalizeGeometry = function(feature) { - os.geo2.normalizeGeometryCoordinates(feature.getGeometry()); -}; - - /** * filter the list of features to only include valid areas * @param {Array} features diff --git a/src/os/source/vectorsource.js b/src/os/source/vectorsource.js index 7c3c0715a..5c8e3063f 100644 --- a/src/os/source/vectorsource.js +++ b/src/os/source/vectorsource.js @@ -1951,12 +1951,13 @@ os.source.Vector.prototype.processImmediate = function(feature) { geom = os.geo.splitOnDateLine(/** @type {!(ol.geom.LineString|ol.geom.MultiLineString)} */ (geom)); geom.osTransform(); feature.setGeometry(geom); - } else if (!geom.get(os.geom.GeometryField.NORMALIZED)) { - // normalize non-point geometries unless they were normalized elsewhere - os.geo2.normalizeGeometryCoordinates(geom); + } else if (geomType === ol.geom.GeometryType.POLYGON || geomType === ol.geom.GeometryType.MULTI_POLYGON) { + geom = os.geo.jsts.splitPolarPolygon( + /** @type {ol.geom.Polygon|ol.geom.MultiPolygon} */ (geom)); } } + os.geo2.normalizeGeometryCoordinates(geom); os.interpolate.interpolateFeature(feature); // make sure the internal feature ID field is set diff --git a/src/os/ui/ol/interaction/dragcircleinteraction.js b/src/os/ui/ol/interaction/dragcircleinteraction.js index 9d17e0b45..1c4419100 100644 --- a/src/os/ui/ol/interaction/dragcircleinteraction.js +++ b/src/os/ui/ol/interaction/dragcircleinteraction.js @@ -55,7 +55,9 @@ os.ui.ol.interaction.DragCircle.prototype.getGeometry = function() { var geom = this.circle2D.getOriginalGeometry(); if (geom) { + geom = os.geo.jsts.splitPolarPolygon(geom); os.geo2.normalizeGeometryCoordinates(geom); + os.interpolate.interpolateGeom(geom); } return geom; diff --git a/src/os/ui/ol/interaction/drawpolygoninteraction.js b/src/os/ui/ol/interaction/drawpolygoninteraction.js index 0982cf5a2..35b629fb9 100644 --- a/src/os/ui/ol/interaction/drawpolygoninteraction.js +++ b/src/os/ui/ol/interaction/drawpolygoninteraction.js @@ -76,20 +76,9 @@ os.ui.ol.interaction.DrawPolygon.prototype.getGeometry = function() { var geom = new ol.geom.Polygon([this.coords]); var method = os.interpolate.getMethod(); geom.set(os.interpolate.METHOD_FIELD, method); - geom.toLonLat(); - - // normalize coordinates prior to validation, or polygons crossing the date line may be broken - os.geo2.normalizeGeometryCoordinates(geom, undefined, os.proj.EPSG4326); - - // then interpolate so the coordinates reflect what was drawn - os.interpolate.beginTempInterpolation(os.proj.EPSG4326, method); + geom = os.geo.jsts.splitPolarPolygon(geom); + os.geo2.normalizeGeometryCoordinates(geom); os.interpolate.interpolateGeom(geom); - os.interpolate.endTempInterpolation(); - - // finally validate the geometry to ensure it's accepted in server queries - geom = os.geo.jsts.validate(geom); - - geom.osTransform(); return geom; }; diff --git a/src/plugin/ogc/query/ogcspatialformatter.js b/src/plugin/ogc/query/ogcspatialformatter.js index 41ff85fb1..d625ccedb 100644 --- a/src/plugin/ogc/query/ogcspatialformatter.js +++ b/src/plugin/ogc/query/ogcspatialformatter.js @@ -1,8 +1,10 @@ goog.provide('plugin.ogc.query.OGCSpatialFormatter'); +goog.require('ol.geom.GeometryType'); +goog.require('ol.geom.LineString'); +goog.require('os.geo'); +goog.require('os.geo2'); goog.require('os.ogc.filter.OGCSpatialFormatter'); - - /** * @param {string=} opt_column * @extends {os.ogc.filter.OGCSpatialFormatter} @@ -22,7 +24,18 @@ plugin.ogc.query.OGCSpatialFormatter.prototype.getGeometry = function(feature) { if (geom) { geom = geom.clone().toLonLat(); - os.geo2.normalizeGeometryCoordinates(geom, undefined, os.proj.EPSG4326); + var target = undefined; + var type = geom.getType(); + + if ((type === ol.geom.GeometryType.POLYGON || type === ol.geom.GeometryType.MULTI_POLYGON) + && os.geo.crossesDateLine(geom)) { + var antimeridian = geom.getExtent()[0] >= -180 ? 180 : -180; + geom = os.geo.jsts.splitPolygonByLine(/** @type {ol.geom.Polygon|ol.geom.MultiPolygon} */ (geom), + new ol.geom.LineString([[antimeridian, -90], [antimeridian, 90]])); + target = 0; + } + + os.geo2.normalizeGeometryCoordinates(geom, target, os.proj.EPSG4326); } return geom; diff --git a/test/os/geo/jsts.test.js b/test/os/geo/jsts.test.js new file mode 100644 index 000000000..68bf6bff2 --- /dev/null +++ b/test/os/geo/jsts.test.js @@ -0,0 +1,107 @@ +goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiPolygon'); +goog.require('ol.geom.Polygon'); +goog.require('os.geo.jsts'); +goog.require('os.proj'); + +describe('os.geo.jsts', function() { + it('should split polygons properly', function() { + var polygon = new ol.geom.Polygon.fromExtent([-2, -2, 2, 2]); + var line = new ol.geom.LineString([[0, -4], [0, 4]]); + + var result = os.geo.jsts.splitPolygonByLine(polygon, line); + expect(result instanceof ol.geom.MultiPolygon).toBe(true); + var polys = result.getPolygons(); + expect(polys.length).toBe(2); + + expect(polys[0].getExtent()).toEqual([-2, -2, 0, 2]); + expect(polys[1].getExtent()).toEqual([0, -2, 2, 2]); + + polys.forEach(function(poly) { + expect(os.geo.isClosed(poly.getCoordinates()[0])).toBe(true); + }); + }); + + it('should split polygons with a z-coord properly', function() { + var polygon = new ol.geom.Polygon([[ + [-2, -2, 2], + [2, -2, 4], + [2, 2, 4], + [-2, 2, 2], + [-2, -2, 2]]]); + + var line = new ol.geom.LineString([[0, -4, 0], [0, 4, 0]]); + + var result = os.geo.jsts.splitPolygonByLine(polygon, line); + expect(result instanceof ol.geom.MultiPolygon).toBe(true); + var polys = result.getPolygons(); + expect(polys.length).toBe(2); + + expect(polys[0].getExtent()).toEqual([-2, -2, 0, 2]); + expect(polys[1].getExtent()).toEqual([0, -2, 2, 2]); + + polys.forEach(function(poly) { + var rings = poly.getCoordinates(); + expect(os.geo.isClosed(rings[0])).toBe(true); + + for (var i = 0, n = rings[0].length; i < n; i++) { + var coord = rings[0][i]; + if (coord[0] === -2) { + expect(coord[2]).toBe(2); + } else if (coord[0] === 0) { + expect(coord[2]).toBe(3); + } else if (coord[0] === 2) { + expect(coord[2]).toBe(4); + } + } + }); + }); + + it('should split north polar polygons properly', function() { + proj4.defs('EPSG:3413', + '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + + var polygon = new ol.geom.Polygon([[ + [0, 85], + [90, 85], + [180, 85], + [270, 85], + [0, 85]]]); + + var result = os.geo.jsts.splitPolarPolygon(polygon); + expect(result instanceof ol.geom.MultiPolygon).toBe(true); + var polys = result.getPolygons(); + expect(polys.length).toBe(2); + + expect(polys[0].getExtent()).toEqual([-180, 85, 0, 90]); + expect(polys[1].getExtent()).toEqual([0, 85, 180, 90]); + + polys.forEach(function(poly) { + expect(os.geo.isClosed(poly.getCoordinates()[0])).toBe(true); + }); + }); + + it('should split south polar polygons properly', function() { + proj4.defs('EPSG:3031', + '+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + + var polygon = new ol.geom.Polygon([[ + [0, -85], + [90, -85], + [180, -85], + [270, -85], + [0, -85]]]); + + var result = os.geo.jsts.splitPolarPolygon(polygon); + expect(result instanceof ol.geom.MultiPolygon).toBe(true); + var polys = result.getPolygons(); + expect(polys.length).toBe(2); + + expect(polys[0].getExtent()).toEqual([-180, -90, 0, -85]); + expect(polys[1].getExtent()).toEqual([0, -90, 180, -85]); + + polys.forEach(function(poly) { + expect(os.geo.isClosed(poly.getCoordinates()[0])).toBe(true); + }); + }); +});