import React from 'react'; import d3 from 'd3'; import cn from 'classnames'; import TranslatedComponent from './TranslatedComponent'; import { wrapCtxMenu } from '../utils/UtilityFunctions'; /** * Round to avoid floating point precision errors * @param {Boolean} selected Band selected * @param {number} sum Band power sum * @param {number} avail Total available power * @return {string} CSS Class name */ function getClass(selected, sum, avail) { return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary'; } /** * Get the # label for a Priority band * @param {number} val Priority Band Watt value * @param {number} index Priority Band index * @param {Function} wattScale Watt Scale function * @return {number} label / text */ function bandText(val, index, wattScale) { return (val > 0 && wattScale(val) > 13) ? index + 1 : null; } /** * Power Bands Component * Renders the SVG to simulate in-game power bands */ export default class PowerBands extends TranslatedComponent { static propTypes = { bands: React.PropTypes.array.isRequired, available: React.PropTypes.number.isRequired, width: React.PropTypes.number.isRequired, code: React.PropTypes.string, }; /** * Constructor * @param {Object} props React Component properties * @param {Object} context React Component context */ constructor(props, context) { super(props); this.wattScale = d3.scale.linear(); this.pctScale = d3.scale.linear().domain([0, 1]); this.wattAxis = d3.svg.axis().scale(this.wattScale).outerTickSize(0).orient('top').tickFormat(context.language.formats.r2); this.pctAxis = d3.svg.axis().scale(this.pctScale).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.rPct); this._updateDimensions = this._updateDimensions.bind(this); this._updateScales = this._updateScales.bind(this); this._selectNone = this._selectNone.bind(this); this._hidetip = () => this.context.tooltip(); let maxBand = props.bands[props.bands.length - 1]; this.state = { maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum), ret: {}, dep: {} }; if (props.width) { this._updateDimensions(props, context.sizeRatio); } } /** * Update dimensions based on properties and scale * @param {Object} props React Component properties * @param {number} scale size ratio / scale */ _updateDimensions(props, scale) { let barHeight = Math.round(20 * scale); let innerHeight = (barHeight * 2) + 2; let mTop = Math.round(25 * scale); let mBottom = Math.round(25 * scale); let mLeft = Math.round(45 * scale); let mRight = Math.round(140 * scale); let innerWidth = props.width - mLeft - mRight; this._updateScales(innerWidth, this.state.maxPwr, props.available); this.setState({ barHeight, innerHeight, mTop, mBottom, mLeft, mRight, innerWidth, height: innerHeight + mTop + mBottom, retY: (barHeight / 2), depY: (barHeight * 1.5) - 1 }); } /** * Select no bands */ _selectNone() { this.setState({ ret : {}, dep: {} }); } /** * Select a retracted band * @param {number} index Band index */ _selectRet(index) { let ret = this.state.ret; if(ret[index]) { delete ret[index]; } else { ret[index] = 1; } this.setState({ ret: Object.assign({}, ret) }); } /** * Select a deployed band * @param {number} index Band index */ _selectDep(index) { let dep = this.state.dep; if(dep[index]) { delete dep[index]; } else { dep[index] = 1; } this.setState({ dep: Object.assign({}, dep) }); } /** * Update scale * @param {number} innerWidth SVG innerwidth * @param {number} maxPwr Maximum power level MJ (deployed or available) * @param {number} available Available power MJ */ _updateScales(innerWidth, maxPwr, available) { this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true); this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true); } /** * Update state based on property and context changes * @param {Object} nextProps Incoming/Next properties * @param {Object} nextContext Incoming/Next context */ componentWillReceiveProps(nextProps, nextContext) { let { innerWidth, maxPwr } = this.state; let { language, sizeRatio } = this.context; let maxBand = nextProps.bands[nextProps.bands.length - 1]; let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum); if (language !== nextContext.language) { this.wattAxis.tickFormat(nextContext.language.formats.r2); this.pctAxis.tickFormat(nextContext.language.formats.rPct); } if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed this._updateScales(innerWidth, nextMaxPwr, nextProps.available); this.setState({ maxPwr: nextMaxPwr }); } else if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) { this._updateDimensions(nextProps, nextContext.sizeRatio); } } /** * Render the power bands * @return {React.Component} Power bands */ render() { if (!this.props.width) { return null; } let { wattScale, pctScale, context, props, state } = this; let { translate, formats } = context.language; let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis let { available, bands, width } = props; let { innerWidth, maxPwr, ret, dep } = state; let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum * 2 >= available }); let deployed = []; let retracted = []; let retSelected = Object.keys(ret).length > 0; let depSelected = Object.keys(dep).length > 0; let retSum = 0; let depSum = 0; for (let i = 0; i < bands.length; i++) { let b = bands[i]; retSum += (!retSelected || ret[i]) ? b.retracted : 0; depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0; if (b.retracted > 0) { let retLbl = bandText(b.retracted, i, wattScale); retracted.push(); if (retLbl) { retracted.push({retLbl} ); } } if (b.retracted > 0 || b.deployed > 0) { let depLbl = bandText(b.deployed + b.retracted, i, wattScale); deployed.push(); if (depLbl) { deployed.push({depLbl} ); } } } return ( {retracted} {deployed} d3.select(elem).call(this.wattAxis) } className='watt axis'> { let axis = d3.select(elem); axis.call(this.pctAxis); axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass); }} className='pct axis' transform={`translate(0,${state.innerHeight})`}> {translate('ret')} {translate('dep')} {f2(Math.max(0, retSum))} ({pct1(Math.max(0, retSum / available))}) {f2(Math.max(0, depSum))} ({pct1(Math.max(0, depSum / available))}) ); } }