From 89ec9bf07a84051e3a877653b72690edf43d48a7 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Fri, 6 Jul 2012 12:16:02 +0200 Subject: [PATCH] Update ol.Projection --- Makefile | 1 - src/all.js | 1 + src/ol/Projection.js | 257 -------------------------- src/ol/projection.js | 368 ++++++++++++++++++++++++++++++++++++++ src/ol/projection_test.js | 87 +++++++++ 5 files changed, 456 insertions(+), 258 deletions(-) delete mode 100644 src/ol/Projection.js create mode 100644 src/ol/projection.js create mode 100644 src/ol/projection_test.js diff --git a/Makefile b/Makefile index ff829060b0..683661c3ba 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,6 @@ GSLINT_EXCLUDES= \ src/ol/Loc.js \ src/ol/Map.js \ src/ol/Popup.js \ - src/ol/Projection.js \ src/ol/renderer/Composite.js \ src/ol/renderer/LayerRenderer.js \ src/ol/renderer/MapRenderer.js \ diff --git a/src/all.js b/src/all.js index 17dcebca05..015da3bdb1 100644 --- a/src/all.js +++ b/src/all.js @@ -6,6 +6,7 @@ goog.require('ol.Extent'); goog.require('ol.LayerView'); goog.require('ol.MVCArray'); goog.require('ol.MVCObject'); +goog.require('ol.Projection'); goog.require('ol.TileBounds'); goog.require('ol.TileCoord'); goog.require('ol.TileGrid'); diff --git a/src/ol/Projection.js b/src/ol/Projection.js deleted file mode 100644 index 78e38d7d09..0000000000 --- a/src/ol/Projection.js +++ /dev/null @@ -1,257 +0,0 @@ -goog.provide('ol.Projection'); -goog.require('ol.UnreferencedBounds'); - -/** - * @export - * @constructor - * @param {string} code Projection identifier. - */ -ol.Projection = function(code) { - - /** - * @private - * @type {string} - */ - this.code_ = code; - - /** - * @private - * @type {string|undefined} - */ - this.units_ = undefined; - - /** - * @private - * @type {Object} - */ - this.proj_ = null; - - /** - * @private - * @type {ol.UnreferencedBounds} - */ - this.extent_ = null; - -}; - - -/** - * @return {string} Code. - */ -ol.Projection.prototype.getCode = function() { - return this.code_; -}; - -/** - * @param {string} code Code. - */ -ol.Projection.prototype.setCode = function(code) { - this.code_ = code; -}; - -/** - * @return {string|undefined} Units abbreviation. - */ -ol.Projection.prototype.getUnits = function() { - return this.units_; -}; - -/** - * @param {string} units Units abbreviation. - */ -ol.Projection.prototype.setUnits = function(units) { - this.units_ = units; -}; - -/** - * Get the validity extent of the coordinate reference system. - * - * @return {ol.UnreferencedBounds} The valididty extent. - */ -ol.Projection.prototype.getExtent = function() { - if (goog.isNull(this.extent_)) { - var defs = ol.Projection.defaults[this.code_]; - if (goog.isDef(defs)) { - var ext = defs.maxExtent; - if (goog.isDef(ext)) { - this.setExtent(new ol.UnreferencedBounds(ext[0],ext[1],ext[2],ext[3])); - } - } - } - return this.extent_; -}; - -/** - * @param {!ol.UnreferencedBounds} extent Validity extent. - */ -ol.Projection.prototype.setExtent = function(extent) { - this.extent_ = extent; -}; - -/** - * Transforms is an object, with from properties, each of which may - * have a to property. This allows you to define projections without - * requiring support for proj4js to be included. - * - * This object has keys which correspond to a 'source' projection object. The - * keys should be strings, corresponding to the projection.getCode() value. - * Each source projection object should have a set of destination projection - * keys included in the object. - * - * Each value in the destination object should be a transformation function, - * where the function is expected to be passed an object with a .x and a .y - * property. The function should return the object, with the .x and .y - * transformed according to the transformation function. - * - * Note - Properties on this object should not be set directly. To add a - * transform method to this object, use the method. For an - * example of usage, see the OpenLayers.Layer.SphericalMercator file. - * - * @type {Object} - */ -ol.Projection.transforms = {}; - -/** - * Defaults for the SRS codes known to OpenLayers (currently EPSG:4326, CRS:84, - * urn:ogc:def:crs:EPSG:6.6:4326, EPSG:900913, EPSG:3857, EPSG:102113 and - * EPSG:102100). Keys are the SRS code, values are units, maxExtent (the - * validity extent for the SRS) and yx (true if this SRS is known to have a - * reverse axis order). - * - * @type {Object} - */ -ol.Projection.defaults = { - "EPSG:4326": { - units: "degrees", - maxExtent: [-180, -90, 180, 90], - yx: true - }, - "CRS:84": { - units: "degrees", - maxExtent: [-180, -90, 180, 90] - }, - "EPSG:900913": { - units: "m", - maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34] - } -}; - -/** - * Set a custom transform method between two projections. Use this method in - * cases where the proj4js lib is not available or where custom projections - * need to be handled. - * - * @param {string} from The code for the source projection. - * @param {string} to The code for the destination projection. - * @param {function(Object)} method A function that takes an object with x and - * y properties as an argument and transforms that point from the source to - * the destination projection in place. The original point should be - * modified. - */ -ol.Projection.addTransform = function(from, to, method) { - if (method === ol.Projection.nullTransform) { - var defaults = ol.Projection.defaults[from]; - if (defaults && !ol.Projection.defaults[to]) { - ol.Projection.defaults[to] = defaults; - } - } - if(!ol.Projection.transforms[from]) { - ol.Projection.transforms[from] = {}; - } - ol.Projection.transforms[from][to] = method; -}; - -/** - * Transform a point coordinate from one projection to another. - * - * @param {Object} point Object with x and y properties. - * @param {ol.Projection} source Source projection. - * @param {ol.Projection} dest Destination projection. - */ -ol.Projection.transform = function(point, source, dest) { - goog.asserts.assertObject(point); - goog.asserts.assertObject(source); - goog.asserts.assertObject(dest); - if (source.proj_ && dest.proj_) { - // TODO: implement Proj4js handling - // point = Proj4js.transform(source.proj_, dest.proj_, point); - } else { - var sourceCode = source.getCode(); - var destCode = dest.getCode(); - var transforms = ol.Projection.transforms; - if (transforms[sourceCode] && transforms[sourceCode][destCode]) { - transforms[sourceCode][destCode](point); - } - } -}; - -/** - * A null transformation - useful for defining projection aliases when - * proj4js is not available: - * - * ol.Projection.addTransform("EPSG:3857", "EPSG:900913", - * ol.Projection.nullTransform); - * ol.Projection.addTransform("EPSG:900913", "EPSG:3857", - * ol.Projection.nullTransform); - * - * @type {function(Object)} - */ -ol.Projection.nullTransform = function(point) { - return point; -}; - -/** - * Note: Transforms for web mercator <-> geographic - * OpenLayers recognizes EPSG:3857, EPSG:900913, EPSG:102113 and EPSG:102100. - * OpenLayers originally started referring to EPSG:900913 as web mercator. - * The EPSG has declared EPSG:3857 to be web mercator. - * ArcGIS 10 recognizes the EPSG:3857, EPSG:102113, and EPSG:102100 as - * equivalent. See http://blogs.esri.com/Dev/blogs/arcgisserver/archive/2009/11/20/ArcGIS-Online-moving-to-Google-_2F00_-Bing-tiling-scheme_3A00_-What-does-this-mean-for-you_3F00_.aspx#12084. - * For geographic, OpenLayers recognizes EPSG:4326, CRS:84 and - * urn:ogc:def:crs:EPSG:6.6:4326. OpenLayers also knows about the reverse axis - * order for EPSG:4326. - */ -(function() { - - var pole = 20037508.34; - - function inverseMercator(xy) { - xy.x = 180 * xy.x / pole; - xy.y = 180 / Math.PI * (2 * Math.atan(Math.exp((xy.y / pole) * Math.PI)) - Math.PI / 2); - return xy; - } - - function forwardMercator(xy) { - xy.x = xy.x * pole / 180; - xy.y = Math.log(Math.tan((90 + xy.y) * Math.PI / 360)) / Math.PI * pole; - return xy; - } - - function map(base, codes) { - var add = ol.Projection.addTransform; - var same = ol.Projection.nullTransform; - var i, len, code, other, j; - for (i=0, len=codes.length; i=0; --i) { - map(mercator[i], geographic); - } - for (i=geographic.length-1; i>=0; --i) { - map(geographic[i], mercator); - } - -})(); diff --git a/src/ol/projection.js b/src/ol/projection.js new file mode 100644 index 0000000000..177d584f22 --- /dev/null +++ b/src/ol/projection.js @@ -0,0 +1,368 @@ +goog.provide('ol.Projection'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.math.Coordinate'); +goog.require('goog.object'); +goog.require('ol.Extent'); + + +/** + * @typedef {function(goog.math.Coordinate): goog.math.Coordinate} + */ +ol.TransformFunction; + + +/** + * @enum {string} + */ +ol.ProjectionUnits = { + DEGREES: 'degrees', + METERS: 'm' +}; + + + +/** + * @constructor + * @param {string} code Code. + * @param {ol.ProjectionUnits} units Units. + * @param {ol.Extent} extent Extent. + */ +ol.Projection = function(code, units, extent) { + + /** + * @private + * @type {string} + */ + this.code_ = code; + + /** + * @private + * @type {ol.ProjectionUnits} + */ + this.units_ = units; + + /** + * @private + * @type {ol.Extent} + */ + this.extent_ = extent; + +}; + + +/** + * @return {string} Code. + */ +ol.Projection.prototype.getCode = function() { + return this.code_; +}; + + +/** + * @return {ol.Extent} Extent. + */ +ol.Projection.prototype.getExtent = function() { + return this.extent_; +}; + + +/** + * @return {ol.ProjectionUnits} Units. + */ +ol.Projection.prototype.getUnits = function() { + return this.units_; +}; + + +/** + * @private + * @type {Object.} + */ +ol.Projection.projections_ = {}; + + +/** + * @private + * @type {Object.>} + */ +ol.Projection.transforms_ = {}; + + +/** + * @param {Array.} projections Projections. + */ +ol.Projection.addEquivalentProjections = function(projections) { + ol.Projection.addProjections(projections); + goog.array.forEach(projections, function(source) { + goog.array.forEach(projections, function(destination) { + ol.Projection.addTransform( + source, destination, ol.Projection.identityTransform); + }); + }); +}; + + +/** + * @param {Array.} projections1 Projections. + * @param {Array.} projections2 Projections. + * @param {ol.TransformFunction} forwardTransform Forward transform. + * @param {ol.TransformFunction} inverseTransform Inverse transform. + */ +ol.Projection.addEquivalentTransforms = + function(projections1, projections2, forwardTransform, inverseTransform) { + goog.array.forEach(projections1, function(projection1) { + goog.array.forEach(projections2, function(projection2) { + ol.Projection.addTransform(projection1, projection2, forwardTransform); + ol.Projection.addTransform(projection2, projection1, inverseTransform); + }); + }); +}; + + +/** + * @param {ol.Projection} projection Projection. + */ +ol.Projection.addProjection = function(projection) { + var projections = ol.Projection.projections_; + var code = projection.getCode(); + goog.asserts.assert(!goog.object.containsKey(projections, code)); + projections[code] = projection; +}; + + +/** + * @param {Array.} projections Projections. + */ +ol.Projection.addProjections = function(projections) { + goog.array.forEach(projections, function(projection) { + ol.Projection.addProjection(projection); + }); +}; + + +/** + * @param {ol.Projection} source Source. + * @param {ol.Projection} destination Destination. + * @param {ol.TransformFunction} transform Transform. + */ +ol.Projection.addTransform = function(source, destination, transform) { + var projections = ol.Projection.projections_; + var sourceCode = source.getCode(); + goog.asserts.assert(goog.object.containsKey(projections, sourceCode)); + var destinationCode = destination.getCode(); + goog.asserts.assert(goog.object.containsKey(projections, destinationCode)); + var transforms = ol.Projection.transforms_; + if (!goog.object.containsKey(transforms, sourceCode)) { + transforms[sourceCode] = {}; + } + goog.asserts.assert( + !goog.object.containsKey(transforms[sourceCode], destinationCode)); + transforms[sourceCode][destinationCode] = transform; +}; + + +/** + * @param {string} code Code. + * @return {ol.Projection} Projection. + */ +ol.Projection.createFromCode = function(code) { + var projections = ol.Projection.projections_; + goog.asserts.assert(goog.object.containsKey(projections, code)); + return projections[code]; +}; + + +/** + * @param {ol.Projection} projection1 Projection 1. + * @param {ol.Projection} projection2 Projection 2. + * @return {boolean} Equivalent. + */ +ol.Projection.equivalent = function(projection1, projection2) { + if (projection1.getUnits() != projection2.getUnits()) { + return false; + } else { + var transform = ol.Projection.getTransform(projection1, projection2); + return transform === ol.Projection.identityTransform; + } +}; + + +/** + * @param {ol.Projection} source Source. + * @param {ol.Projection} destination Destination. + * @return {ol.TransformFunction} Transform. + */ +ol.Projection.getTransform = function(source, destination) { + var transforms = ol.Projection.transforms_; + var sourceCode = source.getCode(); + var destinationCode = destination.getCode(); + goog.asserts.assert(goog.object.containsKey(transforms, sourceCode)); + goog.asserts.assert( + goog.object.containsKey(transforms[sourceCode], destinationCode)); + return transforms[sourceCode][destinationCode]; +}; + + +/** + * @param {string} sourceCode Source code. + * @param {string} destinationCode Destination code. + * @return {ol.TransformFunction} Transform. + */ +ol.Projection.getTransformFromCodes = function(sourceCode, destinationCode) { + var source = ol.Projection.createFromCode(sourceCode); + var destination = ol.Projection.createFromCode(destinationCode); + return ol.Projection.getTransform(source, destination); +}; + + +/** + * @param {goog.math.Coordinate} point Point. + * @return {goog.math.Coordinate} Point. + */ +ol.Projection.identityTransform = function(point) { + return point.clone(); +}; + + +/** + * @param {goog.math.Coordinate} point Point. + * @param {ol.Projection} source Source. + * @param {ol.Projection} destination Destination. + * @return {goog.math.Coordinate} Point. + */ +ol.Projection.transform = function(point, source, destination) { + var transform = ol.Projection.getTransform(source, destination); + return transform(point); +}; + + +/** + * @param {goog.math.Coordinate} point Point. + * @param {string} sourceCode Source code. + * @param {string} destinationCode Destination code. + * @return {goog.math.Coordinate} Point. + */ +ol.Projection.transformWithCodes = + function(point, sourceCode, destinationCode) { + var transform = ol.Projection.getTransformFromCodes( + sourceCode, destinationCode); + return transform(point); +}; + + +/** + * @const + * @type {number} + */ +ol.Projection.EPSG_3857_RADIUS = 6378137; + + +/** + * @param {goog.math.Coordinate} point Point. + * @return {goog.math.Coordinate} Point. + */ +ol.Projection.forwardSphericalMercator = function(point) { + var x = ol.Projection.EPSG_3857_RADIUS * Math.PI * point.x / 180; + var y = ol.Projection.EPSG_3857_RADIUS * + Math.log(Math.tan(Math.PI * (point.y + 90) / 360)); + return new goog.math.Coordinate(x, y); +}; + + +/** + * @param {goog.math.Coordinate} point Point. + * @return {goog.math.Coordinate} Point. + */ +ol.Projection.inverseSphericalMercator = function(point) { + var x = 180 * point.x / (ol.Projection.EPSG_3857_RADIUS * Math.PI); + var y = 360 * Math.atan( + Math.exp(point.y / ol.Projection.EPSG_3857_RADIUS)) / Math.PI - 90; + return new goog.math.Coordinate(x, y); +}; + + +/** + * @const + * @type {number} + */ +ol.Projection.EPSG_3857_HALF_SIZE = Math.PI * ol.Projection.EPSG_3857_RADIUS; + + +/** + * @const + * @type {ol.Extent} + */ +ol.Projection.EPSG_3857_EXTENT = new ol.Extent( + ol.Projection.EPSG_3857_HALF_SIZE, + ol.Projection.EPSG_3857_HALF_SIZE, + -ol.Projection.EPSG_3857_HALF_SIZE, + -ol.Projection.EPSG_3857_HALF_SIZE); + + +/** + * @type {Array.} + */ +ol.Projection.EPSG_3857_LIKE_CODES = [ + 'EPSG:3857', + 'EPSG:102113', + 'EPSG:102100', + 'EPSG:900913' +]; + + +/** + * @const + * @type {Array.} + */ +ol.Projection.EPSG_3857_LIKE_PROJECTIONS = goog.array.map( + ol.Projection.EPSG_3857_LIKE_CODES, + function(code) { + return new ol.Projection( + code, + ol.ProjectionUnits.METERS, + ol.Projection.EPSG_3857_EXTENT); + }); + + +/** + * @const + * @type {ol.Extent} + */ +ol.Projection.EPSG_4326_EXTENT = new ol.Extent(180, 90, -180, -90); + + +/** + * @type {Array.} + */ +ol.Projection.EPSG_4326_LIKE_CODES = [ + 'EPSG:4326', + 'CRS:84', + 'urn:ogc:def:crs:EPSG:6.6:4326' +]; + + +/** + * @const + * @type {Array.} + */ +ol.Projection.EPSG_4326_LIKE_PROJECTIONS = goog.array.map( + ol.Projection.EPSG_4326_LIKE_CODES, + function(code) { + return new ol.Projection( + code, + ol.ProjectionUnits.DEGREES, + ol.Projection.EPSG_4326_EXTENT); + }); + + +ol.Projection.addEquivalentProjections( + ol.Projection.EPSG_3857_LIKE_PROJECTIONS); +ol.Projection.addEquivalentProjections( + ol.Projection.EPSG_4326_LIKE_PROJECTIONS); +ol.Projection.addEquivalentTransforms( + ol.Projection.EPSG_4326_LIKE_PROJECTIONS, + ol.Projection.EPSG_3857_LIKE_PROJECTIONS, + ol.Projection.forwardSphericalMercator, + ol.Projection.inverseSphericalMercator); diff --git a/src/ol/projection_test.js b/src/ol/projection_test.js new file mode 100644 index 0000000000..a47a037882 --- /dev/null +++ b/src/ol/projection_test.js @@ -0,0 +1,87 @@ +goog.require('goog.array'); +goog.require('goog.math.Coordinate'); +goog.require('goog.testing.jsunit'); +goog.require('ol.Projection'); + + +function _testAllEquivalent(codes) { + var projections = goog.array.map(codes, ol.Projection.createFromCode); + goog.array.forEach(projections, function(source) { + goog.array.forEach(projections, function(destination) { + assertTrue(ol.Projection.equivalent(source, destination)); + }); + }); +} + + +function testEpsg3857Equivalence() { + _testAllEquivalent([ + 'EPSG:3857', + 'EPSG:102100', + 'EPSG:102113', + 'EPSG:900913' + ]); +} + + +function testEpsg4326Equivalence() { + _testAllEquivalent([ + 'CRS:84', + 'urn:ogc:def:crs:EPSG:6.6:4326', + 'EPSG:4326' + ]); +} + + +function testIdentityTransform() { + var epsg4326 = ol.Projection.createFromCode('EPSG:4326'); + var uniqueObject = {}; + var sourcePoint = new goog.math.Coordinate(uniqueObject, uniqueObject); + var destinationPoint = ol.Projection.transform( + sourcePoint, epsg4326, epsg4326); + assertFalse(sourcePoint === destinationPoint); + assertTrue(destinationPoint.x === sourcePoint.x); + assertTrue(destinationPoint.y === sourcePoint.y); +} + + +function testForwardSphericalMercatorOrigin() { + var point = ol.Projection.transformWithCodes( + new goog.math.Coordinate(0, 0), 'EPSG:4326', 'EPSG:3857'); + assertNotNullNorUndefined(point); + assertEquals(0, point.x); + assertRoughlyEquals(0, point.y, 1e-9); +} + + +function testInverseSphericalMercatorOrigin() { + var point = ol.Projection.transformWithCodes( + new goog.math.Coordinate(0, 0), 'EPSG:3857', 'EPSG:4326'); + assertNotNullNorUndefined(point); + assertEquals(0, point.x); + assertEquals(0, point.y); +} + + +function testForwardSphericalMercatorAlastaira() { + // http://alastaira.wordpress.com/2011/01/23/the-google-maps-bing-maps-spherical-mercator-projection/ + var point = ol.Projection.transformWithCodes( + new goog.math.Coordinate(-5.625, 52.4827802220782), + 'EPSG:4326', + 'EPSG:900913'); + assertNotNullNorUndefined(point); + assertRoughlyEquals(-626172.13571216376, point.x, 1e-9); + assertRoughlyEquals(6887893.4928337997, point.y, 1e-9); +} + + +function testInverseSphericalMercatorAlastaira() { + // http://alastaira.wordpress.com/2011/01/23/the-google-maps-bing-maps-spherical-mercator-projection/ + var point = ol.Projection.transformWithCodes( + new goog.math.Coordinate(-626172.13571216376, 6887893.4928337997), + 'EPSG:900913', + 'EPSG:4326'); + assertNotNullNorUndefined(point); + assertRoughlyEquals(-5.625, point.x, 1e-9); + assertRoughlyEquals(52.4827802220782, point.y, 1e-9); +}