define([ './BaseComponent', 'svg/SVGUtilities', 'svg/SVGShapes', ], ( BaseComponent, svg, SVGShapes ) => { 'use strict'; function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) { container.appendChild(svg.make( attrs.fill === 'none' ? 'polyline' : 'polygon', Object.assign({ 'points': ( (x + dx) + ' ' + (y - dy) + ' ' + x + ' ' + y + ' ' + (x + dx) + ' ' + (y + dy) ), }, attrs) )); } class Arrowhead { constructor(propName) { this.propName = propName; } getConfig(theme) { return theme.connect.arrow[this.propName]; } short(theme) { const arrow = this.getConfig(theme); const join = arrow.attrs['stroke-linejoin'] || 'miter'; const t = arrow.attrs['stroke-width'] * 0.5; const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5; if(join === 'round') { return lineStroke + t; } else { const h = arrow.height / 2; const w = arrow.width; const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1); return lineStroke + arrowDistance; } } render(layer, theme, {x, y, dir}) { const config = this.getConfig(theme); drawHorizontalArrowHead(layer, { x: x + this.short(theme) * dir, y, dx: config.width * dir, dy: config.height / 2, attrs: config.attrs, }); } width(theme) { return this.short(theme) + this.getConfig(theme).width; } height(theme) { return this.getConfig(theme).height; } lineGap(theme, lineAttrs) { const arrow = this.getConfig(theme); const short = this.short(theme); if(arrow.attrs.fill === 'none') { const h = arrow.height / 2; const w = arrow.width; const safe = short + (lineAttrs['stroke-width'] / 2) * (w / h); return (short + safe) / 2; } else { return short + arrow.width / 2; } } } const ARROWHEADS = [ { render: () => {}, width: () => 0, height: () => 0, lineGap: () => 0, }, new Arrowhead('single'), new Arrowhead('double'), ]; function makeWavyLineHeights(height) { return [ 0, -height * 2 / 3, -height, -height * 2 / 3, 0, height * 2 / 3, height, height * 2 / 3, ]; } class ConnectingLine { renderFlat(container, {x1, x2, y}, attrs) { const ww = attrs['wave-width']; const hh = attrs['wave-height']; if(!ww || !hh) { container.appendChild(svg.make('line', Object.assign({ 'x1': x1, 'y1': y, 'x2': x2, 'y2': y, }, attrs))); return; } const heights = makeWavyLineHeights(hh); const dw = ww / heights.length; let p = 0; let points = ''; for(let x = x1; x + dw <= x2; x += dw) { points += ( x + ' ' + (y + heights[(p ++) % heights.length]) + ' ' ); } points += x2 + ' ' + y; container.appendChild(svg.make('polyline', Object.assign({ points, }, attrs))); } renderRev(container, {xL1, xL2, y1, y2, xR}, attrs) { const r = (y2 - y1) / 2; const ww = attrs['wave-width']; const hh = attrs['wave-height']; if(!ww || !hh) { container.appendChild(svg.make('path', Object.assign({ 'd': ( 'M' + xL1 + ' ' + y1 + 'L' + xR + ' ' + y1 + 'A' + r + ' ' + r + ' 0 0 1 ' + xR + ' ' + y2 + 'L' + xL2 + ' ' + y2 ), }, attrs))); return; } const heights = makeWavyLineHeights(hh); const dw = ww / heights.length; let p = 0; let points = ''; for(let x = xL1; x + dw <= xR; x += dw) { points += ( x + ' ' + (y1 + heights[(p ++) % heights.length]) + ' ' ); } const ym = (y1 + y2) / 2; for(let t = 0; t + dw / r <= Math.PI; t += dw / r) { const h = heights[(p ++) % heights.length]; points += ( (xR + Math.sin(t) * (r - h)) + ' ' + (ym - Math.cos(t) * (r - h)) + ' ' ); } for(let x = xR; x - dw >= xL2; x -= dw) { points += ( x + ' ' + (y2 - heights[(p ++) % heights.length]) + ' ' ); } points += xL2 + ' ' + y2; container.appendChild(svg.make('polyline', Object.assign({ points, }, attrs))); } } const CONNECTING_LINE = new ConnectingLine(); class Connect extends BaseComponent { separation({label, agentNames, options}, env) { const config = env.theme.connect; const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; let labelWidth = ( env.textSizer.measure(config.label.attrs, label).width ); if(labelWidth > 0) { labelWidth += config.label.padding * 2; } const info1 = env.agentInfos.get(agentNames[0]); if(agentNames[0] === agentNames[1]) { env.addSpacing(agentNames[0], { left: 0, right: ( info1.currentMaxRad + Math.max( labelWidth + lArrow.width(env.theme), rArrow.width(env.theme) ) + config.loopbackRadius ), }); } else { const info2 = env.agentInfos.get(agentNames[1]); env.addSeparation( agentNames[0], agentNames[1], info1.currentMaxRad + info2.currentMaxRad + labelWidth + Math.max( lArrow.width(env.theme), rArrow.width(env.theme) ) * 2 ); } } renderSelfConnect({label, agentNames, options}, env) { /* jshint -W071 */ // TODO: find appropriate abstractions const config = env.theme.connect; const from = env.agentInfos.get(agentNames[0]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; const height = label ? ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ) : 0; const lineX = from.x + from.currentMaxRad; const y0 = env.primaryY; const x0 = ( lineX + lArrow.width(env.theme) + (label ? config.label.padding : 0) ); const clickable = env.makeRegion(); const renderedText = SVGShapes.renderBoxedText(label, { x: x0 - config.mask.padding.left, y: y0 - height + config.label.margin.top, padding: config.mask.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.label.loopbackAttrs, boxLayer: env.maskLayer, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, }); const labelW = (label ? ( renderedText.width + config.label.padding - config.mask.padding.left - config.mask.padding.right ) : 0); const r = config.loopbackRadius; const x1 = Math.max(lineX + rArrow.width(env.theme), x0 + labelW); const y1 = y0 + r * 2; const lineAttrs = config.lineAttrs[options.line]; CONNECTING_LINE.renderRev(env.shapeLayer, { xL1: lineX + lArrow.lineGap(env.theme, lineAttrs), xL2: lineX + rArrow.lineGap(env.theme, lineAttrs), y1: y0, y2: y1, xR: x1, }, lineAttrs); lArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y0, dir: 1}); rArrow.render(env.shapeLayer, env.theme, {x: lineX, y: y1, dir: 1}); const raise = Math.max(height, lArrow.height(env.theme) / 2); const arrowDip = rArrow.height(env.theme) / 2; clickable.insertBefore(svg.make('rect', { 'x': lineX, 'y': y0 - raise, 'width': x1 + r - lineX, 'height': raise + r * 2 + arrowDip, 'fill': 'transparent', }), clickable.firstChild); return y1 + Math.max( arrowDip + env.theme.minActionMargin, env.theme.actionMargin ); } renderSimpleConnect({label, agentNames, options}, env) { const config = env.theme.connect; const from = env.agentInfos.get(agentNames[0]); const to = env.agentInfos.get(agentNames[1]); const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; const dir = (from.x < to.x) ? 1 : -1; const height = ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ); const x0 = from.x + from.currentMaxRad * dir; const x1 = to.x - to.currentMaxRad * dir; const y = env.primaryY; const clickable = env.makeRegion(); SVGShapes.renderBoxedText(label, { x: (x0 + x1) / 2, y: y - height + config.label.margin.top, padding: config.mask.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.label.attrs, boxLayer: env.maskLayer, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, }); const lineAttrs = config.lineAttrs[options.line]; CONNECTING_LINE.renderFlat(env.shapeLayer, { x1: x0 + lArrow.lineGap(env.theme, lineAttrs) * dir, x2: x1 - rArrow.lineGap(env.theme, lineAttrs) * dir, y, }, lineAttrs); lArrow.render(env.shapeLayer, env.theme, {x: x0, y, dir}); rArrow.render(env.shapeLayer, env.theme, {x: x1, y, dir: -dir}); const arrowSpread = Math.max( lArrow.height(env.theme), rArrow.height(env.theme) ) / 2; clickable.insertBefore(svg.make('rect', { 'x': Math.min(x0, x1), 'y': y - Math.max(height, arrowSpread), 'width': Math.abs(x1 - x0), 'height': Math.max(height, arrowSpread) + arrowSpread, 'fill': 'transparent', }), clickable.firstChild); return y + Math.max( arrowSpread + env.theme.minActionMargin, env.theme.actionMargin ); } renderPre({label, agentNames, options}, env) { const config = env.theme.connect; const lArrow = ARROWHEADS[options.left]; const rArrow = ARROWHEADS[options.right]; const height = ( env.textSizer.measureHeight(config.label.attrs, label) + config.label.margin.top + config.label.margin.bottom ); let arrowH = lArrow.height(env.theme); if(agentNames[0] !== agentNames[1]) { arrowH = Math.max(arrowH, rArrow.height(env.theme)); } return { agentNames, topShift: Math.max(arrowH / 2, height), }; } render(stage, env) { if(stage.agentNames[0] === stage.agentNames[1]) { return this.renderSelfConnect(stage, env); } else { return this.renderSimpleConnect(stage, env); } } } BaseComponent.register('connect', new Connect()); return Connect; });