Set text fonts.
This handles font substyles, such as fontSize.
This is exposed in the annotations example.
This also handles textAlign, textBaseline, color, and textOpacity.
Much of the infrastructure for other text styles is in place.
MODIFY
examples/annotations/index.jade
examples/annotations/index.jade
@@ -47,12 +47,11 @@ block append mainContent
.form-group
label(for='edit-name') Name
input#edit-name(option='name')
+ .row
+ .col-md-6
.form-group
- label(for='edit-name') Label
- input#edit-name(option='label')
- .form-group
- label(for='edit-name') Description
- input#edit-name(option='description', type='textarea')
+ label(for='edit-description') Description
+ input#edit-description(option='description', type='textarea')
.form-group(annotation-types='point')
label(for='edit-radius') Radius
input#edit-radius(option='radius', format='positive')
@@ -102,6 +101,36 @@ block append mainContent
option(value='bevel') Bevel
option(value='round') Round
option(value='miter-clip') Miter-Clip
+ .col-md-6
+ .form-group
+ label(for='edit-label') Label
+ input#edit-label(option='label')
+ .form-group(annotation-types='all')
+ label(for='edit-color') Label Color
+ input#edit-color(option='color', format='color', optiontype='label')
+ .form-group(annotation-types='all')
+ label(for='edit-textOpacity') Label Opacity
+ input#edit-textOpacity(option='textOpacity', format='opacity', optiontype='label')
+ .form-group(annotation-types='all', title='This is of the form [italic|oblique] [small-caps] [bold|bolder|lighter|<weight>] [<stretch>] <size>[/<line height>] <family>')
+ label(for='edit-font') Font
+ input#edit-font(option='font', optiontype='label')
+ .form-group(annotation-types='all', title='Horizontal alignment')
+ label(for='edit-textAlign') Horizontal Align.
+ select#edit-textAlign(option='textAlign', format='text', optiontype='label')
+ option(value='middle') Middle
+ option(value='start') Start
+ option(value='end') End
+ option(value='left') Left
+ option(value='right') Right
+ .form-group(annotation-types='all', title='Verticall alignment')
+ label(for='edit-textBaseline') Vertical Align.
+ select#edit-textAlign(option='textBaseline', format='text', optiontype='label')
+ option(value='middle') Middle
+ option(value='top') Top
+ option(value='hanging') Hanging
+ option(value='alphabetic') Alphabetic
+ option(value='ideographic') Ideographic
+ option(value='bottom') Bottom
.form-group
#edit-validation-error
.modal-footer

MODIFY
examples/annotations/main.js
examples/annotations/main.js
@@ -299,7 +299,7 @@ $(function () {
function show_edit_dialog(id) {
var annotation = layer.annotationById(id),
type = annotation.type(),
- typeMatch = new RegExp('(^| )' + type + '( |$)'),
+ typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'),
opt = annotation.options(),
dlg = $('#editdialog');
@@ -322,14 +322,23 @@ $(function () {
return;
}
ctl.show();
+ switch ($('[option]', ctl).attr('optiontype')) {
+ case 'label':
+ value = (opt.labelStyle || {})[key];
+ break;
+ default:
value = opt.style[key];
+ break;
+ }
switch (format) {
case 'color':
// always show colors as hex values
- value = geo.util.convertColorToHex(value);
+ value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0});
break;
}
+ if (value !== undefined) {
$('[option]', ctl).val('' + value);
+ }
});
dlg.one('shown.bs.modal', function () {
$('[option="name"]', dlg).focus();
@@ -348,9 +357,10 @@ $(function () {
id = dlg.attr('annotation-id'),
annotation = layer.annotationById(id),
type = annotation.type(),
- typeMatch = new RegExp('(^| )' + type + '( |$)'),
+ typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'),
error,
- newopt = {};
+ newopt = {},
+ labelopt = {};
// validate form values
$('.form-group[annotation-types]').each(function () {
@@ -365,7 +375,14 @@ $(function () {
if (value === undefined) {
error = $('label', ctl).text() + ' is not a valid value';
} else {
+ switch ($('[option]', ctl).attr('optiontype')) {
+ case 'label':
+ labelopt[key] = value;
+ break;
+ default:
newopt[key] = value;
+ break;
+ }
}
});
if (error) {
@@ -375,7 +392,7 @@ $(function () {
annotation.name($('[option="name"]', dlg).val());
annotation.label($('[option="label"]', dlg).val() || null);
annotation.description($('[option="description"]', dlg).val() || '');
- annotation.options({style: newopt}).draw();
+ annotation.options({style: newopt, labelStyle: labelopt}).draw();
dlg.modal('hide');
// refresh the annotation list

MODIFY
src/annotation.js
src/annotation.js
@@ -8,6 +8,7 @@ var registerAnnotation = require('./registry').registerAnnotation;
var lineFeature = require('./lineFeature');
var pointFeature = require('./pointFeature');
var polygonFeature = require('./polygonFeature');
+var textFeature = require('./textFeature');
var annotationId = 0;
@@ -507,7 +508,8 @@ var annotation = function (type, args) {
var coor = this._geojsonCoordinates(gcs),
geotype = this._geojsonGeometryType(),
styles = this._geojsonStyles(),
- objStyle = this.options('style'),
+ objStyle = this.options('style') || {},
+ objLabelStyle = this.options('labelStyle') || {},
i, key, value;
if (!coor || !coor.length || !geotype) {
return;
@@ -540,6 +542,16 @@ var annotation = function (type, args) {
obj.properties[key] = value;
}
}
+ for (i = 0; i < textFeature.usedStyles.length; i += 1) {
+ key = textFeature.usedStyles[i];
+ value = util.ensureFunction(objLabelStyle[key])();
+ if (value !== undefined) {
+ if (key.toLowerCase().match(/color$/)) {
+ value = util.convertColorToHex(value);
+ }
+ obj.properties['label' + key.charAt(0).toUpperCase() + key.slice(1)] = value;
+ }
+ }
if (includeCrs) {
var map = this.layer().map();
gcs = (gcs === null ? map.gcs() : (

MODIFY
src/annotationLayer.js
src/annotationLayer.js
@@ -82,6 +82,17 @@ var annotationLayer = function (args) {
'strokeOpacity': {dataType: 'opacity', keys: ['strokeOpacity', 'stroke-opacity']},
'strokeWidth': {dataType: 'positive', keys: ['strokeWidth', 'stroke-width']}
};
+ textFeature.usedStyles.forEach(function (key) {
+ geojsonStyleProperties[key] = {
+ option: 'labelStyle',
+ dataType: ['visible', 'rotateWithMap', 'scaleWithMap'].indexOf(key) >= 0 ? 'boolean' : 'text',
+ keys: [
+ key,
+ 'label' + key.charAt(0).toUpperCase() + key.slice(1),
+ key.replace(/([A-Z])/g, '-$1').toLowerCase(),
+ 'label-' + key.replace(/([A-Z])/g, '-$1').toLowerCase()]
+ };
+ });
m_options = $.extend(true, {}, {
dblClickTime: 300,
@@ -485,9 +496,8 @@ var annotationLayer = function (args) {
if ($.inArray(type, annotationList) < 0) {
return;
}
- if (!options.style) {
- options.style = {};
- }
+ options.style = options.style || {};
+ options.labelStyle = options.labelStyle || {};
delete options.annotationType;
// the geoJSON reader can only emit line, polygon, and point
switch (feature.featureType) {
@@ -540,7 +550,7 @@ var annotationLayer = function (args) {
feature.style.get(key)(data, data_idx), prop.dataType);
}
if (value !== undefined) {
- options.style[key] = value;
+ options[prop.option || 'style'][key] = value;
}
});
/* Delete property keys we have used */
@@ -625,6 +635,9 @@ var annotationLayer = function (args) {
}
break;
case 'opacity':
+ if (value === undefined || value === null || value === '') {
+ return;
+ }
value = +value;
if (isNaN(value) || value < 0 || value > 1) {
return;
@@ -781,7 +794,7 @@ var annotationLayer = function (args) {
if (d.style && d.style[key] !== undefined) {
return d.style[key];
}
- return (m_this.options('defaultLableStyle') || {})[key];
+ return (m_this.options('defaultLabelStyle') || {})[key];
};
});
m_labelFeature = (m_labelLayer || m_this).createFeature('text', {

MODIFY
src/canvas/object.js
src/canvas/object.js
@@ -3,7 +3,8 @@ var sceneObject = require('../sceneObject');
/**
* Canvas specific subclass of object which rerenders when the object is drawn.
- * @class geo.canvas.object
+ * @class
+ * @alias geo.canvas.object
* @extends geo.sceneObject
*/
@@ -20,7 +21,8 @@ var canvas_object = function (arg) {
sceneObject.call(this);
var m_this = this,
- s_draw = this.draw;
+ s_draw = this.draw,
+ m_canvasProperties = {};
/**
* This must be overridden by any feature that needs to render.
@@ -28,8 +30,33 @@ var canvas_object = function (arg) {
this._renderOnCanvas = function () {
};
+ /**
+ * Check if a property has already been set on a canvas's context. If so,
+ * don't set it again. Some browsers are much slower if the properties are
+ * set, even if no change is made.
+ *
+ * @param {CanvasRenderingContext2D} [context] The canvas context to modify.
+ * If `undefined`, clear the internal property buffer.
+ * @param {string} [key] The property to set on the canvas.
+ * @param {object} [value] The value for the property.
+ * @returns {this} The current object.
+ */
+ this._canvasProperty = function (context, key, value) {
+ if (!context || !key) {
+ m_canvasProperties = {};
+ return m_this;
+ }
+ if (m_canvasProperties[key] !== value) {
+ m_canvasProperties[key] = value;
+ context[key] = value;
+ }
+ return m_this;
+ };
+
/**
* Redraw the object.
+ *
+ * @returns {this} The current object.
*/
this.draw = function () {
m_this._update();
@@ -43,4 +70,3 @@ var canvas_object = function (arg) {
inherit(canvas_object, sceneObject);
module.exports = canvas_object;
-

MODIFY
src/canvas/textFeature.js
src/canvas/textFeature.js
var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var textFeature = require('../textFeature');
+var util = require('../util');
/**
* Create a new instance of class canvas.textFeature.
@@ -28,37 +29,116 @@ var canvas_textFeature = function (arg) {
/**
* @private
*/
- var m_this = this;
+ var m_this = this,
+ m_defaultFont = 'bold 16px sans-serif',
+ /* This regexp parses css font specifications into style, variant,
+ * weight, stretch, size, line height, and family. It is based on a
+ * regexp here: https://stackoverflow.com/questions/10135697/regex-to-parse-any-css-font,
+ * but has been modified to fix some issues and handle font stretch. */
+ m_cssFontRegExp = new RegExp(
+ '^\\s*' +
+ '(?=(?:(?:[-a-z0-9]+\\s+){0,3}(italic|oblique))?)' +
+ '(?=(?:(?:[-a-z0-9]+\\s+){0,3}(small-caps))?)' +
+ '(?=(?:(?:[-a-z0-9]+\\s+){0,3}(bold(?:er)?|lighter|[1-9]00))?)' +
+ '(?=(?:(?:[-a-z0-9]+\\s+){0,3}((?:ultra-|extra-|semi-)?(?:condensed|expanded)))?)' +
+ '(?:(?:normal|\\1|\\2|\\3|\\4)\\s+){0,4}' +
+ '((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\\d]+(?:\\%|in|[cem]m|ex|p[ctx]))' +
+ '(?:/(normal|[.\\d]+(?:\\%|in|[cem]m|ex|p[ctx])))?\\s+' +
+ '([-,\\"\\sa-z]+?)\\s*$', 'i');
+
+ /**
+ * Get the font for a specific data item. This falls back to the default
+ * font if the value is unset or doesn't contain sufficient information.
+ *
+ * @param {boolean} useSubValues If truthy, check all font styles (such as
+ * `fontSize`, `lineHeight`, etc., and override the code `font` style
+ * with those values. If falsy, only use `font`.
+ * @param {object} d The current data element.
+ * @param {number} i The index of the current data element.
+ * @returns {string} The font style.
+ */
+ this.getFontFromStyles = function (useSubValues, d, i) {
+ var font = m_this.style.get('font')(d, i) || m_defaultFont;
+ if (useSubValues) {
+ var parts = m_cssFontRegExp.exec(font);
+ if (parts === null) {
+ parts = m_cssFontRegExp.exec(m_defaultFont);
+ }
+ parts[1] = m_this.style.get('fontStyle')(d, i) || parts[1];
+ parts[2] = m_this.style.get('fontVariant')(d, i) || parts[2];
+ parts[3] = m_this.style.get('fontWeight')(d, i) || parts[3];
+ parts[4] = m_this.style.get('fontStretch')(d, i) || parts[4];
+ parts[5] = m_this.style.get('fontSize')(d, i) || parts[5] || '16px';
+ parts[6] = m_this.style.get('lineHeight')(d, i) || parts[6];
+ parts[7] = m_this.style.get('fontFamily')(d, i) || parts[7] || 'sans-serif';
+ font = (parts[1] || '') + ' ' + (parts[2] || '') + ' ' +
+ (parts[3] || '') + ' ' + (parts[4] || '') + ' ' +
+ (parts[5] || '') + (parts[6] ? '/' + parts[6] : '') + ' ' +
+ parts[7];
+ }
+ return font;
+ };
/**
* Render the data on the canvas.
* @protected
- * @param {object} context2d the canvas context to draw in.
- * @param {object} map the parent map object.
+ * @param {CanvasRenderingContext2D} context2d The canvas context to draw in.
+ * @param {geo.map} map The parent map object.
*/
this._renderOnCanvas = function (context2d, map) {
var data = m_this.data(),
posFunc = m_this.style.get('position'),
textFunc = m_this.style.get('text'),
- text, pos;
+ fontFromSubValues, text, pos, val, opac;
+ /* 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
+ * skip it. */
+ fontFromSubValues = ['fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'lineHeight', 'fontFamily'].some(function (key) {
+ return m_this.style(key) !== null && m_this.style(key) !== undefined;
+ });
data.forEach(function (d, i) {
+ val = m_this.style.get('visible')(d, i);
+ if (!val && val !== undefined) {
+ return;
+ }
// TODO: get the position position without transform. If it is outside
// of the map to an extent that there is no chance of text showing,
- // skip further processing. Also, don't change canvas properties (such
- // as font) if they haven't changed.
+ // skip further processing.
pos = m_this.featureGcsToDisplay(posFunc(d, i));
text = textFunc(d, i);
+ 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');
+ val = m_this.style.get('color')(d, i) || {r: 0, g: 0, b: 0};
+ opac = m_this.style.get('textOpacity')(d, i);
+ if (opac === undefined || opac === null) {
+ opac = 1;
+ }
+ if (val.a !== undefined && val.a !== null && val.a !== 1) {
+ opac *= val.a;
+ }
+ if (opac <= 0) {
+ return;
+ }
+ m_this._canvasProperty(context2d, 'globalAlpha', opac > 1 ? 1 : opac);
+ m_this._canvasProperty(context2d, 'fillStyle', util.convertColorToHex(val));
- context2d.font = 'bold 16px sans-serif'; // style | variant | weight | stretch | size/line-height | family
- context2d.textAlign = 'center'; // start, end, left, right, center
- context2d.textBaseline = 'middle'; // top, hanging, middle, alphabetic, ideographic, bottom
- context2d.direction = 'inherit'; // ltr, rtl, inherit
- context2d.fillStyle = 'black'; // css color or style
+ // TODO: fetch and use other properties:
+ // 'direction', 'rotation', 'rotateWithMap', 'scale',
+ // 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOffset',
+ // 'shadowBlur', 'shadowRotate'
+ m_this._canvasProperty(context2d, 'direction', 'inherit'); // ltr, rtl, inherit
+ /*
+ ctx.shadowColor = "black";
+ ctx.shadowOffsetX = 5;
+ ctx.shadowOffsetY = 5;
+ ctx.shadowBlur = 7;
+ */
- // TODO: rotation, maxWidth, offset
context2d.fillText(text, pos.x, pos.y);
});
+ m_this._canvasProperty(context2d, 'globalAlpha', 1);
};
this._init(arg);

MODIFY
src/textFeature.js
src/textFeature.js
@@ -37,13 +37,16 @@ var feature = require('./feature');
* `ideographic`, or `bottom`.
* @property {string|function} [style.direction='inherit'] Text direction. One
* of `ltr`, `rtl`, or `inherit`.
- * @property {geo.geoColor|function} [style.color='black'] Text color.
+ * @property {geo.geoColor|function} [style.color='black'] Text color. May
+ * include opacity.
* @property {number|function} [style.opacity=1] The opacity of the text. If
* the color includes opacity, this is combined with that value.
* @property {number|function} [style.rotation=0] Text rotation in radians.
* @property {boolean|function} [style.rotateWithMap=false] If truthy, rotate
* the text when the map rotates. Otherwise, the text is always in the
* same orientation.
+ * @property {number|function} [style.scale=4] The zoom basis value used when
+ * `scaleWithMap` is truthy.
* @property {boolean|function} [style.scaleWithMap=false] If truthy, use the
* `scale` style as the basis of the map zoom value for the font size.
* The size is scaled from this point.
@@ -51,6 +54,15 @@ var feature = require('./feature');
* 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 {geo.screenPosition|function} [style.shadowOffset] Offset for a
+ * text shadow. This is applied before rotation.
+ * @property {number|null|function} [style.shadowBlur] If not null, add a text
+ * shadow with this much blur.
+ * @property {boolean|function} [style.shadowRotate=false] If truthy, rotate
+ * the shadow offset based on the text rotation (the `shadowOffset` is
+ * the offset if the text has a 0 rotation).
*/
/**
@@ -166,8 +178,9 @@ var textFeature = function (arg) {
textFeature.usedStyles = [
'visible', 'font', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch',
'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline',
- 'direction', 'color', 'opacity', 'rotation', 'rotateWithMap', 'scaleWithMap',
- 'offset', 'width'
+ 'direction', 'color', 'textOpacity', 'rotation', 'rotateWithMap', 'scale',
+ 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOffset',
+ 'shadowBlur', 'shadowRotate'
];
/**

MODIFY
src/util/index.js
src/util/index.js
@@ -286,6 +286,24 @@ var util = module.exports = {
}
return value;
},
+ /**
+ * Convert a color to a css rgba() value.
+ *
+ * @param {geo.geoColorObject} color The color object to convert.
+ * @returns {string} A color string.
+ * @memberof geo.util
+ */
+ convertColorToRGBA: function (color) {
+ var rgb = util.convertColor(color);
+ if (!rgb) {
+ rgb = {r: 0, g: 0, b: 0};
+ }
+ if (rgb.a === undefined) {
+ rgb.a = 1;
+ }
+ return 'rgba(' + Math.round(rgb.r * 255) + ', ' + Math.round(rgb.g * 255) +
+ ', ' + Math.round(rgb.b * 255) + ', ' + rgb.a + ')';
+ },
/**
* Normalize a coordinate object into {@link geo.geoPosition} form. The

Set text fonts.

This handles font substyles, such as fontSize.

This is exposed in the annotations example.

This also handles textAlign, textBaseline, color, and textOpacity.

Much of the infrastructure for other text styles is in place.