diff --git a/examples/graticule.js b/examples/graticule.js
index 468938ce61..1cb67c310a 100644
--- a/examples/graticule.js
+++ b/examples/graticule.js
@@ -27,6 +27,8 @@ var graticule = new ol.Graticule({
color: 'rgba(255,120,0,0.9)',
width: 2,
lineDash: [0.5, 4]
- })
+ }),
+ showLabels: true
});
+
graticule.setMap(map);
diff --git a/externs/olx.js b/externs/olx.js
index 01445f7d27..6f3870360d 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -112,7 +112,14 @@ olx.LogoOptions.prototype.src;
* @typedef {{map: (ol.Map|undefined),
* maxLines: (number|undefined),
* strokeStyle: (ol.style.Stroke|undefined),
- * targetSize: (number|undefined)}}
+ * targetSize: (number|undefined),
+ * showLabels: (boolean|undefined),
+ * lonLabelFormatter: (undefined|function(number):string),
+ * latLabelFormatter: (undefined|function(number):string),
+ * lonLabelPosition: (number|undefined),
+ * latLabelPosition: (number|undefined),
+ * lonLabelStyle: (ol.style.Text|undefined),
+ * latLabelStyle: (ol.style.Text|undefined)}}
*/
olx.GraticuleOptions;
@@ -157,6 +164,106 @@ olx.GraticuleOptions.prototype.strokeStyle;
olx.GraticuleOptions.prototype.targetSize;
+/**
+ * Render a label with the respective latitude/longitude for each graticule
+ * line. Default is false.
+ *
+ * @type {boolean|undefined}
+ * @api
+ */
+olx.GraticuleOptions.prototype.showLabels;
+
+
+/**
+ * Label formatter for longitudes. This function is called with the longitude as
+ * argument, and should return a formatted string representing the longitude.
+ * By default, labels are formatted as degrees, minutes, seconds and hemisphere.
+ *
+ * @type {undefined|function(number):string}
+ * @api
+ */
+olx.GraticuleOptions.prototype.lonLabelFormatter;
+
+
+/**
+ * Label formatter for latitudes. This function is called with the latitude as
+ * argument, and should return a formatted string representing the latitude.
+ * By default, labels are formatted as degrees, minutes, seconds and hemisphere.
+ *
+ * @type {undefined|function(number):string}
+ * @api
+ */
+olx.GraticuleOptions.prototype.latLabelFormatter;
+
+
+/**
+ * Longitude label position in fractions (0..1) of view extent. 0 means at the
+ * bottom of the viewport, 1 means at the top. Default is 0.
+ * @type {number|undefined}
+ * @api
+ */
+olx.GraticuleOptions.prototype.lonLabelPosition;
+
+
+/**
+ * Latitude label position in fractions (0..1) of view extent. 0 means at the
+ * left of the viewport, 1 means at the right. Default is 1.
+ * @type {number|undefined}
+ * @api
+ */
+olx.GraticuleOptions.prototype.latLabelPosition;
+
+
+/**
+ * Longitude label text style. The default is
+ * ```js
+ * new ol.style.Text({
+ * font: '12px Calibri,sans-serif',
+ * textBaseline: 'bottom',
+ * fill: new ol.style.Fill({
+ * color: 'rgba(0,0,0,1)'
+ * }),
+ * stroke: new ol.style.Stroke({
+ * color: 'rgba(255,255,255,1)',
+ * width: 3
+ * })
+ * });
+ * ```
+ * Note that the default's `textBaseline` configuration will not work well for
+ * `lonLabelPosition` configurations that position labels close to the top of
+ * the viewport.
+ *
+ * @type {ol.style.Text|undefined}
+ * @api
+ */
+olx.GraticuleOptions.prototype.lonLabelStyle;
+
+
+/**
+ * Latitude label text style. The default is
+ * ```js
+ * new ol.style.Text({
+ * font: '12px Calibri,sans-serif',
+ * textAlign: 'end',
+ * fill: new ol.style.Fill({
+ * color: 'rgba(0,0,0,1)'
+ * }),
+ * stroke: new ol.style.Stroke({
+ * color: 'rgba(255,255,255,1)',
+ * width: 3
+ * })
+ * });
+ * ```
+ * Note that the default's `textAlign` configuration will not work well for
+ * `latLabelPosition` configurations that position labels close to the left of
+ * the viewport.
+ *
+ * @type {ol.style.Text|undefined}
+ * @api
+ */
+olx.GraticuleOptions.prototype.latLabelStyle;
+
+
/**
* Object literal with config options for interactions.
* @typedef {{handleEvent: function(ol.MapBrowserEvent):boolean}}
diff --git a/src/ol/graticule.js b/src/ol/graticule.js
index e38f1ca8e3..c2fe312cb7 100644
--- a/src/ol/graticule.js
+++ b/src/ol/graticule.js
@@ -1,13 +1,17 @@
goog.provide('ol.Graticule');
+goog.require('ol.coordinate');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
+goog.require('ol.geom.Point');
goog.require('ol.geom.flat.geodesic');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.render.EventType');
+goog.require('ol.style.Fill');
goog.require('ol.style.Stroke');
+goog.require('ol.style.Text');
/**
@@ -104,31 +108,116 @@ ol.Graticule = function(opt_options) {
*/
this.parallels_ = [];
- /**
- * @type {ol.style.Stroke}
- * @private
- */
+ /**
+ * @type {ol.style.Stroke}
+ * @private
+ */
this.strokeStyle_ = options.strokeStyle !== undefined ?
options.strokeStyle : ol.Graticule.DEFAULT_STROKE_STYLE_;
- /**
- * @type {ol.TransformFunction|undefined}
- * @private
- */
+ /**
+ * @type {ol.TransformFunction|undefined}
+ * @private
+ */
this.fromLonLatTransform_ = undefined;
- /**
- * @type {ol.TransformFunction|undefined}
- * @private
- */
+ /**
+ * @type {ol.TransformFunction|undefined}
+ * @private
+ */
this.toLonLatTransform_ = undefined;
- /**
- * @type {ol.Coordinate}
- * @private
- */
+ /**
+ * @type {ol.Coordinate}
+ * @private
+ */
this.projectionCenterLonLat_ = null;
+ /**
+ * @type {Array.
}
+ * @private
+ */
+ this.meridiansLabels_ = null;
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.parallelsLabels_ = null;
+
+ if (options.showLabels == true) {
+ var degreesToString = ol.coordinate.degreesToStringHDMS;
+
+ /**
+ * @type {null|function(number):string}
+ * @private
+ */
+ this.lonLabelFormatter_ = options.lonLabelFormatter == undefined ?
+ degreesToString.bind(this, 'EW') : options.lonLabelFormatter;
+
+ /**
+ * @type {function(number):string}
+ * @private
+ */
+ this.latLabelFormatter_ = options.latLabelFormatter == undefined ?
+ degreesToString.bind(this, 'NS') : options.latLabelFormatter;
+
+ /**
+ * Longitude label position in fractions (0..1) of view extent. 0 means
+ * bottom, 1 means top.
+ * @type {number}
+ * @private
+ */
+ this.lonLabelPosition_ = options.lonLabelPosition == undefined ? 0 :
+ options.lonLabelPosition;
+
+ /**
+ * Latitude Label position in fractions (0..1) of view extent. 0 means left, 1
+ * means right.
+ * @type {number}
+ * @private
+ */
+ this.latLabelPosition_ = options.latLabelPosition == undefined ? 1 :
+ options.latLabelPosition;
+
+ /**
+ * @type {ol.style.Text}
+ * @private
+ */
+ this.lonLabelStyle_ = options.lonLabelStyle !== undefined ? options.lonLabelStyle :
+ new ol.style.Text({
+ font: '12px Calibri,sans-serif',
+ textBaseline: 'bottom',
+ fill: new ol.style.Fill({
+ color: 'rgba(0,0,0,1)'
+ }),
+ stroke: new ol.style.Stroke({
+ color: 'rgba(255,255,255,1)',
+ width: 3
+ })
+ });
+
+ /**
+ * @type {ol.style.Text}
+ * @private
+ */
+ this.latLabelStyle_ = options.latLabelStyle !== undefined ? options.latLabelStyle :
+ new ol.style.Text({
+ font: '12px Calibri,sans-serif',
+ textAlign: 'end',
+ fill: new ol.style.Fill({
+ color: 'rgba(0,0,0,1)'
+ }),
+ stroke: new ol.style.Stroke({
+ color: 'rgba(255,255,255,1)',
+ width: 3
+ })
+ });
+
+ this.meridiansLabels_ = [];
+ this.parallelsLabels_ = [];
+ }
+
this.setMap(options.map !== undefined ? options.map : null);
};
@@ -166,11 +255,39 @@ ol.Graticule.prototype.addMeridian_ = function(lon, minLat, maxLat, squaredToler
var lineString = this.getMeridian_(lon, minLat, maxLat,
squaredTolerance, index);
if (ol.extent.intersects(lineString.getExtent(), extent)) {
+ if (this.meridiansLabels_) {
+ var textPoint = this.getMeridianPoint_(lineString, extent, index);
+ this.meridiansLabels_[index] = {
+ geom: textPoint,
+ text: this.lonLabelFormatter_(lon)
+ };
+ }
this.meridians_[index++] = lineString;
}
return index;
};
+/**
+ * @param {ol.geom.LineString} lineString Meridian
+ * @param {ol.Extent} extent Extent.
+ * @param {number} index Index.
+ * @return {ol.geom.Point} Meridian point.
+ * @private
+ */
+ol.Graticule.prototype.getMeridianPoint_ = function(lineString, extent, index) {
+ var flatCoordinates = lineString.getFlatCoordinates();
+ var clampedBottom = Math.max(extent[1], flatCoordinates[1]);
+ var clampedTop = Math.min(extent[3], flatCoordinates[flatCoordinates.length - 1]);
+ var lat = ol.math.clamp(
+ extent[1] + Math.abs(extent[1] - extent[3]) * this.lonLabelPosition_,
+ clampedBottom, clampedTop);
+ var coordinate = [flatCoordinates[0], lat];
+ var point = this.meridiansLabels_[index] !== undefined ?
+ this.meridiansLabels_[index].geom : new ol.geom.Point(null);
+ point.setCoordinates(coordinate);
+ return point;
+};
+
/**
* @param {number} lat Latitude.
@@ -186,12 +303,41 @@ ol.Graticule.prototype.addParallel_ = function(lat, minLon, maxLon, squaredToler
var lineString = this.getParallel_(lat, minLon, maxLon, squaredTolerance,
index);
if (ol.extent.intersects(lineString.getExtent(), extent)) {
+ if (this.parallelsLabels_) {
+ var textPoint = this.getParallelPoint_(lineString, extent, index);
+ this.parallelsLabels_[index] = {
+ geom: textPoint,
+ text: this.latLabelFormatter_(lat)
+ };
+ }
this.parallels_[index++] = lineString;
}
return index;
};
+/**
+ * @param {ol.geom.LineString} lineString Parallels.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} index Index.
+ * @return {ol.geom.Point} Parallel point.
+ * @private
+ */
+ol.Graticule.prototype.getParallelPoint_ = function(lineString, extent, index) {
+ var flatCoordinates = lineString.getFlatCoordinates();
+ var clampedLeft = Math.max(extent[0], flatCoordinates[0]);
+ var clampedRight = Math.min(extent[2], flatCoordinates[flatCoordinates.length - 2]);
+ var lon = ol.math.clamp(
+ extent[0] + Math.abs(extent[0] - extent[2]) * this.latLabelPosition_,
+ clampedLeft, clampedRight);
+ var coordinate = [lon, flatCoordinates[1]];
+ var point = this.parallelsLabels_[index] !== undefined ?
+ this.parallelsLabels_[index].geom : new ol.geom.Point(null);
+ point.setCoordinates(coordinate);
+ return point;
+};
+
+
/**
* @param {ol.Extent} extent Extent.
* @param {ol.Coordinate} center Center.
@@ -204,6 +350,12 @@ ol.Graticule.prototype.createGraticule_ = function(extent, center, resolution, s
var interval = this.getInterval_(resolution);
if (interval == -1) {
this.meridians_.length = this.parallels_.length = 0;
+ if (this.meridiansLabels_) {
+ this.meridiansLabels_.length = 0;
+ }
+ if (this.parallelsLabels_) {
+ this.parallelsLabels_.length = 0;
+ }
return;
}
@@ -249,6 +401,9 @@ ol.Graticule.prototype.createGraticule_ = function(extent, center, resolution, s
}
this.meridians_.length = idx;
+ if (this.meridiansLabels_) {
+ this.meridiansLabels_.length = idx;
+ }
// Create parallels
@@ -272,6 +427,9 @@ ol.Graticule.prototype.createGraticule_ = function(extent, center, resolution, s
}
this.parallels_.length = idx;
+ if (this.parallelsLabels_) {
+ this.parallelsLabels_.length = idx;
+ }
};
@@ -426,11 +584,28 @@ ol.Graticule.prototype.handlePostCompose_ = function(e) {
var i, l, line;
for (i = 0, l = this.meridians_.length; i < l; ++i) {
line = this.meridians_[i];
- vectorContext.drawLineString(line, null);
+ vectorContext.drawGeometry(line);
}
for (i = 0, l = this.parallels_.length; i < l; ++i) {
line = this.parallels_[i];
- vectorContext.drawLineString(line, null);
+ vectorContext.drawGeometry(line);
+ }
+ var labelData;
+ if (this.meridiansLabels_) {
+ for (i = 0, l = this.meridiansLabels_.length; i < l; ++i) {
+ labelData = this.meridiansLabels_[i];
+ this.lonLabelStyle_.setText(labelData.text);
+ vectorContext.setTextStyle(this.lonLabelStyle_);
+ vectorContext.drawGeometry(labelData.geom);
+ }
+ }
+ if (this.parallelsLabels_) {
+ for (i = 0, l = this.parallelsLabels_.length; i < l; ++i) {
+ labelData = this.parallelsLabels_[i];
+ this.latLabelStyle_.setText(labelData.text);
+ vectorContext.setTextStyle(this.latLabelStyle_);
+ vectorContext.drawGeometry(labelData.geom);
+ }
}
};
diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js
index 4370156ff0..80da8e58b9 100644
--- a/src/ol/typedefs.js
+++ b/src/ol/typedefs.js
@@ -253,6 +253,15 @@ ol.FeatureStyleFunction;
ol.FeatureUrlFunction;
+/**
+ * @typedef {{
+ * geom: ol.geom.Point,
+ * text: string
+ * }}
+ */
+ol.GraticuleLabelDataType;
+
+
/**
* A function that is called to trigger asynchronous canvas drawing. It is
* called with a "done" callback that should be called when drawing is done.
diff --git a/test/spec/ol/graticule.test.js b/test/spec/ol/graticule.test.js
index 3611c3c899..dc628c61d0 100644
--- a/test/spec/ol/graticule.test.js
+++ b/test/spec/ol/graticule.test.js
@@ -9,14 +9,15 @@ goog.require('ol.style.Stroke');
describe('ol.Graticule', function() {
var graticule;
- beforeEach(function() {
+ function createGraticule() {
graticule = new ol.Graticule({
map: new ol.Map({})
});
- });
+ }
describe('#createGraticule', function() {
- it('creates the graticule', function() {
+ it('creates a graticule without labels', function() {
+ createGraticule();
var extent = [-25614353.926475704, -7827151.696402049,
25614353.926475704, 7827151.696402049];
var projection = ol.proj.get('EPSG:3857');
@@ -26,9 +27,32 @@ describe('ol.Graticule', function() {
graticule.createGraticule_(extent, [0, 0], resolution, squaredTolerance);
expect(graticule.getMeridians().length).to.be(13);
expect(graticule.getParallels().length).to.be(3);
+ expect(graticule.meridiansLabels_).to.be(null);
+ expect(graticule.parallelsLabels_).to.be(null);
+ });
+
+ it('creates a graticule with labels', function() {
+ graticule = new ol.Graticule({
+ map: new ol.Map({}),
+ showLabels: true
+ });
+ var extent = [-25614353.926475704, -7827151.696402049,
+ 25614353.926475704, 7827151.696402049];
+ var projection = ol.proj.get('EPSG:3857');
+ var resolution = 39135.75848201024;
+ var squaredTolerance = resolution * resolution / 4.0;
+ graticule.updateProjectionInfo_(projection);
+ graticule.createGraticule_(extent, [0, 0], resolution, squaredTolerance);
+ expect(graticule.meridiansLabels_.length).to.be(13);
+ expect(graticule.meridiansLabels_[0].text).to.be('0° 00′ 00″');
+ expect(graticule.meridiansLabels_[0].geom.getCoordinates()[0]).to.roughlyEqual(0, 1e-9);
+ expect(graticule.parallelsLabels_.length).to.be(3);
+ expect(graticule.parallelsLabels_[0].text).to.be('0° 00′ 00″');
+ expect(graticule.parallelsLabels_[0].geom.getCoordinates()[1]).to.roughlyEqual(0, 1e-9);
});
it('has a default stroke style', function() {
+ createGraticule();
var actualStyle = graticule.strokeStyle_;
expect(actualStyle).not.to.be(undefined);
@@ -36,6 +60,7 @@ describe('ol.Graticule', function() {
});
it('can be configured with a stroke style', function() {
+ createGraticule();
var customStrokeStyle = new ol.style.Stroke({
color: 'rebeccapurple'
});
@@ -49,6 +74,38 @@ describe('ol.Graticule', function() {
expect(actualStyle).to.be(customStrokeStyle);
});
+ it('can be configured with label options', function() {
+ var latLabelStyle = new ol.style.Text();
+ var lonLabelStyle = new ol.style.Text();
+ graticule = new ol.Graticule({
+ map: new ol.Map({}),
+ showLabels: true,
+ lonLabelFormatter: function(lon) {
+ return 'lon: ' + lon.toString();
+ },
+ latLabelFormatter: function(lat) {
+ return 'lat: ' + lat.toString();
+ },
+ lonLabelPosition: 0.9,
+ latLabelPosition: 0.1,
+ lonLabelStyle: lonLabelStyle,
+ latLabelStyle: latLabelStyle
+ });
+ var extent = [-25614353.926475704, -7827151.696402049,
+ 25614353.926475704, 7827151.696402049];
+ var projection = ol.proj.get('EPSG:3857');
+ var resolution = 39135.75848201024;
+ var squaredTolerance = resolution * resolution / 4.0;
+ graticule.updateProjectionInfo_(projection);
+ graticule.createGraticule_(extent, [0, 0], resolution, squaredTolerance);
+ expect(graticule.meridiansLabels_[0].text).to.be('lon: 0');
+ expect(graticule.parallelsLabels_[0].text).to.be('lat: 0');
+ expect(graticule.lonLabelStyle_).to.eql(lonLabelStyle);
+ expect(graticule.latLabelStyle_).to.eql(latLabelStyle);
+ expect(graticule.lonLabelPosition_).to.be(0.9);
+ expect(graticule.latLabelPosition_).to.be(0.1);
+ });
+
});
});