diff --git a/Apps/Sandcastle/gallery/Picking.html b/Apps/Sandcastle/gallery/Picking.html index 22c7c73ff95a..6ae7162463fe 100644 --- a/Apps/Sandcastle/gallery/Picking.html +++ b/Apps/Sandcastle/gallery/Picking.html @@ -38,7 +38,12 @@ Sandcastle.addDefaultToolbarButton('Show Cartographic Position on Mouse Over', function() { var entity = viewer.entities.add({ label : { - show : false + show : false, + showBackground : true, + font : '14px monospace', + horizontalOrigin : Cesium.HorizontalOrigin.LEFT, + verticalOrigin : Cesium.VerticalOrigin.TOP, + pixelOffset : new Cesium.Cartesian2(15, 0) } }); @@ -53,7 +58,9 @@ entity.position = cartesian; entity.label.show = true; - entity.label.text = '(' + longitudeString + ', ' + latitudeString + ')'; + entity.label.text = + 'Lon: ' + (' ' + longitudeString).slice(-7) + '\u00B0' + + '\nLat: ' + (' ' + latitudeString).slice(-7) + '\u00B0'; } else { entity.label.show = false; } @@ -159,7 +166,11 @@ var labelEntity = viewer.entities.add({ label : { show : false, - horizontalOrigin : Cesium.HorizontalOrigin.LEFT + showBackground : true, + font : '14px monospace', + horizontalOrigin : Cesium.HorizontalOrigin.LEFT, + verticalOrigin : Cesium.VerticalOrigin.TOP, + pixelOffset : new Cesium.Cartesian2(15, 0) } }); @@ -181,8 +192,11 @@ labelEntity.position = cartesian; labelEntity.label.show = true; - labelEntity.label.text = '(' + longitudeString + ', ' + latitudeString + ', ' + heightString + ')'; - + labelEntity.label.text = + 'Lon: ' + (' ' + longitudeString).slice(-7) + '\u00B0' + + '\nLat: ' + (' ' + latitudeString).slice(-7) + '\u00B0' + + '\nAlt: ' + (' ' + heightString).slice(-7) + 'm'; + var camera = scene.camera; labelEntity.label.eyeOffset = new Cesium.Cartesian3(0.0, 0.0, camera.frustum.near * 1.5 - Cesium.Cartesian3.distance(cartesian, camera.position)); @@ -201,7 +215,7 @@ handler = handler && handler.destroy(); }; //Sandcastle_End -Sandcastle.finishedLoading(); + Sandcastle.finishedLoading(); } if (typeof Cesium !== "undefined") { startup(Cesium); diff --git a/CHANGES.md b/CHANGES.md index 1d754f687786..7dfff1c2f52d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Change Log * Added the ability to blend a `Model` with a color/translucency. Added `color`, `colorBlendMode`, and `colorBlendAmount` properties to `Model`, `ModelGraphics`, and CZML. Added `ColorBlendMode` enum. [#4547](https://github.com/AnalyticalGraphicsInc/cesium/pull/4547) * Added new `Label` properties `showBackground`, `backgroundColor`, and `backgroundPadding` to the primitive, Entity, and CZML layers. * Added new enum `VerticalOrigin.BASELINE`. Previously, `VerticalOrigin.BOTTOM` would sometimes align to the baseline depending on the contents of a label. +* Added support for newlines (`\n`) in Cesium `Label`s and CZML. [#2402](https://github.com/AnalyticalGraphicsInc/cesium/issues/2402) * Fixed tooltips for gallery thumbnails in Sandcastle [#4702](https://github.com/AnalyticalGraphicsInc/cesium/pull/4702) * Fixed `Rectangle.union` to correctly account for rectangles that cross the IDL [#4732](https://github.com/AnalyticalGraphicsInc/cesium/pull/4732). * Fixed texture rotation for `RectangleGeometry` [#2737](https://github.com/AnalyticalGraphicsInc/cesium/issues/2737) diff --git a/Documentation/Images/Billboard.setHorizontalOrigin.png b/Documentation/Images/Billboard.setHorizontalOrigin.png index 01ac63db3ccc..77ce870e0a60 100644 Binary files a/Documentation/Images/Billboard.setHorizontalOrigin.png and b/Documentation/Images/Billboard.setHorizontalOrigin.png differ diff --git a/Source/DataSources/LabelGraphics.js b/Source/DataSources/LabelGraphics.js index e3aacdaca9eb..3f5396ccd463 100644 --- a/Source/DataSources/LabelGraphics.js +++ b/Source/DataSources/LabelGraphics.js @@ -28,7 +28,7 @@ define([ * @constructor * * @param {Object} [options] Object with the following properties: - * @param {Property} [options.text] A Property specifying the text. + * @param {Property} [options.text] A Property specifying the text. Explicit newlines '\n' are supported. * @param {Property} [options.font='10px sans-serif'] A Property specifying the CSS font. * @param {Property} [options.style=LabelStyle.FILL] A Property specifying the {@link LabelStyle}. * @param {Property} [options.fillColor=Color.WHITE] A Property specifying the fill {@link Color}. @@ -110,6 +110,7 @@ define([ /** * Gets or sets the string Property specifying the text of the label. + * Explicit newlines '\n' are supported. * @memberof LabelGraphics.prototype * @type {Property} */ diff --git a/Source/Scene/Billboard.js b/Source/Scene/Billboard.js index f50cd4eca1ca..b51d5d8dc59c 100644 --- a/Source/Scene/Billboard.js +++ b/Source/Scene/Billboard.js @@ -472,7 +472,7 @@ define([ * to the left, center, or right of its anchor position. *

*
- *
+ *
*
* @memberof Billboard.prototype * @type {HorizontalOrigin} diff --git a/Source/Scene/HorizontalOrigin.js b/Source/Scene/HorizontalOrigin.js index db1737fcf688..87f5d981370e 100644 --- a/Source/Scene/HorizontalOrigin.js +++ b/Source/Scene/HorizontalOrigin.js @@ -12,7 +12,7 @@ define([ * of the anchor position. *

*
- *
+ *
*
* * @exports HorizontalOrigin diff --git a/Source/Scene/Label.js b/Source/Scene/Label.js index ca1b68d2e8ef..67502ac5d74c 100644 --- a/Source/Scene/Label.js +++ b/Source/Scene/Label.js @@ -671,7 +671,7 @@ define([ * to the left, center, or right of its anchor position. *

*
- *
+ *
*
* @memberof Label.prototype * @type {HorizontalOrigin} diff --git a/Source/Scene/LabelCollection.js b/Source/Scene/LabelCollection.js index 2ff096b9706f..f7416ffc40d7 100644 --- a/Source/Scene/LabelCollection.js +++ b/Source/Scene/LabelCollection.js @@ -55,6 +55,9 @@ define([ this.dimensions = dimensions; } + // Traditionally, leading is %20 of the font size. + var defaultLineSpacingPercent = 1.2; + var whitePixelCanvasId = 'ID_WHITE_PIXEL'; var whitePixelSize = new Cartesian2(4, 4); var whitePixelBoundingRegion = new BoundingRectangle(1, 1, 1, 1); @@ -135,7 +138,7 @@ define([ // presize glyphs to match the new text length glyphs.length = textLength; - var showBackground = label._showBackground && (glyphs.length > 0); + var showBackground = label._showBackground && (text.split('\n').join('').length > 0); var backgroundBillboard = label._backgroundBillboard; var backgroundBillboardCollection = labelCollection._backgroundBillboardCollection; if (!showBackground) { @@ -267,17 +270,32 @@ define([ label._repositionAllGlyphs = true; } + function calculateWidthOffset(lineWidth, horizontalOrigin, backgroundPadding) { + if (horizontalOrigin === HorizontalOrigin.CENTER) { + return -lineWidth / 2; + } else if (horizontalOrigin === HorizontalOrigin.RIGHT) { + return -(lineWidth + backgroundPadding.x); + } + return backgroundPadding.x; + } + // reusable Cartesian2 instances var glyphPixelOffset = new Cartesian2(); var scratchBackgroundPadding = new Cartesian2(); function repositionAllGlyphs(label, resolutionScale) { var glyphs = label._glyphs; + var text = label._text; var glyph; var dimensions; - var totalWidth = 0; - var maxDescent = Number.NEGATIVE_INFINITY; - var maxY = 0; + var lastLineWidth = 0; + var maxLineWidth = 0; + var lineWidths = []; + var maxGlyphDescent = Number.NEGATIVE_INFINITY; + var maxGlyphY = 0; + var numberOfLines = 1; + var glyphIndex = 0; + var glyphLength = glyphs.length; var backgroundBillboard = label._backgroundBillboard; var backgroundPadding = scratchBackgroundPadding; @@ -285,89 +303,109 @@ define([ (defined(backgroundBillboard) ? label._backgroundPadding : Cartesian2.ZERO), backgroundPadding); - var glyphIndex = 0; - var glyphLength = glyphs.length; for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) { - glyph = glyphs[glyphIndex]; - dimensions = glyph.dimensions; - maxY = Math.max(maxY, dimensions.height - dimensions.descent); - maxDescent = Math.max(maxDescent, dimensions.descent); - - //Computing the total width must also account for the kering that occurs between letters. - totalWidth += dimensions.width - dimensions.bounds.minx; - if (glyphIndex < glyphLength - 1) { - totalWidth += glyphs[glyphIndex + 1].dimensions.bounds.minx; + if (text.charAt(glyphIndex) === '\n') { + lineWidths.push(lastLineWidth); + ++numberOfLines; + lastLineWidth = 0; + } else { + glyph = glyphs[glyphIndex]; + dimensions = glyph.dimensions; + maxGlyphY = Math.max(maxGlyphY, dimensions.height - dimensions.descent); + maxGlyphDescent = Math.max(maxGlyphDescent, dimensions.descent); + + //Computing the line width must also account for the kerning that occurs between letters. + lastLineWidth += dimensions.width - dimensions.bounds.minx; + if (glyphIndex < glyphLength - 1) { + lastLineWidth += glyphs[glyphIndex + 1].dimensions.bounds.minx; + } + maxLineWidth = Math.max(maxLineWidth, lastLineWidth); } } - var maxHeight = maxY + maxDescent; + lineWidths.push(lastLineWidth); + var maxLineHeight = maxGlyphY + maxGlyphDescent; var scale = label._scale; var horizontalOrigin = label._horizontalOrigin; - var widthOffset = 0; - if (horizontalOrigin === HorizontalOrigin.CENTER) { - widthOffset -= totalWidth / 2 * scale; - } else if (horizontalOrigin === HorizontalOrigin.RIGHT) { - widthOffset -= (totalWidth + backgroundPadding.x) * scale; - } else { - widthOffset += backgroundPadding.x * scale; - } + var verticalOrigin = label._verticalOrigin; + var lineIndex = 0; + var lineWidth = lineWidths[lineIndex]; + var widthOffset = calculateWidthOffset(lineWidth, horizontalOrigin, backgroundPadding); + var lineSpacing = defaultLineSpacingPercent * maxLineHeight; + var otherLinesHeight = lineSpacing * (numberOfLines - 1); - glyphPixelOffset.x = widthOffset * resolutionScale; + glyphPixelOffset.x = widthOffset * scale * resolutionScale; glyphPixelOffset.y = 0; - var verticalOrigin = label._verticalOrigin; + var lineOffsetY = 0; for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) { - glyph = glyphs[glyphIndex]; - dimensions = glyph.dimensions; - - if (verticalOrigin === VerticalOrigin.BASELINE) { - glyphPixelOffset.y = -dimensions.descent * scale; - } else if (verticalOrigin === VerticalOrigin.TOP) { - glyphPixelOffset.y = -(maxY - dimensions.height + dimensions.descent + backgroundPadding.y) * scale; - } else if (verticalOrigin === VerticalOrigin.CENTER) { - glyphPixelOffset.y = -(maxY - dimensions.height) / 2 * scale - dimensions.descent * scale; + if (text.charAt(glyphIndex) === '\n') { + ++lineIndex; + lineOffsetY += lineSpacing; + lineWidth = lineWidths[lineIndex]; + widthOffset = calculateWidthOffset(lineWidth, horizontalOrigin, backgroundPadding); + glyphPixelOffset.x = widthOffset * scale * resolutionScale; } else { - // VerticalOrigin.BOTTOM - glyphPixelOffset.y = (maxDescent - dimensions.descent + backgroundPadding.y) * scale; - } + glyph = glyphs[glyphIndex]; + dimensions = glyph.dimensions; + + if (verticalOrigin === VerticalOrigin.TOP) { + glyphPixelOffset.y = dimensions.height - maxGlyphY - backgroundPadding.y; + } else if (verticalOrigin === VerticalOrigin.CENTER) { + glyphPixelOffset.y = (otherLinesHeight + dimensions.height - maxGlyphY) / 2; + } else if (verticalOrigin === VerticalOrigin.BASELINE) { + glyphPixelOffset.y = otherLinesHeight; + } else { + // VerticalOrigin.BOTTOM + glyphPixelOffset.y = otherLinesHeight + maxGlyphDescent + backgroundPadding.y; + } + glyphPixelOffset.y = (glyphPixelOffset.y - dimensions.descent - lineOffsetY) * scale * resolutionScale; - glyphPixelOffset.y *= resolutionScale; + if (defined(glyph.billboard)) { + glyph.billboard._setTranslate(glyphPixelOffset); + } - if (defined(glyph.billboard)) { - glyph.billboard._setTranslate(glyphPixelOffset); + //Compute the next x offset taking into acocunt the kerning performed + //on both the current letter as well as the next letter to be drawn + //as well as any applied scale. + if (glyphIndex < glyphLength - 1) { + var nextGlyph = glyphs[glyphIndex + 1]; + glyphPixelOffset.x += ((dimensions.width - dimensions.bounds.minx) + nextGlyph.dimensions.bounds.minx) * scale * resolutionScale; + } } + } - //Compute the next x offset taking into acocunt the kerning performed - //on both the current letter as well as the next letter to be drawn - //as well as any applied scale. - if (glyphIndex < glyphLength - 1) { - var nextGlyph = glyphs[glyphIndex + 1]; - glyphPixelOffset.x += ((dimensions.width - dimensions.bounds.minx) + nextGlyph.dimensions.bounds.minx) * scale * resolutionScale; + if (defined(backgroundBillboard) && (text.split('\n').join('').length > 0)) { + if (horizontalOrigin === HorizontalOrigin.CENTER) { + widthOffset = -maxLineWidth / 2 - backgroundPadding.x; + } else if (horizontalOrigin === HorizontalOrigin.RIGHT) { + widthOffset = -(maxLineWidth + backgroundPadding.x * 2); + } else { + widthOffset = 0; } - } + glyphPixelOffset.x = widthOffset * scale * resolutionScale; - if (defined(backgroundBillboard) && (glyphLength > 0)) { - glyphPixelOffset.x = (widthOffset - backgroundPadding.x * scale) * resolutionScale; - if (verticalOrigin === VerticalOrigin.BASELINE) { - glyphPixelOffset.y = -backgroundPadding.y * scale - maxDescent * scale; - } else if (verticalOrigin === VerticalOrigin.TOP) { - glyphPixelOffset.y = -(maxY - maxHeight) * scale - maxDescent * scale; + if (verticalOrigin === VerticalOrigin.TOP) { + glyphPixelOffset.y = maxLineHeight - maxGlyphY - maxGlyphDescent; } else if (verticalOrigin === VerticalOrigin.CENTER) { - glyphPixelOffset.y = -(maxY - maxHeight) / 2 * scale - maxDescent * scale; + glyphPixelOffset.y = (maxLineHeight - maxGlyphY) / 2 - maxGlyphDescent; + } else if (verticalOrigin === VerticalOrigin.BASELINE) { + glyphPixelOffset.y = -backgroundPadding.y - maxGlyphDescent; } else { // VerticalOrigin.BOTTOM glyphPixelOffset.y = 0; } - glyphPixelOffset.y *= resolutionScale; - backgroundBillboard.width = totalWidth + (backgroundPadding.x * 2); - backgroundBillboard.height = maxHeight + (backgroundPadding.y * 2); + glyphPixelOffset.y = glyphPixelOffset.y * scale * resolutionScale; + + backgroundBillboard.width = maxLineWidth + (backgroundPadding.x * 2); + backgroundBillboard.height = maxLineHeight + otherLinesHeight + (backgroundPadding.y * 2); backgroundBillboard._setTranslate(glyphPixelOffset); } } function destroyLabel(labelCollection, label) { var glyphs = label._glyphs; - for ( var i = 0, len = glyphs.length; i < len; ++i) { + for (var i = 0, len = glyphs.length; i < len; ++i) { unbindGlyph(labelCollection, glyphs[i]); } if (defined(label._backgroundBillboard)) { @@ -628,7 +666,7 @@ define([ LabelCollection.prototype.removeAll = function() { var labels = this._labels; - for ( var i = 0, len = labels.length; i < len; ++i) { + for (var i = 0, len = labels.length; i < len; ++i) { destroyLabel(this, labels[i]); } diff --git a/Specs/Scene/LabelCollectionSpec.js b/Specs/Scene/LabelCollectionSpec.js index 8a87183fa3ab..1477b8a87b0f 100644 --- a/Specs/Scene/LabelCollectionSpec.js +++ b/Specs/Scene/LabelCollectionSpec.js @@ -1133,6 +1133,10 @@ defineSuite([ return Cartesian2.clone(label._glyphs[index].billboard._translate, new Cartesian2()); } + function getBackgroundBillboardVertexTranslate(label) { + return Cartesian2.clone(label._backgroundBillboard._translate, new Cartesian2()); + } + it('sets billboard properties properly when they change on the label', function() { var position1 = new Cartesian3(1.0, 2.0, 3.0); var position2 = new Cartesian3(4.0, 5.0, 6.0); @@ -1396,7 +1400,8 @@ defineSuite([ var label = labels.add({ text : 'apl', font : '90px "Open Sans"', - horizontalOrigin : HorizontalOrigin.CENTER + horizontalOrigin : HorizontalOrigin.CENTER, + showBackground : true }); scene.renderForSpecs(); @@ -1404,6 +1409,7 @@ defineSuite([ var offset0 = getGlyphBillboardVertexTranslate(label, 0); var offset1 = getGlyphBillboardVertexTranslate(label, 1); var offset2 = getGlyphBillboardVertexTranslate(label, 2); + var offsetBack = getBackgroundBillboardVertexTranslate(label); label.horizontalOrigin = HorizontalOrigin.LEFT; scene.renderForSpecs(); @@ -1412,11 +1418,13 @@ defineSuite([ expect(getGlyphBillboardVertexTranslate(label, 0).x).toBeGreaterThan(offset0.x); expect(getGlyphBillboardVertexTranslate(label, 1).x).toBeGreaterThan(offset1.x); expect(getGlyphBillboardVertexTranslate(label, 2).x).toBeGreaterThan(offset2.x); + expect(getBackgroundBillboardVertexTranslate(label).x).toBeGreaterThan(offsetBack.x); // Y offset should be unchanged expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(offset0.y); expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(offset1.y); expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(offset2.y); + expect(getBackgroundBillboardVertexTranslate(label).y).toEqual(offsetBack.y); label.horizontalOrigin = HorizontalOrigin.RIGHT; scene.renderForSpecs(); @@ -1425,11 +1433,13 @@ defineSuite([ expect(getGlyphBillboardVertexTranslate(label, 0).x).toBeLessThan(offset0.x); expect(getGlyphBillboardVertexTranslate(label, 1).x).toBeLessThan(offset1.x); expect(getGlyphBillboardVertexTranslate(label, 2).x).toBeLessThan(offset2.x); + expect(getBackgroundBillboardVertexTranslate(label).x).toBeLessThan(offsetBack.x); // Y offset should be unchanged expect(getGlyphBillboardVertexTranslate(label, 0).y).toEqual(offset0.y); expect(getGlyphBillboardVertexTranslate(label, 1).y).toEqual(offset1.y); expect(getGlyphBillboardVertexTranslate(label, 2).y).toEqual(offset2.y); + expect(getBackgroundBillboardVertexTranslate(label).y).toEqual(offsetBack.y); }); it('should set vertexTranslate of billboards correctly when scale is changed', function() { @@ -1666,6 +1676,23 @@ defineSuite([ expect(dimensions.height).toBeLessThan(originalDimensions.height); expect(dimensions.descent).toBeLessThanOrEqualTo(originalDimensions.descent); }); + + it('should increase label height and decrease width when adding newlines', function() { + var label = labels.add({ + text : 'apl apl apl', + }); + scene.renderForSpecs(); + + var originalBbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO); + + label.text = 'apl\napl\napl'; + scene.renderForSpecs(); + var newlinesBbox = Label.getScreenSpaceBoundingBox(label, Cartesian2.ZERO); + + expect(newlinesBbox.width).toBeLessThan(originalBbox.width); + expect(newlinesBbox.height).toBeGreaterThan(originalBbox.height); + }); + }, 'WebGL'); it('computes bounding sphere in 3D', function() {