From 0b8a0298f9c9d05070aecf3951fc85e40db397a3 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 11:25:23 +0200 Subject: [PATCH 01/12] Add functions for createing geodesic lines --- lib/OpenLayers/Geometry/LineString.js | 136 ++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/lib/OpenLayers/Geometry/LineString.js b/lib/OpenLayers/Geometry/LineString.js index 72ecbb2549..28fc9ab2ed 100644 --- a/lib/OpenLayers/Geometry/LineString.js +++ b/lib/OpenLayers/Geometry/LineString.js @@ -640,3 +640,139 @@ OpenLayers.Geometry.LineString = OpenLayers.Class(OpenLayers.Geometry.Curve, { CLASS_NAME: "OpenLayers.Geometry.LineString" }); + + +/** + * Function: OpenLayers.Geometry.LineString.geodesic + * + * Parameters: + * interpolate - {function(number): OpenLayers.Geometry.Point} Interpolate + * function. + * transform - {function(OpenLayers.Geometry.Point): OpenLayers.Geometry.Point} + * Transform from longitude/latitude to projected coordinates. + * squaredTolerance - {number} Squared tolerance. + * + * Returns: + * {OpenLayers.Geometry.LineString} + */ +OpenLayers.Geometry.LineString.geodesic = + function(interpolate, transform, squaredTolerance) { + // FIXME reduce garbage generation + // FIXME optimize stack operations + + var components = []; + + var geoA = interpolate(0); + var geoB = interpolate(1); + + var a = transform(geoA); + var b = transform(geoB); + + var geoStack = [geoB, geoA]; + var stack = [b, a]; + var fractionStack = [1, 0]; + + var fractions = {}; + + var maxIterations = 1e5; + var geoM, m, fracA, fracB, fracM, key; + + while (--maxIterations > 0 && fractionStack.length > 0) { + // Pop the a coordinate off the stack + fracA = fractionStack.pop(); + geoA = geoStack.pop(); + a = stack.pop(); + // Add the a coordinate if it has not been added yet + key = fracA.toString(); + if (!(key in fractions)) { + components.push(a); + fractions[key] = true; + } + // Pop the b coordinate off the stack + fracB = fractionStack.pop(); + geoB = geoStack.pop(); + b = stack.pop(); + // Find the m point between the a and b coordinates + fracM = (fracA + fracB) / 2; + geoM = interpolate(fracM); + m = transform(geoM); + if (OpenLayers.Geometry.distanceSquaredToSegment(m, {x1: a.x, y1: a.y, + x2: b.x, y2: b.y}).distance < squaredTolerance) { + // If the m point is sufficiently close to the straight line, then + // we discard it. Just use the b coordinate and move on to the next + // line segment. + components.push(b); + key = fracB.toString(); + fractions[key] = true; + } else { + // Otherwise, we need to subdivide the current line segment. + // Split it into two and push the two line segments onto the stack. + fractionStack.push(fracB, fracM, fracM, fracA); + stack.push(b, m, m, a); + geoStack.push(geoB, geoM, geoM, geoA); + } + } + + return new OpenLayers.Geometry.LineString(components); +}; + + +/** + * Function: OpenLayers.Geometry.LineString.geodesicMeridian + * Generate a meridian (line at constant longitude). + * + * Parameters: + * lon - {number} Longitude. + * lat1 - {number} Latitude 1. + * lat2 - {number} Latitude 2. + * projection - {OpenLayers.Projection} Projection. + * squaredTolerance - {number} Squared tolerance. + * + * Returns: + * {OpenLayers.Geometry.LineString} Line geometry for the meridian at . + */ +OpenLayers.Geometry.LineString.geodesicMeridian = + function(lon, lat1, lat2, projection, squaredTolerance) { + var epsg4326Projection = new OpenLayers.Projection('EPSG:4326'); + return OpenLayers.Geometry.LineString.geodesic( + function(frac) { + return new OpenLayers.Geometry.Point( + lon, lat1 + ((lat2 - lat1) * frac)); + }, + function(point) { + return point.transform(epsg4326Projection, projection); + }, + squaredTolerance + ); +}; + + +/** + * Function: OpenLayers.Geometry.LineString.geodesicParallel + * Generate a parallel (line at constant latitude). + * + * Parameters: + * lat - {number} Latitude. + * lon1 - {number} Longitude 1. + * lon2 - {number} Longitude 2. + * projection {OpenLayers.Projection} Projection. + * squaredTolerance - {number} Squared tolerance. + * + * Returns: + * {OpenLayers.Geometry.LineString} Line geometry for the parallel at . + */ +OpenLayers.Geometry.LineString.geodesicParallel = + function(lat, lon1, lon2, projection, squaredTolerance) { + var epsg4326Projection = new OpenLayers.Projection('EPSG:4326'); + return OpenLayers.Geometry.LineString.geodesic( + function(frac) { + return new OpenLayers.Geometry.Point( + lon1 + ((lon2 - lon1) * frac), lat); + }, + function(point) { + return point.transform(epsg4326Projection, projection); + }, + squaredTolerance + ); +}; + From 1d5cd8e6999c0567cacd6143be5791766021032a Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 11:26:02 +0200 Subject: [PATCH 02/12] Rewrite the graticule control This also introduces a new worldExtent option to the projection defaults. --- lib/OpenLayers/Control/Graticule.js | 667 +++++++++++++++++++--------- lib/OpenLayers/Projection.js | 10 +- 2 files changed, 453 insertions(+), 224 deletions(-) diff --git a/lib/OpenLayers/Control/Graticule.js b/lib/OpenLayers/Control/Graticule.js index 1a5d8dbfa0..b849ff6bf0 100644 --- a/lib/OpenLayers/Control/Graticule.js +++ b/lib/OpenLayers/Control/Graticule.js @@ -14,54 +14,55 @@ /** * Class: OpenLayers.Control.Graticule * The Graticule displays a grid of latitude/longitude lines reprojected on - * the map. - * The grid is specified by a list of widths in degrees (with height the same as the width). - * The grid height can be specified as a multiple of the width. - * This allows making the grid more regular in specific regions of some projections. - * The grid can also be specified by lists of both widths and heights. - * This allows matching externally-defined grid systems. - * + * the map. + * The grid is specified by a list of widths in degrees (with height the same as + * the width). The grid height can be specified as a multiple of the width. + * This allows making the grid more regular in specific regions of some + * projections. The grid can also be specified by lists of both widths and + * heights. This allows matching externally-defined grid systems. + * * Inherits from: * - - * + * */ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { /** * APIProperty: autoActivate * {Boolean} Activate the control when it is added to a map. Default is - * true. + * true. */ autoActivate: true, - + /** - * APIProperty: intervals - * {Array(Float)} A list of possible graticule widths in degrees. - */ - intervals: [ 45, 30, 20, 10, 5, 2, 1, - 0.5, 0.2, 0.1, 0.05, 0.01, - 0.005, 0.002, 0.001 ], + * APIProperty: intervals + * {Array(Float)} A list of possible graticule widths in degrees. + */ + intervals: [90, 45, 30, 20, 10, 5, 2, 1, + 0.5, 0.2, 0.1, 0.05, 0.01, + 0.005, 0.002, 0.001 + ], /** - * APIProperty: intervalHeights - * {Array(Float)} A list of possible graticule heights in degrees. - * Length must match intervals array. - * Default is null (same as widths). - */ + * APIProperty: intervalHeights + * {Array(Float)} A list of possible graticule heights in degrees. + * Length must match intervals array. + * Default is null (same as widths). + */ intervalHeights: null, - + /** * Property: intervalHeightFactor - * {Number} Factor to compute graticule height from the width. - * Can be used to match existing grid systems, or improve regularity of grid. - * Default is 1. + * {Number} Factor to compute graticule height from the width. + * Can be used to match existing grid systems, or improve regularity of + * grid. Default is 1. * Ignored if intervalHeights are set explicitly. */ intervalHeightFactor: 1, /** * APIProperty: displayInLayerSwitcher - * {Boolean} Allows the Graticule control to be switched on and off by + * {Boolean} Allows the Graticule control to be switched on and off by * LayerSwitcher control. Defaults is true. */ displayInLayerSwitcher: true, @@ -75,7 +76,9 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { /** * APIProperty: numPoints * {Integer} The number of points to use in each graticule line. Higher - * numbers result in a smoother curve for projected maps + * numbers result in a smoother curve for projected maps. + * *Deprecated*. The number of points is now determined by adaptive + * quantization. */ numPoints: 50, @@ -87,7 +90,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { /** * APIProperty: layerName - * {String} The name to be displayed in the layer switcher, default is set + * {String} The name to be displayed in the layer switcher, default is set * by {}. */ layerName: null, @@ -107,31 +110,32 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { /** * APIProperty: labelLonYOffset - * {Integer} the offset of the longitude (X) label from the bottom of the map. + * {Integer} the offset of the longitude (X) label from the bottom of the + * map. */ labelLonYOffset: 2, - + /** * APIProperty: labelLatXOffset * {Integer} the offset of the latitude (Y) label from the right of the map. */ labelLatXOffset: -2, - + /** * APIProperty: lineSymbolizer * {symbolizer} the symbolizer used to render lines */ lineSymbolizer: { - strokeColor: "#333", - strokeWidth: 1, - strokeOpacity: 0.5 - }, + strokeColor: "#333", + strokeWidth: 1, + strokeOpacity: 0.5 + }, /** * APIProperty: labelSymbolizer * {symbolizer} the symbolizer used to render labels */ - labelSymbolizer: {}, + labelSymbolizer: null, /** * Property: gratLayer @@ -139,11 +143,65 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { */ gratLayer: null, + /** + * Property: epsg4326Projection + * {OpenLayers.Projection} + */ + epsg4326Projection: null, + + /** + * Property: projection + * {OpenLayers.Projection} The projection of the graticule. + */ + projection: null, + + /** + * Property: projectionCenterLonLat + * {OpenLayers.LonLat} The center of the projection's validty extent. + */ + projectionCenterLonLat: null, + + /** + * Property: maxLat + * {number} + */ + maxLat: Infinity, + + /** + * Propety: maxLon + * {number} + */ + maxLon: Infinity, + + /** + * Property: minLat + * {number} + */ + minLat: -Infinity, + + /** + * Property: minLon + * {number} + */ + minLon: -Infinity, + + /** + * Property: meridians + * {Array.} + */ + meridians: null, + + /** + * Property. parallels + * {Array.} + */ + parallels: null, + /** * Constructor: OpenLayers.Control.Graticule * Create a new graticule control to display a grid of latitude longitude * lines. - * + * * Parameters: * options - {Object} An optional object whose properties will be used * to extend the control. @@ -152,67 +210,80 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { options = options || {}; options.layerName = options.layerName || OpenLayers.i18n("Graticule"); OpenLayers.Control.prototype.initialize.apply(this, [options]); - - this.labelSymbolizer.stroke = false; - this.labelSymbolizer.fill = false; - this.labelSymbolizer.label = "${label}"; - this.labelSymbolizer.labelAlign = "${labelAlign}"; - this.labelSymbolizer.labelXOffset = "${xOffset}"; - this.labelSymbolizer.labelYOffset = "${yOffset}"; + + this.labelSymbolizer = { + stroke: false, + fill: false, + label: "${label}", + labelAlign: "${labelAlign}", + labelXOffset: "${xOffset}", + labelYOffset: "${yOffset}" + }; + + this.epsg4326Projection = new OpenLayers.Projection('EPSG:4326'); + this.parallels = []; + this.meridians = []; }, /** * APIMethod: destroy */ destroy: function() { - this.deactivate(); - OpenLayers.Control.prototype.destroy.apply(this, arguments); + this.deactivate(); + OpenLayers.Control.prototype.destroy.apply(this, arguments); if (this.gratLayer) { this.gratLayer.destroy(); this.gratLayer = null; } }, - + /** * Method: draw * * initializes the graticule layer and does the initial update - * + * * Returns: * {DOMElement} */ draw: function() { OpenLayers.Control.prototype.draw.apply(this, arguments); if (!this.gratLayer) { - var gratStyle = new OpenLayers.Style({},{ - rules: [new OpenLayers.Rule({'symbolizer': - {"Point":this.labelSymbolizer, - "Line":this.lineSymbolizer} + var gratStyle = new OpenLayers.Style({}, { + rules: [new OpenLayers.Rule({ + 'symbolizer': { + "Point": this.labelSymbolizer, + "Line": this.lineSymbolizer + } })] }); this.gratLayer = new OpenLayers.Layer.Vector(this.layerName, { - styleMap: new OpenLayers.StyleMap({'default':gratStyle}), + styleMap: new OpenLayers.StyleMap({ + 'default': gratStyle + }), visibility: this.visible, - displayInLayerSwitcher: this.displayInLayerSwitcher + displayInLayerSwitcher: this.displayInLayerSwitcher, + // Prefer the canvas renderer to avoid coordinate range issues + // with graticule lines + renderers: ['Canvas', 'VML', 'SVG'] }); } return this.div; }, - /** + /** * APIMethod: activate */ activate: function() { if (OpenLayers.Control.prototype.activate.apply(this, arguments)) { this.map.addLayer(this.gratLayer); - this.map.events.register('moveend', this, this.update); + this.map.events.register('moveend', this, this.update); this.update(); - return true; + return true; } else { return false; } }, - + /** * APIMethod: deactivate */ @@ -225,190 +296,344 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { return false; } }, + /** * Method: update * * calculates the grid to be displayed and actually draws it - * + * * Returns: * {DOMElement} */ update: function() { + var map = this.map; //wait for the map to be initialized before proceeding - var mapBounds = this.map.getExtent(); - if (!mapBounds) { + var extent = this.map.getExtent(); + if (!extent) { return; } - - //clear out the old grid - this.gratLayer.destroyFeatures(); - - //get the projection objects required - var llProj = new OpenLayers.Projection("EPSG:4326"); - var mapProj = this.map.getProjectionObject(); - var mapRes = this.map.getResolution(); - - //if the map is in lon/lat, then the lines are straight and only one - //point is required - if (mapProj.proj && mapProj.proj.projName == "longlat") { - this.numPoints = 1; + var center = map.getCenter(); + var projection = map.getProjectionObject(); + var resolution = map.getResolution(); + var squaredTolerance = resolution * resolution / 4; + + var updateProjectionInfo = !this.projection || !this.projection.equals(projection); + + if (updateProjectionInfo) { + this.updateProjectionInfo(projection); } - - //get the map center in EPSG:4326 - var mapCenter = this.map.getCenter(); //lon and lat here are really map x and y - var mapCenterLL = new OpenLayers.Pixel(mapCenter.lon, mapCenter.lat); - OpenLayers.Projection.transform(mapCenterLL, mapProj, llProj); - - /* This block of code determines the lon/lat interval to use for the - * grid by calculating the diagonal size of one grid cell at the map - * center. Iterates through the intervals array until the diagonal - * length is less than the targetSize option. - */ - //find lat/lon interval that results in a grid of less than the target size - var testSq = this.targetSize*mapRes; - testSq *= testSq; //compare squares rather than doing a square root to save time - for (var i=0; i north and west -> east + // * When the projection center is not inside the extent. + // With these conditions, a valid grid will be available for all + // projections within their validity extent. Outside the validity + // extent, partial grid lines may occur. + var minLon, minLat, maxLon, maxLat; + var pixelSize = this.map.getGeodesicPixelSize(); + if (pixelSize.w < 0.5 && pixelSize.h < 0.5 && + extent4326.right > extent4326.left && + extent4326.top > extent4326.bottom && + Math.abs(extent4326.bottom) / extent4326.bottom == + Math.abs(extent4326.top) / extent4326.top && + Math.abs(extent4326.left) / extent4326.left == + Math.abs(extent4326.right) / extent4326.right) { + minLon = Math.max(extent4326.left, this.minLon); + minLat = Math.max(extent4326.bottom, this.minLat); + maxLon = Math.min(extent4326.right, this.maxLon); + maxLat = Math.min(extent4326.top, this.maxLat); + } else { + minLon = this.minLon; + minLat = this.minLat; + maxLon = this.maxLon; + maxLat = this.maxLat; + } + + var size = this.map.getSize(); + var idx, prevIdx, lon, visibleIntervals, interval, intervalIndex; + var centerLon, centerLat; + + // Create meridians + + // Start with the coarsest interval, then refine until we reach the + // desired targetSize + visibleIntervals = Math.ceil(size.w / this.targetSize); + intervalIndex = 0; do { - newPoint = newPoint.offset({x: -intervalX, y: 0}); - mapXY = OpenLayers.Projection.transform(newPoint.clone(), llProj, mapProj); - centerLatPoints.unshift(newPoint); - } while (mapBounds.containsPixel(mapXY) && ++iter<1000); - newPoint = mapCenterLL.clone(); - do { - newPoint = newPoint.offset({x: intervalX, y: 0}); - mapXY = OpenLayers.Projection.transform(newPoint.clone(), llProj, mapProj); - centerLatPoints.push(newPoint); - } while (mapBounds.containsPixel(mapXY) && ++iter<1000); - - //now generate a line for each node in the central lat and lon lines - //first loop over constant longitude - var lines = []; - for(var i=0; i < centerLatPoints.length; ++i) { - var lon = centerLatPoints[i].x; - var pointList = []; - var labelPoint = null; - var latEnd = Math.min(centerLonPoints[0].y, 90); - var latStart = Math.max(centerLonPoints[centerLonPoints.length - 1].y, -90); - var latDelta = (latEnd - latStart)/this.numPoints; - var lat = latStart; - for(var j=0; j<= this.numPoints; ++j) { - var gridPoint = new OpenLayers.Geometry.Point(lon,lat); - gridPoint.transform(llProj, mapProj); - pointList.push(gridPoint); - lat += latDelta; - if (gridPoint.y >= mapBounds.bottom && !labelPoint) { - labelPoint = gridPoint; + interval = this.intervals[intervalIndex++]; + centerLon = Math.floor(centerLonLat.lon / interval) * interval; + + lon = Math.max(centerLon, this.minLon); + lon = Math.min(centerLon, this.maxLon); + + idx = this.addMeridian( + lon, minLat, maxLat, squaredTolerance, extentGeom, 0); + + while (lon != this.minLon) { + lon = Math.max(lon - interval, this.minLon); + prevIdx = idx; + idx = this.addMeridian( + lon, minLat, maxLat, squaredTolerance, extentGeom, idx); + if (prevIdx == idx) { + // bail out if we're producing lines that are not visible + break; } } - if (this.labelled) { - //keep track of when this grid line crosses the map bounds to set - //the label position - //labels along the bottom, add the offset up into the map - //TODO add option for labels on top - var labelPos = new OpenLayers.Geometry.Point(labelPoint.x,mapBounds.bottom); - var labelAttrs = { - value: lon, - label: this.labelled?OpenLayers.Util.getFormattedLonLat(lon, "lon", this.labelFormat):"", - labelAlign: "cb", - xOffset: 0, - yOffset: this.labelLonYOffset - }; - this.gratLayer.addFeatures(new OpenLayers.Feature.Vector(labelPos,labelAttrs)); - } - var geom = new OpenLayers.Geometry.LineString(pointList); - lines.push(new OpenLayers.Feature.Vector(geom)); - } - - //now draw the lines of constant latitude - for (var j=0; j < centerLonPoints.length; ++j) { - lat = centerLonPoints[j].y; - if (lat<-90 || lat>90) { //latitudes only valid between -90 and 90 - continue; + + lon = Math.max(centerLon, this.minLon); + lon = Math.min(centerLon, this.maxLon); + + while (lon != this.maxLon) { + lon = Math.min(lon + interval, this.maxLon); + prevIdx = idx; + idx = this.addMeridian( + lon, minLat, maxLat, squaredTolerance, extentGeom, idx); + if (prevIdx == idx) { + // bail out if we're producing lines that are not visible + break; + } } - var pointList = []; - var lonStart = centerLatPoints[0].x; - var lonEnd = centerLatPoints[centerLatPoints.length - 1].x; - var lonDelta = (lonEnd - lonStart)/this.numPoints; - var lon = lonStart; - var labelPoint = null; - for(var i=0; i <= this.numPoints ; ++i) { - var gridPoint = new OpenLayers.Geometry.Point(lon,lat); - gridPoint.transform(llProj, mapProj); - pointList.push(gridPoint); - lon += lonDelta; - if (gridPoint.x < mapBounds.right) { - labelPoint = gridPoint; + } while (intervalIndex < this.intervals.length && + idx <= visibleIntervals); + + this.meridians.length = idx; + + // Create parallels + + // Start with the coarsest interval, then refine until we reach the + // desired targetSize + visibleIntervals = Math.ceil(size.h / this.targetSize); + intervalIndex = 0; + labelPoint = null; + gridSize = 0; + do { + interval = this.intervalHeights ? + this.intervalHeights[intervalIndex++] : + this.intervals[intervalIndex++] * this.intervalHeightFactor; + centerLat = Math.floor(centerLonLat.lat / interval) * interval; + + lat = Math.max(centerLat, this.minLat); + lat = Math.min(centerLat, this.maxLat); + + idx = this.addParallel( + lat, minLon, maxLon, squaredTolerance, extentGeom, 0); + + while (lat != this.minLat) { + lat = Math.max(lat - interval, this.minLat); + prevIdx = idx; + idx = this.addParallel( + lat, minLon, maxLon, squaredTolerance, extentGeom, idx); + if (prevIdx == idx) { + // bail out if we're producing lines that are not visible + break; } } - if (this.labelled) { - //keep track of when this grid line crosses the map bounds to set - //the label position - //labels along the right, add offset left into the map - //TODO add option for labels on left - var labelPos = new OpenLayers.Geometry.Point(mapBounds.right, labelPoint.y); - var labelAttrs = { - value: lat, - label: this.labelled?OpenLayers.Util.getFormattedLonLat(lat, "lat", this.labelFormat):"", - labelAlign: "rb", - xOffset: this.labelLatXOffset, - yOffset: 2 - }; - this.gratLayer.addFeatures(new OpenLayers.Feature.Vector(labelPos,labelAttrs)); + + lat = Math.max(centerLat, this.minLat); + lat = Math.min(centerLat, this.maxLat); + + while (lat != this.maxLat) { + lat = Math.min(lat + interval, this.maxLat); + prevIdx = idx; + idx = this.addParallel( + lat, minLon, maxLon, squaredTolerance, extentGeom, idx); + if (prevIdx == idx) { + // bail out if we're producing lines that are not visible + break; + } } - var geom = new OpenLayers.Geometry.LineString(pointList); - lines.push(new OpenLayers.Feature.Vector(geom)); - } - this.gratLayer.addFeatures(lines); + + } while (intervalIndex < this.intervals.length && + idx <= visibleIntervals); + + this.parallels.length = idx; }, - + + /** + * Method: updateProjectionInfo + * + * Parameters: + * projection - {OpenLayers.Projection} Projection. + */ + updateProjectionInfo: function(projection) { + var defaults = OpenLayers.Projection.defaults[projection.getCode()]; + var extent = defaults && defaults.maxExtent ? + OpenLayers.Bounds.fromArray(defaults.maxExtent) : + this.map.getMaxExtent(); + var worldExtent = defaults && defaults.worldExtent ? + defaults.worldExtent : [-180, -90, 180, 90]; + + this.maxLat = worldExtent[3]; + this.maxLon = worldExtent[2]; + this.minLat = worldExtent[1]; + this.minLon = worldExtent[0]; + + // Use the center of the transformed projection extent rather than the + // transformed center of the projection extent. This way we avoid issues + // with polar projections where [0, 0] cannot be transformed. + this.projectionCenterLonLat = extent.clone().transform( + projection, this.epsg4326Projection).getCenterLonLat(); + + this.projection = projection; + }, + + /** + * Method: addMeridian + * + * Parameters: + * lon - {number} Longitude. + * minLat - {number} Minimum latitude. + * maxLat - {number} Maximum latitude. + * squaredTolerance - {number} Squared tolerance. + * extent - {OpenLayers.Geometry.Polygon} Extent. + * index {number} Index. + * + * Returns: + * {number} Index. + */ + addMeridian: function( + lon, minLat, maxLat, squaredTolerance, extentGeom, index) { + var lineString = OpenLayers.Geometry.LineString.geodesicMeridian( + lon, minLat, maxLat, this.projection, squaredTolerance); + var split = lineString.split(new OpenLayers.Geometry.LineString( + extentGeom.components[0].components.slice(0, 2))); + if (split || extentGeom.intersects(lineString)) { + this.meridians[index++] = + new OpenLayers.Feature.Vector(lineString, { + lon: lon, + labelPoint: split ? split[0].components[1] : undefined + }); + } + return index; + }, + + /** + * Method: addParallel + * + * Parameters: + * lat - {number} Latitude. + * minLon - {number} Minimum longitude. + * maxLon - {number} Maximum longitude. + * squaredTolerance - {number} Squared tolerance. + * extent - {OpenLayers.Geometry.Polygon} Extent. + * index - {number} Index. + * + * Returns: + * {number} Index. + */ + addParallel: function( + lat, minLon, maxLon, squaredTolerance, extentGeom, index) { + var lineString = OpenLayers.Geometry.LineString.geodesicParallel( + lat, minLon, maxLon, this.projection, squaredTolerance); + var split = lineString.split(new OpenLayers.Geometry.LineString( + extentGeom.components[0].components.slice(1, 3))); + if (split || extentGeom.intersects(lineString)) { + this.parallels[index++] = + new OpenLayers.Feature.Vector(lineString, { + lat: lat, + labelPoint: split ? split[0].components[1] : undefined + }); + } + return index; + }, + CLASS_NAME: "OpenLayers.Control.Graticule" }); - diff --git a/lib/OpenLayers/Projection.js b/lib/OpenLayers/Projection.js index 56b221651c..216f75871d 100644 --- a/lib/OpenLayers/Projection.js +++ b/lib/OpenLayers/Projection.js @@ -167,22 +167,26 @@ OpenLayers.Projection.transforms = {}; * {Object} 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, EPSG:102100 and OSGEO:41001). Keys are the SRS code, values are - * units, maxExtent (the validity extent for the SRS) and yx (true if this SRS + * units, maxExtent (the validity extent for the SRS in projected coordinates), + * worldExtent (the world's extent in EPSG:4326) and yx (true if this SRS * is known to have a reverse axis order). */ OpenLayers.Projection.defaults = { "EPSG:4326": { units: "degrees", maxExtent: [-180, -90, 180, 90], + worldExtent: [-180, -90, 180, 90], yx: true }, "CRS:84": { units: "degrees", - maxExtent: [-180, -90, 180, 90] + maxExtent: [-180, -90, 180, 90], + worldExtent: [-180, -90, 180, 90] }, "EPSG:900913": { units: "m", - maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34] + maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34], + worldExtent: [-180, -89, 180, 89] } }; From 08d8c5ad1503ad81daa4ddac3e4b9ea9c8c33bd0 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 11:26:43 +0200 Subject: [PATCH 03/12] Update examples to use projection default options This also adds a graticule control to the polar-projections example. --- examples/graticule.html | 15 ++--- examples/polar-projections.js | 106 +++++++++++++++------------------- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/examples/graticule.html b/examples/graticule.html index c5a116d9f0..008b4fc4c2 100644 --- a/examples/graticule.html +++ b/examples/graticule.html @@ -26,7 +26,12 @@ @@ -114,3 +114,4 @@

Graticule Example

+ diff --git a/examples/polar-projections.js b/examples/polar-projections.js index ac717fbd97..e275b65cf5 100644 --- a/examples/polar-projections.js +++ b/examples/polar-projections.js @@ -1,81 +1,69 @@ var map, layer, overlay; -var projectionOptions = { - 'EPSG:3574': { - projection: new OpenLayers.Projection('EPSG:3574'), - units: 'm', - maxExtent: new OpenLayers.Bounds(-5505054, -5505054, 5505054, 5505054), - maxResolution: 5505054 / 128, - numZoomLevels: 18 - }, - 'EPSG:3576': { - projection: new OpenLayers.Projection('EPSG:3576'), - units: 'm', - maxExtent: new OpenLayers.Bounds(-5505054, -5505054, 5505054, 5505054), - maxResolution: 5505054 / 128, - numZoomLevels: 18 - }, - 'EPSG:3571': { - projection: new OpenLayers.Projection('EPSG:3571'), - units: 'm', - maxExtent: new OpenLayers.Bounds(-5505054, -5505054, 5505054, 5505054), - maxResolution: 5505054 / 128, - numZoomLevels: 18 - }, - 'EPSG:3573': { - projection: new OpenLayers.Projection('EPSG:3573'), - units: 'm', - maxExtent: new OpenLayers.Bounds(-5505054, -5505054, 5505054, 5505054), - maxResolution: 5505054 / 128, - numZoomLevels: 18 - } +OpenLayers.Projection.defaults['EPSG:3574'] = { + maxExtent: [-5505054, -5505054, 5505054, 5505054], + worldExtent: [-180.0, 0.0, 180.0, 90.0], + units: 'm' +}; +OpenLayers.Projection.defaults['EPSG:3576'] = { + maxExtent: [-5505054, -5505054, 5505054, 5505054], + worldExtent: [-180.0, 0.0, 180.0, 90.0], + units: 'm' +}; +OpenLayers.Projection.defaults['EPSG:3571'] = { + maxExtent: [-5505054, -5505054, 5505054, 5505054], + worldExtent: [-180.0, 0.0, 180.0, 90.0], + units: 'm' +}; +OpenLayers.Projection.defaults['EPSG:3573'] = { + maxExtent: [-5505054, -5505054, 5505054, 5505054], + worldExtent: [-180.0, 0.0, 180.0, 90.0], + units: 'm' }; function setProjection() { projCode = this.innerHTML; - var oldExtent = map.getExtent(); var oldCenter = map.getCenter(); var oldProjection = map.getProjectionObject(); - - // map projection is controlled by the base layer - map.baseLayer.addOptions(projectionOptions[projCode]); - + + // the base layer controls the map projection + layer.addOptions({projection: projCode}); + // with the base layer updated, the map has the new projection now var newProjection = map.getProjectionObject(); // transform the center of the old projection, not the extent - map.setCenter( - oldCenter.transform(oldProjection, newProjection, - map.getZoomForExtent(oldExtent.transform(oldProjection, newProjection)) - )); - - for (var i=map.layers.length-1; i>=0; --i) { - // update grid settings - map.layers[i].addOptions(projectionOptions[projCode]); - // redraw layer - just in case center and zoom are the same in old and - // new projection - map.layers[i].redraw(); - } + map.setCenter(oldCenter.transform(oldProjection, newProjection)); + + // update overlay layers here + overlay.addOptions({projection: newProjection}); + + // re-fetch images for all layers + layer.redraw(); + overlay.redraw(); } function init() { - map = new OpenLayers.Map('map'); layer = new OpenLayers.Layer.WMS( - 'world', - 'http://v2.suite.opengeo.org/geoserver/wms', - {layers: 'world', version: '1.1.1'}, - projectionOptions['EPSG:3574'] + 'countries', + 'http://suite.opengeo.org/geoserver/wms', + {layers: 'opengeo:borders', version: '1.1.1'}, + {projection: 'EPSG:3574', displayOutsideMaxExtent: true} ); overlay = new OpenLayers.Layer.WMS( - 'world', - 'http://v2.suite.opengeo.org/geoserver/wms', - {transparent: 'true', layers: 'world:borders', styles: 'line'}, - projectionOptions['EPSG:3574'] + 'cities', + 'http://suite.opengeo.org/geoserver/wms', + {layers: 'opengeo:cities', version: '1.1.1', transparent: true}, + {projection: 'EPSG:3574', displayOutsideMaxExtent: true, + isBaseLayer: false} ); - overlay.isBaseLayer = false; - map.addLayers([layer, overlay]); - map.zoomToMaxExtent(); - + map = new OpenLayers.Map('map', { + center: [25000, 25000], + zoom: 1, + layers: [layer, overlay] + }); + map.addControl(new OpenLayers.Control.Graticule()); + // add behaviour to dom elements document.getElementById('epsg3574').onclick = setProjection; document.getElementById('epsg3576').onclick = setProjection; From 41ae609402da8d07a8bdbdf3195aa7c2de9bf8bc Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 11:45:48 +0200 Subject: [PATCH 04/12] Mention graticule related changes in release notes. --- notes/2.14.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/notes/2.14.md b/notes/2.14.md index 7391470118..16d4c89c93 100644 --- a/notes/2.14.md +++ b/notes/2.14.md @@ -13,3 +13,16 @@ More info on: http://googlegeodevelopers.blogspot.nl/2013/04/an-update-on-javasc `OpenLayers.Layer.Google` now uses v3 by default. v2 has been removed from the library. Please note that the layer types are different between v2 and v3, so if you were using e.g. `type: G_SATELLITE_MAP` you will now need to use `type: google.maps.MapTypeId.SATELLITE` instead. Also make sure you include the v3 version of the GMaps library, e.g.: `` + +## More options in OpenLayers.Projection.defaults + +In addition to `maxExtent`, `units` and `xy`, projection defaults now include +a `worldExtent` option. This is the extent of the world in geographic coordinates. For most projections, `[-180, -90, 180, 90]` will make sense. But for polar projections, it is recommended to use a world extent that excludes the invisible hemisphere. So for Northern polar projections, `[-180, 0, 180, 90]` would be a good setting. + +## OpenLayers.Control.Graticule rewrite + +* The Graticule control has a `numPoints` option, which is now deprecated. The graticule lines are optimized with adaptive quantization instead. +* The Graticul control now uses the projection specific maxExtent and worldExtent settings from `OpenLayers.Projection.defaults`. Make sure to create an `OpenLayers.Projection.defaults` entry for your projection when using a graticule. +* The control's layer (`gratLayer`) now prefers the Canvas renderer. This avoids coordinate range issues with the SVG renderer. +* Previously, meridians were only labelled at the bottom, and parallels at the right border of the map. To better support polar projections, meridians are now also labelled at the left border, and parallels at the top border, if they do not have an intersection point with the bottom or right border. +* To avoid maps with missing or short grid lines, make sure you do not use a graticule when working with a projection outside of its validity extent or recommended viewing area. From 68a191cd506a0d4784b41c5092ef051d22257ec5 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 11:51:30 +0200 Subject: [PATCH 05/12] Fixing compiler issues --- lib/OpenLayers/Control/Graticule.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/OpenLayers/Control/Graticule.js b/lib/OpenLayers/Control/Graticule.js index b849ff6bf0..b774b7d145 100644 --- a/lib/OpenLayers/Control/Graticule.js +++ b/lib/OpenLayers/Control/Graticule.js @@ -448,7 +448,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { } var size = this.map.getSize(); - var idx, prevIdx, lon, visibleIntervals, interval, intervalIndex; + var idx, prevIdx, lon, lat, visibleIntervals, interval, intervalIndex; var centerLon, centerLat; // Create meridians @@ -502,8 +502,6 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { // desired targetSize visibleIntervals = Math.ceil(size.h / this.targetSize); intervalIndex = 0; - labelPoint = null; - gridSize = 0; do { interval = this.intervalHeights ? this.intervalHeights[intervalIndex++] : From 0adcfa2984d64957608ab1b0c8eed27588166ab3 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 11:56:45 +0200 Subject: [PATCH 06/12] Additional release notes and documentation. --- examples/graticule.html | 2 ++ notes/2.14.md | 1 + 2 files changed, 3 insertions(+) diff --git a/examples/graticule.html b/examples/graticule.html index 008b4fc4c2..54368a10ce 100644 --- a/examples/graticule.html +++ b/examples/graticule.html @@ -31,6 +31,8 @@ worldExtent: [-180, 30, 180, 90], units: 'm' } + // Allow date line wrapping for 10 worlds by using -1800 and 1800 + // instead of -180 and 180. OpenLayers.Projection.defaults["EPSG:4326"].worldExtent = [-1800, -90, 1800, 90]; var graticuleCtl1, graticuleCtl2; var map1, map2; diff --git a/notes/2.14.md b/notes/2.14.md index 16d4c89c93..ffc0a970ef 100644 --- a/notes/2.14.md +++ b/notes/2.14.md @@ -26,3 +26,4 @@ a `worldExtent` option. This is the extent of the world in geographic coordinate * The control's layer (`gratLayer`) now prefers the Canvas renderer. This avoids coordinate range issues with the SVG renderer. * Previously, meridians were only labelled at the bottom, and parallels at the right border of the map. To better support polar projections, meridians are now also labelled at the left border, and parallels at the top border, if they do not have an intersection point with the bottom or right border. * To avoid maps with missing or short grid lines, make sure you do not use a graticule when working with a projection outside of its validity extent or recommended viewing area. +* To support date line wrapping, make sure you adjust the `worldExtent` setting for your projection in OpenLayers.Projection.defaults. To wrap the date line 10 times, set it to `[-1800, -90, 1800, 90]`. From 6c738e2c0d5cde02800d9747aa5465d73d8f4986 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 15:13:47 +0200 Subject: [PATCH 07/12] Simplify projection configuration --- examples/graticule.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/graticule.html b/examples/graticule.html index 54368a10ce..1d8dfdc036 100644 --- a/examples/graticule.html +++ b/examples/graticule.html @@ -28,8 +28,7 @@ Proj4js.defs["EPSG:42304"]="+title=Atlas of Canada, LCC +proj=lcc +lat_1=49 +lat_2=77 +lat_0=49 +lon_0=-95 +x_0=0 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs"; OpenLayers.Projection.defaults["EPSG:42304"] = { maxExtent: [-2200000,-712631,3072800,3840000], - worldExtent: [-180, 30, 180, 90], - units: 'm' + worldExtent: [-180, 0, 180, 90] } // Allow date line wrapping for 10 worlds by using -1800 and 1800 // instead of -180 and 180. From ea879e7b9a28afd953656dce2adfad91761db3ca Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 15:19:04 +0200 Subject: [PATCH 08/12] Fix typos --- lib/OpenLayers/Control/Graticule.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/OpenLayers/Control/Graticule.js b/lib/OpenLayers/Control/Graticule.js index b774b7d145..88bbe72078 100644 --- a/lib/OpenLayers/Control/Graticule.js +++ b/lib/OpenLayers/Control/Graticule.js @@ -157,7 +157,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { /** * Property: projectionCenterLonLat - * {OpenLayers.LonLat} The center of the projection's validty extent. + * {OpenLayers.LonLat} The center of the projection's validity extent. */ projectionCenterLonLat: null, @@ -407,7 +407,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { var centerLonLat = center.clone().transform( this.projection, this.epsg4326Projection); // If centerLonLat could not be transformed (e.g. [0, 0] in polar - // prjections), we shift the center a bit to get a result. + // projections), we shift the center a bit to get a result. if (isNaN(centerLonLat.lon) || isNaN(centerLonLat.lat)) { centerLonLat = center.add(0.000000001, 0.000000001).transform( this.projection, this.epsg4326Projection); From 210ba306428f9aedeff98d424910e3509a382536 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 16 Oct 2014 15:21:11 +0200 Subject: [PATCH 09/12] Fix clamping of lon and lat --- lib/OpenLayers/Control/Graticule.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/OpenLayers/Control/Graticule.js b/lib/OpenLayers/Control/Graticule.js index 88bbe72078..cdaf7fdc71 100644 --- a/lib/OpenLayers/Control/Graticule.js +++ b/lib/OpenLayers/Control/Graticule.js @@ -462,7 +462,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { centerLon = Math.floor(centerLonLat.lon / interval) * interval; lon = Math.max(centerLon, this.minLon); - lon = Math.min(centerLon, this.maxLon); + lon = Math.min(lon, this.maxLon); idx = this.addMeridian( lon, minLat, maxLat, squaredTolerance, extentGeom, 0); @@ -479,7 +479,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { } lon = Math.max(centerLon, this.minLon); - lon = Math.min(centerLon, this.maxLon); + lon = Math.min(lon, this.maxLon); while (lon != this.maxLon) { lon = Math.min(lon + interval, this.maxLon); @@ -509,7 +509,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { centerLat = Math.floor(centerLonLat.lat / interval) * interval; lat = Math.max(centerLat, this.minLat); - lat = Math.min(centerLat, this.maxLat); + lat = Math.min(lat, this.maxLat); idx = this.addParallel( lat, minLon, maxLon, squaredTolerance, extentGeom, 0); @@ -526,7 +526,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { } lat = Math.max(centerLat, this.minLat); - lat = Math.min(centerLat, this.maxLat); + lat = Math.min(lat, this.maxLat); while (lat != this.maxLat) { lat = Math.min(lat + interval, this.maxLat); From a0b1e69053956d508b61243ea97bea4c1272f2a7 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 17 Oct 2014 16:13:04 +0200 Subject: [PATCH 10/12] Clearer release notes --- notes/2.14.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/2.14.md b/notes/2.14.md index ffc0a970ef..b8fa21b6c9 100644 --- a/notes/2.14.md +++ b/notes/2.14.md @@ -22,7 +22,7 @@ a `worldExtent` option. This is the extent of the world in geographic coordinate ## OpenLayers.Control.Graticule rewrite * The Graticule control has a `numPoints` option, which is now deprecated. The graticule lines are optimized with adaptive quantization instead. -* The Graticul control now uses the projection specific maxExtent and worldExtent settings from `OpenLayers.Projection.defaults`. Make sure to create an `OpenLayers.Projection.defaults` entry for your projection when using a graticule. +* The Graticul control now uses the projection specific maxExtent and worldExtent settings from `OpenLayers.Projection.defaults`. Make sure to create an `OpenLayers.Projection.defaults` entry for your projection when using a Graticule control. * The control's layer (`gratLayer`) now prefers the Canvas renderer. This avoids coordinate range issues with the SVG renderer. * Previously, meridians were only labelled at the bottom, and parallels at the right border of the map. To better support polar projections, meridians are now also labelled at the left border, and parallels at the top border, if they do not have an intersection point with the bottom or right border. * To avoid maps with missing or short grid lines, make sure you do not use a graticule when working with a projection outside of its validity extent or recommended viewing area. From 1a7ddfe690c22cdc16ddb27d32b7344e9dbe7291 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 17 Oct 2014 16:17:32 +0200 Subject: [PATCH 11/12] Simplify projection configuration --- examples/polar-projections.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/polar-projections.js b/examples/polar-projections.js index e275b65cf5..b0ced9ea5f 100644 --- a/examples/polar-projections.js +++ b/examples/polar-projections.js @@ -2,23 +2,19 @@ var map, layer, overlay; OpenLayers.Projection.defaults['EPSG:3574'] = { maxExtent: [-5505054, -5505054, 5505054, 5505054], - worldExtent: [-180.0, 0.0, 180.0, 90.0], - units: 'm' + worldExtent: [-180.0, 0.0, 180.0, 90.0] }; OpenLayers.Projection.defaults['EPSG:3576'] = { maxExtent: [-5505054, -5505054, 5505054, 5505054], - worldExtent: [-180.0, 0.0, 180.0, 90.0], - units: 'm' + worldExtent: [-180.0, 0.0, 180.0, 90.0] }; OpenLayers.Projection.defaults['EPSG:3571'] = { maxExtent: [-5505054, -5505054, 5505054, 5505054], - worldExtent: [-180.0, 0.0, 180.0, 90.0], - units: 'm' + worldExtent: [-180.0, 0.0, 180.0, 90.0] }; OpenLayers.Projection.defaults['EPSG:3573'] = { maxExtent: [-5505054, -5505054, 5505054, 5505054], - worldExtent: [-180.0, 0.0, 180.0, 90.0], - units: 'm' + worldExtent: [-180.0, 0.0, 180.0, 90.0] }; function setProjection() { From 4da4feeab0998e97d6a1ab0b00d99eb5ea2555dd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 17 Oct 2014 16:19:51 +0200 Subject: [PATCH 12/12] Fixing typos --- lib/OpenLayers/Control/Graticule.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/OpenLayers/Control/Graticule.js b/lib/OpenLayers/Control/Graticule.js index cdaf7fdc71..218a54b23f 100644 --- a/lib/OpenLayers/Control/Graticule.js +++ b/lib/OpenLayers/Control/Graticule.js @@ -581,7 +581,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { * minLat - {number} Minimum latitude. * maxLat - {number} Maximum latitude. * squaredTolerance - {number} Squared tolerance. - * extent - {OpenLayers.Geometry.Polygon} Extent. + * extentGeom - {OpenLayers.Geometry.Polygon} Extent. * index {number} Index. * * Returns: @@ -611,7 +611,7 @@ OpenLayers.Control.Graticule = OpenLayers.Class(OpenLayers.Control, { * minLon - {number} Minimum longitude. * maxLon - {number} Maximum longitude. * squaredTolerance - {number} Squared tolerance. - * extent - {OpenLayers.Geometry.Polygon} Extent. + * extentGeom - {OpenLayers.Geometry.Polygon} Extent. * index - {number} Index. * * Returns: