//////////////////////////////////////////////////////////////////////////////
// Caption
//////////////////////////////////////////////////////////////////////////////
import CGObject from './CGObject';
import Box from './Box';
import Font from './Font';
import Color from './Color';
import utils from './Utils';
import * as d3 from 'd3';
/**
* Captions are used to add additional annotation to the map.
*
* ### Action and Events
*
* Action | Viewer Method | Caption Method | Event
* -------------------------------------------|------------------------------------------------|----------------------|-----
* [Add](../docs.html#adding-records) | [addCaptions()](Viewer.html#addCaptions) | - | captions-add
* [Update](../docs.html#updating-records) | [updateCaptions()](Viewer.html#updateCaptions) | [update()](#update) | captions-update
* [Remove](../docs.html#removing-records) | [removeCaptions()](Viewer.html#removeCaptions) | [remove()](C#remove) | captions-remove
* [Reorder](../docs.html#reordering-records) | [moveCaption()](Viewer.html#moveCaption) | [move()](#move) | captions-reorder
* [Read](../docs.html#reading-records) | [captions()](Viewer.html#captions) | - | -
*
* <a name="attributes"></a>
* ### Attributes
*
* Attribute | Type | Description
* ---------------------------------|-----------|------------
* [name](#name) | String | Text of the caption
* [position](#position) | String\|Object | Where to draw the caption [Default: 'middle-center']. See {@link Position} for details.
* [anchor](#anchor) | String\|Object | Where to anchor the caption box to the position [Default: 'middle-center']. See {@link Anchor} for details.
* [font](#font) | String | A string describing the font [Default: 'SansSerif, plain, 8']. See {@link Font} for details.
* [fontColor](#fontColor) | String | A string describing the color [Default: 'black']. See {@link Color} for details.
* [textAlignment](#textAlignment) | String | Alignment of caption text: *left*, *center*, or *right* [Default: 'left']
* [backgroundColor](#font) | String | A string describing the background color of the caption [Default: 'white']. See {@link Color} for details.
* [on](#on)<sup>ic</sup> | String | Place the caption relative to the 'canvas' or 'map' [Default: 'canvas']
* [visible](CGObject.html#visible) | Boolean | Caption is visible [Default: true]
* [meta](CGObject.html#meta) | Object | [Meta data](tutorial-meta.html) for Caption
*
* <sup>ic</sup> Ignored on Caption creation
*
* ### Examples
*
* @extends CGObject
*/
class Caption extends CGObject {
/**
* Create a new Caption.
* @param {Viewer} viewer - The viewer.
* @param {Object} options - [Attributes](#attributes) used to create the caption.
* @param {Object} [meta] - User-defined [Meta data](../tutorials/details-meta-data.html) to add to the caption.
*/
constructor(viewer, options = {}, meta = {}) {
super(viewer, options, meta);
this.viewer = viewer;
this._name = utils.defaultFor(options.name, '');
this.backgroundColor = options.backgroundColor;
// this.backgroundColor = 'black';
this.fontColor = utils.defaultFor(options.fontColor, 'black');
this.textAlignment = utils.defaultFor(options.textAlignment, 'left');
this.box = new Box(viewer, {
position: utils.defaultFor(options.position, 'middle-center'),
anchor: utils.defaultFor(options.anchor, 'middle-center')
});
// Setting font will refresh the caption and draw
this.font = utils.defaultFor(options.font, 'sans-serif, plain, 8');
// FIXME: go through caption initialization and reduce to calles to Refresh (we only need one)
}
/**
* Return the class name as a string.
* @return {String} - 'Caption'
*/
toString() {
return 'Caption';
}
/**
* @member {Viewer} - Get or set the *Viewer*
*/
get viewer() {
return this._viewer;
}
set viewer(viewer) {
this._viewer = viewer;
viewer._captions.push(this);
}
get visible() {
return this._visible;
}
set visible(value) {
// super.visible = value;
this._visible = value;
this.viewer.refreshCanvasLayer();
// this.viewer ? this.viewer.refreshCanvasLayer() : this.refresh();
// this.refresh();
}
/**
* @member {String} - Get or set where the caption will be relative to. Values: 'map', 'canvas'
*/
get on() {
return this.box.on;
}
set on(value) {
this.clear();
this.box.on = value;
this.refresh();
}
/**
* @member {String} - Get or set the caption [anchor](Anchor.html) position.
*/
get anchor() {
return this.box.anchor;
}
set anchor(value) {
this.clear();
this.box.anchor = value;
this.refresh();
}
/**
* @member {Boolean} - Returns true if the caption is positioned on the map
*/
get onMap() {
return this.position.onMap;
}
/**
* @member {Boolean} - Returns true if the caption is positioned on the canvas
*/
get onCanvas() {
return this.position.onCanvas;
}
/**
* @member {Context} - Get the Context for drawing.
* @private
*/
get ctx() {
const layer = (this.onMap) ? 'foreground' : 'canvas';
return this.canvas.context(layer);
}
/**
* @member {String} - Get or set the caption [position](Position.html).
*/
get position() {
return this.box.position;
}
set position(value) {
this.clear();
this.box.position = value;
// this.refresh();
this.viewer.refreshCanvasLayer();
// FIXME: need to update anchor
}
/**
* @member {Color} - Get or set the backgroundColor. When setting the color, a string representing the color or a {@link Color} object can be used. For details see {@link Color}.
*/
get backgroundColor() {
return this._backgroundColor;
}
set backgroundColor(color) {
// this._backgroundColor.color = color;
if (color === undefined) {
this._backgroundColor = this.viewer.settings.backgroundColor;
} else if (color.toString() === 'Color') {
this._backgroundColor = color;
} else {
this._backgroundColor = new Color(color);
}
this.refresh();
}
/**
* @member {Font} - Get or set the font. When setting the font, a string representing the font or a {@link Font} object can be used. For details see {@link Font}.
*/
get font() {
return this._font;
}
set font(value) {
if (value.toString() === 'Font') {
this._font = value;
} else {
this._font = new Font(value);
}
this.refresh();
}
/**
* @member {Color} - Get or set the fontColor. When setting the color, a string representing the color or a {@link Color} object can be used. For details see {@link Color}.
*/
get fontColor() {
// return this._fontColor.rgbaString;
return this._fontColor;
}
set fontColor(value) {
if (value.toString() === 'Color') {
this._fontColor = value;
} else {
this._fontColor = new Color(value);
}
this.refresh();
}
/**
* @member {String} - Get or set the text alignment. Possible values are *left*, *center*, or *right*.
*/
get textAlignment() {
return this._textAlignment;
}
set textAlignment(value) {
if ( utils.validate(value, ['left', 'center', 'right']) ) {
this._textAlignment = value;
}
this.refresh();
}
/**
* @member {String} - Get or set the text shown for this caption.
*/
get name() {
return this._name || '';
}
set name(value) {
this._name = value;
this.refresh();
}
/**
* @member {String} - Get the name split into an array of lines.
* @private
*/
get lines() {
return this.name.split('\n');
}
/**
* Update caption [attributes](#attributes).
* See [updating records](../docs.html#s.updating-records) for details.
* @param {Object} attributes - Object describing the properties to change
*/
update(attributes) {
this.viewer.updateCaptions(this, attributes);
}
/**
* Move the map to center the caption. Only works with caption positioned on
* the map (not the canvas).
* @param {Number} duration - Duration of move animation
*/
moveTo(duration=1000) {
this.position.moveTo(duration);
}
/**
* Recalculates the *Caption* size and position.
* @private
*/
refresh() {
const box = this.box;
if (!box) { return; }
this.clear();
// Padding is half line height/font size
box.padding = this.font.size / 2;
// Calculate Caption Width
const lines = this.lines;
const fonts = lines.map( () => this.font.css );
const itemWidths = Font.calculateWidths(this.ctx, fonts, lines);
const width = d3.max(itemWidths) + (box.padding * 2);
// Calculate height of Caption
// - height of each line; plus padding between line; plus padding;
const lineHeight = this.font.size + box.padding;
const height = (lineHeight * lines.length) + box.padding;
box.resize(width, height);
this.draw();
}
/**
* Fill the background of the caption with the background color.
* @private
*/
fillBackground() {
const box = this.box;
this.ctx.fillStyle = this.backgroundColor.rgbaString;
box.clear(this.ctx);
this.ctx.fillRect(box.x, box.y, box.width, box.height);
}
/**
* Invert the colors of the caption (i.e. backgroundColor and fontColor).
*/
invertColors() {
this.update({
backgroundColor: this.backgroundColor.invert().rgbaString,
fontColor: this.fontColor.invert().rgbaString
});
}
/**
* Highlight the caption by drawing a box around it.
* @param {Color} color - Color of the highlighting outline
*/
highlight(color = this.fontColor) {
if (!this.visible) { return; }
// let ctx = this.canvas.context('background');
// ctx.fillStyle = color;
// ctx.fillRect(this.originX, this.originY, this.width, this.height);
const ctx = this.canvas.context('ui');
ctx.lineWidth = 1;
ctx.strokeStyle = color.rgbaString;
const box = this.box;
// ctx.strokeRect(box.x, box.y, box.width, box.height);
const corner = Math.min((box.height / 4), 4);
ctx.beginPath();
ctx.roundRect(box.x, box.y, box.width, box.height, [corner]);
ctx.stroke();
}
/**
* Returns the x position for drawing the caption text. Depnds on the textAlignment.
* @private
*/
textX() {
const box = this.box;
if (this.textAlignment === 'left') {
return box.leftPadded;
} else if (this.textAlignment === 'center') {
return box.centerX;
} else if (this.textAlignment === 'right') {
return box.rightPadded;
}
}
/**
* Clear the box containing this caption.
*/
clear() {
this.box.clear(this.ctx);
}
/**
* Draw the caption
*/
draw() {
if (!this.visible) { return; }
const ctx = this.ctx;
const box = this.box;
// Update the box origin if relative to the map
box.refresh();
this.fillBackground();
// ctx.textBaseline = 'top';
ctx.textBaseline = 'alphabetic'; // The default baseline works best across canvas and svg
ctx.font = this.font.css;
ctx.textAlign = this.textAlignment;
// Draw Text Label
ctx.fillStyle = this.fontColor.rgbaString;
// ctx.fillText(this.name, box.paddedX, box.paddedY);
const lineHeight = (box.height - box.padding) / this.lines.length;
// let lineY = box.paddedY;
let lineY = box.y + lineHeight;
for (let i = 0, len = this.lines.length; i < len; i++) {
ctx.fillText(this.lines[i], this.textX(), lineY);
lineY += lineHeight;
}
}
/**
* Remove caption
*/
remove() {
// const viewer = this.viewer;
// viewer._captions = viewer._captions.remove(this);
// viewer.clear('canvas');
// viewer.refreshCanvasLayer();
this.viewer.removeCaptions(this);
}
/**
* Move this caption to a new index in the array of Viewer captions.
* @param {Number} newIndex - New index for this caption (0-based)
*/
move(newIndex) {
const currentIndex = this.viewer.captions().indexOf(this);
this.viewer.moveCaption(currentIndex, newIndex);
}
/**
* Returns JSON representing the object
*/
toJSON(options = {}) {
const json = {
name: this.name,
position: this.position.toJSON(options),
textAlignment: this.textAlignment,
font: this.font.string,
fontColor: this.fontColor.rgbaString,
backgroundColor: this.backgroundColor.rgbaString,
// visible: this.visible
};
if (this.position.onMap) {
json.anchor = this.anchor.toJSON();
}
// Optionally add default values
if (!this.visible || options.includeDefaults) {
json.visible = this.visible;
}
return json;
}
}
export default Caption;