mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-08 22:33:24 +00:00
281 lines
9.5 KiB
JavaScript
281 lines
9.5 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import * as 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: PropTypes.array.isRequired,
|
|
available: PropTypes.number.isRequired,
|
|
width: PropTypes.number.isRequired,
|
|
code: PropTypes.string,
|
|
};
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {Object} props React Component properties
|
|
* @param {Object} context React Component context
|
|
*/
|
|
constructor(props, context) {
|
|
super(props);
|
|
this.wattScale = d3.scaleLinear();
|
|
this.pctScale = d3.scaleLinear().domain([0, 1]);
|
|
this.wattAxis = d3.axisTop(this.wattScale).tickSizeOuter(0).tickFormat(context.language.formats.r2);
|
|
this.pctAxis = d3.axisBottom(this.pctScale).tickSizeOuter(0).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 } = formats; // wattFmt, pctFmt
|
|
let { available, bands } = props;
|
|
let { innerWidth, ret, dep } = state;
|
|
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum > available * 0.4 });
|
|
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(<rect
|
|
key={'rB' + i}
|
|
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
|
|
height={state.barHeight}
|
|
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
|
|
y={1}
|
|
onClick={this._selectRet.bind(this, i)}
|
|
className={getClass(ret[i], b.retractedSum, available)}
|
|
/>);
|
|
|
|
if (retLbl) {
|
|
retracted.push(<text
|
|
key={'rT' + i}
|
|
dy='0.5em'
|
|
textAnchor='middle'
|
|
height={state.barHeight}
|
|
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
|
|
y={state.retY}
|
|
onClick={this._selectRet.bind(this, i)}
|
|
className='primary-bg'>{retLbl}</text>
|
|
);
|
|
}
|
|
}
|
|
|
|
if (b.retracted > 0 || b.deployed > 0) {
|
|
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
|
|
|
|
deployed.push(<rect
|
|
key={'dB' + i}
|
|
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
|
|
height={state.barHeight}
|
|
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
|
|
y={state.barHeight + 1}
|
|
onClick={this._selectDep.bind(this, i)}
|
|
className={getClass(dep[i], b.deployedSum, available)}
|
|
/>);
|
|
|
|
if (depLbl) {
|
|
deployed.push(<text
|
|
key={'dT' + i}
|
|
dy='0.5em'
|
|
textAnchor='middle'
|
|
height={state.barHeight}
|
|
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
|
|
y={state.depY}
|
|
onClick={this._selectDep.bind(this, i)}
|
|
className='primary-bg'>{depLbl}</text>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
|
|
<g transform={`translate(${state.mLeft},${state.mTop})`}>
|
|
<g className='power-band'>{retracted}</g>
|
|
<g className='power-band'>{deployed}</g>
|
|
<g ref={ (elem) => d3.select(elem).call(this.wattAxis) } className='watt axis'></g>
|
|
<g ref={ (elem) => {
|
|
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})`}></g>
|
|
<line x1={pctScale(0.4)} x2={pctScale(0.4)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
|
|
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'retracted')} onMouseLeave={this._hidetip}>{translate('ret')}</text>
|
|
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'deployed', { orientation: 's', cap: 1 })} onMouseLeave={this._hidetip}>{translate('dep')}</text>
|
|
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum))} ({pct1(Math.max(0, retSum / available))})</text>
|
|
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum))} ({pct1(Math.max(0, depSum / available))})</text>
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|
|
}
|