Source: EvowareTableFile.js

/**
 * Roboliq: Automation for liquid-handling robots
 * @copyright 2017, ETH Zurich, Ellis Whitehead
 * @license GPL-3.0
 */

/**
 * Module to load and save Evoware table files.
 *
 * @module
 */

import _ from 'lodash';
import assert from 'assert';
//import {sprintf} from 'sprintf-js';
import * as EvowareUtils from './EvowareUtils.js';
import * as EvowareCarrierFile from './EvowareCarrierFile.js';
import M from './Medley.js';

/**
 * Represent a hotel object in an Evoware table file
 * @class module:evoware/EvowareTableFile.HotelObject
 * @param {integer} parentCarrierId - carrier ID for the carrier holding this hotel
 * @param {integer} gridIndex - grid index of the hotel
 */
export class HotelObject {
	constructor(parentCarrierId, gridIndex) {
		this.parentCarrierId = parentCarrierId;
		this.gridIndex = gridIndex;
	}
}

/**
 * Represent an external object in an Evoware table file
 * @class module:evoware/EvowareTableFile.ExternalObject
 * @param {integer} n1 - Value of unknown significance (4 for System, 0 for others?)
 * @param {integer} n2 - I think this is the on-screen display index
 * @param {string} carrierName - carrier name for the carrier holding this hotel/object
 */
export class ExternalObject {
	constructor(n1, n2, carrierName) {
		this.n1 = n1;
		this.n2 = n2;
		this.carrierName = carrierName;
	}
}

/**
 * Parses an Evoware `.esc` script file, extracting the table setup.
 * @param {EvowareCarrierData} carrierData
 * @param {string} filename
 * @return {object} a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 */
export function load(carrierData, filename) {
	const lines = new EvowareUtils.EvowareSemicolonFile(filename, 7);
	lines.next() // TODO: should we check whether this is equal to "--{ RPG }--"?
	//println(lsLine.takeWhile(_ != "--{ RPG }--").length)
	const [, l] = lines.nextSplit();
	lines.skip(0);
	const tableFile = parse14(carrierData, l, lines);
	//println("parseFile: "+rest.takeWhile(_ != "--{ RPG }--"))
	return tableFile;
}

/**
 * Parse a table.
 *
 * @param {EvowareCarrierData} carrierData
 * @param {array} l - array of string representing the elements of the current line
 * @param {EvowareSemicolonFile} lines - array of lines from the Carrier.cfg
 * @return {object} a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 */
function parse14(carrierData, l, lines) {
	//import configFile._
	const carrierIdsInternal = parse14_getCarrierIds(_.initial(l));
	//console.log("carrierIdsInternal: "+JSON.stringify(carrierIdsInternal));
	const internalObjects = parse14_getLabwareObjects(carrierData, carrierIdsInternal, lines);
	//console.log("internalObjects: "+JSON.stringify(internalObjects));
	const hotelObjects = parse14_getHotelObjects(lines);
	const externalObjects = parse14_getExternalObjects(lines);
	const externalSiteIdToLabwareModelName = parse14_getExternalLabwares(lines);
	const externalCarrierNameToGridIndexList = parse14_getExternalCarrierGrids(externalObjects, lines);

	// FIXME: for debug only
	//const gridToCarrierIdInternal = _(carrierIdsInternal).map((id, index) => [index.toString(), id]).filter(([, id]) => id > -1).fromPairs().value();
	//console.log("gridToCarrierIdInternal: "+JSON.stringify(gridToCarrierIdInternal));
	// ENDFIX

	function set(carrierName, gridIndex, siteIndex, propertyName, propertyValue) {
		const c = _.get(layout, carrierName, {});
		if (_.isUndefined(gridIndex)) {
			M.setMut(c, propertyName, propertyValue);
		}
		else {
			const g = _.get(c, gridIndex, {});
			if (_.isUndefined(siteIndex)) {
				M.setMut(g, propertyName, propertyValue);
			}
			else {
				const s = _.get(g, siteIndex, {});
				M.setMut(s, propertyName, propertyValue);
				M.setMut(g, siteIndex, s);
			}
			M.setMut(c, gridIndex, g);
		}
		M.setMut(layout, carrierName, c);
	}

	// Get list of all carriers on the table
	const carrierAndGridList = [];
	// Internal carriers
	carrierIdsInternal.forEach((carrierId, gridIndex) => {
		if (carrierId > -1) {
			const carrier = carrierData.getCarrierById(carrierId);
			carrierAndGridList.push([carrier.name, gridIndex, "internal", true]);
		}
	});
	// Hotel carriers
	hotelObjects.forEach(o => {
		const carrier = carrierData.getCarrierById(o.parentCarrierId);
		carrierAndGridList.push([carrier.name, o.gridIndex, "hotel", true]);
	});
	// External objects
	externalObjects.forEach((external, i) => {
		const [carrierName, gridIndex] = externalCarrierNameToGridIndexList[i];
		carrierAndGridList.push([carrierName, gridIndex, "external", _.pick(external, 'n1', 'n2')]);
	});

	// Sort the list by gridIndex
	const carrierAndGridList1 = _.sortBy(carrierAndGridList, l => l[1]);
	//console.log(JSON.stringify(carrierAndGridList1, null, '\t'));

	// Populate the carrier/grid layout information in gridIndex-order
	const layout = {};
	carrierAndGridList1.forEach(([carrierName, gridIndex, propertyName, propertyValue]) => {
		set(carrierName, gridIndex, undefined, propertyName, propertyValue);
	});

	// Add to layout the internal site labels and labware
	internalObjects.forEach(([carrierName, gridIndex, siteIndex, label, labwareModelName]) => {
		if (!_.isEmpty(label))
			set(carrierName, gridIndex, siteIndex, 'label', label);
		set(carrierName, gridIndex, siteIndex, 'labwareModelName', labwareModelName);
	});
	// Add to layout the external site labware
	externalSiteIdToLabwareModelName.forEach(([carrierId, labwareModelName]) => {
		const carrier = carrierData.getCarrierById(carrierId);
		const result = _.find(externalCarrierNameToGridIndexList, ([carrierName,]) => carrierName === carrier.name);
		assert(!_.isUndefined(result));
		const [, gridIndex] = result;
		set(carrier.name, gridIndex, 1, 'labwareModelName', labwareModelName);
	});

	return layout;
}

/**
 * Extract array where the array index is the grid index and the value is the carrier ID.
 * This information is on the first line of the table definition.
 * A -1 value for the carrier ID means that there is no carrier at that grid.
 * @param  {array} l - elements of line
 * @return {array} array of carrier IDs on this table
 */
function parse14_getCarrierIds(l) {
	return l.map(s => parseInt(s));
}

/**
 * Get array of labwares on the table.
 * @param  {EvowareCarrierData} carrierData
 * @param  {EvowareSemicolonFile} lines - lines of table file
 * @return {array} an array of tuples (carrier name, gridIndex, siteIndex, site label, labware model name)
 */
function parse14_getLabwareObjects(carrierData, carrierIdsInternal, lines) {
	const result = [];
	carrierIdsInternal.forEach((carrierId, gridIndex) => {
		if (carrierId > -1) {
			const carrier = carrierData.getCarrierById(carrierId);
			const [n0, l0] = lines.nextSplit();
			const [n1, l1] = lines.nextSplit();
			//console.log({n0, l0, n1, l1, carrierId, carrierName: carrierData.carrierIdToName[carrierId], carrier})
			assert(n0 === 998 && n1 === 998 && parseInt(l0[0]) === carrier.siteCount);
			//println(iGrid+": "+carrier)
			_.times(carrier.siteCount, siteIndex => {
				const labwareModelName = l0[siteIndex+1];
				if (!_.isEmpty(labwareModelName)) {
					const item = [carrier.name, gridIndex, siteIndex+1, l1[siteIndex], labwareModelName];
					result.push(item);
				}
			});
		}
		else {
			lines.skip(1);
		}
	});
	return result;
}

/**
 * Parse the hotel objects
 * @param  {EvowareSemicolonFile} lines - lines of table file
 * @return {array} an array of HotelObjects
 */
function parse14_getHotelObjects(lines) {
	const [n0, l0] = lines.nextSplit();
	assert(n0 === 998);
	const count = parseInt(l0[0]);
	return _.times(count, () => {
		const [n, l] = lines.nextSplit();
		assert(n == 998);
		const id = parseInt(l[0]);
		const iGrid = parseInt(l[1]);
		return new HotelObject(id, iGrid);
	});
}

/**
 * Parse the external objects.
 * @param  {EvowareSemicolonFile} lines - lines of table file
 * @return {array} an array of external objects
 */
function parse14_getExternalObjects(lines) {
	const [n0, l0] = lines.nextSplit();
	assert(n0 === 998);
	const count = parseInt(l0[0]);
	return _.times(count, () => {
		const [n, l] = lines.nextSplit();
		assert(n == 998);
		const n1 = parseInt(l[0]);
		const n2 = parseInt(l[1]);
		const carrierName = l[2];
		return new ExternalObject(n1, n2, carrierName);
	});
}

/**
 * Parse labware on external sites
 * @param  {EvowareSemicolonFile} lines - lines of table file
 * @return {object} list of tuples (carrier ID, labware model name)
 */
function parse14_getExternalLabwares(lines) {
	const [n0, l0] = lines.nextSplit();
	assert(n0 === 998);
	const count = parseInt(l0[0]);
	return _.times(count, i => {
		const [n, l] = lines.nextSplit();
		assert(n == 998);
		const carrierId = parseInt(l[0]);
		const labwareModelName = l[1];
		return [carrierId, labwareModelName];
	});
}

function parse14_getExternalCarrierGrids(externalObjects, lines) {
	return externalObjects.map(external => {
		const [n, l] = lines.nextSplit();
		assert(n === 998);
		//console.log("carrierName: "+external.carrierName);
		// we need to force the system liquid to be on grid -1
		const gridIndex = (external.carrierName === "System") ? -1 : parseInt(l[0]);
		return [external.carrierName, gridIndex];
	});
}

/**
 * Create a string representation of an Evoware table layout
 * @param  {EvowareCarrierData} carrierData - data loaded from an evoware carrier file
 * @param  {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 * @return {string} string representation of table layout
 */
export function toStrings(carrierData, table) {
	const l1 = [
		"00000000",
		"20000101_000000 No log in       ",
		"                                                                                                                                ",
		"No user logged in                                                                                                               ",
		"--{ RES }--",
		"V;200",
		"--{ CFG }--",
		"999;219;32;"
	];
	const s2 = toString_internalCarriers(carrierData, table);
	const l3 = toStrings_internalLabware(carrierData, table);
	const l4 = toStrings_hotels(carrierData, table);
	const l5 = toStrings_externals(carrierData, table);
	const l6 = [
		"996;0;0;",
		"--{ RPG }--"
	];
	const l = _.flatten([l1, s2, l3, l4, l5, l6]);
	//console.log(l.join("\n"));
	return l;
}

/**
 * Create a string representation of the internal carriers
 * @param  {EvowareCarrierData} carrierData - data loaded from an evoware carrier file
 * @param  {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 * @return {string} string representation of internal carriers
 */
export function toString_internalCarriers(carrierData, table) {
	// Get list [[gridIndex, carrier.id]] for internal sites
	// [[a, b]]
	const gridToCarrierName_l = _(table).map((c, carrierName) => {
		//console.log({c, carrierName})
		return _.map(c, (x, gridIndexText) => {
			if (x.internal) {
				try {
					const gridIndex = parseInt(gridIndexText);
					return [gridIndex, carrierName];
				} catch(e) {
					// Do nothing
				}
			}
			return undefined;
		});
	}).flatten().compact().value();
	const gridToCarrierName_m = _.fromPairs(gridToCarrierName_l);
	//console.log({gridToCarrierId_l, gridToCarrierId_m})

	const l = _.times(99, gridIndex => {
		const carrierName = gridToCarrierName_m[gridIndex];
		return (_.isEmpty(carrierName)) ? -1 : carrierData.getCarrierByName(carrierName).id;
	});

	return `14;${l.join(";")};`;
}

/**
 * Create a string representation of the internal labware
 * @param  {EvowareCarrierData} carrierData - data loaded from an evoware carrier file
 * @param  {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 * @return {string} string representation of internal labware
 */
export function toStrings_internalLabware(carrierData, table) {
	const items0 = [];
	_.forEach(table, (c, carrierName) => {
		_.forEach(c, (g, gridIndexText) => {
			if (g.internal === true) {
				const carrierId = _.get(carrierData.getCarrierByName(carrierName), 'id', -1);
				items0.push({carrierName, carrierId, gridIndex: parseInt(gridIndexText), g});
			}
		});
	});
	// Sort by gridIndex
	const items = _.sortBy(items0, 'gridIndex');
	//console.log("items:")
	//console.log(items)
	const gridIndexToItem = _(items).map(x => [x.gridIndex, x]).fromPairs().value();
	//console.log({gridIndexToItem})

	return _.flatten(_.times(99, gridIndex => {
		const item = gridIndexToItem[gridIndex];
		if (_.isUndefined(item)) {
			return "998;0;";
		}
		else {
			const carrier = carrierData.getCarrierByName(item.carrierName);
			//console.log({item, carrier})
			//val sSiteCount = if (carrier.nSites > 0) carrier.nSites.toString else ""
			const namesAndLabels = _.times(carrier.siteCount, siteIndex => {
				//console.log({g: item.g})
				const labwareModelName = _.get(item.g, [siteIndex + 1, 'labwareModelName'], "");
				const label = _.get(item.g, [siteIndex + 1, 'label'], "");
				return {labwareModelName, label};
			})
			return [
				`998;${carrier.siteCount};${_.map(namesAndLabels, x => x.labwareModelName).join(';')};`,
				`998;${_.map(namesAndLabels, x => x.label).join(';')};`,
			]
		}
	}));
}

/**
 * Create a string representation of the hotels
 * @param  {EvowareCarrierData} carrierData - data loaded from an evoware carrier file
 * @param  {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 * @return {string} string representation of hotels
 */
export function toStrings_hotels(carrierData, table) {
	const hotelItems0 = [];
	_.forEach(table, (c, carrierName) => {
		_.forEach(c, (g, gridIndexText) => {
			if (g.hotel === true) {
				const carrierId = carrierData.getCarrierByName(carrierName).id;
				hotelItems0.push([carrierId, parseInt(gridIndexText)]);
			}
		});
	});
	//console.log({hotelItems0});
	const hotelItems = _.sortBy(hotelItems0, x => x[1]);
	return _.flatten([
		`998;${hotelItems.length};`,
		hotelItems.map(([carrierId, gridIndex]) => `998;${carrierId};${gridIndex};`)
	]);
}

/**
 * Create a string representation of external carriers
 * @param  {EvowareCarrierData} carrierData - data loaded from an evoware carrier file
 * @param  {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName}
 * @return {string} string representation of external carriers
 */
export function toStrings_externals(carrierData, table) {
	const items0 = [];
	_.forEach(table, (c, carrierName) => {
		_.forEach(c, (g, gridIndexText) => {
			if (g.external) {
				const carrierId = _.get(carrierData.getCarrierByName(carrierName), 'id', -1);
				items0.push({carrierName, carrierId, gridIndex: parseInt(gridIndexText), g});
			}
		});
	});
	// Sort by carrierId
	const items = _.sortBy(items0, 'carrierId');
	// Generate list of external objects and their carriers
	const l1 = _.flatten([
		`998;${items.length};`,
		items.map(({carrierName, g}) => `998;${g.external.n1};${g.external.n2};${carrierName};`)
	]);
	// Generate list of labware models
	const itemsWithLabware = items.filter(item => _.has(item, "g.1.labwareModelName"));
	const l2 = _.flatten([
		`998;${itemsWithLabware.length};`,
		itemsWithLabware.map(({carrierId, g}) => `998;${carrierId};${_.get(g, "1.labwareModelName")};`)
	]);
	// Generate grid list
	const l3 = items.map(({gridIndex}) => `998;${(gridIndex === -1) ? 1 : gridIndex};`);

	return _.concat(l1, l2, l3);
}