Files
coriolis/src/app/components/ModalImport.jsx
Cmdr McDonald 7db76ecba0 Lint fixes
2017-04-23 18:36:20 +01:00

569 lines
18 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import Router from '../Router';
import Persist from '../stores/Persist';
import { Ships } from 'coriolis-data/dist';
import Ship from '../shipyard/Ship';
import { ModuleNameToGroup, Insurance } from '../shipyard/Constants';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons';
import { outfitURL } from '../utils/UrlGenerators';
import * as CompanionApiUtils from '../utils/CompanionApiUtils';
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
const mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 };
const standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 };
const bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 };
/**
* Check is slot is empty
* @param {Object} slot Slot model
* @return {Boolean} True if empty
*/
function isEmptySlot(slot) {
return slot.maxClass == this && slot.m === null;
}
/**
* Determine if a build is valid
* @param {string} shipId Ship ID
* @param {string} code Serialzied ship build 'code'
* @param {string} name Build name
* @throws {string} If build is not valid
*/
function validateBuild(shipId, code, name) {
let shipData = Ships[shipId];
if (!shipData) {
throw '"' + shipId + '" is not a valid Ship Id!';
}
if (typeof name != 'string' || name.length == 0) {
throw shipData.properties.name + ' build "' + name + '" must be a string at least 1 character long!';
}
if (typeof code != 'string' || code.length < 10) {
throw shipData.properties.name + ' build "' + name + '" is not valid!';
}
try {
let ship = new Ship(shipId, shipData.properties, shipData.slots);
ship.buildFrom(code);
} catch (e) {
throw shipData.properties.name + ' build "' + name + '" is not valid!';
}
}
/**
* Convert a ship-loadout JSON object to a Coriolis build
* @param {Object} detailedBuild ship-loadout
* @return {Object} Coriolis build
*/
function detailedJsonToBuild(detailedBuild) {
let ship;
if (!detailedBuild.name) {
throw 'Build Name missing!';
}
if (!detailedBuild.name.trim()) {
throw 'Build Name must be a string at least 1 character long!';
}
try {
ship = fromDetailedBuild(detailedBuild);
} catch (e) {
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
}
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
}
/**
* Import Modal
*/
export default class ModalImport extends TranslatedComponent {
static propTypes = {
builds: PropTypes.object, // Optional: Import object
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this.state = {
builds: props.builds,
canEdit: !props.builds,
comparisons: null,
shipDiscount: null,
moduleDiscount: null,
errorMsg: null,
importString: null,
importValid: false,
insurance: null
};
this._process = this._process.bind(this);
this._import = this._import.bind(this);
this._importBackup = this._importBackup.bind(this);
this._importDetailedArray = this._importDetailedArray.bind(this);
this._importTextBuild = this._importTextBuild.bind(this);
this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this);
this._validateImport = this._validateImport.bind(this);
}
/**
* Import a Coriolis backup
* @param {Object} importData Backup Data
* @throws {string} If import fails
*/
_importBackup(importData) {
if (importData.builds && typeof importData.builds == 'object') {
for (let shipId in importData.builds) {
for (let buildName in importData.builds[shipId]) {
validateBuild(shipId, importData.builds[shipId][buildName], buildName);
}
}
this.setState({ builds: importData.builds });
} else {
throw 'builds must be an object!';
}
if (importData.comparisons) {
for (let compName in importData.comparisons) {
let comparison = importData.comparisons[compName];
for (let i = 0, l = comparison.builds.length; i < l; i++) {
let build = comparison.builds[i];
if (!importData.builds[build.shipId] || !importData.builds[build.shipId][build.buildName]) {
throw build.shipId + ' build "' + build.buildName + '" data is missing!';
}
}
}
this.setState({ comparisons: importData.comparisons });
}
// Check for old/deprecated discounts
if (importData.discounts instanceof Array && importData.discounts.length == 2) {
this.setState({ shipDiscount: importData.discounts[0], moduleDiscount: importData.discounts[1] });
}
// Check for ship discount
if (!isNaN(importData.shipDiscount)) {
this.setState({ shipDiscount: importData.shipDiscount * 1 });
}
// Check for module discount
if (!isNaN(importData.moduleDiscount)) {
this.setState({ shipDiscount: importData.moduleDiscount * 1 });
}
if (typeof importData.insurance == 'string') {
let insurance = importData.insurance.toLowerCase();
if (Insurance[insurance] !== undefined) {
this.setState({ insurance });
} else {
throw 'Invalid insurance type: ' + insurance;
}
}
}
/**
* Import an array of ship-loadout objects / builds
* @param {Array} importArr Array of ship-loadout JSON Schema builds
*/
_importDetailedArray(importArr) {
let builds = {};
for (let i = 0, l = importArr.length; i < l; i++) {
let build = detailedJsonToBuild(importArr[i]);
if (!builds[build.shipId]) {
builds[build.shipId] = {};
}
builds[build.shipId][build.name] = build.code;
}
this.setState({ builds });
}
/**
* Import a build direct from the companion API
* @param {string} build JSON from the companion API information
* @throws {string} if parse/import fails
*/
_importCompanionApiBuild(build) {
const shipModel = CompanionApiUtils.shipModelFromJson(build);
const ship = CompanionApiUtils.shipFromJson(build);
let builds = {};
builds[shipModel] = {};
builds[shipModel]['Imported ' + Ships[shipModel].properties.name] = ship.toString();
this.setState({ builds, singleBuild: true });
}
/**
* Import a text build from ED Shipyard
* @param {string} buildStr Build string
* @throws {string} If parse / import fails
*/
_importTextBuild(buildStr) {
let buildName = textBuildRegex.exec(buildStr)[1].trim();
let shipName = buildName.toLowerCase();
let shipId = null;
for (let sId in Ships) {
if (Ships[sId].properties.name.toLowerCase() == shipName) {
shipId = sId;
break;
}
}
if (!shipId) {
throw 'No such ship found: "' + buildName + '"';
}
let lines = buildStr.split('\n');
let ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
ship.buildWith(null);
for (let i = 1; i < lines.length; i++) {
let line = lines[i].trim();
if (!line) { continue; }
if (line.substring(0, 3) == '---') { break; }
let parts = lineRegex.exec(line);
if (!parts) { throw 'Error parsing: "' + line + '"'; }
let typeSize = parts[1];
let cl = parts[2];
let rating = parts[3];
let mount = parts[4];
let missile = parts[5];
let name = parts[6].trim();
let slot, group;
if (isNaN(typeSize)) { // Standard or Hardpoint
if (typeSize.length == 1) { // Hardpoint
let slotClass = mountMap[typeSize];
if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
slot = ship.hardpoints.find(isEmptySlot, slotClass);
if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; }
group = ModuleNameToGroup[name.toLowerCase()];
let hp = ModuleUtils.findHardpoint(group, cl, rating, group ? null : name, mount, missile);
if (!hp) { throw 'Unknown component: "' + line + '"'; }
ship.use(slot, hp, true);
} else if (typeSize == 'BH') {
let bhId = bhMap[name.toLowerCase()];
if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; }
ship.useBulkhead(bhId, true);
} else if (standardMap[typeSize] != undefined) {
let standardIndex = standardMap[typeSize];
if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; }
ship.use(ship.standard[standardIndex], ModuleUtils.standard(standardIndex, cl + rating), true);
} else {
throw 'Unknown component: "' + line + '"';
}
} else {
if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; }
slot = ship.internal.find(isEmptySlot, typeSize);
if (!slot) { throw 'No internal slot available for: "' + line + '"'; }
group = ModuleNameToGroup[name.toLowerCase()];
let intComp = ModuleUtils.findInternal(group, cl, rating, group ? null : name);
if (!intComp) { throw 'Unknown component: "' + line + '"'; }
ship.use(slot, intComp);
}
}
let builds = {};
builds[shipId] = {};
builds[shipId]['Imported ' + buildName] = ship.toString();
this.setState({ builds, singleBuild: true });
}
/**
* Validate the import string / text box contents
* @param {SyntheticEvent} event Event
* @throws {string} If validation fails
*/
_validateImport(event) {
let importData = null;
let importString = event.target.value.trim();
this.setState({
builds: null,
comparisons: null,
shipDiscount: null,
moduleDiscount: null,
errorMsg: null,
importValid: false,
insurance: null,
singleBuild: false,
importString,
});
if (!importString) {
return;
}
try {
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
this._importTextBuild(importString);
} else { // JSON Build data
importData = JSON.parse(importString);
if (!importData || typeof importData != 'object') {
throw 'Must be an object or array!';
}
if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information
this._importCompanionApiBuild(importData); // Single sihp definition
} else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != null) { // Only the companion API has this information
this._importCompanionApiBuild(importData.ship); // Complete API dump
} else if (importData instanceof Array) { // Must be detailed export json
this._importDetailedArray(importData);
} else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export
this._importDetailedArray([importData]); // Convert to array with singleobject
this.setState({ singleBuild: true });
} else { // Using Backup JSON
this._importBackup(importData);
}
}
} catch (e) {
// console.log(e.stack);
this.setState({ errorMsg: (typeof e == 'string') ? e : 'Cannot Parse the data!' });
return;
}
this.setState({ importValid: true });
};
/**
* Process imported data
*/
_process() {
let builds = null, comparisons = null;
// If only importing a single build go straight to the outfitting page
if (this.state.singleBuild) {
builds = this.state.builds;
let shipId = Object.keys(builds)[0];
let name = Object.keys(builds[shipId])[0];
Router.go(outfitURL(shipId, builds[shipId][name], name));
return;
}
if (this.state.builds) {
builds = {}; // Create new builds object such that orginal name retained, but can be renamed
for (let shipId in this.state.builds) {
let shipbuilds = this.state.builds[shipId];
builds[shipId] = {};
for (let buildName in shipbuilds) {
builds[shipId][buildName] = {
code: shipbuilds[buildName],
useName: buildName
};
}
}
}
if (this.state.comparisons) {
comparisons = {};
for (let name in this.state.comparisons) {
comparisons[name] = Object.assign({ useName: name }, this.state.comparisons[name]);
}
}
this.setState({ processed: true, builds, comparisons });
};
/**
* Import parsed, processed data and save
*/
_import() {
let state = this.state;
if (state.builds) {
let builds = state.builds;
for (let shipId in builds) {
for (let buildName in builds[shipId]) {
let build = builds[shipId][buildName];
let name = build.useName.trim();
if (name) {
Persist.saveBuild(shipId, name, build.code);
}
}
}
}
if (state.comparisons) {
let comparisons = state.comparisons;
for (let comp in comparisons) {
let comparison = comparisons[comp];
let useName = comparison.useName.trim();
if (useName) {
Persist.saveComparison(useName, comparison.builds, comparison.facets);
}
}
}
if (state.shipDiscount !== undefined) {
Persist.setShipDiscount(state.shipDiscount);
}
if (state.moduleDiscount !== undefined) {
Persist.setModuleDiscount(state.moduleDiscount);
}
if (state.insurance) {
Persist.setInsurance(state.insurance);
}
this.context.hideModal();
};
/**
* Capture build name changes
* @param {Object} item Build/Comparison import object
* @param {SyntheticEvent} e Event
*/
_changeName(item, e) {
item.useName = e.target.value;
this.forceUpdate();
}
/**
* If imported data is already provided process immediately on mount
*/
componentWillMount() {
if (this.props.builds) {
this._process();
}
}
/**
* If textarea is shown focus on mount
*/
componentDidMount() {
if (!this.props.builds && this.importField) {
this.importField.focus();
}
}
/**
* Render the import modal
* @return {React.Component} Modal contents
*/
render() {
let translate = this.context.language.translate;
let state = this.state;
let importStage;
if (!state.processed) {
importStage = (
<div>
<textarea className='cb json' ref={node => this.importField = node} onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
<button id='proceed' className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
<div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
</div>
);
} else {
let comparisonTable, edit, buildRows = [];
if (state.comparisons) {
let comparisonRows = [];
for (let name in state.comparisons) {
let comparison = state.comparisons[name];
let hasComparison = Persist.hasComparison(comparison.useName);
comparisonRows.push(
<tr key={name} className='cb'>
<td>
<input type='text' onChange={this._changeName.bind(this, comparison)} value={comparison.useName}/>
</td>
<td style={{ textAlign:'center' }} className={ cn('cap', { warning: hasComparison, disabled: comparison.useName == '' }) }>
{translate(comparison.useName == '' ? 'skip' : (hasComparison ? 'overwrite' : 'create'))}
</td>
</tr>
);
}
comparisonTable = (
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }} >
<thead>
<tr>
<th style={{ textAlign:'left' }}>{translate('comparison')}</th>
<th>{translate('action')}</th>
</tr>
</thead>
<tbody>
{comparisonRows}
</tbody>
</table>
);
}
if(this.state.canEdit) {
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({ processed: false })}>{translate('edit data')}</button>;
}
let builds = this.state.builds;
for (let shipId in builds) {
let shipBuilds = builds[shipId];
for (let buildName in shipBuilds) {
let b = shipBuilds[buildName];
let hasBuild = Persist.hasBuild(shipId, b.useName);
buildRows.push(
<tr key={shipId + buildName} className='cb'>
<td>{Ships[shipId].properties.name}</td>
<td><input type='text' onChange={this._changeName.bind(this, b)} value={b.useName}/></td>
<td style={{ textAlign: 'center' }} className={cn('cap', { warning: hasBuild, disabled: b.useName == '' })}>
{translate(b.useName == '' ? 'skip' : (hasBuild ? 'overwrite' : 'create'))}
</td>
</tr>
);
}
}
importStage = (
<div>
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%' }}>
<thead>
<tr>
<th style={{ textAlign: 'left' }} >{translate('ship')}</th>
<th style={{ textAlign: 'left' }} >{translate('build name')}</th>
<th>{translate('action')}</th>
</tr>
</thead>
<tbody>
{buildRows}
</tbody>
</table>
{comparisonTable}
<button id='import' className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
{edit}
</div>
);
}
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
<h2 >{translate('import')}</h2>
{importStage}
<button className={'r dismiss cap'} onClick={this.context.hideModal}>{translate('close')}</button>
</div>;
}
}