Support text rotation, scale, and offset.
MODIFY
examples/annotations/index.jade
examples/annotations/index.jade
@@ -44,11 +44,11 @@ block append mainContent
button.close(type='button', data-dismiss='modal') &times;
h4.modal-title Edit Annotation
.modal-body
+ .row
+ .col-md-6
.form-group
label(for='edit-name') Name
input#edit-name(option='name')
- .row
- .col-md-6
.form-group
label(for='edit-description') Description
input#edit-description(option='description', type='textarea')
@@ -127,7 +127,7 @@ block append mainContent
option(value='end') End
option(value='left') Left
option(value='right') Right
- .form-group(annotation-types='all', title='Verticall alignment')
+ .form-group(annotation-types='all', title='Vertical alignment')
label(for='edit-textBaseline') Vertical Align.
select#edit-textAlign(option='textBaseline', format='text', optiontype='label')
option(value='middle') Middle
@@ -136,6 +136,20 @@ block append mainContent
option(value='alphabetic') Alphabetic
option(value='ideographic') Ideographic
option(value='bottom') Bottom
+ .form-group(annotation-types='all')
+ label(for='edit-rotateWithMap') Rotate with Map
+ select#edit-rotateWithMap(option='rotateWithMap', format='boolean', optiontype='label')
+ option(value='false') No
+ option(value='true') Yes
+ .form-group(annotation-types='all')
+ label(for='edit-rotation') Rotation
+ input#edit-rotation(option='rotation', format='angle', optiontype='label')
+ .form-group(annotation-types='all', title='Set to "false" to disable, "true" to use the specified font size at the current zoom, or a zoom level to use the specified font size at that zoom level.')
+ label(for='edit-textScaled') Base Scale
+ input#edit-textScaled(option='textScaled', format='booleanOrNumber', optiontype='label')
+ .form-group(annotation-types='all', title='This is the x, y offset of the label in pixels from its default position before rotation and scale. Example: "5, -4"')
+ label(for='edit-offset') Offset
+ input#edit-offset(option='offset', format='coordinate2', optiontype='label')
.form-group
#edit-validation-error
.modal-footer

MODIFY
examples/annotations/main.js
examples/annotations/main.js
@@ -334,10 +334,19 @@ $(function () {
break;
}
switch (format) {
+ case 'angle':
+ if (value !== undefined && value !== null && value !== '') {
+ value = '' + +(+value * 180.0 / Math.PI).toFixed(4) + ' deg';
+ }
+ break;
case 'color':
// always show colors as hex values
value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0}, 'needed');
break;
+ case 'coordinate2':
+ if (value !== undefined && value !== null && value !== '') {
+ value = '' + value.x + ', ' + value.y;
+ }
}
if ((value === undefined || value === '' || value === null) && $('[option]', ctl).is('select')) {
value = $('[option] option', ctl).eq(0).val();
@@ -370,12 +379,30 @@ $(function () {
$('.form-group[annotation-types]').each(function () {
var ctl = $(this),
key = $('[option]', ctl).attr('option'),
+ format = $('[option]', ctl).attr('format'),
value, oldvalue;
if (!ctl.attr('annotation-types').match(typeMatch)) {
return;
}
- value = layer.validateAttribute($('[option]', ctl).val(),
- $('[option]', ctl).attr('format'));
+ value = $('[option]', ctl).val();
+ switch (format) {
+ case 'angle':
+ if (/^\s*[.0-9eE]+\s*$/.exec(value)) {
+ value += 'deg';
+ }
+ break;
+ }
+ switch (key) {
+ case 'offset':
+ //DWM::
+ break;
+ case 'textScaled':
+ if (['true', 'on', 'yes'].indexOf(value.trim().toLowerCase()) >= 0) {
+ value = map.zoom();
+ }
+ break;
+ }
+ value = layer.validateAttribute(value, format);
switch ($('[option]', ctl).attr('optiontype')) {
case 'option':
oldvalue = opt[key];

MODIFY
src/annotationLayer.js
src/annotationLayer.js
@@ -85,7 +85,11 @@ var annotationLayer = function (args) {
textFeature.usedStyles.forEach(function (key) {
geojsonStyleProperties[key] = {
option: 'labelStyle',
- dataType: ['visible', 'rotateWithMap', 'scaleWithMap'].indexOf(key) >= 0 ? 'boolean' : 'text',
+ dataType: ['visible', 'rotateWithMap', 'scaleWithMap'].indexOf(key) >= 0 ? 'boolean' : (
+ ['scale'].indexOf(key) >= 0 ? 'booleanOrNumber' : (
+ ['rotation'].indexOf(key) >= 0 ? 'angle' : (
+ ['offset'].indexOf(key) >= 0 ? 'coordinate2' :
+ 'text'))),
keys: [
key,
'label' + key.charAt(0).toUpperCase() + key.slice(1),
@@ -597,7 +601,14 @@ var annotationLayer = function (args) {
* `'no'`, `'true'`, `'on'`, or `'yes'`, falsy values that aren't 0, and
* `true` are handled as booleans. Otherwise, a floating point number that
* isn't NaN or an infinity.
+ * - `coordinate2`: either an object with x and y properties that are
+ * numbers, or a string of the form <x>[,]<y> with optional whitespace, or
+ * a JSON encoded object with x and y values, or a JSON encoded list of at
+ * leasst two numbers.
* - `number`: a floating point number that isn't NaN or an infinity.
+ * - `angle`: a number that represents radians. If followed by one of `deg`,
+ * `grad`, or `turn`, it is converted to radians. An empty string is also
+ * allowed.
* - `text`: any text string.
* @param {number|string|object|boolean} value The value to validate.
* @param {string} dataType The data type for validation.
@@ -605,22 +616,62 @@ var annotationLayer = function (args) {
* `undefined`.
*/
this.validateAttribute = function (value, dataType) {
+ var parts;
+
if (value === undefined || value === null) {
return;
}
switch (dataType) {
+ case 'angle':
+ if (value === '') {
+ break;
+ }
+ parts = /^\s*([-.0-9eE]+)\s*(deg|rad|grad|turn)?\s*$/.exec(('' + value).toLowerCase());
+ if (!parts || !isFinite(parts[1])) {
+ return;
+ }
+ var factor = (parts[2] === 'grad' ? Math.PI / 200 :
+ (parts[2] === 'deg' ? Math.PI / 180 :
+ (parts[2] === 'turn' ? 2 * Math.PI : 1)));
+ value = +parts[1] * factor;
+ break;
+ case 'boolean':
+ value = !!value && ['false', 'no', 'off'].indexOf(('' + value).toLowerCase()) < 0;
+ break;
case 'booleanOrNumber':
if ((!value && value !== 0) || ['true', 'false', 'off', 'on', 'no', 'yes'].indexOf(('' + value).toLowerCase()) >= 0) {
value = !!value && ['false', 'no', 'off'].indexOf(('' + value).toLowerCase()) < 0;
} else {
value = +value;
- if (isNaN(value) || !isFinite(value)) {
+ if (!isFinite(value)) {
return;
}
}
break;
- case 'boolean':
- value = !!value && ['false', 'no', 'off'].indexOf(('' + value).toLowerCase()) < 0;
+ case 'coordinate2':
+ if (value === '') {
+ break;
+ }
+ if (value && isFinite(value.x) && isFinite(value.y)) {
+ value.x = +value.x;
+ value.y = +value.y;
+ break;
+ }
+ try { value = JSON.parse(value); } catch (err) { }
+ if (value && isFinite(value.x) && isFinite(value.y)) {
+ value.x = +value.x;
+ value.y = +value.y;
+ break;
+ }
+ if (Array.isArray(value) && isFinite(value[0]) && isFinite(value[1])) {
+ value = {x: +value[0], y: +value[1]};
+ break;
+ }
+ parts = /^\s*([-.0-9eE]+)\s*,?\s*([-.0-9eE]+)\s*$/.exec('' + value);
+ if (!parts || !isFinite(parts[1]) || !isFinite(parts[2])) {
+ return;
+ }
+ value = {x: +parts[1], y: +parts[2]};
break;
case 'color':
value = util.convertColor(value);
@@ -630,7 +681,7 @@ var annotationLayer = function (args) {
break;
case 'number':
value = +value;
- if (isNaN(value) || !isFinite(value)) {
+ if (!util.isNonNullFinite(value)) {
return;
}
break;
@@ -645,7 +696,7 @@ var annotationLayer = function (args) {
break;
case 'positive':
value = +value;
- if (isNaN(value) || !isFinite(value) || value <= 0) {
+ if (!isFinite(value) || value <= 0) {
return;
}
break;

MODIFY
src/canvas/textFeature.js
src/canvas/textFeature.js
@@ -2,6 +2,7 @@ var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var textFeature = require('../textFeature');
var util = require('../util');
+var mat3 = require('gl-mat3');
/**
* Create a new instance of class canvas.textFeature.
@@ -89,7 +90,11 @@ var canvas_textFeature = function (arg) {
var data = m_this.data(),
posFunc = m_this.style.get('position'),
textFunc = m_this.style.get('text'),
- fontFromSubValues, text, pos, val;
+ mapRotation = map.rotation(),
+ mapZoom = map.zoom(),
+ fontFromSubValues, text, pos, val,
+ rotation, rotateWithMap, scale, offset,
+ transform, lastTransform = util.mat3AsArray();
/* If any of the font styles other than `font` have values, then we need to
* construct a single font value from the subvalues. Otherwise, we can
@@ -119,12 +124,40 @@ var canvas_textFeature = function (arg) {
m_this._canvasProperty(context2d, 'font', m_this.getFontFromStyles(fontFromSubValues, d, i));
m_this._canvasProperty(context2d, 'textAlign', m_this.style.get('textAlign')(d, i) || 'center');
m_this._canvasProperty(context2d, 'textBaseline', m_this.style.get('textBaseline')(d, i) || 'middle');
+ m_this._canvasProperty(context2d, 'direction', m_this.style.get('direction')(d, i) || 'inherit');
+ /* rotation, scale, and offset */
+ rotation = m_this.style.get('rotation')(d, i) || 0;
+ rotateWithMap = m_this.style.get('rotateWithMap')(d, i) && mapRotation;
+ scale = m_this.style.get('textScaled')(d, i);
+ scale = util.isNonNullFinite(scale) ? Math.pow(2, mapZoom - scale) : null;
+ offset = m_this.style.get('offset')(d, i);
+ transform = util.mat3AsArray();
+ if (rotation || rotateWithMap || (scale && scale !== 1) || (offset && (offset.x || offset.y))) {
+ mat3.translate(transform, transform, [pos.x, pos.y]);
+ if (rotateWithMap && mapRotation) {
+ mat3.rotate(transform, transform, mapRotation);
+ }
+ mat3.translate(transform, transform, [
+ offset && offset.x ? +offset.x : 0,
+ offset && offset.y ? +offset.y : 0]);
+ if (rotation) {
+ mat3.rotate(transform, transform, rotation);
+ }
+ if (scale && scale !== 1) {
+ mat3.scale(transform, transform, [scale, scale]);
+ }
+ mat3.translate(transform, transform, [-pos.x, -pos.y]);
+ }
+ if (lastTransform[0] !== transform[0] || lastTransform[1] !== transform[1] ||
+ lastTransform[3] !== transform[3] || lastTransform[4] !== transform[4] ||
+ lastTransform[6] !== transform[6] || lastTransform[7] !== transform[7]) {
+ context2d.setTransform(transform[0], transform[1], transform[3], transform[4], transform[6], transform[7]);
+ mat3.copy(lastTransform, transform);
+ }
// TODO: fetch and use other properties:
- // 'direction', 'rotation', 'rotateWithMap', 'scale',
- // 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOffset',
- // 'shadowBlur', 'shadowRotate', 'shadowOpacity'
- m_this._canvasProperty(context2d, 'direction', 'inherit'); // ltr, rtl, inherit
+ // 'shadowColor', 'shadowOffset', 'shadowBlur', 'shadowRotate',
+ // 'shadowOpacity'
/*
ctx.shadowColor = "black";
ctx.shadowOffsetX = 5;
@@ -135,6 +168,7 @@ var canvas_textFeature = function (arg) {
context2d.fillText(text, pos.x, pos.y);
});
m_this._canvasProperty(context2d, 'globalAlpha', 1);
+ context2d.setTransform(1, 0, 0, 1, 0, 0);
};
this._init(arg);

MODIFY
src/textFeature.js
src/textFeature.js
@@ -52,8 +52,6 @@ var feature = require('./feature');
* The size is scaled from this point.
* @property {geo.screenPosition|function} [style.offset] Offset from the
* default position for the text. This is applied before rotation.
- * @property {number|function} [style.width] The maximum width of the text in
- * pixels. `null` or 0 for no maximum.
* @property {geo.geoColor|function} [style.shadowColor='black'] Text shadow
* color. May include opacity.
* @property {number|function} [style.shadowOpacity=1] The opacity of the
@@ -151,7 +149,7 @@ var textFeature = function (arg) {
color: { r: 0, g: 0, b: 0 },
rotation: 0, /* in radians */
rotateWithMap: false,
- scaleWithMap: false,
+ textScaled: false,
position: function (d) { return d; },
text: function (d) { return d.text; }
},
@@ -181,9 +179,9 @@ var textFeature = function (arg) {
textFeature.usedStyles = [
'visible', 'font', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch',
'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline',
- 'direction', 'color', 'textOpacity', 'rotation', 'rotateWithMap', 'scale',
- 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOpacity',
- 'shadowOffset', 'shadowBlur', 'shadowRotate'
+ 'direction', 'color', 'textOpacity', 'rotation', 'rotateWithMap',
+ 'textScaled', 'offset', 'shadowColor', 'shadowOpacity', 'shadowOffset',
+ 'shadowBlur', 'shadowRotate'
];
/**

MODIFY
src/util/index.js
src/util/index.js
@@ -177,6 +177,17 @@ var util = module.exports = {
}
},
+ /**
+ * Check if a value coerces to a number that is finite, not a NaN, and not
+ * `null` or `false`.
+ *
+ * @param {object} val The value to check.
+ * @returns {boolean} True if `val` is a non-null, non-false, finite number.
+ */
+ isNonNullFinite: function (val) {
+ return isFinite(val) && val !== null && val !== false;
+ },
+
/**
* Return a random string of length n || 8. The string consists of
* mixed-case ASCII alphanumerics.
@@ -409,6 +420,22 @@ var util = module.exports = {
return [0, 0, 0];
},
+ /**
+ * Create a `mat3` that is always an array. This should only be used if it
+ * will not be used in a WebGL context. Plain arrays usually use 64-bit
+ * float values, whereas `mat3` defaults to 32-bit floats.
+ *
+ * @returns {array} Identity `mat3` compatible array.
+ * @memberof geo.util
+ */
+ mat3AsArray: function () {
+ return [
+ 1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 1
+ ];
+ },
+
/**
* Create a `mat4` that is always an array. This should only be used if it
* will not be used in a WebGL context. Plain arrays usually use 64-bit

Support text rotation, scale, and offset.