let util = require('../../../../util'); class Label { constructor(body,options) { this.body = body; this.pointToSelf = false; this.baseSize = undefined; this.fontOptions = {}; this.setOptions(options); this.size = {top: 0, left: 0, width: 0, height: 0, yLine: 0}; // could be cached } setOptions(options, allowDeletion = false) { this.nodeOptions = options; // We want to keep the font options seperated from the node options. // The node options have to mirror the globals when they are not overruled. this.fontOptions = util.deepExtend({},options.font, true); if (options.label !== undefined) { this.labelDirty = true; } if (options.font !== undefined) { Label.parseOptions(this.fontOptions, options, allowDeletion); if (typeof options.font === 'string') { this.baseSize = this.fontOptions.size; } else if (typeof options.font === 'object') { if (options.font.size !== undefined) { this.baseSize = options.font.size; } } } } static parseOptions(parentOptions, newOptions, allowDeletion = false) { if (typeof newOptions.font === 'string') { let newOptionsArray = newOptions.font.split(" "); parentOptions.size = newOptionsArray[0].replace("px",''); parentOptions.face = newOptionsArray[1]; parentOptions.color = newOptionsArray[2]; } else if (typeof newOptions.font === 'object') { util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); } parentOptions.size = Number(parentOptions.size); } /** * Main function. This is called from anything that wants to draw a label. * @param ctx * @param x * @param y * @param selected * @param baseline */ draw(ctx, x, y, selected, baseline = 'middle') { // if no label, return if (this.nodeOptions.label === undefined) return; // check if we have to render the label let viewFontSize = this.fontOptions.size * this.body.view.scale; if (this.nodeOptions.label && viewFontSize < this.nodeOptions.scaling.label.drawThreshold - 1) return; // update the size cache if required this.calculateLabelSize(ctx, selected, x, y, baseline); // create the fontfill background this._drawBackground(ctx); // draw text this._drawText(ctx, selected, x, y, baseline); } /** * Draws the label background * @param {CanvasRenderingContext2D} ctx * @private */ _drawBackground(ctx) { if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { ctx.fillStyle = this.fontOptions.background; let lineMargin = 2; switch (this.fontOptions.align) { case 'middle': ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); break; case 'top': ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); break; case 'bottom': ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); break; default: ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height); break; } } } /** * * @param ctx * @param x * @param baseline * @private */ _drawText(ctx, selected, x, y, baseline = 'middle') { let fontSize = this.fontOptions.size; let viewFontSize = fontSize * this.body.view.scale; // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) if (viewFontSize >= this.nodeOptions.scaling.label.maxVisible) { fontSize = Number(this.nodeOptions.scaling.label.maxVisible) / this.body.view.scale; } let yLine = this.size.yLine; let [fontColor, strokeColor] = this._getColor(viewFontSize); [x, yLine] = this._setAlignment(ctx, x, yLine, baseline); // configure context for drawing the text ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + fontSize + "px " + this.fontOptions.face; ctx.fillStyle = fontColor; // When the shape is a box and the textAlign property is 'left', make label left-justified if (this.options.shape === 'box' && this.options.font.textAlign === 'left') { ctx.textAlign = 'left'; x = x - (this.size.wideth >> 1); // Shift label 1/2-way (>>1 == div by 2) left } else { ctx.textAlign = 'center'; } // set the strokeWidth if (this.fontOptions.strokeWidth > 0) { ctx.lineWidth = this.fontOptions.strokeWidth; ctx.strokeStyle = strokeColor; ctx.lineJoin = 'round'; } // draw the text for (let i = 0; i < this.lineCount; i++) { if (this.fontOptions.strokeWidth > 0) { ctx.strokeText(this.lines[i], x, yLine); } ctx.fillText(this.lines[i], x, yLine); yLine += fontSize; } } _setAlignment(ctx, x, yLine, baseline) { // check for label alignment (for edges) // TODO: make alignment for nodes if (this.fontOptions.align !== 'horizontal' && this.pointToSelf === false) { x = 0; yLine = 0; let lineMargin = 2; if (this.fontOptions.align === 'top') { ctx.textBaseline = 'alphabetic'; yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers } else if (this.fontOptions.align === 'bottom') { ctx.textBaseline = 'hanging'; yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers } else { ctx.textBaseline = 'middle'; } } else { ctx.textBaseline = baseline; } return [x,yLine]; } /** * fade in when relative scale is between threshold and threshold - 1. * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. * * @param viewFontSize * @returns {*[]} * @private */ _getColor(viewFontSize) { let fontColor = this.fontOptions.color || '#000000'; let strokeColor = this.fontOptions.strokeColor || '#ffffff'; if (viewFontSize <= this.nodeOptions.scaling.label.drawThreshold) { let opacity = Math.max(0, Math.min(1, 1 - (this.nodeOptions.scaling.label.drawThreshold - viewFontSize))); fontColor = util.overrideOpacity(fontColor, opacity); strokeColor = util.overrideOpacity(strokeColor, opacity); } return [fontColor, strokeColor]; } /** * * @param ctx * @param selected * @returns {{width: number, height: number}} */ getTextSize(ctx, selected = false) { let size = { width: this._processLabel(ctx,selected), height: this.fontOptions.size * this.lineCount, lineCount: this.lineCount }; return size; } /** * * @param ctx * @param selected * @param x * @param y * @param baseline */ calculateLabelSize(ctx, selected, x = 0, y = 0, baseline = 'middle') { if (this.labelDirty === true) { this.size.width = this._processLabel(ctx,selected); } this.size.height = this.fontOptions.size * this.lineCount; this.size.left = x - this.size.width * 0.5; this.size.top = y - this.size.height * 0.5; this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; if (baseline === "hanging") { this.size.top += 0.5 * this.fontOptions.size; this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers this.size.yLine += 4; // distance from node } this.labelDirty = false; } /** * This calculates the width as well as explodes the label string and calculates the amount of lines. * @param ctx * @param selected * @returns {number} * @private */ _processLabel(ctx,selected) { let width = 0; let lines = ['']; let lineCount = 0; if (this.nodeOptions.label !== undefined) { lines = String(this.nodeOptions.label).split('\n'); lineCount = lines.length; ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face; width = ctx.measureText(lines[0]).width; for (let i = 1; i < lineCount; i++) { let lineWidth = ctx.measureText(lines[i]).width; width = lineWidth > width ? lineWidth : width; } } this.lines = lines; this.lineCount = lineCount; return width; } } export default Label;