From 2f9f0c1c54675a8b7a6451c1605bb9980db48f36 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 30 Jun 2017 16:03:53 -0400 Subject: [PATCH 01/11] Support annotation labels. This adds a new feature type, `geo.textFeature`, where the data items have a position, text, and styling. Currently, this is only available for the canvas renderer, and not all styles are hooked through. Annotations now have `label`, `showLabel`, `labelStyle`, and `description` properties. The annotation layer has a `showLabels` option for toggling all labels. Update documentation of annotation.js and annotationLayer.js. This needs to process and pass through styles to the renderer. It needs to generate a synthetic font style if the font substyles are used. It needs to support rotation, scaling, and offset on text features. It needs tests. The annotation example should allow label adjustments, including determining where placement is based from. Placement basis needs to be an annotation option. --- examples/annotations/index.jade | 11 +- examples/annotations/main.css | 2 +- examples/annotations/main.js | 14 +- src/annotation.js | 588 ++++++++++++++++++++++---------- src/annotationLayer.js | 298 +++++++++++----- src/canvas/index.js | 1 + src/canvas/textFeature.js | 75 ++++ src/index.js | 1 + src/textFeature.js | 194 +++++++++++ 9 files changed, 898 insertions(+), 286 deletions(-) create mode 100644 src/canvas/textFeature.js create mode 100644 src/textFeature.js diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index 83fee41329..5babac7fab 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -21,6 +21,9 @@ block append mainContent .form-group(title='If enabled, immediately after adding one annotation, you can add another without either left-clicking or selecting a button.') label(for='keepadding') Keep adding annotations input#keepadding(param-name='keepadding', type='checkbox', placeholder='false') + .form-group(title='If disabled, hide all annotation labels.') + label(for='showLabels') Show annotation labels + input#showLabels(param-name='labels', type='checkbox', placeholder='true') .form-group#annotationheader .shortlabel Created Annotations a.entry-remove-all(action='remove-all', title='Delete all annotations') ✖ @@ -44,6 +47,12 @@ block append mainContent .form-group label(for='edit-name') Name input#edit-name(option='name') + .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') .form-group(annotation-types='point') label(for='edit-radius') Radius input#edit-radius(option='radius', format='positive') @@ -62,7 +71,7 @@ block append mainContent label(for='edit-fillOpacity') Fill Opacity input#edit-fillOpacity(option='fillOpacity', format='opacity') .form-group(annotation-types='point polygon rectangle') - label(for='edit-stroke') Stroke + label(for='edit-stroke') Stroke select#edit-stroke(option='stroke', format='boolean') option(value='true') Yes option(value='false') No diff --git a/examples/annotations/main.css b/examples/annotations/main.css index 5beccb9fc4..c6ae219f44 100644 --- a/examples/annotations/main.css +++ b/examples/annotations/main.css @@ -4,7 +4,7 @@ position: absolute; left: 10px; top: 80px; - z-index: 1; + z-index: 1000; border-radius: 5px; border: 1px solid grey; box-shadow: 1px 1px 3px black; diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 5a005462a5..95c370b2fd 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -12,6 +12,7 @@ $(function () { var query = utils.getQuery(); $('#clickadd').prop('checked', query.clickadd !== 'false'); $('#keepadding').prop('checked', query.keepadding === 'true'); + $('#showLabels').prop('checked', query.labels !== 'false'); if (query.lastannotation) { $('.annotationtype button').removeClass('lastused'); $('.annotationtype button#' + query.lastannotation).addClass('lastused'); @@ -52,7 +53,8 @@ $(function () { // create an annotation layer layer = map.createLayer('annotation', { renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, - annotations: query.renderer ? undefined : geo.listAnnotations() + annotations: query.renderer ? undefined : geo.listAnnotations(), + showLabels: query.labels !== 'false' }); // bind to the mouse click and annotation mode events layer.geoOn(geo.event.mouseclick, mouseClickToStart); @@ -122,6 +124,12 @@ $(function () { if (!param || value === query[param]) { return; } + switch (param) { + case 'labels': + layer.options('showLabels', '' + value !== 'false'); + layer.draw(); + break; + } query[param] = value; if (value === '' || (ctl.attr('placeholder') && value === ctl.attr('placeholder'))) { @@ -299,6 +307,8 @@ $(function () { dlg.attr('annotation-id', id); dlg.attr('annotation-type', type); $('[option="name"]', dlg).val(annotation.name()); + $('[option="label"]', dlg).val(annotation.label(undefined, true)); + $('[option="description"]', dlg).val(annotation.description()); // populate each control with the current value of the annotation $('.form-group[annotation-types]').each(function () { var ctl = $(this), @@ -363,6 +373,8 @@ $(function () { return; } annotation.name($('[option="name"]', dlg).val()); + annotation.label($('[option="label"]', dlg).val() || null); + annotation.description($('[option="description"]', dlg).val() || ''); annotation.options({style: newopt}).draw(); dlg.modal('hide'); diff --git a/src/annotation.js b/src/annotation.js index 4ba03bee5b..ed6143bdd0 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -20,18 +20,22 @@ var annotationState = { var annotationActionOwner = 'annotationAction'; /** - * Base annotation class + * Base annotation class. * - * @class geo.annotation - * @param {string} type the type of annotation. These should be registered + * @class + * @alias geo.annotation + * @param {string} type The type of annotation. These should be registered * with utils.registerAnnotation and can be listed with same function. - * @param {object?} options Inidividual annotations have additional options. - * @param {string} [options.name] A name for the annotation. This defaults to + * @param {object?} [args] Individual annotations have additional options. + * @param {string} [args.name] A name for the annotation. This defaults to * the type with a unique ID suffixed to it. - * @param {geo.annotationLayer} [options.layer] a reference to the controlling + * @param {geo.annotationLayer} [arg.layer] A reference to the controlling * layer. This is used for coordinate transforms. - * @param {string} [options.state] initial annotation state. One of the - * annotation.state values. + * @param {string} [args.state] Initial annotation state. One of the + * `geo.annotation.state` values. + * @param {boolean|string[]} [args.showLabel=true] `true` to show the + * annotation label on annotations in done or edit states. Alternately, a + * list of states in which to show the label. Falsy to not show the label. * @returns {geo.annotation} */ var annotation = function (type, args) { @@ -41,10 +45,12 @@ var annotation = function (type, args) { } annotationId += 1; - var m_options = $.extend({}, args || {}), + var m_options = $.extend({}, {showLabel: true}, args || {}), m_id = annotationId, m_name = m_options.name || ( type.charAt(0).toUpperCase() + type.substr(1) + ' ' + annotationId), + m_label = m_options.label || null, + m_description = m_options.description || undefined, m_type = type, m_layer = m_options.layer, /* one of annotationState.* */ @@ -52,6 +58,8 @@ var annotation = function (type, args) { delete m_options.state; delete m_options.layer; delete m_options.name; + delete m_options.label; + delete m_options.description; /** * Clean up any resources that the annotation is using. @@ -62,7 +70,7 @@ var annotation = function (type, args) { /** * Get a unique annotation id. * - * @returns {number} the annotation id. + * @returns {number} The annotation id. */ this.id = function () { return m_id; @@ -71,16 +79,113 @@ var annotation = function (type, args) { /** * Get or set the name of this annotation. * - * @param {string|undefined} arg if undefined, return the name, otherwise - * change it. - * @returns {this|string} the current name or this annotation. + * @param {string|undefined} arg If `undefined`, return the name, otherwise + * change it. When setting the name, the value is trimmed of + * whitespace. The name will not be changed to an empty string. + * @returns {this|string} The current name or this annotation. */ this.name = function (arg) { if (arg === undefined) { return m_name; } if (arg !== null && ('' + arg).trim()) { - m_name = ('' + arg).trim(); + arg = ('' + arg).trim(); + if (arg !== m_name) { + m_name = arg; + this.modified(); + } + } + return this; + }; + + /** + * Get or set the label of this annotation. + * + * @param {string|null|undefined} arg If `undefined`, return the label, + * otherwise change it. `null` to clear the label. + * @param {boolean} noFallback If not truthy and the label is `null`, return + * the name, otherwise return the actual value for label. + * @returns {this|string} The current label or this annotation. + */ + this.label = function (arg, noFallback) { + if (arg === undefined) { + return m_label === null && !noFallback ? m_name : m_label; + } + if (arg !== m_label) { + m_label = arg; + this.modified(); + } + return this; + }; + + /** + * Return the coordinate associated with the label. + * + * @returns {geo.geoPosition|undefined} The map gcs position for the label, + * or `undefined` if no such position exists. + */ + this._labelPosition = function () { + var coor = this._coordinates(), position = {x: 0, y: 0}, i; + if (!coor || !coor.length) { + return undefined; + } + if (coor.length === 1) { + return coor[0]; + } + for (i = 0; i < coor.length; i += 1) { + position.x += coor[i].x; + position.y += coor[i].y; + } + position.x /= coor.length; + position.y /= coor.length; + return position; + }; + + /** + * If the label should be shown, get a record of the label that can be used + * in a `geo.textFeature`. + * + * @returns {geo.annotationLayer.labelRecord|undefined} A label record, or + * `undefined` if it should not be shown. + */ + this.labelRecord = function () { + var show = this.options('showLabel'); + if (!show) { + return; + } + var state = this.state(); + if ((show === true && state === annotationState.create) || + (show !== true && show.indexOf(state) < 0)) { + return; + } + var style = this.options('labelStyle'); + var labelRecord = { + text: this.label(), + position: this._labelPosition() + }; + if (!labelRecord.position) { + return; + } + if (style) { + labelRecord.style = style; + } + return labelRecord; + }; + + /** + * Get or set the description of this annotation. + * + * @param {string|undefined} arg If `undefined`, return the description, + * otherwise change it. + * @returns {this|string} The current description or this annotation. + */ + this.description = function (arg) { + if (arg === undefined) { + return m_description; + } + if (arg !== m_description) { + m_description = arg; + this.modified(); } return this; }; @@ -103,9 +208,10 @@ var annotation = function (type, args) { /** * Get or set the state of this annotation. * - * @param {string|undefined} arg if undefined, return the state, otherwise - * change it. - * @returns {this|string} the current state or this annotation. + * @param {string|undefined} arg If `undefined`, return the state, + * otherwise change it. This should be one of the + * `geo.annotation.state` values. + * @returns {this|string} The current state or this annotation. */ this.state = function (arg) { if (arg === undefined) { @@ -125,9 +231,9 @@ var annotation = function (type, args) { /** * Return actions needed for the specified state of this annotation. * - * @param {string} state: the state to return actions for. Defaults to + * @param {string} [state] The state to return actions for. Defaults to * the current state. - * @returns {array}: a list of actions. + * @returns {geo.actionRecord[]} A list of actions. */ this.actions = function () { return []; @@ -136,24 +242,26 @@ var annotation = function (type, args) { /** * Process any actions for this annotation. * - * @param {object} evt: the action event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The action event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if the + * annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.processAction = function () { + return undefined; }; /** * Set or get options. * - * @param {string|object} arg1 if undefined, return the options object. If - * a string, either set or return the option of that name. If an object, - * update the options with the object's values. - * @param {object} arg2 if arg1 is a string and this is defined, set the - * option to this value. - * @returns {object|this} if options are set, return the layer, otherwise - * return the requested option or the set of options. + * @param {string|object} [arg1] If `undefined`, return the options object. + * If a string, either set or return the option of that name. If an + * object, update the options with the object's values. + * @param {object} [arg2] If `arg1` is a string and this is defined, set + * the option to this value. + * @returns {object|this} If options are set, return the annotation, + * otherwise return the requested option or the set of options. */ this.options = function (arg1, arg2) { if (arg1 === undefined) { @@ -177,6 +285,16 @@ var annotation = function (type, args) { delete m_options.name; this.name(name); } + if (m_options.label !== undefined) { + var label = m_options.label; + delete m_options.label; + this.label(label); + } + if (m_options.description !== undefined) { + var description = m_options.description; + delete m_options.description; + this.description(description); + } this.modified(); return this; }; @@ -184,13 +302,14 @@ var annotation = function (type, args) { /** * Set or get style. * - * @param {string|object} arg1 if undefined, return the options.style object. - * If a string, either set or return the style of that name. If an - * object, update the style with the object's values. - * @param {object} arg2 if arg1 is a string and this is defined, set the - * style to this value. - * @returns {object|this} if styles are set, return the layer, otherwise - * return the requested style or the set of styles. + * @param {string|object} [arg1] If `undefined`, return the current style + * object. If a string and `arg2` is undefined, return the style + * associated with the specified key. If a string and `arg2` is defined, + * set the named style to the specified value. Otherwise, extend the + * current style with the values in the specified object. + * @param {*} [arg2] If `arg1` is a string, the new value for that style. + * @returns {object|this} Either the entire style object, the value of a + * specific style, or the current class instance. */ this.style = function (arg1, arg2) { if (arg1 === undefined) { @@ -209,15 +328,16 @@ var annotation = function (type, args) { }; /** - * Set or get edit style. + * Set or get edit style. These are the styles used in edit and create mode. * - * @param {string|object} arg1 if undefined, return the options.editstyle - * object. If a string, either set or return the style of that name. If - * an object, update the style with the object's values. - * @param {object} arg2 if arg1 is a string and this is defined, set the - * style to this value. - * @returns {object|this} if styles are set, return the layer, otherwise - * return the requested style or the set of styles. + * @param {string|object} [arg1] If `undefined`, return the current style + * object. If a string and `arg2` is undefined, return the style + * associated with the specified key. If a string and `arg2` is defined, + * set the named style to the specified value. Otherwise, extend the + * current style with the values in the specified object. + * @param {*} [arg2] If `arg1` is a string, the new value for that style. + * @returns {object|this} Either the entire style object, the value of a + * specific style, or the current class instance. */ this.editstyle = function (arg1, arg2) { if (arg1 === undefined) { @@ -238,7 +358,7 @@ var annotation = function (type, args) { /** * Get the type of this annotation. * - * @returns {string} the annotation type. + * @returns {string} The annotation type. */ this.type = function () { return m_type; @@ -247,11 +367,11 @@ var annotation = function (type, args) { /** * Get a list of renderable features for this annotation. The list index is * functionally a z-index for the feature. Each entry is a dictionary with - * the key as the feature name (such as line, quad, or polygon), and the - * value a dictionary of values to pass to the feature constructor, such as - * style and coordinates. + * the key as the feature name (such as `line`, `quad`, or `polygon`), and + * the value a dictionary of values to pass to the feature constructor, such + * as `style` and `coordinates`. * - * @returns {array} an array of features. + * @returns {array} An array of features. */ this.features = function () { return []; @@ -259,32 +379,36 @@ var annotation = function (type, args) { /** * Handle a mouse click on this annotation. If the event is processed, - * evt.handled should be set to true to prevent further processing. + * evt.handled should be set to `true` to prevent further processing. * - * @param {geo.event} evt the mouse click event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The mouse click event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if + * the annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.mouseClick = function (evt) { + return undefined; }; /** * Handle a mouse move on this annotation. * - * @param {geo.event} evt the mouse move event. - * @returns {boolean|string} true to update the annotation, falsy to not + * @param {geo.event} evt The mouse move event. + * @returns {boolean} Truthy to update the annotation, falsy to not * update anything. */ this.mouseMove = function (evt) { + return undefined; }; /** * Get coordinates associated with this annotation in the map gcs coordinate * system. * - * @param {array} coordinates: an optional array of coordinates to set. - * @returns {array} an array of coordinates. + * @param {geo.geoPosition[]} [coordinates] An optional array of coordinates + * to set. + * @returns {geo.geoPosition[]} The current array of coordinates. */ this._coordinates = function (coordinates) { return []; @@ -293,9 +417,9 @@ var annotation = function (type, args) { /** * Get coordinates associated with this annotation. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @returns {array} an array of coordinates. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {geo.geoPosition[]} An array of coordinates. */ this.coordinates = function (gcs) { var coord = this._coordinates() || []; @@ -313,6 +437,8 @@ var annotation = function (type, args) { /** * Mark this annotation as modified. This just marks the parent layer as * modified. + * + * @returns {this} The annotation. */ this.modified = function () { if (this.layer()) { @@ -323,6 +449,8 @@ var annotation = function (type, args) { /** * Draw this annotation. This just updates and draws the parent layer. + * + * @returns {this} The annotation. */ this.draw = function () { if (this.layer()) { @@ -336,7 +464,7 @@ var annotation = function (type, args) { * Return a list of styles that should be preserved in a geojson * representation of the annotation. * - * @return {array} a list of style names to store. + * @returns {string[]} A list of style names to store. */ this._geojsonStyles = function () { return [ @@ -346,31 +474,33 @@ var annotation = function (type, args) { }; /** - * Return the coordinates to be stored in a geojson geometery object. + * Return the coordinates to be stored in a geojson geometry object. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @return {array} an array of flattened coordinates in the ingcs coordinate - * system. Undefined if this annotation is incompelte. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {array} An array of flattened coordinates in the interface gcs + * coordinate system. `undefined` if this annotation is incomplete. */ this._geojsonCoordinates = function (gcs) { + return []; }; /** * Return the geometry type that is used to store this annotation in geojson. * - * @return {string} a geojson geometry type. + * @returns {string} A geojson geometry type. */ this._geojsonGeometryType = function () { + return ''; }; /** * Return the annotation as a geojson object. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @param {boolean} includeCrs: if true, include the coordinate system. - * @return {object} the annotation as a geojson object, or undefined if it + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @param {boolean} [includeCrs] If truthy, include the coordinate system. + * @returns {object} The annotation as a geojson object, or `undefined` if it * should not be represented (for instance, while it is being created). */ this.geojson = function (gcs, includeCrs) { @@ -394,6 +524,12 @@ var annotation = function (type, args) { annotationId: this.id() } }; + if (m_label) { + obj.properties.label = m_label; + } + if (m_description) { + obj.properties.description = m_description; + } for (i = 0; i < styles.length; i += 1) { key = styles[i]; value = util.ensureFunction(objStyle[key])(); @@ -421,18 +557,37 @@ var annotation = function (type, args) { }; /** - * Rectangle annotation class + * Rectangle annotation class. * * Rectangles are always rendered as polygons. This could be changed -- if no * stroke is specified, the quad feature would be sufficient and work on more * renderers. * - * Must specify: - * corners: a list of four corners {x: x, y: y} in map gcs coordinates. - * May specify: - * style. - * fill, fillColor, fillOpacity, stroke, strokeWidth, strokeColor, - * strokeOpacity + * @class + * @alias geo.rectangleAnnotation + * @extends geo.annotation + * + * @param {object?} [args] Options for the annotation. + * @param {string} [args.name] A name for the annotation. This defaults to + * the type with a unique ID suffixed to it. + * @param {string} [args.state] initial annotation state. One of the + * annotation.state values. + * @param {boolean|string[]} [args.showLabel=true] `true` to show the + * annotation label on annotations in done or edit states. Alternately, a + * list of states in which to show the label. Falsy to not show the label. + * @param {geo.geoPosition[]} [args.corners] A list of four corners in map + * gcs coordinates. These must be in order around the perimeter of the + * rectangle (in either direction). + * @param {geo.geoPosition[]} [args.coordinates] An alternate name for + * `args.corners`. + * @param {object} [args.style] The style to apply to a finished rectangle. + * This uses styles for polygons, including `fill`, `fillColor`, + * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and + * `strokeOpacity`. + * @param {object} [args.editstyle] The style to apply to a rectangle in edit + * mode. This uses styles for polygons and lines, including `fill`, + * `fillColor`, `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and + * `strokeOpacity`. */ var rectangleAnnotation = function (args) { 'use strict'; @@ -471,9 +626,9 @@ var rectangleAnnotation = function (args) { /** * Return actions needed for the specified state of this annotation. * - * @param {string} state: the state to return actions for. Defaults to + * @param {string} [state] The state to return actions for. Defaults to * the current state. - * @returns {array}: a list of actions. + * @returns {geo.actionRecord[]} A list of actions. */ this.actions = function (state) { if (!state) { @@ -497,10 +652,11 @@ var rectangleAnnotation = function (args) { /** * Process any actions for this annotation. * - * @param {object} evt: the action event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The action event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if the + * annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.processAction = function (evt) { var layer = this.layer(); @@ -524,7 +680,7 @@ var rectangleAnnotation = function (args) { /** * Get a list of renderable features for this annotation. * - * @returns {array} an array of features. + * @returns {array} An array of features. */ this.features = function () { var opt = this.options(), @@ -558,8 +714,9 @@ var rectangleAnnotation = function (args) { * Get coordinates associated with this annotation in the map gcs coordinate * system. * - * @param {array} coordinates: an optional array of coordinates to set. - * @returns {array} an array of coordinates. + * @param {geo.geoPosition[]} [coordinates] An optional array of coordinates + * to set. + * @returns {geo.geoPosition[]} The current array of coordinates. */ this._coordinates = function (coordinates) { if (coordinates && coordinates.length >= 4) { @@ -571,12 +728,12 @@ var rectangleAnnotation = function (args) { }; /** - * Return the coordinates to be stored in a geojson geometery object. + * Return the coordinates to be stored in a geojson geometry object. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @return {array} an array of flattened coordinates in the ingcs coordinate - * system. Undefined if this annotation is incompelte. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {array} An array of flattened coordinates in the interface gcs + * coordinate system. `undefined` if this annotation is incomplete. */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); @@ -594,7 +751,7 @@ var rectangleAnnotation = function (args) { /** * Return the geometry type that is used to store this annotation in geojson. * - * @return {string} a geojson geometry type. + * @returns {string} A geojson geometry type. */ this._geojsonGeometryType = function () { return 'Polygon'; @@ -604,7 +761,7 @@ var rectangleAnnotation = function (args) { * Return a list of styles that should be preserved in a geojson * representation of the annotation. * - * @return {array} a list of style names to store. + * @returns {string[]} A list of style names to store. */ this._geojsonStyles = function () { return [ @@ -615,8 +772,8 @@ var rectangleAnnotation = function (args) { /** * Set three corners based on an initial corner and a mouse event. * - * @param {array} an array of four corners to update. - * @param {geo.event} evt the mouse move event. + * @param {geo.geoPosition} corners An array of four corners to update. + * @param {geo.event} evt The mouse move event. */ this._setCornersFromMouse = function (corners, evt) { var map = this.layer().map(), @@ -632,8 +789,8 @@ var rectangleAnnotation = function (args) { /** * Handle a mouse move on this annotation. * - * @param {geo.event} evt the mouse move event. - * @returns {boolean|string} true to update the annotation, falsy to not + * @param {geo.event} evt The mouse move event. + * @returns {boolean} Truthy to update the annotation, falsy to not * update anything. */ this.mouseMove = function (evt) { @@ -649,12 +806,13 @@ var rectangleAnnotation = function (args) { /** * Handle a mouse click on this annotation. If the event is processed, - * evt.handled should be set to true to prevent further processing. + * evt.handled should be set to `true` to prevent further processing. * - * @param {geo.event} evt the mouse click event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The mouse click event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if + * the annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.mouseClick = function (evt) { var layer = this.layer(); @@ -696,15 +854,31 @@ registerAnnotation('rectangle', rectangleAnnotation, rectangleRequiredFeatures); * When complete, polygons are rendered as polygons. During creation they are * rendered as lines and polygons. * - * Must specify: - * vertices: a list of vertices {x: x, y: y} in map gcs coordinates. - * May specify: - * style. - * fill, fillColor, fillOpacity, stroke, strokeWidth, strokeColor, - * strokeOpacity - * editstyle. - * fill, fillColor, fillOpacity, stroke, strokeWidth, strokeColor, - * strokeOpacity + * @class + * @alias geo.polygonAnnotation + * @extends geo.annotation + * + * @param {object?} [args] Options for the annotation. + * @param {string} [args.name] A name for the annotation. This defaults to + * the type with a unique ID suffixed to it. + * @param {string} [args.state] initial annotation state. One of the + * annotation.state values. + * @param {boolean|string[]} [args.showLabel=true] `true` to show the + * annotation label on annotations in done or edit states. Alternately, a + * list of states in which to show the label. Falsy to not show the label. + * @param {geo.geoPosition[]} [args.vertices] A list of vertices in map gcs + * coordinates. These must be in order around the perimeter of the + * polygon (in either direction). + * @param {geo.geoPosition[]} [args.coordinates] An alternate name for + * `args.vertices`. + * @param {object} [args.style] The style to apply to a finished polygon. + * This uses styles for polygons, including `fill`, `fillColor`, + * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and + * `strokeOpacity`. + * @param {object} [args.editstyle] The style to apply to a polygon in edit + * mode. This uses styles for polygons and lines, including `fill`, + * `fillColor`, `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and + * `strokeOpacity`. */ var polygonAnnotation = function (args) { 'use strict'; @@ -757,7 +931,7 @@ var polygonAnnotation = function (args) { * is done, this is just a single polygon. During creation this can be a * polygon and line at z-levels 1 and 2. * - * @returns {array} an array of features. + * @returns {array} An array of features. */ this.features = function () { var opt = this.options(), @@ -799,8 +973,9 @@ var polygonAnnotation = function (args) { * Get coordinates associated with this annotation in the map gcs coordinate * system. * - * @param {array} coordinates: an optional array of coordinates to set. - * @returns {array} an array of coordinates. + * @param {geo.geoPosition[]} [coordinates] An optional array of coordinates + * to set. + * @returns {geo.geoPosition[]} The current array of coordinates. */ this._coordinates = function (coordinates) { if (coordinates) { @@ -812,8 +987,8 @@ var polygonAnnotation = function (args) { /** * Handle a mouse move on this annotation. * - * @param {geo.event} evt the mouse move event. - * @returns {boolean|string} true to update the annotation, falsy to not + * @param {geo.event} evt The mouse move event. + * @returns {boolean} Truthy to update the annotation, falsy to not * update anything. */ this.mouseMove = function (evt) { @@ -829,12 +1004,13 @@ var polygonAnnotation = function (args) { /** * Handle a mouse click on this annotation. If the event is processed, - * evt.handled should be set to true to prevent further processing. + * evt.handled should be set to `true` to prevent further processing. * - * @param {geo.event} evt the mouse click event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The mouse click event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if + * the annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.mouseClick = function (evt) { var layer = this.layer(); @@ -887,12 +1063,12 @@ var polygonAnnotation = function (args) { }; /** - * Return the coordinates to be stored in a geojson geometery object. + * Return the coordinates to be stored in a geojson geometry object. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @return {array} an array of flattened coordinates in the ingcs coordinate - * system. Undefined if this annotation is incompelte. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {array} An array of flattened coordinates in the interface gcs + * coordinate system. `undefined` if this annotation is incomplete. */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); @@ -910,7 +1086,7 @@ var polygonAnnotation = function (args) { /** * Return the geometry type that is used to store this annotation in geojson. * - * @return {string} a geojson geometry type. + * @returns {string} A geojson geometry type. */ this._geojsonGeometryType = function () { return 'Polygon'; @@ -920,7 +1096,7 @@ var polygonAnnotation = function (args) { * Return a list of styles that should be preserved in a geojson * representation of the annotation. * - * @return {array} a list of style names to store. + * @returns {string[]} A list of style names to store. */ this._geojsonStyles = function () { return [ @@ -936,17 +1112,31 @@ polygonRequiredFeatures[lineFeature.capabilities.basic] = [annotationState.creat registerAnnotation('polygon', polygonAnnotation, polygonRequiredFeatures); /** - * Line annotation class + * Line annotation class. * - * Must specify: - * vertices: a list of vertices {x: x, y: y} in map gcs coordinates. - * May specify: - * style. - * strokeWidth, strokeColor, strokeOpacity, strokeOffset, closed, lineCap, - * lineJoin - * editstyle. - * strokeWidth, strokeColor, strokeOpacity, strokeOffset, closed, lineCap, - * lineJoin + * @class + * @alias geo.lineAnnotation + * @extends geo.annotation + * + * @param {object?} [args] Options for the annotation. + * @param {string} [args.name] A name for the annotation. This defaults to + * the type with a unique ID suffixed to it. + * @param {string} [args.state] initial annotation state. One of the + * annotation.state values. + * @param {boolean|string[]} [args.showLabel=true] `true` to show the + * annotation label on annotations in done or edit states. Alternately, a + * list of states in which to show the label. Falsy to not show the label. + * @param {geo.geoPosition[]} [args.vertices] A list of vertices in map gcs + * coordinates. + * @param {geo.geoPosition[]} [args.coordinates] An alternate name for + * `args.corners`. + * @param {object} [args.style] The style to apply to a finished line. + * This uses styles for lines, including `strokeWidth`, `strokeColor`, + * `strokeOpacity`, `strokeOffset`, `closed`, `lineCap`, and `lineJoin`. + * @param {object} [args.editstyle] The style to apply to a line in edit + * mode. This uses styles for lines, including `strokeWidth`, + * `strokeColor`, `strokeOpacity`, `strokeOffset`, `closed`, `lineCap`, + * and `lineJoin`. */ var lineAnnotation = function (args) { 'use strict'; @@ -999,7 +1189,7 @@ var lineAnnotation = function (args) { /** * Get a list of renderable features for this annotation. * - * @returns {array} an array of features. + * @returns {array} An array of features. */ this.features = function () { var opt = this.options(), @@ -1030,8 +1220,9 @@ var lineAnnotation = function (args) { * Get coordinates associated with this annotation in the map gcs coordinate * system. * - * @param {array} coordinates: an optional array of coordinates to set. - * @returns {array} an array of coordinates. + * @param {geo.geoPosition[]} [coordinates] An optional array of coordinates + * to set. + * @returns {geo.geoPosition[]} The current array of coordinates. */ this._coordinates = function (coordinates) { if (coordinates) { @@ -1043,8 +1234,8 @@ var lineAnnotation = function (args) { /** * Handle a mouse move on this annotation. * - * @param {geo.event} evt the mouse move event. - * @returns {boolean|string} true to update the annotation, falsy to not + * @param {geo.event} evt The mouse move event. + * @returns {boolean} Truthy to update the annotation, falsy to not * update anything. */ this.mouseMove = function (evt) { @@ -1060,12 +1251,13 @@ var lineAnnotation = function (args) { /** * Handle a mouse click on this annotation. If the event is processed, - * evt.handled should be set to true to prevent further processing. + * evt.handled should be set to `true` to prevent further processing. * - * @param {geo.event} evt the mouse click event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The mouse click event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if + * the annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.mouseClick = function (evt) { var layer = this.layer(); @@ -1121,9 +1313,9 @@ var lineAnnotation = function (args) { /** * Return actions needed for the specified state of this annotation. * - * @param {string} state: the state to return actions for. Defaults to + * @param {string} [state] The state to return actions for. Defaults to * the current state. - * @returns {array}: a list of actions. + * @returns {geo.actionRecord[]} A list of actions. */ this.actions = function (state) { if (!state) { @@ -1151,10 +1343,11 @@ var lineAnnotation = function (args) { /** * Process any actions for this annotation. * - * @param {object} evt: the action event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The action event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if the + * annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.processAction = function (evt) { var layer = this.layer(); @@ -1182,12 +1375,12 @@ var lineAnnotation = function (args) { }; /** - * Return the coordinates to be stored in a geojson geometery object. + * Return the coordinates to be stored in a geojson geometry object. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @return {array} an array of flattened coordinates in the ingcs coordinate - * system. Undefined if this annotation is incompelte. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {array} An array of flattened coordinates in the interface gcs + * coordinate system. `undefined` if this annotation is incomplete. */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); @@ -1204,7 +1397,7 @@ var lineAnnotation = function (args) { /** * Return the geometry type that is used to store this annotation in geojson. * - * @return {string} a geojson geometry type. + * @returns {string} A geojson geometry type. */ this._geojsonGeometryType = function () { return 'LineString'; @@ -1214,7 +1407,7 @@ var lineAnnotation = function (args) { * Return a list of styles that should be preserved in a geojson * representation of the annotation. * - * @return {array} a list of style names to store. + * @returns {string[]} A list of style names to store. */ this._geojsonStyles = function () { return [ @@ -1229,18 +1422,33 @@ lineRequiredFeatures[lineFeature.capabilities.basic] = [annotationState.create]; registerAnnotation('line', lineAnnotation, lineRequiredFeatures); /** - * Point annotation class + * Point annotation classa. * - * Must specify: - * position: {x: x, y: y} in map gcs coordinates. - * May specify: - * style. - * radius, fill, fillColor, fillOpacity, stroke, strokeWidth, strokeColor, - * strokeOpacity, scaled + * @class + * @alias geo.poinyAnnotation + * @extends geo.annotation * - * If scaled is false, the point is not scaled with zoom level. If it is true, - * the radius is based on the zoom level at first instantiation. Otherwise, if - * it is a number, the radius is used at that zoom level. + * @param {object?} [args] Options for the annotation. + * @param {string} [args.name] A name for the annotation. This defaults to + * the type with a unique ID suffixed to it. + * @param {string} [args.state] initial annotation state. One of the + * annotation.state values. + * @param {boolean|string[]} [args.showLabel=true] `true` to show the + * annotation label on annotations in done or edit states. Alternately, a + * list of states in which to show the label. Falsy to not show the label. + * @param {geo.geoPosition} [args.position] A coordinate in map gcs + * coordinates. + * @param {geo.geoPosition[]} [args.coordinates] An array with one coordinate + * to use in place of `args.position`. + * @param {object} [args.style] The style to apply to a finished point. + * This uses styles for points, including `radius`, `fill`, `fillColor`, + * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, `strokeOpacity`, + * and `scaled`. If `scaled` is `false`, the point is not scaled with + * zoom level. If it is `true`, the radius is based on the zoom level at + * first instantiation. Otherwise, if it is a number, the radius is used + * at that zoom level. + * @param {object} [args.editstyle] The style to apply to a line in edit + * mode. This uses styles for lines. */ var pointAnnotation = function (args) { 'use strict'; @@ -1269,7 +1477,7 @@ var pointAnnotation = function (args) { /** * Get a list of renderable features for this annotation. * - * @returns {array} an array of features. + * @returns {array} An array of features. */ this.features = function () { var opt = this.options(), @@ -1315,8 +1523,9 @@ var pointAnnotation = function (args) { * Get coordinates associated with this annotation in the map gcs coordinate * system. * - * @param {array} coordinates: an optional array of coordinates to set. - * @returns {array} an array of coordinates. + * @param {geo.geoPosition[]} [coordinates] An optional array of coordinates + * to set. + * @returns {geo.geoPosition[]} The current array of coordinates. */ this._coordinates = function (coordinates) { if (coordinates && coordinates.length >= 1) { @@ -1330,12 +1539,13 @@ var pointAnnotation = function (args) { /** * Handle a mouse click on this annotation. If the event is processed, - * evt.handled should be set to true to prevent further processing. + * evt.handled should be set to `true` to prevent further processing. * - * @param {geo.event} evt the mouse click event. - * @returns {boolean|string} true to update the annotation, 'done' if the - * annotation was completed (changed from create to done state), 'remove' - * if the annotation should be removed, falsy to not update anything. + * @param {geo.event} evt The mouse click event. + * @returns {boolean|string} `true` to update the annotation, `'done'` if + * the annotation was completed (changed from create to done state), + * `'remove'` if the annotation should be removed, falsy to not update + * anything. */ this.mouseClick = function (evt) { if (this.state() !== annotationState.create) { @@ -1354,7 +1564,7 @@ var pointAnnotation = function (args) { * Return a list of styles that should be preserved in a geojson * representation of the annotation. * - * @return {array} a list of style names to store. + * @returns {string[]} A list of style names to store. */ this._geojsonStyles = function () { return [ @@ -1363,12 +1573,12 @@ var pointAnnotation = function (args) { }; /** - * Return the coordinates to be stored in a geojson geometery object. + * Return the coordinates to be stored in a geojson geometry object. * - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @return {array} an array of flattened coordinates in the ingcs coordinate - * system. Undefined if this annotation is incompelte. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {array} An array of flattened coordinates in the interface gcs + * coordinate system. `undefined` if this annotation is incomplete. */ this._geojsonCoordinates = function (gcs) { var src = this.coordinates(gcs); @@ -1381,7 +1591,7 @@ var pointAnnotation = function (args) { /** * Return the geometry type that is used to store this annotation in geojson. * - * @return {string} a geojson geometry type. + * @returns {string} A geojson geometry type. */ this._geojsonGeometryType = function () { return 'Point'; diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 52c14d59bf..8f1a728a60 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -6,25 +6,41 @@ var registry = require('./registry'); var transform = require('./transform'); var $ = require('jquery'); var Mousetrap = require('mousetrap'); +var textFeature = require('./textFeature'); + +/** + * @typedef {object} geo.annotationLayer.labelRecord + * @property {string} text The text of the label + * @property {geo.geoPosition} position The position of the label in map gcs + * coordinates. + * @property {object} [style] A `geo.textFeature` style object. + */ /** * Layer to handle direct interactions with different features. Annotations * (features) can be created by calling mode() or cancelled * with mode(null). * - * @class geo.annotationLayer + * @class + * @alias geo.annotationLayer * @extends geo.featureLayer - * @param {object?} options - * @param {number} [options.dblClickTime=300] The delay in milliseconds that - * is treated as a double-click when working with annotations. - * @param {number} [options.adjacentPointProximity=5] The minimum distance in + * @param {object} [args] Layer options. + * @param {number} [args.dblClickTime=300] The delay in milliseconds that is + * treated as a double-click when working with annotations. + * @param {number} [args.adjacentPointProximity=5] The minimum distance in * display coordinates (pixels) between two adjacent points when creating a - * polygon. A value of 0 requires an exact match. - * @param {number} [options.finalPointProximity=10] The maximum distance in + * polygon or line. A value of 0 requires an exact match. + * @param {number} [args.continousPointProximity=5] The minimum distance in + * display coordinates (pixels) between two adjacent points when dragging + * to create an annotation. `false` disables continuous drawing mode. + * @param {number} [args.finalPointProximity=10] The maximum distance in * display coordinates (pixels) between the starting point and the mouse * coordinates to signal closing a polygon. A value of 0 requires an exact * match. A negative value disables closing a polygon by clicking on the * start point. + * @param {boolean} [args.showLabels=true] Truthy to show feature labels that + * are allowed by the associated feature to be shown. + * @param {object} [args.defaultLabelStyle] Default styles for labels. * @returns {geo.annotationLayer} */ var annotationLayer = function (args) { @@ -41,12 +57,15 @@ var annotationLayer = function (args) { var m_this = this, s_init = this._init, s_exit = this._exit, + s_draw = this.draw, s_update = this._update, m_buildTime = timestamp(), m_options, m_mode = null, m_annotations = [], - m_features = []; + m_features = [], + m_labelFeature, + m_labelLayer; var geojsonStyleProperties = { 'closed': {dataType: 'boolean', keys: ['closed', 'close']}, @@ -70,14 +89,15 @@ var annotationLayer = function (args) { // in pixels; set to continuousPointProximity to false to disable // continuous drawing modes. continuousPointProximity: 5, - finalPointProximity: 10 // in pixels, 0 is exact + finalPointProximity: 10, // in pixels, 0 is exact + showLabels: true }, args); /** * Process an action event. If we are in rectangle-creation mode, this * creates a rectangle. * - * @param {geo.event} evt the selection event. + * @param {geo.event} evt The selection event. */ this._processAction = function (evt) { var update; @@ -92,10 +112,10 @@ var annotationLayer = function (args) { /** * Handle updating the current annotation based on an update state. * - * @param {string|undefined} update: truthy to update. 'done' if the - * annotation was completed and the mode should return to null. 'remove' - * to remove the current annotation and set the mode to null. Falsy to do - * nothing. + * @param {string|undefined} update Truthy to update. `'done'` if the + * annotation was completed and the mode should return to `null`. + * `'remove'` to remove the current annotation and set the mode to `null`. + * Falsy to do nothing. */ this._updateFromEvent = function (update) { switch (update) { @@ -109,7 +129,6 @@ var annotationLayer = function (args) { } if (update) { m_this.modified(); - m_this._update(); m_this.draw(); } }; @@ -118,14 +137,13 @@ var annotationLayer = function (args) { * Handle mouse movement. If there is a current annotation, the movement * event is sent to it. * - * @param {geo.event} evt the mouse move event. + * @param {geo.event} evt The mouse move event. */ this._handleMouseMove = function (evt) { if (this.mode() && this.currentAnnotation) { var update = this.currentAnnotation.mouseMove(evt); if (update) { m_this.modified(); - m_this._update(); m_this.draw(); } } @@ -135,7 +153,7 @@ var annotationLayer = function (args) { * Handle mouse clicks. If there is a current annotation, the click event is * sent to it. * - * @param {geo.event} evt the mouse click event. + * @param {geo.event} evt The mouse click event. */ this._handleMouseClick = function (evt) { if (this.mode() && this.currentAnnotation) { @@ -147,13 +165,13 @@ var annotationLayer = function (args) { /** * Set or get options. * - * @param {string|object} arg1 if undefined, return the options object. If - * a string, either set or return the option of that name. If an object, - * update the options with the object's values. - * @param {object} arg2 if arg1 is a string and this is defined, set the - * option to this value. - * @returns {object|this} if options are set, return the layer, otherwise - * return the requested option or the set of options. + * @param {string|object} [arg1] If `undefined`, return the options object. + * If a string, either set or return the option of that name. If an + * object, update the options with the object's values. + * @param {object} [arg2] If `arg1` is a string and this is defined, set + * the option to this value. + * @returns {object|this} If options are set, return the annotation, + * otherwise return the requested option or the set of options. */ this.options = function (arg1, arg2) { if (arg1 === undefined) { @@ -174,14 +192,14 @@ var annotationLayer = function (args) { /** * Calculate the display distance for two coordinate in the current map. * - * @param {object} coord1 the first coordinates. - * @param {string|geo.transform} [gcs1] undefined to use the interface gcs, - * null to use the map gcs, 'display' if the coordinates are already in - * display coordinates, or any other transform. - * @param {object} coord2 the second coordinates. - * @param {string|geo.transform} [gcs2] undefined to use the interface gcs, - * null to use the map gcs, 'display' if the coordinates are already in - * display coordinates, or any other transform. + * @param {geo.geoPosition|geo.screenPosition} coord1 The first coordinates. + * @param {string|geo.transform|null} gcs1 `undefined` to use the interface + * gcs, `null` to use the map gcs, `'display`' if the coordinates are + * already in display coordinates, or any other transform. + * @param {geo.geoPosition|geo.screenPosition} coord2 the second coordinates. + * @param {string|geo.transform|null} [gcs2] `undefined` to use the interface + * gcs, `null` to use the map gcs, `'display`' if the coordinates are + * already in display coordinates, or any other transform. * @returns {number} the Euclidian distance between the two coordinates. */ this.displayDistance = function (coord1, gcs1, coord2, gcs2) { @@ -204,9 +222,10 @@ var annotationLayer = function (args) { /** * Add an annotation to the layer. The annotation could be in any state. * - * @param {object} annotation the annotation to add. - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform + * @param {geo.annotation} annotation Te annotation to add. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @returns {this} The current layer. */ this.addAnnotation = function (annotation, gcs) { var pos = $.inArray(annotation, m_annotations); @@ -224,7 +243,6 @@ var annotationLayer = function (args) { gcs, map.gcs(), annotation._coordinates())); } this.modified(); - this._update(); this.draw(); m_this.geoTrigger(geo_event.annotation.add, { annotation: annotation @@ -236,10 +254,10 @@ var annotationLayer = function (args) { /** * Remove an annotation from the layer. * - * @param {object} annotation the annotation to remove. - * @param {boolean} update if false, don't update the layer after removing + * @param {geo.annoation} annotation The annotation to remove. + * @param {boolean} update If `false`, don't update the layer after removing * the annotation. - * @returns {boolean} true if an annotation was removed. + * @returns {boolean} `true` if an annotation was removed. */ this.removeAnnotation = function (annotation, update) { var pos = $.inArray(annotation, m_annotations); @@ -251,7 +269,6 @@ var annotationLayer = function (args) { m_annotations.splice(pos, 1); if (update !== false) { this.modified(); - this._update(); this.draw(); } m_this.geoTrigger(geo_event.annotation.remove, { @@ -264,11 +281,11 @@ var annotationLayer = function (args) { /** * Remove all annotations from the layer. * - * @param {boolean} skipCreating: if true, don't remove annotations that are - * in the create state. - * @param {boolean} update if false, don't update the layer after removing - * the annotation. - * @returns {number} the number of annotations that were removed. + * @param {boolean} [skipCreating] If truthy, don't remove annotations that + * are in the create state. + * @param {boolean} [update] If `false`, don't update the layer after + * removing the annotation. + * @returns {number} The number of annotations that were removed. */ this.removeAllAnnotations = function (skipCreating, update) { var removed = 0, annotation, pos = 0; @@ -283,7 +300,6 @@ var annotationLayer = function (args) { } if (removed && update !== false) { this.modified(); - this._update(); this.draw(); } return removed; @@ -292,7 +308,7 @@ var annotationLayer = function (args) { /** * Get the list of annotations on the layer. * - * @returns {array} An array of annotations. + * @returns {geo.annoation[]} An array of annotations. */ this.annotations = function () { return m_annotations.slice(); @@ -301,7 +317,9 @@ var annotationLayer = function (args) { /** * Get an annotation by its id. * - * @returns {geo.annotation} The selected annotation or undefined. + * @param {number} id The annotation ID. + * @returns {geo.annotation} The selected annotation or `undefined` if none + * matches the id. */ this.annotationById = function (id) { if (id !== undefined && id !== null) { @@ -316,10 +334,10 @@ var annotationLayer = function (args) { }; /** - * Get or set the current mode. The mode is either null for nothing being + * Get or set the current mode. The mode is either `null` for nothing being * created, or the name of the type of annotation that is being created. * - * @param {string|null} arg the new mode or undefined to get the current + * @param {string|null} [arg] The new mode or `undefined` to get the current * mode. * @returns {string|null|this} The current mode or the layer. */ @@ -382,23 +400,23 @@ var annotationLayer = function (args) { * Return the current set of annotations as a geojson object. Alternately, * add a set of annotations from a geojson object. * - * @param {object} geojson: if present, add annotations based on the given - * geojson object. If undefined, return the current annotations as - * geojson. This may be a JSON string, a javascript object, or a File - * object. - * @param {boolean} clear: if true, when adding annotations, first remove all - * existing objects. If 'update', update existing annotations and remove - * annotations that no longer exit, If false, update existing + * @param {string|objectFile} [geojson] If present, add annotations based on + * the given geojson object. If `undefined`, return the current + * annotations as geojson. This may be a JSON string, a javascript + * object, or a File object. + * @param {boolean} [clear] If `true`, when adding annotations, first remove + * all existing objects. If `'update'`, update existing annotations and + * remove annotations that no longer exit, If falsy, update existing * annotations and leave unchanged annotations. - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. - * @param {boolean} includeCrs: if true, include the coordinate system in the - * output. - * @return {object|number|undefined} if geojson was undefined, the current + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. + * @param {boolean} [includeCrs] If truthy, include the coordinate system in + * the output. + * @returns {object|number|undefined} If `geojson` was undefined, the current * annotations as a javascript object that can be converted to geojson - * using JSON.stringify. If geojson is specified, either the number of - * annotations now present upon success, or undefined if the value in - * geojson was not able to be parsed. + * using JSON.stringify. If `geojson` is specified, either the number of + * annotations now present upon success, or `undefined` if the value in + * `geojson` was not able to be parsed. */ this.geojson = function (geojson, clear, gcs, includeCrs) { if (geojson !== undefined) { @@ -429,7 +447,6 @@ var annotationLayer = function (args) { }); } this.modified(); - this._update(); this.draw(); return m_annotations.length; } @@ -454,9 +471,9 @@ var annotationLayer = function (args) { * Convert a feature as parsed by the geojson reader into one or more * annotations. * - * @param {geo.feature} feature: the feature to convert. - * @param {string|geo.transform} [gcs] undefined to use the interface gcs, - * null to use the map gcs, or any other transform. + * @param {geo.feature} feature The feature to convert. + * @param {string|geo.transform|null} [gcs] `undefined` to use the interface + * gcs, `null` to use the map gcs, or any other transform. */ this._geojsonFeatureToAnnotation = function (feature, gcs) { var dataList = feature.data(), @@ -516,7 +533,6 @@ var annotationLayer = function (args) { $.each(prop.keys, function (idx, altkey) { if (value === undefined) { value = m_this.validateAttribute(options[altkey], prop.dataType); - return; } }); if (value === undefined) { @@ -558,25 +574,25 @@ var annotationLayer = function (args) { /** * Validate a value for an attribute based on a specified data type. This - * returns a sanitized value or undefined if the value was invalid. Data + * returns a sanitized value or `undefined` if the value was invalid. Data * types include: - * color: a css string, #rrggbb hex string, #rgb hex string, number, or - * object with r, g, b properties in the range of [0-1]. - * opacity: a floating point number in the range [0, 1]. - * positive: a floating point number greater than zero. - * boolean: a string whose lowercase value is 'false', 'off', or 'no', and - * falsy values are false, all else is true. null and undefined are - * still considered invalid values. - * booleanOrNumber: a string whose lowercase value is 'false', 'off', 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. - * number: a floating point number that isn't NaN or an infinity. - * text: any text string. - * @param {number|string|object|boolean} value: the value to validate. - * @param {string} dataType: the data type for validation. - * @returns {number|string|object|boolean|undefined} the sanitized value or - * undefined. + * - `color`: a css string, `#rrggbb` hex string, `#rgb` hex string, number, + * or object with r, g, b properties in the range of [0-1]. + * - `opacity`: a floating point number in the range [0, 1]. + * - `positive`: a floating point number greater than zero. + * - `boolean`: a string whose lowercase value is `'false'`, `'off'`, or + * `'no'`, and falsy values are false, all else is true. `null` and + * `undefined` are still considered invalid values. + * - `booleanOrNumber`: a string whose lowercase value is `'false'`, `'off'`, + * `'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. + * - `number`: a floating point number that isn't NaN or an infinity. + * - `text`: any text string. + * @param {number|string|object|boolean} value The value to validate. + * @param {string} dataType The data type for validation. + * @returns {number|string|object|boolean|undefined} The sanitized value or + * `undefined`. */ this.validateAttribute = function (value, dataType) { if (value === undefined || value === null) { @@ -628,10 +644,13 @@ var annotationLayer = function (args) { }; /** - * Update layer + * Update layer. + * + * @returns {this} The current layer. */ - this._update = function (request) { + this._update = function () { if (m_this.getMTime() > m_buildTime.getMTime()) { + var labels = this.options('showLabels') ? [] : null; /* Interally, we have a set of feature levels (to provide z-index * support), each of which can have data from multiple annotations. We * clear the data on each of these features, then build it up from each @@ -646,6 +665,12 @@ var annotationLayer = function (args) { }); $.each(m_annotations, function (annotation_idx, annotation) { var features = annotation.features(); + if (labels) { + var annotationLabel = annotation.labelRecord(); + if (annotationLabel) { + labels.push(annotationLabel); + } + } $.each(features, function (idx, featureLevel) { if (m_features[idx] === undefined) { m_features[idx] = {}; @@ -719,9 +744,56 @@ var annotationLayer = function (args) { feature.feature.data(feature.data); }); }); + m_this._updateLabels(labels); m_buildTime.modified(); } - s_update.call(m_this, request); + s_update.call(m_this, arguments); + return this; + }; + + /** + * Show or hide annotation labels. Create or destroy a child layer or a + * feature as needed. + * + * @param {object[]|null} labels The list of labels to display of `null` for + * no labels. + * @returns {this} The class instance. + */ + this._updateLabels = function (labels) { + if (!labels || !labels.length) { + m_this.removeLabelFeature(); + return m_this; + } + if (!m_labelFeature) { + var renderer = registry.rendererForFeatures(['text']); + if (renderer !== m_this.renderer()) { + m_labelLayer = registry.createLayer('feature', m_this.map(), {renderer: renderer}); + m_this.addChild(m_labelLayer); + m_labelLayer._update(); + m_this.geoTrigger(geo_event.layerAdd, { + target: m_this, + layer: m_labelLayer + }); + } + var style = {}; + textFeature.usedStyles.forEach(function (key) { + style[key] = function (d, i) { + if (d.style && d.style[key] !== undefined) { + return d.style[key]; + } + return (m_this.options('defaultLableStyle') || {})[key]; + }; + }); + m_labelFeature = (m_labelLayer || m_this).createFeature('text', { + style: style, + gcs: m_this.map().gcs(), + position: function (d) { + return d.position; + } + }); + } + m_labelFeature.data(labels); + return m_this; }; /** @@ -738,7 +810,42 @@ var annotationLayer = function (args) { }; /** - * Initialize + * Remove the label feature if it exists. + * + * @returns {this} The current layer. + */ + this.removeLabelFeature = function () { + if (m_labelLayer) { + m_labelLayer._exit(); + m_this.removeChild(m_labelLayer); + m_this.geoTrigger(geo_event.layerRemove, { + target: m_this, + layer: m_labelLayer + }); + m_labelLayer = m_labelFeature = null; + } + if (m_labelFeature) { + m_this.removeFeature(m_labelFeature); + m_labelFeature = null; + } + return m_this; + }; + + /** + * Update if necessary and draw the layer. + * + * @returns {this} The current layer. + */ + this.draw = function () { + m_this._update(); + s_draw.call(m_this); + return m_this; + }; + + /** + * Initialize. + * + * @returns {this} The current layer. */ this._init = function () { // Call super class init @@ -759,9 +866,12 @@ var annotationLayer = function (args) { }; /** - * Free all resources + * Free all resources. + * + * @returns {this} The current layer. */ this._exit = function () { + m_this.removeLabelFeature(); // Call super class exit s_exit.call(m_this); m_annotations = []; diff --git a/src/canvas/index.js b/src/canvas/index.js index 0935b9dc54..0332013d93 100644 --- a/src/canvas/index.js +++ b/src/canvas/index.js @@ -7,5 +7,6 @@ module.exports = { lineFeature: require('./lineFeature'), pixelmapFeature: require('./pixelmapFeature'), quadFeature: require('./quadFeature'), + textFeature: require('./textFeature'), tileLayer: require('./tileLayer') }; diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js new file mode 100644 index 0000000000..fa1cfd8638 --- /dev/null +++ b/src/canvas/textFeature.js @@ -0,0 +1,75 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var textFeature = require('../textFeature'); + +/** + * Create a new instance of class canvas.textFeature. + * + * @class + * @alias geo.canvas.textFeature + * @extends geo.textFeature + * @extends geo.canvas.object + * + * @param {geo.textFeature.spec} [arg] Options for the feature. + * @returns {geo.canvas.textFeature} The created feature. + */ +var canvas_textFeature = function (arg) { + 'use strict'; + if (!(this instanceof canvas_textFeature)) { + return new canvas_textFeature(arg); + } + + var object = require('./object'); + + arg = arg || {}; + textFeature.call(this, arg); + object.call(this); + + /** + * @private + */ + var m_this = this; + + /** + * Render the data on the canvas. + * @protected + * @param {object} context2d the canvas context to draw in. + * @param {object} 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; + + data.forEach(function (d, i) { + // 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. + pos = m_this.featureGcsToDisplay(posFunc(d, i)); + text = textFunc(d, i); + + 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: rotation, maxWidth, offset + context2d.fillText(text, pos.x, pos.y); + }); + }; + + this._init(arg); + return this; +}; + +inherit(canvas_textFeature, textFeature); + +// Now register it +var capabilities = {}; + +registerFeature('canvas', 'text', canvas_textFeature, capabilities); + +module.exports = canvas_textFeature; diff --git a/src/index.js b/src/index.js index 545e5b7887..e5bc33f61c 100644 --- a/src/index.js +++ b/src/index.js @@ -67,6 +67,7 @@ module.exports = $.extend({ pixelmapFeature: require('./pixelmapFeature'), renderer: require('./renderer'), sceneObject: require('./sceneObject'), + textFeature: require('./textFeature'), tile: require('./tile'), tileCache: require('./tileCache'), tileLayer: require('./tileLayer'), diff --git a/src/textFeature.js b/src/textFeature.js new file mode 100644 index 0000000000..8da5e052b3 --- /dev/null +++ b/src/textFeature.js @@ -0,0 +1,194 @@ +var inherit = require('./inherit'); +var feature = require('./feature'); + +/** + * Object specification for a text feature. + * + * @typedef {geo.feature.spec} geo.textFeature.spec + * @property {geo.geoPosition[]|function} [position] The position of each data + * element. Defaults to the `x`, `y`, and `z` properties of the data + * element. + * @property {string[]|function} [text] The text of each data element. + * Defaults to the `text` property of the data element. + * @property {object} [style] The style to apply to each data element. + * @property {boolean|function} [style.visible=true] If falsy, don't show this + * data element. + * @property {string|function} [style.font] A css font specification. This + * is of the form `[style] [variant] [weight] [stretch] size[/line-height] + * family`. Individual font styles override this value if a style is + * specified in each. See the individual font styles for details. + * @property {string|function} [style.fontStyle='normal'] The font style. One + * of `normal`, `italic`, or `oblique`. + * @property {string|function} [style.fontVariant='normal'] The font variant. + * This can have values such as `small-caps` or `slashed-zero`. + * @property {string|function} [style.fontWeight='normal'] The font weight. + * This may be a numeric value where 400 is normal and 700 is bold, or a + * string such as `bold` or `lighter`. + * @property {string|function} [style.fontStretch='normal'] The font stretch, + * such as `condensed`. + * @property {string|function} [style.fontSize='medium'] The font size. + * @property {string|function} [style.lineHeight='normal'] The font line + * height. + * @property {string|function} [style.fontFamily] The font family. + * @property {string|function} [style.textAlign='center'] The horizontal text + * alignment. One of `start`, `end`, `left`, `right`, or `center`. + * @property {string|function} [style.textBaseline='middle'] The vertical text + * alignment. One of `top`, `hanging`, `middle`, `alphabetic`, + * `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 {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 {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. + * @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. + */ + +/** + * Create a new instance of class textFeature. + * + * @class + * @alias geo.textFeature + * @extends geo.feature + * + * @param {geo.textFeature.spec} [arg] Options for the feature. + * @returns {geo.textFeature} The created feature. + */ +var textFeature = function (arg) { + 'use strict'; + if (!(this instanceof textFeature)) { + return new textFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + var $ = require('jquery'); + + /** + * @private + */ + var m_this = this, + s_init = this._init; + + this.featureType = 'text'; + + /** + * Get/Set position. + * + * @param {array|function} [val] If `undefined`, return the current position + * setting. Otherwise, modify the current position setting. + * @returns {array|function|this} The current position or this feature. + */ + this.position = function (val) { + if (val === undefined) { + return m_this.style('position'); + } else if (val !== m_this.style('position')) { + m_this.style('position', val); + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; + }; + + /** + * Get/Set text. + * + * @param {array|function} [val] If `undefined`, return the current text + * setting. Otherwise, modify the current text setting. + * @returns {array|function|this} The current text or this feature. + */ + this.text = function (val) { + if (val === undefined) { + return m_this.style('text'); + } else if (val !== m_this.style('text')) { + m_this.style('text', val); + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; + }; + + /** + * Initialize. + * + * @param {geo.textFeature.spec} [arg] The feature specification. + */ + this._init = function (arg) { + arg = arg || {}; + s_init.call(m_this, arg); + + var style = $.extend( + {}, + { + font: 'bold 16px sans-serif', + textAlign: 'center', + textBaseline: 'middle', + direction: 'inherit', + color: { r: 0, g: 0, b: 0 }, + rotation: 0, /* in radians */ + rotateWithMap: false, + scaleWithMap: false, + position: function (d) { return d; }, + text: function (d) { return d.text; } + }, + arg.style === undefined ? {} : arg.style + ); + + if (arg.position !== undefined) { + style.position = arg.position; + } + if (arg.text !== undefined) { + style.text = arg.text; + } + + m_this.style(style); + if (style.position) { + m_this.position(style.position); + } + if (style.text) { + m_this.text(style.text); + } + m_this.dataTime().modified(); + }; + + return m_this; +}; + +textFeature.usedStyles = [ + 'visible', 'font', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', + 'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline', + 'direction', 'color', 'opacity', 'rotation', 'rotateWithMap', 'scaleWithMap', + 'offset', 'width' +]; + +/** + * Create a textFeature from an object. + * @see {@link geo.feature.create} + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.textFeature.spec} spec The object specification + * @returns {geo.textFeature|null} + */ +textFeature.create = function (layer, spec) { + 'use strict'; + + spec = spec || {}; + spec.type = 'text'; + return feature.create(layer, spec); +}; + +textFeature.capabilities = { + /* core feature name -- support in any manner */ + feature: 'text' +}; + +inherit(textFeature, feature); +module.exports = textFeature; From 1947886612529a4214861242137c876f12ea7e9a Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 7 Jul 2017 10:27:20 -0400 Subject: [PATCH 02/11] 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. --- examples/annotations/index.jade | 139 +++++++++++++++++++------------- examples/annotations/main.js | 33 ++++++-- src/annotation.js | 14 +++- src/annotationLayer.js | 23 ++++-- src/canvas/object.js | 36 +++++++-- src/canvas/textFeature.js | 104 +++++++++++++++++++++--- src/textFeature.js | 19 ++++- src/util/index.js | 18 +++++ 8 files changed, 297 insertions(+), 89 deletions(-) diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index 5babac7fab..a07a684530 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -47,61 +47,90 @@ block append mainContent .form-group label(for='edit-name') Name input#edit-name(option='name') - .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') - .form-group(annotation-types='point') - label(for='edit-radius') Radius - input#edit-radius(option='radius', format='positive') - .form-group(annotation-types='point', title='Set to "false" to disable, "true" to use the specified radius at the current zoom, or a zoom level to use the specified radius at that zoom level.') - label(for='edit-scaled') Scale with Zoom - input#edit-radius(option='scaled', format='booleanOrNumber') - .form-group(annotation-types='point polygon rectangle') - label(for='edit-fill') Fill - select#edit-stroke(option='fill', format='boolean') - option(value='true') Yes - option(value='false') No - .form-group(annotation-types='point polygon rectangle') - label(for='edit-fillColor') Fill Color - input#edit-fillColor(option='fillColor', format='color') - .form-group(annotation-types='point polygon rectangle') - label(for='edit-fillOpacity') Fill Opacity - input#edit-fillOpacity(option='fillOpacity', format='opacity') - .form-group(annotation-types='point polygon rectangle') - label(for='edit-stroke') Stroke - select#edit-stroke(option='stroke', format='boolean') - option(value='true') Yes - option(value='false') No - .form-group(annotation-types='point polygon rectangle line') - label(for='edit-strokeWidth') Stroke Width - input#edit-strokeWidth(option='strokeWidth', format='positive') - .form-group(annotation-types='point polygon rectangle line') - label(for='edit-strokeColor') Stroke Color - input#edit-strokeColor(option='strokeColor', format='color') - .form-group(annotation-types='point polygon rectangle line') - label(for='edit-strokeOpacity') Stroke Opacity - input#edit-strokeOpacity(option='strokeOpacity', format='opacity') - .form-group(annotation-types='line') - label(for='edit-closed') Closed - select#edit-closed(option='closed', format='boolean') - option(value='true') Yes - option(value='false') No - .form-group(annotation-types='line') - label(for='edit-lineCap') Line End Caps - select#edit-lineCap(option='lineCap', format='text') - option(value='butt') Butt - option(value='round') Round - option(value='square') Square - .form-group(annotation-types='line') - label(for='edit-lineJoin') Line Joins - select#edit-lineJoin(option='lineJoin', format='text') - option(value='miter') Miter - option(value='bevel') Bevel - option(value='round') Round - option(value='miter-clip') Miter-Clip + .row + .col-md-6 + .form-group + 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') + .form-group(annotation-types='point', title='Set to "false" to disable, "true" to use the specified radius at the current zoom, or a zoom level to use the specified radius at that zoom level.') + label(for='edit-scaled') Scale with Zoom + input#edit-radius(option='scaled', format='booleanOrNumber') + .form-group(annotation-types='point polygon rectangle') + label(for='edit-fill') Fill + select#edit-stroke(option='fill', format='boolean') + option(value='true') Yes + option(value='false') No + .form-group(annotation-types='point polygon rectangle') + label(for='edit-fillColor') Fill Color + input#edit-fillColor(option='fillColor', format='color') + .form-group(annotation-types='point polygon rectangle') + label(for='edit-fillOpacity') Fill Opacity + input#edit-fillOpacity(option='fillOpacity', format='opacity') + .form-group(annotation-types='point polygon rectangle') + label(for='edit-stroke') Stroke + select#edit-stroke(option='stroke', format='boolean') + option(value='true') Yes + option(value='false') No + .form-group(annotation-types='point polygon rectangle line') + label(for='edit-strokeWidth') Stroke Width + input#edit-strokeWidth(option='strokeWidth', format='positive') + .form-group(annotation-types='point polygon rectangle line') + label(for='edit-strokeColor') Stroke Color + input#edit-strokeColor(option='strokeColor', format='color') + .form-group(annotation-types='point polygon rectangle line') + label(for='edit-strokeOpacity') Stroke Opacity + input#edit-strokeOpacity(option='strokeOpacity', format='opacity') + .form-group(annotation-types='line') + label(for='edit-closed') Closed + select#edit-closed(option='closed', format='boolean') + option(value='true') Yes + option(value='false') No + .form-group(annotation-types='line') + label(for='edit-lineCap') Line End Caps + select#edit-lineCap(option='lineCap', format='text') + option(value='butt') Butt + option(value='round') Round + option(value='square') Square + .form-group(annotation-types='line') + label(for='edit-lineJoin') Line Joins + select#edit-lineJoin(option='lineJoin', format='text') + option(value='miter') Miter + 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|] [] [/] ') + 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 diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 95c370b2fd..78464c500b 100644 --- a/examples/annotations/main.js +++ b/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(); - value = opt.style[key]; + 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; } - $('[option]', ctl).val('' + value); + 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 { - newopt[key] = value; + 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 diff --git a/src/annotation.js b/src/annotation.js index ed6143bdd0..2003f00525 100644 --- a/src/annotation.js +++ b/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() : ( diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 8f1a728a60..115295b332 100644 --- a/src/annotationLayer.js +++ b/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', { diff --git a/src/canvas/object.js b/src/canvas/object.js index de7d297306..cabb058e6b 100644 --- a/src/canvas/object.js +++ b/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. @@ -29,8 +31,33 @@ var canvas_object = function (arg) { }; /** - * Redraw the object. - */ + * 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(); m_this.renderer()._render(); @@ -43,4 +70,3 @@ var canvas_object = function (arg) { inherit(canvas_object, sceneObject); module.exports = canvas_object; - diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js index fa1cfd8638..c8f0d134ad 100644 --- a/src/canvas/textFeature.js +++ b/src/canvas/textFeature.js @@ -1,6 +1,7 @@ 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); diff --git a/src/textFeature.js b/src/textFeature.js index 8da5e052b3..661cb569b0 100644 --- a/src/textFeature.js +++ b/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' ]; /** diff --git a/src/util/index.js b/src/util/index.js index cdabc14b8f..fc24af6ede 100644 --- a/src/util/index.js +++ b/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 From de61c626471ea2713fd6bfa1141873cd6d94450a Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 10 Jul 2017 08:18:57 -0400 Subject: [PATCH 03/11] Add text opacity support. This adds a utility function to combine a color with an optional opacity and an opacity value into a single color with opacity value. --- examples/annotations/index.jade | 2 +- examples/annotations/main.js | 2 +- src/annotation.js | 43 +++++++++++++++++++-------------- src/canvas/textFeature.js | 26 +++++++++----------- src/textFeature.js | 11 ++++++--- src/util/index.js | 41 ++++++++++++++++++++++++++++--- tests/cases/annotation.js | 18 +++++++------- 7 files changed, 92 insertions(+), 51 deletions(-) diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index a07a684530..c549e29a7e 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -117,7 +117,7 @@ block append mainContent .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='center') Center option(value='start') Start option(value='end') End option(value='left') Left diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 78464c500b..b5279314f0 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -333,7 +333,7 @@ $(function () { switch (format) { case 'color': // always show colors as hex values - value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0}); + value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0}, 'needed'); break; } if (value !== undefined) { diff --git a/src/annotation.js b/src/annotation.js index 2003f00525..85934dd286 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -273,6 +273,13 @@ var annotation = function (type, args) { } if (arg2 === undefined) { m_options = $.extend(true, m_options, arg1); + /* For style objects, reextend them without recursiion. This allows + * setting colors without an opacity field, for instance. */ + ['style', 'editStyle', 'labelStyle'].forEach(function (key) { + if (arg1[key] !== undefined) { + $.extend(m_options[key], arg1[key]); + } + }); } else { m_options[arg1] = arg2; } @@ -340,17 +347,17 @@ var annotation = function (type, args) { * @returns {object|this} Either the entire style object, the value of a * specific style, or the current class instance. */ - this.editstyle = function (arg1, arg2) { + this.editStyle = function (arg1, arg2) { if (arg1 === undefined) { - return m_options.editstyle; + return m_options.editStyle; } if (typeof arg1 === 'string' && arg2 === undefined) { - return m_options.editstyle[arg1]; + return m_options.editStyle[arg1]; } if (arg2 === undefined) { - m_options.editstyle = $.extend(true, m_options.editstyle, arg1); + m_options.editStyle = $.extend(true, m_options.editStyle, arg1); } else { - m_options.editstyle[arg1] = arg2; + m_options.editStyle[arg1] = arg2; } this.modified(); return this; @@ -537,7 +544,7 @@ var annotation = function (type, args) { value = util.ensureFunction(objStyle[key])(); if (value !== undefined) { if (key.toLowerCase().match(/color$/)) { - value = util.convertColorToHex(value); + value = util.convertColorToHex(value, 'needed'); } obj.properties[key] = value; } @@ -547,7 +554,7 @@ var annotation = function (type, args) { value = util.ensureFunction(objLabelStyle[key])(); if (value !== undefined) { if (key.toLowerCase().match(/color$/)) { - value = util.convertColorToHex(value); + value = util.convertColorToHex(value, 'needed'); } obj.properties['label' + key.charAt(0).toUpperCase() + key.slice(1)] = value; } @@ -596,7 +603,7 @@ var annotation = function (type, args) { * This uses styles for polygons, including `fill`, `fillColor`, * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and * `strokeOpacity`. - * @param {object} [args.editstyle] The style to apply to a rectangle in edit + * @param {object} [args.editStyle] The style to apply to a rectangle in edit * mode. This uses styles for polygons and lines, including `fill`, * `fillColor`, `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and * `strokeOpacity`. @@ -619,7 +626,7 @@ var rectangleAnnotation = function (args) { strokeWidth: 3, uniformPolygon: true }, - editstyle: { + editStyle: { fill: true, fillColor: {r: 0.3, g: 0.3, b: 0.3}, fillOpacity: 0.25, @@ -705,7 +712,7 @@ var rectangleAnnotation = function (args) { features = [{ polygon: { polygon: opt.corners, - style: opt.editstyle + style: opt.editStyle } }]; } @@ -887,7 +894,7 @@ registerAnnotation('rectangle', rectangleAnnotation, rectangleRequiredFeatures); * This uses styles for polygons, including `fill`, `fillColor`, * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and * `strokeOpacity`. - * @param {object} [args.editstyle] The style to apply to a polygon in edit + * @param {object} [args.editStyle] The style to apply to a polygon in edit * mode. This uses styles for polygons and lines, including `fill`, * `fillColor`, `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and * `strokeOpacity`. @@ -912,7 +919,7 @@ var polygonAnnotation = function (args) { strokeWidth: 3, uniformPolygon: true }, - editstyle: { + editStyle: { closed: false, fill: true, fillColor: {r: 0.3, g: 0.3, b: 0.3}, @@ -956,7 +963,7 @@ var polygonAnnotation = function (args) { features[1] = { polygon: { polygon: opt.vertices, - style: opt.editstyle + style: opt.editStyle } }; } @@ -964,7 +971,7 @@ var polygonAnnotation = function (args) { features[2] = { line: { line: opt.vertices, - style: opt.editstyle + style: opt.editStyle } }; } @@ -1145,7 +1152,7 @@ registerAnnotation('polygon', polygonAnnotation, polygonRequiredFeatures); * @param {object} [args.style] The style to apply to a finished line. * This uses styles for lines, including `strokeWidth`, `strokeColor`, * `strokeOpacity`, `strokeOffset`, `closed`, `lineCap`, and `lineJoin`. - * @param {object} [args.editstyle] The style to apply to a line in edit + * @param {object} [args.editStyle] The style to apply to a line in edit * mode. This uses styles for lines, including `strokeWidth`, * `strokeColor`, `strokeOpacity`, `strokeOffset`, `closed`, `lineCap`, * and `lineJoin`. @@ -1176,7 +1183,7 @@ var lineAnnotation = function (args) { lineCap: 'butt', lineJoin: 'miter' }, - editstyle: { + editStyle: { line: function (d) { /* Return an array that has the same number of items as we have * vertices. */ @@ -1212,7 +1219,7 @@ var lineAnnotation = function (args) { features = [{ line: { line: opt.vertices, - style: opt.editstyle + style: opt.editStyle } }]; break; @@ -1459,7 +1466,7 @@ registerAnnotation('line', lineAnnotation, lineRequiredFeatures); * zoom level. If it is `true`, the radius is based on the zoom level at * first instantiation. Otherwise, if it is a number, the radius is used * at that zoom level. - * @param {object} [args.editstyle] The style to apply to a line in edit + * @param {object} [args.editStyle] The style to apply to a line in edit * mode. This uses styles for lines. */ var pointAnnotation = function (args) { diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js index c8f0d134ad..ec8d7472fe 100644 --- a/src/canvas/textFeature.js +++ b/src/canvas/textFeature.js @@ -89,7 +89,7 @@ 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, opac; + fontFromSubValues, text, pos, val; /* 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 @@ -97,11 +97,20 @@ var canvas_textFeature = function (arg) { fontFromSubValues = ['fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'lineHeight', 'fontFamily'].some(function (key) { return m_this.style(key) !== null && m_this.style(key) !== undefined; }); + /* Clear the canvas property buffer */ + m_this._canvasProperty(); data.forEach(function (d, i) { val = m_this.style.get('visible')(d, i); if (!val && val !== undefined) { return; } + val = util.convertColorAndOpacity( + m_this.style.get('color')(d, i), m_this.style.get('textOpacity')(d, i)); + if (val.a === 0) { + return; + } + m_this._canvasProperty(context2d, 'globalAlpha', val.a); + m_this._canvasProperty(context2d, 'fillStyle', util.convertColorToHex(val)); // 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. @@ -110,24 +119,11 @@ 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'); - 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)); // TODO: fetch and use other properties: // 'direction', 'rotation', 'rotateWithMap', 'scale', // 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOffset', - // 'shadowBlur', 'shadowRotate' + // 'shadowBlur', 'shadowRotate', 'shadowOpacity' m_this._canvasProperty(context2d, 'direction', 'inherit'); // ltr, rtl, inherit /* ctx.shadowColor = "black"; diff --git a/src/textFeature.js b/src/textFeature.js index 661cb569b0..5da26ea15a 100644 --- a/src/textFeature.js +++ b/src/textFeature.js @@ -39,8 +39,8 @@ var feature = require('./feature'); * of `ltr`, `rtl`, or `inherit`. * @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.textOpacity=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 @@ -56,6 +56,9 @@ var feature = require('./feature'); * 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 + * shadow. If the color includes opacity, this is combined with that + * value. * @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 @@ -179,8 +182,8 @@ textFeature.usedStyles = [ 'visible', 'font', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline', 'direction', 'color', 'textOpacity', 'rotation', 'rotateWithMap', 'scale', - 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOffset', - 'shadowBlur', 'shadowRotate' + 'scaleWithMap', 'offset', 'width', 'shadowColor', 'shadowOpacity', + 'shadowOffset', 'shadowBlur', 'shadowRotate' ]; /** diff --git a/src/util/index.js b/src/util/index.js index fc24af6ede..2f2d68c522 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -257,18 +257,53 @@ var util = module.exports = { b: ((color & 0xff)) / 255 }; } - if (opacity !== undefined) { + if (opacity !== undefined && color && color.r !== undefined) { color.a = opacity; } return color; }, + /** + * Convert a color (possibly with opacity) and an optional opacity value to + * a color object that always has opacity. The opacity is guaranteed to be + * within [0-1]. A valid color object is always returned. + * + * @param {geo.geoColor} [color] Any valid color input. If an invalid value + * or no value is supplied, the `defaultColor` is used. + * @param {number} [opacity=1] A value from [0-1]. This is multipled with + * the opacity from `color`. + * @param {geo.geoColorObject} [defaultColor={r: 0, g: 0, b: 0}] The color + * to use if an invalid color is supplied. + * @returns {geo.geoColorObject} An rgba color object. + * @memberof geo.util + */ + convertColorAndOpacity: function (color, opacity, defaultColor) { + color = util.convertColor(color); + if (!color || color.r === undefined || color.g === undefined || color.b === undefined) { + color = util.convertColor(defaultColor || {r: 0, g: 0, b: 0}); + } + if (!color || color.r === undefined || color.g === undefined || color.b === undefined) { + color = {r: 0, g: 0, b: 0}; + } + color = { + r: isFinite(color.r) && color.r >= 0 ? (color.r <= 1 ? +color.r : 1) : 0, + g: isFinite(color.g) && color.g >= 0 ? (color.g <= 1 ? +color.g : 1) : 0, + b: isFinite(color.b) && color.b >= 0 ? (color.b <= 1 ? +color.b : 1) : 0, + a: isFinite(color.a) && color.a >= 0 ? (color.a <= 1 ? +color.a : 1) : 1 + }; + if (isFinite(opacity) && opacity < 1) { + color.a = opacity <= 0 ? 0 : color.a * opacity; + } + return color; + }, + /** * Convert a color to a six or eight digit hex value prefixed with #. * * @param {geo.geoColorObject} color The color object to convert. * @param {boolean} [allowAlpha] If truthy and `color` has a defined `a` - * value, include the alpha channel in the output. + * value, include the alpha channel in the output. If `'needed'`, only + * include the alpha channel if it is set and not 1. * @returns {string} A color string. * @memberof geo.util */ @@ -281,7 +316,7 @@ var util = module.exports = { (Math.round(rgb.g * 255) << 8) + Math.round(rgb.b * 255)).toString(16).slice(1); } - if (rgb.a !== undefined && allowAlpha) { + if (rgb.a !== undefined && allowAlpha && (rgb.a < 1 || allowAlpha !== 'needed')) { value += (256 + Math.round(rgb.a * 255)).toString(16).slice(1); } return value; diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index cda1fd38ed..cfb82f1376 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -120,9 +120,9 @@ describe('geo.annotation', function () { expect(ann.options().coordinates).toBe(undefined); expect(ann._coordinates()).toBe(testval); }); - it('style and editstyle', function () { + it('style and editStyle', function () { var ann = geo.annotation.annotation('test', { - layer: layer, style: {testopt: 30}, editstyle: {testopt: 50}}); + layer: layer, style: {testopt: 30}, editStyle: {testopt: 50}}); expect(ann.options('style').testopt).toBe(30); expect(ann.style().testopt).toBe(30); expect(ann.style('testopt')).toBe(30); @@ -130,13 +130,13 @@ describe('geo.annotation', function () { expect(ann.style().testopt).toBe(40); expect(ann.style({testopt: 30})).toBe(ann); expect(ann.style().testopt).toBe(30); - expect(ann.options('editstyle').testopt).toBe(50); - expect(ann.editstyle().testopt).toBe(50); - expect(ann.editstyle('testopt')).toBe(50); - expect(ann.editstyle('testopt', 60)).toBe(ann); - expect(ann.editstyle().testopt).toBe(60); - expect(ann.editstyle({testopt: 50})).toBe(ann); - expect(ann.editstyle().testopt).toBe(50); + expect(ann.options('editStyle').testopt).toBe(50); + expect(ann.editStyle().testopt).toBe(50); + expect(ann.editStyle('testopt')).toBe(50); + expect(ann.editStyle('testopt', 60)).toBe(ann); + expect(ann.editStyle().testopt).toBe(60); + expect(ann.editStyle({testopt: 50})).toBe(ann); + expect(ann.editStyle().testopt).toBe(50); }); it('coordinates', function () { var ann = geo.annotation.annotation('test', {layer: layer}); From 56da1e03b6be86092e96d2f54d827268f15fbfe8 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 11 Jul 2017 11:45:45 -0400 Subject: [PATCH 04/11] Accept css color names in any case. Better handle blanks in the annotations example. Show a show label option in the annotations example. --- examples/annotations/index.jade | 7 +++++- examples/annotations/main.js | 40 ++++++++++++++++++++++++--------- src/annotation.js | 3 +++ src/util/index.js | 13 ++++++----- tests/cases/colors.js | 5 +++++ 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index c549e29a7e..a0d23c1d69 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -102,9 +102,14 @@ block append mainContent option(value='round') Round option(value='miter-clip') Miter-Clip .col-md-6 - .form-group + .form-group(title='The label defaults to the annotation name') label(for='edit-label') Label input#edit-label(option='label') + .form-group(annotation-types='all', title='The label will only be shown if both this and the global option are selected') + label(for='edit-showLabel') Show Label + select#edit-showLabel(option='showLabel', format='boolean', optiontype='option') + option(value='true') Yes + option(value='false') No .form-group(annotation-types='all') label(for='edit-color') Label Color input#edit-color(option='color', format='color', optiontype='label') diff --git a/examples/annotations/main.js b/examples/annotations/main.js index b5279314f0..2f062e35fc 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -323,6 +323,9 @@ $(function () { } ctl.show(); switch ($('[option]', ctl).attr('optiontype')) { + case 'option': + value = opt[key]; + break; case 'label': value = (opt.labelStyle || {})[key]; break; @@ -336,9 +339,10 @@ $(function () { value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0}, 'needed'); break; } - if (value !== undefined) { - $('[option]', ctl).val('' + value); + if ((value === undefined || value === '' || value === null) && $('[option]', ctl).is('select')) { + value = $('[option] option', ctl).eq(0).val(); } + $('[option]', ctl).val(value === undefined ? '' : '' + value); }); dlg.one('shown.bs.modal', function () { $('[option="name"]', dlg).focus(); @@ -356,31 +360,47 @@ $(function () { var dlg = $('#editdialog'), id = dlg.attr('annotation-id'), annotation = layer.annotationById(id), + opt = annotation.options(), type = annotation.type(), typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'), - error, - newopt = {}, - labelopt = {}; + newopt = {style: {}, labelStyle: {}}, + error; // validate form values $('.form-group[annotation-types]').each(function () { var ctl = $(this), key = $('[option]', ctl).attr('option'), - value; + value, oldvalue; if (!ctl.attr('annotation-types').match(typeMatch)) { return; } value = layer.validateAttribute($('[option]', ctl).val(), $('[option]', ctl).attr('format')); - if (value === undefined) { + switch ($('[option]', ctl).attr('optiontype')) { + case 'option': + oldvalue = opt[key]; + break; + case 'label': + oldvalue = (opt.labelStyle || {})[key]; + break; + default: + oldvalue = opt.style[key]; + break; + } + if (value === oldvalue || (oldvalue === undefined && value === '')) { + // don't change anything + } else if (value === undefined) { error = $('label', ctl).text() + ' is not a valid value'; } else { switch ($('[option]', ctl).attr('optiontype')) { + case 'option': + newopt[key] = value; + break; case 'label': - labelopt[key] = value; + newopt.labelStyle[key] = value; break; default: - newopt[key] = value; + newopt.style[key] = value; break; } } @@ -392,7 +412,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, labelStyle: labelopt}).draw(); + annotation.options(newopt).draw(); dlg.modal('hide'); // refresh the annotation list diff --git a/src/annotation.js b/src/annotation.js index 85934dd286..0141493f95 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -539,6 +539,9 @@ var annotation = function (type, args) { if (m_description) { obj.properties.description = m_description; } + if (this.options('showLabel') === false) { + obj.properties.showLabel = this.options('showLabel'); + } for (i = 0; i < styles.length; i += 1) { key = styles[i]; value = util.ensureFunction(objStyle[key])(); diff --git a/src/util/index.js b/src/util/index.js index 2f2d68c522..84156d141a 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -222,8 +222,9 @@ var util = module.exports = { } var opacity; if (typeof color === 'string') { - if (util.cssColors.hasOwnProperty(color)) { - color = util.cssColors[color]; + var lowerColor = color.toLowerCase(); + if (util.cssColors.hasOwnProperty(lowerColor)) { + color = util.cssColors[lowerColor]; } else if (color.charAt(0) === '#') { if (color.length === 4 || color.length === 5) { /* interpret values of the form #rgb as #rrggbb and #rgba as @@ -239,13 +240,13 @@ var util = module.exports = { } color = parseInt(color.slice(1, 7), 16); } - } else if (color === 'transparent') { + } else if (lowerColor === 'transparent') { opacity = color = 0; - } else if (color.indexOf('(') >= 0) { + } else if (lowerColor.indexOf('(') >= 0) { for (var idx = 0; idx < util.cssColorConversions.length; idx += 1) { - var match = util.cssColorConversions[idx].regex.exec(color); + var match = util.cssColorConversions[idx].regex.exec(lowerColor); if (match) { - return util.cssColorConversions[idx].process(color, match); + return util.cssColorConversions[idx].process(lowerColor, match); } } } diff --git a/tests/cases/colors.js b/tests/cases/colors.js index 42fc1343e3..b5db942403 100644 --- a/tests/cases/colors.js +++ b/tests/cases/colors.js @@ -28,6 +28,8 @@ describe('geo.util.convertColor', function () { 'green': {r: 0, g: 128 / 255, b: 0}, 'blue': {r: 0, g: 0, b: 1}, 'steelblue': {r: 70 / 255, g: 130 / 255, b: 180 / 255}, + 'SteelBlue': {r: 70 / 255, g: 130 / 255, b: 180 / 255}, + 'STEELBLUE': {r: 70 / 255, g: 130 / 255, b: 180 / 255}, // rgb() and rgba() 'rgb(18, 86, 171)': {r: 18 / 255, g: 86 / 255, b: 171 / 255}, 'rgb(18 86 171)': {r: 18 / 255, g: 86 / 255, b: 171 / 255}, @@ -40,6 +42,7 @@ describe('geo.util.convertColor', function () { 'rgba(10% 35% 63.2% 40%)': {r: 0.1, g: 0.35, b: 0.632, a: 0.4}, 'rgba(10% 35% 63.2% / 40%)': {r: 0.1, g: 0.35, b: 0.632, a: 0.4}, 'rgba(100e-1% .35e2% 6.32e1% 40%)': {r: 0.1, g: 0.35, b: 0.632, a: 0.4}, + 'RGBA(100E-1% .35E2% 6.32E1% 40%)': {r: 0.1, g: 0.35, b: 0.632, a: 0.4}, // hsl() and hsla() 'hsl(120, 100%, 25%)': {r: 0, g: 0.5, b: 0}, 'hsla(120, 100%, 25%)': {r: 0, g: 0.5, b: 0}, @@ -49,11 +52,13 @@ describe('geo.util.convertColor', function () { 'hsl(120deg 100% 25%)': {r: 0, g: 0.5, b: 0}, 'hsl(133.33grad 100% 25%)': {r: 0, g: 0.5, b: 0}, 'hsl(2.0944rad 100% 25%)': {r: 0, g: 0.5, b: 0}, + 'HSL(2.0944RAD 100% 25%)': {r: 0, g: 0.5, b: 0}, 'hsl(.33333turn 100% 25%)': {r: 0, g: 0.5, b: 0}, 'hsl(207 44% 49%)': {r: 0.2744, g: 0.51156, b: 0.7056}, 'hsl(207 100% 50%)': {r: 0, g: 0.55, b: 1}, // transparent 'transparent': {r: 0, g: 0, b: 0, a: 0}, + 'TRANSPARENT': {r: 0, g: 0, b: 0, a: 0}, // unknown strings 'none': 'none' }; From 07ec8c94e4c72c7d273c3b495981c8e42a514965 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 14 Jul 2017 13:17:30 -0400 Subject: [PATCH 05/11] Support text rotation, scale, and offset. --- examples/annotations/index.jade | 22 +++++++++--- examples/annotations/main.js | 31 ++++++++++++++-- src/annotationLayer.js | 63 +++++++++++++++++++++++++++++---- src/canvas/textFeature.js | 44 ++++++++++++++++++++--- src/textFeature.js | 10 +++--- src/util/index.js | 27 ++++++++++++++ 6 files changed, 174 insertions(+), 23 deletions(-) diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index a0d23c1d69..56af55105a 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -44,11 +44,11 @@ block append mainContent button.close(type='button', data-dismiss='modal') × h4.modal-title Edit Annotation .modal-body - .form-group - label(for='edit-name') Name - input#edit-name(option='name') .row .col-md-6 + .form-group + label(for='edit-name') Name + input#edit-name(option='name') .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 diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 2f062e35fc..a5d44483dd 100644 --- a/examples/annotations/main.js +++ b/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]; diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 115295b332..81de6ae59e 100644 --- a/src/annotationLayer.js +++ b/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 [,] 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; diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js index ec8d7472fe..6844bcd909 100644 --- a/src/canvas/textFeature.js +++ b/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); diff --git a/src/textFeature.js b/src/textFeature.js index 5da26ea15a..ce810324aa 100644 --- a/src/textFeature.js +++ b/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' ]; /** diff --git a/src/util/index.js b/src/util/index.js index 84156d141a..65606c8bf9 100644 --- a/src/util/index.js +++ b/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 From 18745478d03144a6ea98421213246faa6effd688 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 14 Jul 2017 14:38:43 -0400 Subject: [PATCH 06/11] Support text shadow and stroke. --- examples/annotations/index.jade | 35 ++++++++++++++++---- examples/annotations/main.js | 3 -- src/annotationLayer.js | 14 ++++++-- src/canvas/textFeature.js | 58 +++++++++++++++++++++++---------- src/textFeature.js | 6 +++- src/util/index.js | 4 +-- 6 files changed, 88 insertions(+), 32 deletions(-) diff --git a/examples/annotations/index.jade b/examples/annotations/index.jade index 56af55105a..f022e669cf 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.jade @@ -102,6 +102,7 @@ block append mainContent option(value='round') Round option(value='miter-clip') Miter-Clip .col-md-6 + //- label .form-group(title='The label defaults to the annotation name') label(for='edit-label') Label input#edit-label(option='label') @@ -110,15 +111,22 @@ block append mainContent select#edit-showLabel(option='showLabel', format='boolean', optiontype='option') option(value='true') Yes option(value='false') No - .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|] [] [/] ') label(for='edit-font') Font input#edit-font(option='font', optiontype='label') + .form-group(annotation-types='all', title='This applies to both the filled text and the stroke') + label(for='edit-textOpacity') Label Opacity + input#edit-textOpacity(option='textOpacity', format='opacity', optiontype='label') + .form-group(annotation-types='all', title='The color of the filled text. Use an rgba() form to specify opacity.') + label(for='edit-color') Fill Color + input#edit-color(option='color', format='color', optiontype='label') + .form-group(annotation-types='all', title='The color of a stroke around the text. If used with Fill Color, this adds a perimeter half the Stroke Width outside of the text. Use an rgba() form to specify opacity.') + label(for='edit-textStrokeColor') Stroke Color + input#edit-textStrokeColor(option='textStrokeColor', format='textStrokeColor', optiontype='label') + .form-group(annotation-types='all') + label(for='edit-textStrokeWidth') Stroke Width + input#edit-textStrokeWidth(option='textStrokeWidth', format='numberOrBlank', optiontype='label') + //- positioning .form-group(annotation-types='all', title='Horizontal alignment') label(for='edit-textAlign') Horizontal Align. select#edit-textAlign(option='textAlign', format='text', optiontype='label') @@ -150,6 +158,21 @@ block append mainContent .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') + //- shadow options + .form-group(annotation-types='all') + label(for='edit-shadowColor') Shadow Color + input#edit-shadowColor(option='shadowColor', format='shadowColor', optiontype='label') + .form-group(annotation-types='all') + label(for='edit-shadowBlur') Shadow Blur + input#edit-shadowBlur(option='shadowBlur', format='numberOrBlank', optiontype='label') + .form-group(annotation-types='all', title='This is the x, y shadowOffset of the shadow in pixels from its default position before rotation. Example: "5, -4"') + label(for='edit-shadowOffset') Shadow Offset + input#edit-shadowOffset(option='shadowOffset', format='coordinate2', optiontype='label') + .form-group(annotation-types='all', title='Enable to rotate the shadow absed on the label\'s rotation.') + label(for='edit-shadowRotate') Rotate Shadow + select#edit-shadowRotate(option='shadowRotate', format='boolean', optiontype='label') + option(value='false') No + option(value='true') Yes .form-group #edit-validation-error .modal-footer diff --git a/examples/annotations/main.js b/examples/annotations/main.js index a5d44483dd..91cd8ae9a2 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -393,9 +393,6 @@ $(function () { break; } switch (key) { - case 'offset': - //DWM:: - break; case 'textScaled': if (['true', 'on', 'yes'].indexOf(value.trim().toLowerCase()) >= 0) { value = map.zoom(); diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 81de6ae59e..6694674af4 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -88,8 +88,9 @@ var annotationLayer = function (args) { dataType: ['visible', 'rotateWithMap', 'scaleWithMap'].indexOf(key) >= 0 ? 'boolean' : ( ['scale'].indexOf(key) >= 0 ? 'booleanOrNumber' : ( ['rotation'].indexOf(key) >= 0 ? 'angle' : ( - ['offset'].indexOf(key) >= 0 ? 'coordinate2' : - 'text'))), + ['offset', 'shadowOffset'].indexOf(key) >= 0 ? 'coordinate2' : ( + ['shadowBlur, strokeWidth'].indexOf(key) >= 0 ? 'numberOrBlank' : + 'text')))), keys: [ key, 'label' + key.charAt(0).toUpperCase() + key.slice(1), @@ -685,6 +686,15 @@ var annotationLayer = function (args) { return; } break; + case 'numberOrBlank': + if (value === '') { + break; + } + value = +value; + if (!util.isNonNullFinite(value)) { + return; + } + break; case 'opacity': if (value === undefined || value === null || value === '') { return; diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js index 6844bcd909..1e6f5e3843 100644 --- a/src/canvas/textFeature.js +++ b/src/canvas/textFeature.js @@ -3,6 +3,7 @@ var registerFeature = require('../registry').registerFeature; var textFeature = require('../textFeature'); var util = require('../util'); var mat3 = require('gl-mat3'); +var vec3 = require('gl-vec3'); /** * Create a new instance of class canvas.textFeature. @@ -92,7 +93,7 @@ var canvas_textFeature = function (arg) { textFunc = m_this.style.get('text'), mapRotation = map.rotation(), mapZoom = map.zoom(), - fontFromSubValues, text, pos, val, + fontFromSubValues, text, pos, visible, color, blur, stroke, width, rotation, rotateWithMap, scale, offset, transform, lastTransform = util.mat3AsArray(); @@ -105,17 +106,18 @@ var canvas_textFeature = function (arg) { /* Clear the canvas property buffer */ m_this._canvasProperty(); data.forEach(function (d, i) { - val = m_this.style.get('visible')(d, i); - if (!val && val !== undefined) { + visible = m_this.style.get('visible')(d, i); + if (!visible && visible !== undefined) { return; } - val = util.convertColorAndOpacity( + color = util.convertColorAndOpacity( m_this.style.get('color')(d, i), m_this.style.get('textOpacity')(d, i)); - if (val.a === 0) { + stroke = util.convertColorAndOpacity( + m_this.style.get('textStrokeColor')(d, i), m_this.style.get('textOpacity')(d, i), {r: 0, g: 0, b: 0, a: 0}); + if (color.a === 0 && stroke.a === 0) { return; } - m_this._canvasProperty(context2d, 'globalAlpha', val.a); - m_this._canvasProperty(context2d, 'fillStyle', util.convertColorToHex(val)); + m_this._canvasProperty(context2d, 'fillStyle', util.convertColorToRGBA(color)); // 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. @@ -154,17 +156,37 @@ var canvas_textFeature = function (arg) { context2d.setTransform(transform[0], transform[1], transform[3], transform[4], transform[6], transform[7]); mat3.copy(lastTransform, transform); } - - // TODO: fetch and use other properties: - // 'shadowColor', 'shadowOffset', 'shadowBlur', 'shadowRotate', - // 'shadowOpacity' - /* - ctx.shadowColor = "black"; - ctx.shadowOffsetX = 5; - ctx.shadowOffsetY = 5; - ctx.shadowBlur = 7; - */ - + /* shadow */ + color = util.convertColorAndOpacity( + m_this.style.get('shadowColor')(d, i), undefined, {r: 0, g: 0, b: 0, a: 0}); + if (color.a) { + offset = m_this.style.get('shadowOffset')(d, i); + blur = m_this.style.get('shadowBlur')(d, i); + } + if (color.a && ((offset && (offset.x || offset.y)) || blur)) { + m_this._canvasProperty(context2d, 'shadowColor', util.convertColorToRGBA(color)); + if (offset && (rotation || rotateWithMap) && m_this.style.get('shadowRotate')(d, i)) { + transform = [+offset.x, +offset.y, 0]; + vec3.rotateZ(transform, transform, [0, 0, 0], + rotation + (rotateWithMap ? mapRotation : 0)); + offset = {x: transform[0], y: transform[1]}; + } + m_this._canvasProperty(context2d, 'shadowOffsetX', offset && offset.x ? +offset.x : 0); + m_this._canvasProperty(context2d, 'shadowOffsetY', offset && offset.y ? +offset.y : 0); + m_this._canvasProperty(context2d, 'shadowBlur', blur || 0); + } else { + m_this._canvasProperty(context2d, 'shadowColor', 'rgba(0,0,0,0)'); + } + /* draw the text */ + if (stroke.a) { + width = m_this.style.get('textStrokeWidth')(d, i); + if (isFinite(width) && width > 0) { + m_this._canvasProperty(context2d, 'strokeStyle', util.convertColorToRGBA(stroke)); + m_this._canvasProperty(context2d, 'lineWidth', width); + context2d.strokeText(text, pos.x, pos.y); + m_this._canvasProperty(context2d, 'shadowColor', 'rgba(0,0,0,0)'); + } + } context2d.fillText(text, pos.x, pos.y); }); m_this._canvasProperty(context2d, 'globalAlpha', 1); diff --git a/src/textFeature.js b/src/textFeature.js index ce810324aa..30257ebe94 100644 --- a/src/textFeature.js +++ b/src/textFeature.js @@ -64,6 +64,10 @@ var feature = require('./feature'); * @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). + * @property {geo.geoColor|function} [style.textStrokeColor='transparent'] Text + * stroke color. May include opacity. + * @property {geo.geoColor|function} [style.textStrokeWidth=0] Text stroke + * width in pixels. */ /** @@ -181,7 +185,7 @@ textFeature.usedStyles = [ 'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline', 'direction', 'color', 'textOpacity', 'rotation', 'rotateWithMap', 'textScaled', 'offset', 'shadowColor', 'shadowOpacity', 'shadowOffset', - 'shadowBlur', 'shadowRotate' + 'shadowBlur', 'shadowRotate', 'textStrokeColor', 'textStrokeWidth' ]; /** diff --git a/src/util/index.js b/src/util/index.js index 65606c8bf9..891b0b0b7d 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -301,9 +301,9 @@ var util = module.exports = { r: isFinite(color.r) && color.r >= 0 ? (color.r <= 1 ? +color.r : 1) : 0, g: isFinite(color.g) && color.g >= 0 ? (color.g <= 1 ? +color.g : 1) : 0, b: isFinite(color.b) && color.b >= 0 ? (color.b <= 1 ? +color.b : 1) : 0, - a: isFinite(color.a) && color.a >= 0 ? (color.a <= 1 ? +color.a : 1) : 1 + a: util.isNonNullFinite(color.a) && color.a >= 0 && color.a < 1 ? +color.a : 1 }; - if (isFinite(opacity) && opacity < 1) { + if (util.isNonNullFinite(opacity) && opacity < 1) { color.a = opacity <= 0 ? 0 : color.a * opacity; } return color; From 83feaab65cc37d7b0f8bbc6fbe2c0e4b4ce28fe7 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 14 Jul 2017 15:51:50 -0400 Subject: [PATCH 07/11] Add tests for the new utility code. --- src/util/index.js | 6 +++--- tests/cases/colors.js | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/util/index.js b/src/util/index.js index 891b0b0b7d..eaf51ac4a1 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -227,7 +227,7 @@ var util = module.exports = { * @memberof geo.util */ convertColor: function (color) { - if (color === undefined || (color.r !== undefined && + if (color === undefined || color === null || (color.r !== undefined && color.g !== undefined && color.b !== undefined)) { return color; } @@ -345,11 +345,11 @@ var util = module.exports = { if (!rgb) { rgb = {r: 0, g: 0, b: 0}; } - if (rgb.a === undefined) { + if (!util.isNonNullFinite(rgb.a) || rgb.a > 1) { rgb.a = 1; } return 'rgba(' + Math.round(rgb.r * 255) + ', ' + Math.round(rgb.g * 255) + - ', ' + Math.round(rgb.b * 255) + ', ' + rgb.a + ')'; + ', ' + Math.round(rgb.b * 255) + ', ' + +((+rgb.a).toFixed(5)) + ')'; }, /** diff --git a/tests/cases/colors.js b/tests/cases/colors.js index b5db942403..5488044a78 100644 --- a/tests/cases/colors.js +++ b/tests/cases/colors.js @@ -217,3 +217,51 @@ describe('geo.util.convertColorToHex', function () { }); }); }); + +describe('geo.util.convertColorToRGBA', function () { + 'use strict'; + + var geo = require('../test-utils').geo; + /* The first entry is the expected result, the second is the input */ + var rgbaTests = [ + ['rgba(0, 0, 0, 1)', null], + ['rgba(0, 0, 0, 1)', 'black'], + ['rgba(0, 0, 0, 1)', '#000'], + ['rgba(0, 0, 0, 1)', {r: 0, g: 0, b: 0}], + ['rgba(0, 0, 0, 0.50196)', '#00000080'], + ['rgba(100, 120, 140, 0.5)', {r: 0.393, g: 0.4706, b: 0.548, a: 0.5}], + ['rgba(100, 120, 140, 1)', {r: 0.393, g: 0.4706, b: 0.548, a: 2}], + ['rgba(100, 120, 140, 1)', {r: 0.393, g: 0.4706, b: 0.548}], + ['rgba(100, 120, 140, 1)', {r: 0.393, g: 0.4706, b: 0.548, a: 'bad'}] + ]; + + $.each(rgbaTests, function (idx, record) { + it('test ' + idx + ' - ' + record[0], function () { + expect(geo.util.convertColorToRGBA(record[1])).toEqual(record[0]); + }); + }); +}); + +describe('geo.util.convertColorAndOpacity', function () { + 'use strict'; + + var geo = require('../test-utils').geo; + /* The first entry is the expected result, the second are the input arguments + */ + var candoTests = [ + [{r: 0, g: 0, b: 0, a: 1}, []], + [{r: 0, g: 0, b: 0, a: 0}, [undefined, undefined, 'transparent']], + [{r: 0, g: 0, b: 0, a: 1}, [undefined, undefined, 'no such color']], + [{r: 0, g: 0, b: 0, a: 0.5}, [undefined, undefined, {r: 0, g: 0, b: 0, a: 0.5}]], + [{r: 1, g: 1, b: 1, a: 1}, ['white', undefined, {r: 0, g: 0, b: 0, a: 0.5}]], + [{r: 0, g: 0, b: 0, a: 0.2}, [undefined, 0.4, {r: 0, g: 0, b: 0, a: 0.5}]], + [{r: 1, g: 1, b: 1, a: 0.4}, ['white', 0.4, {r: 0, g: 0, b: 0, a: 0.5}]], + [{r: 1, g: 1, b: 1, a: 0.4}, ['white', 0.4]] + ]; + + $.each(candoTests, function (idx, record) { + it('test ' + idx + ' - ' + record[0], function () { + expect(geo.util.convertColorAndOpacity.apply(geo.util, record[1])).toEqual(record[0]); + }); + }); +}); From b95616c7e81a648a6f6690e85f92c9874e8c8021 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 17 Jul 2017 10:24:07 -0400 Subject: [PATCH 08/11] Add tests for textFeature. Remove the direction property -- it isn't supported by Firefox and only obscurely supported by Chrome. --- src/canvas/textFeature.js | 11 +- src/textFeature.js | 15 +-- tests/cases/textFeature.js | 210 +++++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 tests/cases/textFeature.js diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js index 1e6f5e3843..42dee405f6 100644 --- a/src/canvas/textFeature.js +++ b/src/canvas/textFeature.js @@ -77,12 +77,21 @@ var canvas_textFeature = function (arg) { (parts[3] || '') + ' ' + (parts[4] || '') + ' ' + (parts[5] || '') + (parts[6] ? '/' + parts[6] : '') + ' ' + parts[7]; + font = font.trim().replace(/\s\s+/g, ' '); } return font; }; /** * Render the data on the canvas. + * + * This does not currently support multiline text or word wrapping, since + * canvas doesn't implement that directly. To support these, each text item + * would need to be split on line breaks, and have the width of the text + * calculated with context2d.measureText to determine word wrapping. This + * would also need to calculate the effective line height from the font + * specification. + * * @protected * @param {CanvasRenderingContext2D} context2d The canvas context to draw in. * @param {geo.map} map The parent map object. @@ -126,7 +135,6 @@ 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; @@ -193,7 +201,6 @@ var canvas_textFeature = function (arg) { context2d.setTransform(1, 0, 0, 1, 0, 0); }; - this._init(arg); return this; }; diff --git a/src/textFeature.js b/src/textFeature.js index 30257ebe94..70a99f1e52 100644 --- a/src/textFeature.js +++ b/src/textFeature.js @@ -35,8 +35,6 @@ var feature = require('./feature'); * @property {string|function} [style.textBaseline='middle'] The vertical text * alignment. One of `top`, `hanging`, `middle`, `alphabetic`, * `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. May * include opacity. * @property {number|function} [style.textOpacity=1] The opacity of the text. @@ -54,9 +52,6 @@ var feature = require('./feature'); * default position for the text. This is applied before rotation. * @property {geo.geoColor|function} [style.shadowColor='black'] Text shadow * color. May include opacity. - * @property {number|function} [style.shadowOpacity=1] The opacity of the - * shadow. If the color includes opacity, this is combined with that - * value. * @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 @@ -149,7 +144,6 @@ var textFeature = function (arg) { font: 'bold 16px sans-serif', textAlign: 'center', textBaseline: 'middle', - direction: 'inherit', color: { r: 0, g: 0, b: 0 }, rotation: 0, /* in radians */ rotateWithMap: false, @@ -177,15 +171,16 @@ var textFeature = function (arg) { m_this.dataTime().modified(); }; + this._init(arg); return m_this; }; textFeature.usedStyles = [ 'visible', 'font', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', - 'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline', - 'direction', 'color', 'textOpacity', 'rotation', 'rotateWithMap', - 'textScaled', 'offset', 'shadowColor', 'shadowOpacity', 'shadowOffset', - 'shadowBlur', 'shadowRotate', 'textStrokeColor', 'textStrokeWidth' + 'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline', 'color', + 'textOpacity', 'rotation', 'rotateWithMap', 'textScaled', 'offset', + 'shadowColor', 'shadowOffset', 'shadowBlur', 'shadowRotate', + 'textStrokeColor', 'textStrokeWidth' ]; /** diff --git a/tests/cases/textFeature.js b/tests/cases/textFeature.js new file mode 100644 index 0000000000..69f7fe9a11 --- /dev/null +++ b/tests/cases/textFeature.js @@ -0,0 +1,210 @@ +// Test geo.textFeature and geo.canvas.textFeature + +var $ = require('jquery'); +var mockAnimationFrame = require('../test-utils').mockAnimationFrame; +var stepAnimationFrame = require('../test-utils').stepAnimationFrame; +var unmockAnimationFrame = require('../test-utils').unmockAnimationFrame; +var geo = require('../test-utils').geo; +var logCanvas2D = require('../test-utils').logCanvas2D; + +describe('geo.textFeature', function () { + 'use strict'; + + var testText = [ + { + text: 'First - Basic', + x: 20, + y: 10 + }, { + text: 'Second - invisible', + x: 20, + y: 11, + visible: false + }, { + text: 'Third - font', + x: 20, + y: 12, + font: '40px serif' + }, { + text: 'Fourth - font by sub-properties', + x: 20, + y: 13, + fontSize: '40px', + fontFamily: 'serif', + fontStyle: 'italic', + fontVariant: 'small-caps', + fontWeight: 'bold', + fontStretch: 'condensed', + lineHeight: '60px' + }, { + text: 'Fifth - clear', + x: 20, + y: 14, + color: 'rgba(120, 120, 120, 0)' + }, { + text: 'Sixth - color and opacity', + x: 20, + y: 15, + color: 'pink', + textOpacity: 0.8 + }, { + text: 'Seventh - rotation', + x: 20, + y: 16, + rotation: 6 * Math.PI / 180, + rotateWithMap: true + }, { + text: 'Eighth - scale', + x: 20, + y: 17, + textScaled: 8 + }, { + text: 'Ninth - offset', + x: 20, + y: 18, + offset: {x: 10, y: 14} + }, { + text: 'Tenth - shadow', + x: 20, + y: 19, + shadowColor: '#00F8', + shadowOffset: {x: 2, y: -3}, + shadowBlur: 3, + shadowRotate: true, + rotation: Math.PI / 2 + }, { + text: 'Eleventh - stroke', + x: 20, + y: 20, + color: 'transparent', + textStrokeColor: 'black', + textStrokeWidth: 1 + }, { + text: 'Twelfth - invalid main font property', + x: 20, + y: 21, + font: 'not a css font string', + lineHeight: '60px' + }, { + text: 'Thirteenth - \u0646\u0635 \u0628\u0633\u064a\u0637 - mixed direction and unicode - \ud83d\ude03', + x: 20, + y: 22 + }, { + text: 'Fourteenth - stroke and fill', + x: 20, + y: 23, + color: 'yellow', + textStrokeColor: 'black', + textStrokeWidth: 4 + } + ]; + + function create_map(opts) { + var node = $('
').css({width: '640px', height: '360px'}); + $('#map').remove(); + $('body').append(node); + opts = $.extend({}, opts); + opts.node = node; + return geo.map(opts); + } + + describe('create', function () { + it('create function', function () { + var map, layer, text; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'canvas'}); + text = geo.textFeature.create(layer); + expect(text instanceof geo.textFeature).toBe(true); + }); + }); + + describe('Check private class mathods', function () { + var map, layer, text; + it('_init', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + text = geo.textFeature({layer: layer}); + expect(text.style('font')).toBe('bold 16px sans-serif'); + text._init({style: {font: 'serif'}}); + expect(text.style('font')).toBe('serif'); + text._init({position: 'a', text: 'b'}); + expect(text.position()).toBe('a'); + expect(text.text()).toBe('b'); + text._init({style: {position: 'c', text: 'd'}}); + expect(text.position()).toBe('c'); + expect(text.text()).toBe('d'); + }); + }); + + describe('Check class accessors', function () { + var map, layer, text; + var pos = [[[0, 0], [10, 5], [5, 10]]]; + it('position', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + text = geo.textFeature({layer: layer}); + expect(text.position()('a')).toBe('a'); + text.position(pos); + expect(text.position()).toEqual(pos); + text.position(function () { return 'b'; }); + expect(text.position()('a')).toEqual('b'); + + text = geo.textFeature({layer: layer, position: pos}); + expect(text.position()).toEqual(pos); + }); + + it('text', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + text = geo.textFeature({layer: layer}); + expect(text.text()({text: 'a'})).toBe('a'); + text.text(pos); + expect(text.text()).toEqual(pos); + text.text(function () { return 'b'; }); + expect(text.text()('a')).toEqual('b'); + + text = geo.textFeature({layer: layer, text: pos}); + expect(text.text()).toEqual(pos); + }); + }); + + /* This is a basic integration test of geo.canvas.textFeature. */ + describe('geo.canvas.textFeature', function () { + var map, layer, text, counts, style = {}; + it('basic usage', function () { + mockAnimationFrame(); + logCanvas2D(); + map = create_map(); + layer = map.createLayer('feature', {renderer: 'canvas'}); + geo.textFeature.usedStyles.forEach(function (key) { + style[key] = function (d, i) { + return d[key]; + }; + }); + text = layer.createFeature('text', {style: style}).data(testText); + text.draw(); + stepAnimationFrame(); + counts = $.extend({}, window._canvasLog.counts); + expect(counts.fillText).not.toBeLessThan(12); + expect(counts.strokeText).not.toBeLessThan(2); + map.rotation(5 * Math.PI / 180); + stepAnimationFrame(); + counts = $.extend({}, window._canvasLog.counts); + expect(counts.fillText).not.toBeLessThan(24); + expect(counts.strokeText).not.toBeLessThan(4); + unmockAnimationFrame(); + }); + it('getFontFromStyles', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: 'canvas'}); + geo.textFeature.usedStyles.forEach(function (key) { + style[key] = function (d, i) { + return d[key]; + }; + }); + text = layer.createFeature('text', {style: style}).data(testText); + expect(text.getFontFromStyles(true, text.data()[3], 3)).toBe('italic small-caps bold condensed 40px/60px serif'); + expect(text.getFontFromStyles(true, text.data()[11], 11)).toBe('bold 16px/60px sans-serif'); + }); + }); +}); From a22213c075915364cb1e8aace18f7f7289129736 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 17 Jul 2017 13:07:16 -0400 Subject: [PATCH 09/11] Add tests for annotation labels. --- src/annotationLayer.js | 26 +++++----- src/util/index.js | 4 +- tests/cases/annotation.js | 78 +++++++++++++++++++++++++++- tests/cases/annotationLayer.js | 93 +++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 18 deletions(-) diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 6694674af4..7a9a44d714 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -640,35 +640,35 @@ var annotationLayer = function (args) { 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) { + if ((!value && value !== 0 && value !== '') || ['true', 'false', 'off', 'on', 'no', 'yes'].indexOf(('' + value).toLowerCase()) >= 0) { value = !!value && ['false', 'no', 'off'].indexOf(('' + value).toLowerCase()) < 0; } else { - value = +value; - if (!isFinite(value)) { + if (!util.isNonNullFinite(value)) { return; } + value = +value; } break; case 'coordinate2': if (value === '') { break; } - if (value && isFinite(value.x) && isFinite(value.y)) { + if (value && util.isNonNullFinite(value.x) && util.isNonNullFinite(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)) { + if (value && util.isNonNullFinite(value.x) && util.isNonNullFinite(value.y)) { value.x = +value.x; value.y = +value.y; break; } - if (Array.isArray(value) && isFinite(value[0]) && isFinite(value[1])) { + if (Array.isArray(value) && util.isNonNullFinite(value[0]) && util.isNonNullFinite(value[1])) { value = {x: +value[0], y: +value[1]}; break; } - parts = /^\s*([-.0-9eE]+)\s*,?\s*([-.0-9eE]+)\s*$/.exec('' + value); + parts = /^\s*([-.0-9eE]+)(?:\s+|\s*,)\s*([-.0-9eE]+)\s*$/.exec('' + value); if (!parts || !isFinite(parts[1]) || !isFinite(parts[2])) { return; } @@ -681,19 +681,19 @@ var annotationLayer = function (args) { } break; case 'number': - value = +value; if (!util.isNonNullFinite(value)) { return; } + value = +value; break; case 'numberOrBlank': if (value === '') { break; } - value = +value; if (!util.isNonNullFinite(value)) { return; } + value = +value; break; case 'opacity': if (value === undefined || value === null || value === '') { @@ -835,12 +835,12 @@ var annotationLayer = function (args) { */ this._updateLabels = function (labels) { if (!labels || !labels.length) { - m_this.removeLabelFeature(); + m_this._removeLabelFeature(); return m_this; } if (!m_labelFeature) { var renderer = registry.rendererForFeatures(['text']); - if (renderer !== m_this.renderer()) { + if (renderer !== m_this.renderer().api()) { m_labelLayer = registry.createLayer('feature', m_this.map(), {renderer: renderer}); m_this.addChild(m_labelLayer); m_labelLayer._update(); @@ -888,7 +888,7 @@ var annotationLayer = function (args) { * * @returns {this} The current layer. */ - this.removeLabelFeature = function () { + this._removeLabelFeature = function () { if (m_labelLayer) { m_labelLayer._exit(); m_this.removeChild(m_labelLayer); @@ -945,7 +945,7 @@ var annotationLayer = function (args) { * @returns {this} The current layer. */ this._exit = function () { - m_this.removeLabelFeature(); + m_this._removeLabelFeature(); // Call super class exit s_exit.call(m_this); m_annotations = []; diff --git a/src/util/index.js b/src/util/index.js index eaf51ac4a1..a194ca530c 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -179,13 +179,13 @@ var util = module.exports = { /** * Check if a value coerces to a number that is finite, not a NaN, and not - * `null` or `false`. + * `null`, `false`, or the empty string. * * @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 isFinite(val) && val !== null && val !== false && val !== ''; }, /** diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index cfb82f1376..de274f99b2 100644 --- a/tests/cases/annotation.js +++ b/tests/cases/annotation.js @@ -35,6 +35,9 @@ describe('geo.annotation', function () { expect(ann.state()).toBe(geo.annotation.state.done); expect(ann.id()).toBeGreaterThan(0); expect(ann.name()).toBe('Test ' + ann.id()); + expect(ann.label()).toBe('Test ' + ann.id()); + expect(ann.label(undefined, true)).toBe(null); + expect(ann.description()).toBe(undefined); expect(ann.layer()).toBe(undefined); expect(ann.features()).toEqual([]); expect(ann.coordinates()).toEqual([]); @@ -72,6 +75,26 @@ describe('geo.annotation', function () { expect(ann.name('')).toBe(ann); expect(ann.name()).toBe('New Name'); }); + it('label', function () { + var ann = geo.annotation.annotation('test'); + expect(ann.label()).toBe('Test ' + ann.id()); + expect(ann.label(undefined, true)).toBe(null); + expect(ann.label('New Label')).toBe(ann); + expect(ann.label()).toBe('New Label'); + expect(ann.label('')).toBe(ann); + expect(ann.label()).toBe(''); + expect(ann.label(null)).toBe(ann); + expect(ann.label()).toBe('Test ' + ann.id()); + expect(ann.label(undefined, true)).toBe(null); + }); + it('description', function () { + var ann = geo.annotation.annotation('test'); + expect(ann.description()).toBe(undefined); + expect(ann.description('New Description')).toBe(ann); + expect(ann.description()).toBe('New Description'); + expect(ann.description('')).toBe(ann); + expect(ann.description()).toBe(''); + }); it('layer', function () { var ann = geo.annotation.annotation('test'); expect(ann.layer()).toBe(undefined); @@ -103,10 +126,16 @@ describe('geo.annotation', function () { expect(ann.options().testopt).toBe(40); expect(ann.options({testopt: 30})).toBe(ann); expect(ann.options().testopt).toBe(30); - /* name and coordinates are handled specially */ + /* name, label, description, and coordinates are handled specially */ ann.options('name', 'newname'); expect(ann.options().name).toBe(undefined); expect(ann.name()).toBe('newname'); + ann.options('label', 'newlabel'); + expect(ann.options().label).toBe(undefined); + expect(ann.label()).toBe('newlabel'); + ann.options('description', 'newdescription'); + expect(ann.options().description).toBe(undefined); + expect(ann.description()).toBe('newdescription'); var coord = null, testval = [[1, 2], [3, 4]]; ann._coordinates = function (arg) { if (arg !== undefined) { @@ -168,6 +197,9 @@ describe('geo.annotation', function () { var ann = geo.annotation.annotation('test', { layer: layer, style: {fillColor: 'red'}, + labelStyle: {color: 'blue', textStrokeColor: 'rgba(0, 255, 0, 0.5)'}, + label: 'testLabel', + description: 'testDescription', name: 'testAnnotation' }); expect(ann.geojson()).toBe(undefined); @@ -188,6 +220,11 @@ describe('geo.annotation', function () { expect(geojson.geometry.coordinates[1]).toBeCloseTo(42.849775); expect(geojson.properties.name).toBe('testAnnotation'); expect(geojson.properties.fillColor).toBe('#ff0000'); + expect(geojson.properties.description).toBe('testDescription'); + expect(geojson.properties.label).toBe('testLabel'); + expect(geojson.properties.labelColor).toBe('#0000ff'); + expect(geojson.properties.labelTextStrokeColor).toBe('#00ff0080'); + expect(geojson.properties.showLabel).toBe(undefined); expect(geojson.crs).toBe(undefined); geojson = ann.geojson('EPSG:3857'); expect(geojson.geometry.coordinates[1]).toBeCloseTo(5289134.103576); @@ -196,6 +233,45 @@ describe('geo.annotation', function () { expect(geojson.crs.properties.name).toBe('EPSG:3857'); geojson = ann.geojson(undefined, true); expect(geojson.crs.properties.name).toBe('EPSG:4326'); + ann.options('showLabel', false); + geojson = ann.geojson(undefined, true); + expect(geojson.properties.showLabel).toBe(false); + }); + it('_labelPosition', function () { + var ann = geo.annotation.annotation('test', { + layer: layer, + name: 'testAnnotation' + }); + ann._coordinates = function () { + }; + expect(ann._labelPosition()).toBe(undefined); + ann._coordinates = function () { + return [{x: 1, y: 2}]; + }; + expect(ann._labelPosition()).toEqual({x: 1, y: 2}); + ann._coordinates = function () { + return [{x: 1, y: 2}, {x: 3, y: 5}, {x: 8, y: 11}]; + }; + expect(ann._labelPosition()).toEqual({x: 4, y: 6}); + }); + it('labelRecord', function () { + var ann = geo.annotation.annotation('test', { + layer: layer, + name: 'testAnnotation' + }); + ann._coordinates = function () { + }; + expect(ann.labelRecord()).toBe(undefined); + ann._coordinates = function () { + return [{x: 1, y: 2}]; + }; + expect(ann.labelRecord().text).toBe('testAnnotation'); + expect(ann.labelRecord().position).toEqual({x: 1, y: 2}); + expect(ann.labelRecord().style).toBe(undefined); + ann.options('labelStyle', {opacity: 0.8}); + expect(ann.labelRecord().style.opacity).toBe(0.8); + ann.options('showLabel', false); + expect(ann.labelRecord()).toBe(undefined); }); }); diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index cfa09a530a..ec67f67fa3 100644 --- a/tests/cases/annotationLayer.js +++ b/tests/cases/annotationLayer.js @@ -238,33 +238,91 @@ describe('geo.annotationLayer', function () { it('validateAttribute', function () { expect(layer.validateAttribute(undefined, 'other')).toBe(undefined); expect(layer.validateAttribute(null, 'other')).toBe(undefined); - expect(layer.validateAttribute('value', 'other')).toBe('value'); + + expect(layer.validateAttribute(0.5, 'angle')).toBe(0.5); + expect(layer.validateAttribute('0.5', 'angle')).toBe(0.5); + expect(layer.validateAttribute('', 'angle')).toBe(''); + expect(layer.validateAttribute(-1, 'angle')).toBe(-1); + expect(layer.validateAttribute('value', 'angle')).toBe(undefined); + expect(layer.validateAttribute('90deg ', 'angle')).toBeCloseTo(Math.PI / 2); + expect(layer.validateAttribute(' 100grad', 'angle')).toBeCloseTo(Math.PI / 2); + expect(layer.validateAttribute('0.25 turn', 'angle')).toBeCloseTo(Math.PI / 2); + expect(layer.validateAttribute('1.57079rad', 'angle')).toBeCloseTo(Math.PI / 2); + expect(layer.validateAttribute('1.57079', 'angle')).toBeCloseTo(Math.PI / 2); + expect(layer.validateAttribute('value', 'boolean')).toBe(true); expect(layer.validateAttribute(true, 'boolean')).toBe(true); expect(layer.validateAttribute(0, 'boolean')).toBe(false); expect(layer.validateAttribute('false', 'boolean')).toBe(false); + expect(layer.validateAttribute(true, 'booleanOrNumber')).toBe(true); + expect(layer.validateAttribute('false', 'booleanOrNumber')).toBe(false); + expect(layer.validateAttribute(0.5, 'booleanOrNumber')).toBe(0.5); + expect(layer.validateAttribute('0.5', 'booleanOrNumber')).toBe(0.5); + expect(layer.validateAttribute(0, 'booleanOrNumber')).toBe(0); + expect(layer.validateAttribute(-1, 'booleanOrNumber')).toBe(-1); + expect(layer.validateAttribute(1.2, 'booleanOrNumber')).toBe(1.2); + expect(layer.validateAttribute('', 'booleanOrNumber')).toBe(undefined); + expect(layer.validateAttribute('value', 'booleanOrNumber')).toBe(undefined); + expect(layer.validateAttribute('not a color', 'color')).toBe(undefined); expect(layer.validateAttribute('yellow', 'color')).toEqual({r: 1, g: 1, b: 0}); expect(layer.validateAttribute('#ffff00', 'color')).toEqual({r: 1, g: 1, b: 0}); expect(layer.validateAttribute('#ff0', 'color')).toEqual({r: 1, g: 1, b: 0}); expect(layer.validateAttribute({r: 1, g: 1, b: 0}, 'color')).toEqual({r: 1, g: 1, b: 0}); + expect(layer.validateAttribute('', 'coordinate2')).toBe(''); + expect(layer.validateAttribute(0.5, 'coordinate2')).toBe(undefined); + expect(layer.validateAttribute({x: 1, y: 2}, 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute({x: 1, y: null}, 'coordinate2')).toEqual(undefined); + expect(layer.validateAttribute('{"x": 1, "y": 2}', 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute('{"x": 1, "y": "value"}', 'coordinate2')).toEqual(undefined); + expect(layer.validateAttribute([1, 2], 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute([1, 2, 3], 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute('1, 2', 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute('1 2', 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute(' 1 , 2 ', 'coordinate2')).toEqual({x: 1, y: 2}); + expect(layer.validateAttribute('1 value', 'coordinate2')).toEqual(undefined); + expect(layer.validateAttribute(0.5, 'opacity')).toBe(0.5); expect(layer.validateAttribute('0.5', 'opacity')).toBe(0.5); expect(layer.validateAttribute(0, 'opacity')).toBe(0); expect(layer.validateAttribute(-1, 'opacity')).toBe(undefined); expect(layer.validateAttribute(1, 'opacity')).toBe(1); expect(layer.validateAttribute(1.2, 'opacity')).toBe(undefined); + expect(layer.validateAttribute('', 'opacity')).toBe(undefined); expect(layer.validateAttribute('value', 'opacity')).toBe(undefined); + expect(layer.validateAttribute(0.5, 'number')).toBe(0.5); + expect(layer.validateAttribute('0.5', 'number')).toBe(0.5); + expect(layer.validateAttribute(0, 'number')).toBe(0); + expect(layer.validateAttribute(-1, 'number')).toBe(-1); + expect(layer.validateAttribute(1.2, 'number')).toBe(1.2); + expect(layer.validateAttribute('1.2e2', 'number')).toBe(120); + expect(layer.validateAttribute('1.2e-2', 'number')).toBe(0.012); + expect(layer.validateAttribute('', 'number')).toBe(undefined); + expect(layer.validateAttribute('value', 'number')).toBe(undefined); + + expect(layer.validateAttribute(0.5, 'numberOrBlank')).toBe(0.5); + expect(layer.validateAttribute('0.5', 'numberOrBlank')).toBe(0.5); + expect(layer.validateAttribute(0, 'numberOrBlank')).toBe(0); + expect(layer.validateAttribute(-1, 'numberOrBlank')).toBe(-1); + expect(layer.validateAttribute(1.2, 'numberOrBlank')).toBe(1.2); + expect(layer.validateAttribute('1.2e2', 'numberOrBlank')).toBe(120); + expect(layer.validateAttribute('1.2e-2', 'numberOrBlank')).toBe(0.012); + expect(layer.validateAttribute('', 'numberOrBlank')).toBe(''); + expect(layer.validateAttribute('value', 'numberOrBlank')).toBe(undefined); + expect(layer.validateAttribute(0.5, 'positive')).toBe(0.5); expect(layer.validateAttribute('0.5', 'positive')).toBe(0.5); expect(layer.validateAttribute(0, 'positive')).toBe(undefined); expect(layer.validateAttribute(-1, 'positive')).toBe(undefined); expect(layer.validateAttribute(1.2, 'positive')).toBe(1.2); expect(layer.validateAttribute('value', 'positive')).toBe(undefined); + + expect(layer.validateAttribute(0.5, 'text')).toBe('0.5'); + expect(layer.validateAttribute('value', 'text')).toBe('value'); }); }); describe('Private utility functions', function () { @@ -278,7 +336,8 @@ describe('geo.annotationLayer', function () { }); point = geo.annotation.pointAnnotation({ layer: layer, - position: {x: 2, y: 3}}); + position: {x: 2, y: 3}, + labelStyle: {opacity: 0.8}}); rect = geo.annotation.rectangleAnnotation({ layer: layer, corners: [{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}]}); @@ -290,6 +349,36 @@ describe('geo.annotationLayer', function () { layer.addAnnotation(rect2); expect(layer.features.length).toBe(1); }); + it('_updateLabels and _removeLabelFeature', function () { + var numChild, canvasLayer, canvasLine; + var labels = [point.labelRecord()]; + /* calling _updateLabels with no argument or an empty list implicitly + * calls _removeLabelFeature. The call to _updateLabels with a list will + * add it back. */ + layer._updateLabels([]); + numChild = layer.children().length; + layer._updateLabels(labels); + expect(layer.children().length).toBe(numChild + 1); + expect(layer.children()[layer.children().length - 1] instanceof geo.featureLayer); + layer._updateLabels(); // implicitly calls _removeLabelFeature + expect(layer.children().length).toBe(numChild); + // canvas layers shouldn't add a sublayer, but instead a feature + canvasLayer = map.createLayer('annotation', { + renderer: 'canvas' + }); + canvasLine = geo.annotation.lineAnnotation({ + layer: layer, + vertices: [{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}]}); + canvasLayer.addAnnotation(canvasLine); + canvasLayer._updateLabels([]); + numChild = canvasLayer.children().length; + canvasLayer._updateLabels(labels); + expect(layer.children()[layer.children().length - 1] instanceof geo.textFeature); + expect(canvasLayer.children().length).toBe(numChild + 1); + canvasLayer._updateLabels(); + expect(canvasLayer.children().length).toBe(numChild); + map.deleteLayer(canvasLayer); + }); it('_handleMouseClick', function () { layer.removeAllAnnotations(); layer.mode('polygon'); From 5ab01dde2b2af33f2124835ca23351bb612e7654 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 24 Jul 2017 11:43:19 -0400 Subject: [PATCH 10/11] Switch from jade to pug. Add a pug linting test. --- .pug-lintrc | 17 +++ CMakeLists.txt | 6 +- examples/animation/{index.jade => index.pug} | 13 +- .../annotations/{index.jade => index.pug} | 4 +- examples/blog-lines/{index.jade => index.pug} | 85 ++++++------ examples/build.js | 7 +- examples/choropleth/index.jade | 1 - examples/choropleth/index.pug | 1 + .../templates/{index.jade => index.pug} | 17 ++- examples/contour/index.jade | 1 - examples/contour/index.pug | 1 + examples/deepzoom/index.jade | 1 - examples/deepzoom/index.pug | 1 + examples/dynamicData/index.jade | 1 - examples/dynamicData/index.pug | 1 + examples/geoJSON/index.jade | 3 - examples/geoJSON/index.pug | 3 + examples/heatmap/index.jade | 124 ------------------ examples/heatmap/index.pug | 123 +++++++++++++++++ examples/hurricanes/{index.jade => index.pug} | 2 +- examples/{index.jade => index.pug} | 13 +- examples/layerEvents/index.jade | 4 - examples/layerEvents/index.pug | 4 + examples/layers/{index.jade => index.pug} | 2 +- examples/legend/index.jade | 1 - examples/legend/index.pug | 1 + examples/lines/{index.jade => index.pug} | 7 +- examples/osm/index.jade | 1 - examples/osm/index.pug | 1 + examples/picking/index.jade | 1 - examples/picking/index.pug | 1 + examples/pixelmap/index.jade | 1 - examples/pixelmap/index.pug | 1 + examples/points/index.jade | 1 - examples/points/index.pug | 1 + examples/polygons/index.jade | 1 - examples/polygons/index.pug | 1 + examples/quads/index.jade | 1 - examples/quads/index.pug | 1 + .../reprojection/{index.jade => index.pug} | 4 +- examples/sld/{index.jade => index.pug} | 8 +- examples/tiles/{index.jade => index.pug} | 16 +-- .../transitions/{index.jade => index.pug} | 2 +- examples/ui/index.jade | 1 - examples/ui/index.pug | 1 + examples/vectors/index.jade | 1 - examples/vectors/index.pug | 1 + examples/widgets/index.jade | 1 - examples/widgets/index.pug | 1 + examples/wms/index.jade | 1 - examples/wms/index.pug | 1 + package.json | 6 +- webpack-examples.config.js | 6 +- 53 files changed, 261 insertions(+), 245 deletions(-) create mode 100644 .pug-lintrc rename examples/animation/{index.jade => index.pug} (83%) rename examples/annotations/{index.jade => index.pug} (99%) rename examples/blog-lines/{index.jade => index.pug} (92%) delete mode 100644 examples/choropleth/index.jade create mode 100644 examples/choropleth/index.pug rename examples/common/templates/{index.jade => index.pug} (77%) delete mode 100644 examples/contour/index.jade create mode 100644 examples/contour/index.pug delete mode 100644 examples/deepzoom/index.jade create mode 100644 examples/deepzoom/index.pug delete mode 100644 examples/dynamicData/index.jade create mode 100644 examples/dynamicData/index.pug delete mode 100644 examples/geoJSON/index.jade create mode 100644 examples/geoJSON/index.pug delete mode 100644 examples/heatmap/index.jade create mode 100644 examples/heatmap/index.pug rename examples/hurricanes/{index.jade => index.pug} (54%) rename examples/{index.jade => index.pug} (86%) delete mode 100644 examples/layerEvents/index.jade create mode 100644 examples/layerEvents/index.pug rename examples/layers/{index.jade => index.pug} (65%) delete mode 100644 examples/legend/index.jade create mode 100644 examples/legend/index.pug rename examples/lines/{index.jade => index.pug} (97%) delete mode 100644 examples/osm/index.jade create mode 100644 examples/osm/index.pug delete mode 100644 examples/picking/index.jade create mode 100644 examples/picking/index.pug delete mode 100644 examples/pixelmap/index.jade create mode 100644 examples/pixelmap/index.pug delete mode 100644 examples/points/index.jade create mode 100644 examples/points/index.pug delete mode 100644 examples/polygons/index.jade create mode 100644 examples/polygons/index.pug delete mode 100644 examples/quads/index.jade create mode 100644 examples/quads/index.pug rename examples/reprojection/{index.jade => index.pug} (96%) rename examples/sld/{index.jade => index.pug} (92%) rename examples/tiles/{index.jade => index.pug} (90%) rename examples/transitions/{index.jade => index.pug} (87%) delete mode 100644 examples/ui/index.jade create mode 100644 examples/ui/index.pug delete mode 100644 examples/vectors/index.jade create mode 100644 examples/vectors/index.pug delete mode 100644 examples/widgets/index.jade create mode 100644 examples/widgets/index.pug delete mode 100644 examples/wms/index.jade create mode 100644 examples/wms/index.pug diff --git a/.pug-lintrc b/.pug-lintrc new file mode 100644 index 0000000000..63e4ce371d --- /dev/null +++ b/.pug-lintrc @@ -0,0 +1,17 @@ +{ +"disallowAttributeInterpolation": true, +"disallowClassAttributeWithStaticValue": true, +"disallowDuplicateAttributes": true, +"disallowIdAttributeWithStaticValue": true, +"disallowLegacyMixinCall": true, +"disallowMultipleLineBreaks": true, +"disallowSpacesInsideAttributeBrackets": true, +"requireClassLiteralsBeforeAttributes": true, +"requireIdLiteralsBeforeAttributes": true, +"requireLowerCaseTags": true, +"requireSpaceAfterCodeOperator": true, +"requireStrictEqualityOperators": true, +"validateAttributeSeparator": { "separator": ", ", "multiLineSeparator": ",\n " }, +"validateDivTags": true, +"validateTemplateString": true +} diff --git a/CMakeLists.txt b/CMakeLists.txt index a186d60626..09237a4d58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,5 +161,9 @@ if(${ESLINT_TESTS}) WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMMAND "${NPM_EXECUTABLE}" "run" "lint" ) - + add_test( + NAME "puglint" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMAND "${NPM_EXECUTABLE}" "run" "puglint" + ) endif() # ESLINT_TESTS diff --git a/examples/animation/index.jade b/examples/animation/index.pug similarity index 83% rename from examples/animation/index.jade rename to examples/animation/index.pug index da9482c5f8..465b1ceb92 100644 --- a/examples/animation/index.jade +++ b/examples/animation/index.pug @@ -1,17 +1,17 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent - div#controls + #controls .form-group(title="The data set to plot.") label(for="dataset") Data Set select#dataset(param-name="dataset", placeholder="Activity") - option(value="adderall", url="AdderallCities2015.csv" title="9555 points") Adderall - option(value="cities", url="cities.csv" title="30101 points") U.S. Cities - option(value="earthquakes", url="earthquakes.json" title="1.3 million points") Earthquakes + option(value="adderall", url="AdderallCities2015.csv", title="9555 points") Adderall + option(value="cities", url="cities.csv", title="30101 points") U.S. Cities + option(value="earthquakes", url="earthquakes.json", title="1.3 million points") Earthquakes span#points-loaded .form-group(title="Number of points. Leave blank for the entire original data set. If a smaller number, only a subset of points will be shown. If a larger number, some of the data will be duplicated with random offsets.") label(for="points") Number of Points - input#points(type="number" min="1" step="100") + input#points(type="number", min="1", step="100") span#points-shown .form-group.style-list label Styles to animate: @@ -57,4 +57,3 @@ block append mainContent span Avg. Update  span#timing-update 0 span  ms - diff --git a/examples/annotations/index.jade b/examples/annotations/index.pug similarity index 99% rename from examples/annotations/index.jade rename to examples/annotations/index.pug index f022e669cf..e5ea4abff5 100644 --- a/examples/annotations/index.jade +++ b/examples/annotations/index.pug @@ -1,4 +1,4 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent #controls @@ -35,7 +35,7 @@ block append mainContent a.entry-remove(action='remove', title='Delete this annotation') ✖ .form-group textarea#geojson(type='textarea', rows=15, autocomplete='off', - autocorrect='off', autocapitalize='off', spellcheck='false') + autocorrect='off', autocapitalize='off', spellcheck='false') #editdialog.modal.fade .modal-dialog diff --git a/examples/blog-lines/index.jade b/examples/blog-lines/index.pug similarity index 92% rename from examples/blog-lines/index.jade rename to examples/blog-lines/index.pug index af569e3985..f0c2722fd8 100644 --- a/examples/blog-lines/index.jade +++ b/examples/blog-lines/index.pug @@ -1,16 +1,16 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent - div#lines_table + #lines_table table(antialiasing=2) tr th(rowspan=2) Feature th(colspan=3) GeoJS - Current - th(colspan=2) Leaflet + th(colspan=2) Leaflet th(rowspan=2) Mapbox GL th(colspan=2) GeoJS 0.10.5 th GeoJS - Current - tr + tr th WebGL th Canvas 2D th SVG @@ -31,7 +31,7 @@ block append mainContent td.geojs(rowspan=2, renderer='d3', version='0.10.5') td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='basic') - td + td p Lines with default options for each rendering method. tr.option(option='color', strokeColor='yellow,black,red', strokeOpacity=1) th Varying Color @@ -45,7 +45,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='color') - td + td p Lines that vary by color from vertex to vertex, cycling through yellow, black, and red. p On corners, the vertex color is at the far end of the miter. There will probably always be a clear color step along the miter line. tr.option(option='stroke', strokeOpacity='0.5,1') @@ -60,7 +60,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='stroke') - td + td p Lines that vary by opacity from vertex to vertex, cycling between 0.5 and 1. tr.option(option='width', strokeWidth='9,12', link_strokeWidth='36,48') th Varying Width @@ -74,7 +74,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='width') - td + td p Lines that vary by width from vertex to vertex, alternating between 6 and 12 pixels. p On corners, the width is virtually at the far end of the miter. tr.option(option='offset', strokeOffset=-1) @@ -89,7 +89,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='offset') - td + td p Lines can be offset to one side or the other of center. This can very from vertex to vertex, but is only shown offset all the way to the left here. p Offset ranges from -1 (left) to +1 (right) along the direction of the stroke. 0 (default) is centered. tr.option(option='linecap-round', lineCap='round') @@ -104,8 +104,8 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='linecap-round') - td - p End caps can be 'butt' (default), 'round', or 'square'. + td + p End caps can be 'butt' (default), 'round', or 'square'. p In GeoJS's WebGL renderer, the end caps can vary by vertex (not shown). tr.option(option='linecap-square', lineCap='square') th Square Line Cap @@ -119,7 +119,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='linecap-square') - td + td p End caps can be 'butt' (default), 'round', or 'square'. tr.option(option='linejoin-bevel', lineJoin='bevel') th Bevel Line Join @@ -133,8 +133,8 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='linejoin-bevel') - td - p Line joins can be 'miter' (default), 'bevel', 'round', or 'miter-clip'. + td + p Line joins can be 'miter' (default), 'bevel', 'round', or 'miter-clip'. p In GeoJS's WebGL renderer, the joins can vary by vertex (not shown). tr.option(option='linejoin-round', lineJoin='round') th Round Line Join @@ -148,7 +148,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='linejoin-round') - td + td p Line joins can be 'miter' (default), 'bevel', 'round', or 'miter-clip'. tr.option(option='linejoin-miterclip', lineJoin='miter-clip', miterLimit=4) th Miter-clip Line Join @@ -162,7 +162,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='linejoin-miterclip') - td + td p Line joins can be 'miter' (default), 'bevel', 'round', or 'miter-clip'. p The miter-clip line join is part of a proposed path standard. If the join exceeds the miter limit, it is beveled at that distance rather than right at the join. tr.option(option='miterlimit', miterLimit=4) @@ -177,7 +177,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='miterlimit') - td + td p Use a miter limit of 4. Other examples use a miter limit of 10 (except the miter-clip line join). p Leaflet doesn't expose miter limit directly, but if SVG is used, the paths can be modified to change the miter limit. tr.option(option='antialiasing0', antialiasing=0) @@ -192,7 +192,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='antialiasing0') - td + td p Antialiasing affects how the edges of lines are smoothed. A value of 1 or 2 produces smooth results, 0 hard results, and large values generate a blurred outline. tr.option(option='antialiasing1-4', antialiasing=6, link_antialiasing=24) th Antialiasing of 1/2 the line width @@ -206,7 +206,7 @@ block append mainContent td.unsupported(rowspan=2) td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='antialiasing1-4') - td + td p Antialiasing affects how the edges of lines are smoothed. A value of 1 or 2 produces smooth results, 0 hard results, and large values generate a blurred outline. tr.option(option='thin-lines', strokeWidth=0.25, referenceLines='false') th Thin Lines @@ -220,7 +220,7 @@ block append mainContent td.geojs(rowspan=2, renderer='d3', version='0.10.5') td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='thin-lines') - td + td p Comparison of a line width of 0.25 pixels. The reference lines are hidden. tr.option(option='roads', data='roads', lines=10000, referenceLines='false', strokeWidth=2, strokeOpacity=1, x=-73.7593015, y=42.8496799, zoom=13) th 10,000 Line Segments @@ -234,40 +234,39 @@ block append mainContent td.geojs(rowspan=2, renderer='d3', version='0.10.5') td.geojs(rowspan=2, renderer='vgl', debug='true') tr.option(option='roads') - td + td p A modest number of line segments from a real-world sample. - div#lines_list + #lines_list b Compare line drawing between libraries, versions, and rendering methods div label(for='feature') Feature select#feature - div#info - div#main_list + #info + #main_list span - div.title GeoJS - Current - WebGL - div.entry + .title GeoJS - Current - WebGL + .entry span - div.title GeoJS - Current - Canvas - div.entry + .title GeoJS - Current - Canvas + .entry span - div.title GeoJS - Current - SVG - div.entry + .title GeoJS - Current - SVG + .entry span - div.title Leaflet - 1.0.2 - Canvas - div.entry + .title Leaflet - 1.0.2 - Canvas + .entry span - div.title Leaflet - 1.0.2 - SVG - div.entry + .title Leaflet - 1.0.2 - SVG + .entry span - div.title Mapbox GL - 0.28.0 - WebGL - div.entry + .title Mapbox GL - 0.28.0 - WebGL + .entry span - div.title GeoJS - 0.10.5 - WebGL - div.entry + .title GeoJS - 0.10.5 - WebGL + .entry span - div.title GeoJS - 0.10.5 - SVG - div.entry + .title GeoJS - 0.10.5 - SVG + .entry span - div.title GeoJS - Current - Debug WebGL - div.entry - + .title GeoJS - Current - Debug WebGL + .entry diff --git a/examples/build.js b/examples/build.js index 31659e4f4f..e564d1490a 100644 --- a/examples/build.js +++ b/examples/build.js @@ -2,7 +2,7 @@ var path = require('path'); var glob = require('glob').sync; var fs = require('fs-extra'); var docco = require('docco').document; -var jade = require('jade'); +var pug = require('pug'); // generate the examples fs.ensureDirSync('dist/examples'); @@ -42,7 +42,7 @@ var examples = glob('examples/*/example.json') json.docHTML = 'docs/' + path.basename(main).replace(/js$/, 'html'); json.bundle = '../bundle.js'; - var fn = jade.compileFile(path.relative('.', path.resolve(dir, 'index.jade')), {pretty: true}); + var fn = pug.compileFile(path.relative('.', path.resolve(dir, 'index.pug')), {pretty: true}); fs.writeFileSync(path.resolve(output, 'index.html'), fn(json)); return json; }); @@ -65,9 +65,8 @@ var data = { fs.copySync('examples/main.js', 'dist/examples/main.js'); fs.copySync('examples/main.css', 'dist/examples/main.css'); -var fn = jade.compileFile('./examples/index.jade', {pretty: true}); +var fn = pug.compileFile('./examples/index.pug', {pretty: true}); fs.writeFileSync( path.resolve('dist', 'examples', 'index.html'), fn(data) ); - diff --git a/examples/choropleth/index.jade b/examples/choropleth/index.jade deleted file mode 100644 index f0100e3d8d..0000000000 --- a/examples/choropleth/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade \ No newline at end of file diff --git a/examples/choropleth/index.pug b/examples/choropleth/index.pug new file mode 100644 index 0000000000..2111d31ffb --- /dev/null +++ b/examples/choropleth/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug \ No newline at end of file diff --git a/examples/common/templates/index.jade b/examples/common/templates/index.pug similarity index 77% rename from examples/common/templates/index.jade rename to examples/common/templates/index.pug index 427508a647..6e15fd694e 100644 --- a/examples/common/templates/index.jade +++ b/examples/common/templates/index.pug @@ -2,20 +2,20 @@ doctype html html(lang="en") head meta(charset="UTF-8") - link(rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon") + link(rel="shortcut icon", href="data:image/x-icon;,", type="image/x-icon") title= title // include the main bundle - script(type='text/javascript' src='#{bundle}' charset='UTF-8') + script(type='text/javascript', src=bundle, charset='UTF-8') // include example sources block exampleHeader // Include example specific files each css in exampleCss - link(rel="stylesheet" href="#{css}") + link(rel="stylesheet", href=css) each src in exampleJs - if(typeof src == "string") - script(type="text/javascript" src="#{src}") + if(typeof src === "string") + script(type="text/javascript", src=src) else script&attributes(src) @@ -47,11 +47,11 @@ html(lang="en") block showSource if docHTML li - a.gj-show-source-link(href="#{docHTML}", target="gj-docco") + a.gj-show-source-link(href=docHTML, target="gj-docco") | Source if !about.hidden li - a.gj-show-about-link(href="#" data-toggle="modal" data-target="#about-modal") + a.gj-show-about-link(href="#", data-toggle="modal", data-target="#about-modal") | About if ! about.hidden @@ -65,8 +65,7 @@ html(lang="en") p.text-left !{about.text} p.text-right © Kitware, Inc. .modal-footer - button.btn.btn-primary(data-dismiss="modal" data-target="#about-modal") Close - + button.btn.btn-primary(data-dismiss="modal", data-target="#about-modal") Close // Add the default main content element block mainContent diff --git a/examples/contour/index.jade b/examples/contour/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/contour/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/contour/index.pug b/examples/contour/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/contour/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/deepzoom/index.jade b/examples/deepzoom/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/deepzoom/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/deepzoom/index.pug b/examples/deepzoom/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/deepzoom/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/dynamicData/index.jade b/examples/dynamicData/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/dynamicData/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/dynamicData/index.pug b/examples/dynamicData/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/dynamicData/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/geoJSON/index.jade b/examples/geoJSON/index.jade deleted file mode 100644 index 9c81c1e705..0000000000 --- a/examples/geoJSON/index.jade +++ /dev/null @@ -1,3 +0,0 @@ -extends ../common/templates/index.jade - -block append mainContent diff --git a/examples/geoJSON/index.pug b/examples/geoJSON/index.pug new file mode 100644 index 0000000000..b3babd0745 --- /dev/null +++ b/examples/geoJSON/index.pug @@ -0,0 +1,3 @@ +extends ../common/templates/index.pug + +block append mainContent diff --git a/examples/heatmap/index.jade b/examples/heatmap/index.jade deleted file mode 100644 index bf404f0a14..0000000000 --- a/examples/heatmap/index.jade +++ /dev/null @@ -1,124 +0,0 @@ -extends ../common/templates/index.jade - -block append mainContent - div#controls - .form-group(title="The data set to plot.") - label(for="dataset") Data Set - select#dataset(param-name="dataset", placeholder="Activity") - option(value="adderall", url="AdderallCities2015.csv" title="9555 points") Adderall - option(value="cities", url="cities.csv" title="30101 points") U.S. Cities - option(value="earthquakes", url="earthquakes.json" title="1.3 million points") Earthquakes - span#points-loaded - .form-group(title="Number of points. Leave blank for the entire original data set. If a smaller number, only a subset of points will be shown. If a larger number, some of the data will be duplicated with random offsets.") - label(for="points") Number of Points - input#points(type="number" min="1" step="100") - span#points-shown - .form-group(title="Delay between movement and heatmap recalculation in milliseconds.") - label(for="updateDelay") Update Delay (ms) - input#updateDelay(type="number" placeholder="50" min=0) - .form-group(title="Binning the data is faster for large sets and slower for small ones. Binning tends to make dense data look somewhat sparser. Smaller bins are closer in appearance to unbinned data but take longer to compute.") - label(for="binned") Bin Data - select#binned(placeholder="auto") - option(value="auto" title="Bin data if there are more points than the number of bins that would be used by default") Auto - option(value="false" title="Do not bin data.") Never - option(value="true" title="Always bin data using the default bin size (1/8th of the total radius).") Always - option(value="3" title="Always bin data using a 3 pixel bin size.") 3 pixel bin size - option(value="5" title="Always bin data using a 5 pixel bin size.") 5 pixel bin size - option(value="10" title="Always bin data using a 10 pixel bin size.") 10 pixel bin size - option(value="15" title="Always bin data using a 15 pixel bin size.") 15 pixel bin size - option(value="20" title="Always bin data using a 20 pixel bin size.") 20 pixel bin size - option(value="25" title="Always bin data using a 25 pixel bin size.") 25 pixel bin size - .form-group(title="Opacity of heatmap layer (0 to 1).") - label(for="opacity") Opacity - input#opacity(type="number" placeholder="0.75" min=0 max=1 step=0.05) - .form-group(title="Value to map to minimum intensity. Leave blank to auto calculate.") - label(for="minIntensity") Min. Intensity - input#minIntensity(type="number") - .form-group(title="Value to map to maximum intensity. Leave blank to auto calculate.") - label(for="maxIntensity") Max. Intensity - input#maxIntensity(type="number") - .form-group(title="Radius of center of points in pixels.") - label(for="radius") Radius - input#radius(type="number" placeholder="25" min=1) - .form-group(title="Radius of blur around points in pixels.") - label(for="blurRadius") Blur Radius - input#blurRadius(type="number" placeholder="15" min=0) - .form-group(title="Use either a Gaussian distribution or a solid circle with a blurred edge for each point. If a Guassian is used, the total radius is the sume of the radius and blur radius values.") - label(for="gaussian") Gaussian Points - input#gaussian(type="checkbox", placeholder="true", checked="checked") - .form-group(title="Color Gradient. Entries with intensities of 0 and 1 are needed to form a valid color gradient.") - label Color Gradient - table.gradient - tr - th(title="Intensity. 0 is minimum, 1 is maximum") I - th(title="Red channel, 0 to 255") R - th(title="Green channel, 0 to 255") G - th(title="Blue channel, 0 to 255") B - th(title="Alpha channel, 0 to 1") A - tr - td - input#gradI1(type="number" min=0 max=1 step=0.01) - td - input#gradR1(type="number" min=0 max=255 step=1) - td - input#gradG1(type="number" min=0 max=255 step=1) - td - input#gradB1(type="number" min=0 max=255 step=1) - td - input#gradA1(type="number" min=0 max=1 step=0.01) - tr - td - input#gradI2(type="number" min=0 max=1 step=0.01) - td - input#gradR2(type="number" min=0 max=255 step=1) - td - input#gradG2(type="number" min=0 max=255 step=1) - td - input#gradB2(type="number" min=0 max=255 step=1) - td - input#gradA2(type="number" min=0 max=1 step=0.01) - tr - td - input#gradI3(type="number" min=0 max=1 step=0.01) - td - input#gradR3(type="number" min=0 max=255 step=1) - td - input#gradG3(type="number" min=0 max=255 step=1) - td - input#gradB3(type="number" min=0 max=255 step=1) - td - input#gradA3(type="number" min=0 max=1 step=0.01) - tr - td - input#gradI4(type="number" min=0 max=1 step=0.01) - td - input#gradR4(type="number" min=0 max=255 step=1) - td - input#gradG4(type="number" min=0 max=255 step=1) - td - input#gradB4(type="number" min=0 max=255 step=1) - td - input#gradA4(type="number" min=0 max=1 step=0.01) - tr - td - input#gradI5(type="number" min=0 max=1 step=0.01) - td - input#gradR5(type="number" min=0 max=255 step=1) - td - input#gradG5(type="number" min=0 max=255 step=1) - td - input#gradB5(type="number" min=0 max=255 step=1) - td - input#gradA5(type="number" min=0 max=1 step=0.01) - tr - td - input#gradI6(type="number" min=0 max=1 step=0.01) - td - input#gradR6(type="number" min=0 max=255 step=1) - td - input#gradG6(type="number" min=0 max=255 step=1) - td - input#gradB6(type="number" min=0 max=255 step=1) - td - input#gradA6(type="number" min=0 max=1 step=0.01) - diff --git a/examples/heatmap/index.pug b/examples/heatmap/index.pug new file mode 100644 index 0000000000..c509980822 --- /dev/null +++ b/examples/heatmap/index.pug @@ -0,0 +1,123 @@ +extends ../common/templates/index.pug + +block append mainContent + #controls + .form-group(title="The data set to plot.") + label(for="dataset") Data Set + select#dataset(param-name="dataset", placeholder="Activity") + option(value="adderall", url="AdderallCities2015.csv", title="9555 points") Adderall + option(value="cities", url="cities.csv", title="30101 points") U.S. Cities + option(value="earthquakes", url="earthquakes.json", title="1.3 million points") Earthquakes + span#points-loaded + .form-group(title="Number of points. Leave blank for the entire original data set. If a smaller number, only a subset of points will be shown. If a larger number, some of the data will be duplicated with random offsets.") + label(for="points") Number of Points + input#points(type="number", min="1", step="100") + span#points-shown + .form-group(title="Delay between movement and heatmap recalculation in milliseconds.") + label(for="updateDelay") Update Delay (ms) + input#updateDelay(type="number", placeholder="50", min=0) + .form-group(title="Binning the data is faster for large sets and slower for small ones. Binning tends to make dense data look somewhat sparser. Smaller bins are closer in appearance to unbinned data but take longer to compute.") + label(for="binned") Bin Data + select#binned(placeholder="auto") + option(value="auto", title="Bin data if there are more points than the number of bins that would be used by default") Auto + option(value="false", title="Do not bin data.") Never + option(value="true", title="Always bin data using the default bin size (1/8th of the total radius).") Always + option(value="3", title="Always bin data using a 3 pixel bin size.") 3 pixel bin size + option(value="5", title="Always bin data using a 5 pixel bin size.") 5 pixel bin size + option(value="10", title="Always bin data using a 10 pixel bin size.") 10 pixel bin size + option(value="15", title="Always bin data using a 15 pixel bin size.") 15 pixel bin size + option(value="20", title="Always bin data using a 20 pixel bin size.") 20 pixel bin size + option(value="25", title="Always bin data using a 25 pixel bin size.") 25 pixel bin size + .form-group(title="Opacity of heatmap layer (0 to 1).") + label(for="opacity") Opacity + input#opacity(type="number", placeholder="0.75", min=0, max=1, step=0.05) + .form-group(title="Value to map to minimum intensity. Leave blank to auto calculate.") + label(for="minIntensity") Min. Intensity + input#minIntensity(type="number") + .form-group(title="Value to map to maximum intensity. Leave blank to auto calculate.") + label(for="maxIntensity") Max. Intensity + input#maxIntensity(type="number") + .form-group(title="Radius of center of points in pixels.") + label(for="radius") Radius + input#radius(type="number", placeholder="25", min=1) + .form-group(title="Radius of blur around points in pixels.") + label(for="blurRadius") Blur Radius + input#blurRadius(type="number", placeholder="15", min=0) + .form-group(title="Use either a Gaussian distribution or a solid circle with a blurred edge for each point. If a Guassian is used, the total radius is the sume of the radius and blur radius values.") + label(for="gaussian") Gaussian Points + input#gaussian(type="checkbox", placeholder="true", checked="checked") + .form-group(title="Color Gradient. Entries with intensities of 0 and 1 are needed to form a valid color gradient.") + label Color Gradient + table.gradient + tr + th(title="Intensity. 0 is minimum, 1 is maximum") I + th(title="Red channel, 0 to 255") R + th(title="Green channel, 0 to 255") G + th(title="Blue channel, 0 to 255") B + th(title="Alpha channel, 0 to 1") A + tr + td + input#gradI1(type="number", min=0, max=1, step=0.01) + td + input#gradR1(type="number", min=0, max=255, step=1) + td + input#gradG1(type="number", min=0, max=255, step=1) + td + input#gradB1(type="number", min=0, max=255, step=1) + td + input#gradA1(type="number", min=0, max=1, step=0.01) + tr + td + input#gradI2(type="number", min=0, max=1, step=0.01) + td + input#gradR2(type="number", min=0, max=255, step=1) + td + input#gradG2(type="number", min=0, max=255, step=1) + td + input#gradB2(type="number", min=0, max=255, step=1) + td + input#gradA2(type="number", min=0, max=1, step=0.01) + tr + td + input#gradI3(type="number", min=0, max=1, step=0.01) + td + input#gradR3(type="number", min=0, max=255, step=1) + td + input#gradG3(type="number", min=0, max=255, step=1) + td + input#gradB3(type="number", min=0, max=255, step=1) + td + input#gradA3(type="number", min=0, max=1, step=0.01) + tr + td + input#gradI4(type="number", min=0, max=1, step=0.01) + td + input#gradR4(type="number", min=0, max=255, step=1) + td + input#gradG4(type="number", min=0, max=255, step=1) + td + input#gradB4(type="number", min=0, max=255, step=1) + td + input#gradA4(type="number", min=0, max=1, step=0.01) + tr + td + input#gradI5(type="number", min=0, max=1, step=0.01) + td + input#gradR5(type="number", min=0, max=255, step=1) + td + input#gradG5(type="number", min=0, max=255, step=1) + td + input#gradB5(type="number", min=0, max=255, step=1) + td + input#gradA5(type="number", min=0, max=1, step=0.01) + tr + td + input#gradI6(type="number", min=0, max=1, step=0.01) + td + input#gradR6(type="number", min=0, max=255, step=1) + td + input#gradG6(type="number", min=0, max=255, step=1) + td + input#gradB6(type="number", min=0, max=255, step=1) + td + input#gradA6(type="number", min=0, max=1, step=0.01) diff --git a/examples/hurricanes/index.jade b/examples/hurricanes/index.pug similarity index 54% rename from examples/hurricanes/index.jade rename to examples/hurricanes/index.pug index 378793a956..a1d4ae6ce4 100644 --- a/examples/hurricanes/index.jade +++ b/examples/hurricanes/index.pug @@ -1,4 +1,4 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent #app-hovered-info diff --git a/examples/index.jade b/examples/index.pug similarity index 86% rename from examples/index.jade rename to examples/index.pug index 08ba2d434b..5f31d1af6e 100644 --- a/examples/index.jade +++ b/examples/index.pug @@ -1,10 +1,10 @@ -extends ./common/templates/index.jade +extends ./common/templates/index.pug mixin thumbnail(ex) .col-md-4 .thumbnail - a(href="#{ex.path}/") - img(src="#{ex.path}/thumb.jpg" onerror="this.src='osm/thumb.jpg';") + a(href=ex.path + "/") + img(src=ex.path + "/thumb.jpg", onerror="this.src='osm/thumb.jpg';") .caption h3= ex.title if ex.about.text @@ -30,9 +30,9 @@ block mainContent .jumbotron .container h1 Welcome to GeoJS - p GeoJS is a javascript library for visualizing geospatial - | data in a browser. Its flexible API provides users with - | the ability to combine multiple visualizations drawn + p GeoJS is a javascript library for visualizing geospatial + | data in a browser. Its flexible API provides users with + | the ability to combine multiple visualizations drawn | in WebGL, canvas, and SVG into a single dynamic map. p What can GeoJS do for you? See the examples below. @@ -46,4 +46,3 @@ block mainContent - i += 1 +thumbnail(examples[iExample]) - iExample += 1 - diff --git a/examples/layerEvents/index.jade b/examples/layerEvents/index.jade deleted file mode 100644 index 8e4e1fc990..0000000000 --- a/examples/layerEvents/index.jade +++ /dev/null @@ -1,4 +0,0 @@ -extends ../common/templates/index.jade - -block append mainContent - button.btn.btn-default.layer-toggle(type="button" data-toggle="button") diff --git a/examples/layerEvents/index.pug b/examples/layerEvents/index.pug new file mode 100644 index 0000000000..f8a70a0d25 --- /dev/null +++ b/examples/layerEvents/index.pug @@ -0,0 +1,4 @@ +extends ../common/templates/index.pug + +block append mainContent + button.btn.btn-default.layer-toggle(type="button", data-toggle="button") diff --git a/examples/layers/index.jade b/examples/layers/index.pug similarity index 65% rename from examples/layers/index.jade rename to examples/layers/index.pug index 8ccf64246c..34f7b631e8 100644 --- a/examples/layers/index.jade +++ b/examples/layers/index.pug @@ -1,4 +1,4 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent // Add some toggles for removing adding layers diff --git a/examples/legend/index.jade b/examples/legend/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/legend/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/legend/index.pug b/examples/legend/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/legend/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/lines/index.jade b/examples/lines/index.pug similarity index 97% rename from examples/lines/index.jade rename to examples/lines/index.pug index 09d73aa229..cd0d8b62cb 100644 --- a/examples/lines/index.jade +++ b/examples/lines/index.pug @@ -1,10 +1,10 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent - div#controls + #controls .form-group(title="Number of lines segments. Leave blank for a small default value.") label(for="lines") Number of Lines - input#lines(type="number" min="1" step="100") + input#lines(type="number", min="1", step="100") span#lines-shown .form-group(title="Stroke Color (any css color). A comma-separated list will cycle through the listed colors. line:(comma-separate list) will cycle through by line. A JSON dictionary will map road categories to colors, with undefined values using the 'other' entry.") label(for="strokeColor") strokeColor @@ -42,4 +42,3 @@ block append mainContent button.preset(strokeColor='', strokeWidth='{"residential":1,"service":0.25,"other":3}', lineCap='round', lineJoin='', miterLimit='', showmap='false', title='Thin lines based on road category') Thin button.preset(strokeColor='line:red,orange,yellow,green,blue,indigo,violet', strokeWidth='line:1,2,4,8,16', lineCap='line:butt,round,square', lineJoin='line:miter,bevel,round,miter-clip', miterLimit=4, title='Properties varied by line') Lines button.preset(strokeColor='red,orange,yellow,green,blue,indigo,violet', strokeWidth='1,2,4,8,16', lineCap='butt,round,square', lineJoin='miter,bevel,round,miter-clip', miterLimit=4, title='Properties varied by line segment') Segments - diff --git a/examples/osm/index.jade b/examples/osm/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/osm/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/osm/index.pug b/examples/osm/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/osm/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/picking/index.jade b/examples/picking/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/picking/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/picking/index.pug b/examples/picking/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/picking/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/pixelmap/index.jade b/examples/pixelmap/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/pixelmap/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/pixelmap/index.pug b/examples/pixelmap/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/pixelmap/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/points/index.jade b/examples/points/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/points/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/points/index.pug b/examples/points/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/points/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/polygons/index.jade b/examples/polygons/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/polygons/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/polygons/index.pug b/examples/polygons/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/polygons/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/quads/index.jade b/examples/quads/index.jade deleted file mode 100644 index a2432ab791..0000000000 --- a/examples/quads/index.jade +++ /dev/null @@ -1 +0,0 @@ -extends ../common/templates/index.jade diff --git a/examples/quads/index.pug b/examples/quads/index.pug new file mode 100644 index 0000000000..4c9bea5c3e --- /dev/null +++ b/examples/quads/index.pug @@ -0,0 +1 @@ +extends ../common/templates/index.pug diff --git a/examples/reprojection/index.jade b/examples/reprojection/index.pug similarity index 96% rename from examples/reprojection/index.jade rename to examples/reprojection/index.pug index c3c3242a53..c4002bb5bf 100644 --- a/examples/reprojection/index.jade +++ b/examples/reprojection/index.pug @@ -1,7 +1,7 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent - div#controls + #controls .form-group(title="The url used to fetch tiles. Use {x}, {y}, {z}, and {s} for templating.") label(for="layer-url") Tile URL select#layer-url.layerparam(param-name="url", list="url-list", placeholder="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png") diff --git a/examples/sld/index.jade b/examples/sld/index.pug similarity index 92% rename from examples/sld/index.jade rename to examples/sld/index.pug index aea4dd5e77..272039a2f7 100644 --- a/examples/sld/index.jade +++ b/examples/sld/index.pug @@ -1,16 +1,16 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent - div#controls + #controls .form-group(title="Select a color palette.") label(for="palette") Color Palette - select#palette.mapparam(param-name="palette", placeholder="YlGn") + select#palette.mapparam(param-name="palette", placeholder="YlGn") .form-group(title="Select number of colors.") label(for="color-count") Number of colors select#color-count.mapparam(param-name="color-count") - + .form-group(title="Discrete or continuous colors.") label(for="palette-type") Palette Type select#palette-type.mapparam(param-name="palette-type", placeholder="continuous") diff --git a/examples/tiles/index.jade b/examples/tiles/index.pug similarity index 90% rename from examples/tiles/index.jade rename to examples/tiles/index.pug index 4c76f12728..b20691587b 100644 --- a/examples/tiles/index.jade +++ b/examples/tiles/index.pug @@ -1,7 +1,7 @@ -extends ../common/templates/index.jade +extends ../common/templates/index.pug block append mainContent - div#controls + #controls .form-group(title="Tiles can be drawn using WebGL, Canvas, HTML, or SVG. WebGL is generally the fastest and HTML the most compatible.") label(for="map-renderer") Renderer select#map-renderer.mapparam(param-name="renderer", placeholder="default") @@ -60,10 +60,10 @@ block append mainContent datalist#url-list option(value="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", credit="© OpenStreetMap contributors") OSM option(value="../../data/tilefancy.png", credit="") Fancy Test Tile - option(value="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png", credit='Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.') Toner Lite + option(value="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png", credit='Map tiles by Date: Wed, 26 Jul 2017 11:41:32 -0400 Subject: [PATCH 11/11] Fix a typo in a comment. Wrap a long line. --- src/annotation.js | 2 +- src/canvas/textFeature.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/annotation.js b/src/annotation.js index 0141493f95..9d28756bd6 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -1444,7 +1444,7 @@ lineRequiredFeatures[lineFeature.capabilities.basic] = [annotationState.create]; registerAnnotation('line', lineAnnotation, lineRequiredFeatures); /** - * Point annotation classa. + * Point annotation class. * * @class * @alias geo.poinyAnnotation diff --git a/src/canvas/textFeature.js b/src/canvas/textFeature.js index 42dee405f6..e85930182c 100644 --- a/src/canvas/textFeature.js +++ b/src/canvas/textFeature.js @@ -109,7 +109,10 @@ var canvas_textFeature = function (arg) { /* 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) { + fontFromSubValues = [ + 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', + 'lineHeight', 'fontFamily' + ].some(function (key) { return m_this.style(key) !== null && m_this.style(key) !== undefined; }); /* Clear the canvas property buffer */