src/converter/ArtistooWriter.js

import Writer from "./Writer.js"

class ArtistooWriter extends Writer {

	constructor( model, config ){
		super( model, config )

		this.mode = config.mode || "html"
		this.out = ""
		this.modelconfig = {}
		this.custommethods = {}
		this.methodDeclarations = ""



		this.FPSMeterPath = config.FPSMeterPath || "https://artistoo.net/examples/fpsmeter.min.js"
		this.browserLibrary = config.browserLibrary || ".artistoo.js" // "https://artistoo.net/examples/artistoo.js"
		this.nodeLibrary = config.nodeLibrary || "https://raw.githubusercontent.com/ingewortel/artistoo/master/build/artistoo-cjs.js"
		this.styleSheet = config.styleSheet || "./modelStyle.css"

		this.logString = "Hi there! Converting " + this.model.from + " to Artistoo...\n\n"

	}

	write(){
		if( this.mode === "html" ){
			//console.log( this.writeHTML() )
			this.target.innerHTML = this.writeHTML()
		}
		this.writeLog()
	}


	writeHTML(){
		return this.writeHTMLHead() +
			this.writeConfig() +
			this.setInitialisation() +
			this.customMethodsString() +
			this.writeBasicScript() +
			this.writeHTMLBody()
	}

	writeNode(){
		return "let CPM = require(\"../../build/artistoo-cjs.js\")" +
			this.writeConfig() +
			this.writeBasicScript()
	}

	writeHTMLHead( ){

		let string = "<html lang=\"en\"><head><meta http-equiv=\"Content-Type\" " +
			"content=\"text/html; charset=UTF-8\">\n" +
			"\t<title>" + this.model.modelInfo.title + "</title>\n"+
			"\t<link rel=\"stylesheet\" href=\"" + this.styleSheet + "\" />" +
			/*"\t<style type=\"text/css\">\n" +
			"\t\t body{\n"+
			"\t\t\t font-family: \"HelveticaNeue-Light\", \"Helvetica Neue Light\", \"Helvetica Neue\", " +
			"Helvetica, Arial, \"Lucida Grande\", sans-serif; \n" +
			"\t\t\t padding : 15px; \n" +
			"\t\t} \n" +
			"\t\t td { \n" +
			"\t\t\t padding: 10px; \n" +
			"\t\t\t vertical-align: top; \n" +
			"\t\t } \n" +
			"\t </style> \n" +*/
			"\t" + "<script src=\"" + this.browserLibrary + "\"></script> \n" + //'\t <script src="https://artistoo.net/examples/artistoo.js"></script> \n' +
			"\t" + "<script src=\"" + this.FPSMeterPath + "\"></script> \n\n" +
			"<script> \n\n\n" +
			"\"use strict\" \n" +
			"var sim, meter \n\n"

		return(string)

	}

	writeHTMLBody(){

		let modelDesc = this.model.modelInfo.desc
		modelDesc = this.htmlNewLine( modelDesc )

		return "</script> \n\n" +
			"</head>\n" +
			"<body onload=\"initialize();parent.window.model = sim\"> \n" +
			"<h1>"+this.model.modelInfo.title + "</h1> \n"+
			"<p>\n\t" + modelDesc + "\n" +
			"</p>\n" +
			"</body> \n" +
			"</html>"

	}

	writeConfig(){
		this.setModelConfig()
		return "let config = " + this.objToString( this.modelconfig ) + "\n\n"
	}

	customMethodsString(){
		let string = "let custommethods = {\n"
		for( let m of Object.keys( this.custommethods ) ){
			string += "\t" + m + " : " + m + ",\n"
		}
		return string + "}"
	}

	/* TO DO */
	setModelConfig(){

		// Initialize structure
		let config = {
			conf : {},
			simsettings : {
				zoom : 2,
				CANVASCOLOR : "EEEEEE"
			}
		}

		// Time information; warn if start time != 0
		if( this.model.timeInfo.start !== 0 ){
			this.conversionWarnings.time.push(
				"Morpheus model time starts at t = " + this.model.timeInfo.start +
				". Interpreting time before that as a burnin time, but in Artistoo " +
				" time will restart at t = 0 after this burnin."
			)
		}
		config.simsettings.BURNIN = parseInt( this.model.timeInfo.start )
		config.simsettings.RUNTIME = parseInt( this.model.timeInfo.duration )
		config.simsettings.RUNTIME_BROWSER = parseInt( this.model.timeInfo.duration )

		// Grid information, warn if grid has to be converted.
		config.ndim = this.model.grid.ndim
		config.field_size = this.model.grid.extents
		if( this.model.grid.geometry === "hexagonal" ){
			this.conversionWarnings.grid.push(
				"Grid of type 'hexagonal' is not yet supported in Artistoo. " +
				"Converting to square 2D lattice instead. You may have to adjust some parameters, " +
				"especially where neighborhood sizes matter (eg PerimeterConstraint, Adhesion)."
			)
		}
		config.torus = []
		const dimNames = ["x","y","z"]
		for( let d = 0; d < config.ndim; d++ ){
			const bound = this.model.grid.boundaries[d]
			switch( bound ){
			case "periodic" :
				config.torus.push( true )
				break
			case "noflux" :
				config.torus.push( false )
				break
			default :
				config.torus.push( true )
				this.conversionWarnings.grid.push(
					"unknown boundary condition in " + dimNames[d] + "-dimension: " +
					bound + "; reverting to default periodic boundary."
				)
			}

			// special case for "linear" geometry, which in Artistoo is just
			// a 2D grid with a field_size [x,1] and torus = [x, false].
			if( this.model.grid.geometry === "linear" ){
				config.torus[1] = false
			}
		}
		if( !isNaN( this.model.grid.neighborhood.distance ) ){
			this.conversionWarnings.grid.push(
				"You are trying to set a neighborhood with distance = " +
				this.model.grid.neighborhood.distance + ", " +
				"but this is currently not supported in Artistoo. Reverting to" +
				"default (Moore) neighborhood; behaviour may change."
			)
		}
		if( !isNaN( this.model.grid.neighborhood.order ) &&  this.model.grid.neighborhood.order !== 2 ){
			this.conversionWarnings.grid.push(
				"You are trying to set a neighborhood with order = " +
				this.model.grid.neighborhood.order + ", " +
				"but this is currently not supported in Artistoo. Reverting to" +
				"default (Moore) neighborhood; behaviour may change."
			)
		}

		// CPM kinetics
		config.conf.T = this.model.kinetics.T
		config.conf.seed = this.model.kinetics.seed

		this.modelconfig = config

		// CellKinds
		config.simsettings.NRCELLS = this.model.initCellKindVector( 0, false )
		config.simsettings.SHOWBORDERS = this.model.initCellKindVector( true, false )
		config.simsettings.CELLCOLOR = this.model.initCellKindVector( "333333", false )
		for( let k = 1; k < this.model.cellKinds.count - 1; k++ ){
			// Overwrite cellcolors for all kinds except the first with a
			// randomly generated color.
			config.simsettings.CELLCOLOR[k] =
				Math.floor(Math.random()*16777215).toString(16).toUpperCase()
		}

		// Constraints
		// First constraints that can go in the main conf object (via auto-adder)
		let constraintString = ""
		for( let cName of Object.keys( this.model.constraints.constraints ) ){
			const constraintArray = this.model.constraints.constraints[cName]
			for( let ci = 0; ci < constraintArray.length; ci++ ){
				const constraintConf = constraintArray[ci]
				constraintString += this.addConstraintToConfig( cName, constraintConf )
			}
		}
		if( constraintString !== "" ){
			this.addCustomMethod( "addConstraints", "", constraintString )
		}
	}

	addCustomMethod( methodName, args, contentString ){
		if( this.custommethods.hasOwnProperty( methodName ) ){
			throw( "Cannot add two custom methods of the same name!" )
		}
		this.custommethods[methodName] = methodName
		this.methodDeclarations += "function " + methodName + "( " + args + "){\n\n\t" +
			contentString + "\n}\n\n"
	}

	addConstraintToConfig( cName, cConf ){

		const autoAdded = {
			ActivityConstraint : true,
			Adhesion : true,
			VolumeConstraint : true,
			PerimeterConstraint : true,
			BarrierConstraint : true
		}

		// Constraints that can be directly added to config.conf:
		if( autoAdded.hasOwnProperty(cName) ) {
			for (let parameter of Object.keys(cConf)) {
				this.modelconfig.conf[parameter] = cConf[parameter]
			}

			// ActivityConstraint special case; set ACTCOLOR
			if (cName === "ActivityConstraint") {
				// check which kinds have activity; skip background
				let hasAct = []
				for (let k = 1; k < this.model.cellKinds.count; k++) {
					if (cConf.LAMBDA_ACT[k] > 0 && cConf.MAX_ACT[k] > 0) {
						hasAct.push(true)
					} else {
						hasAct.push(false)
					}
				}
				this.modelconfig.simsettings.ACTCOLOR = hasAct
			}

			// Another special case for the PerimeterConstraint, which may
			// have to be converted depending on 'mode' and the 'ShapeSurface'.
			else if (cName === "PerimeterConstraint") {
				switch (cConf.mode) {
				case "surface" :
					// do nothing
					break
				case "aspherity" : {
					// correct the 'target' perimeter.
					let P = cConf.P
					const volume = this.modelconfig.conf.V
					for (let i = 0; i < P.length; i++) {
						if (this.modelconfig.ndim === 2) {
							P[i] = 4 * P[i] * 2 * Math.sqrt(volume[i] * Math.PI)
						} else if (this.modelconfig.ndim === 3) {
							P[i] = P[i] * 4 * Math.PI * Math.pow((3 / 4) * volume[i] / Math.PI, 2 / 3)
						}
						P[i] = parseFloat(P[i])
					}
					this.conversionWarnings.constraints.push(
						"Artistoo does not support the 'aspherity' mode of the Morpheus <SurfaceConstraint>." +
						"Adding a regular PerimeterConstraint (mode 'surface') instead. I am converting the " +
						"target perimeter with an educated guess, but behaviour may be slightly different; " +
						"please check parameters."
					)

					this.modelconfig.conf.P = P
					break
				}

				}
				delete this.modelconfig.conf.mode
			}

			return ""
		}

		// For the other constraints, add them by overwriting the
		// Simulation.addConstraints() method. Return the string of code
		// to add in this method.
		const otherSupportedConstraints = {
			LocalConnectivityConstraint : true,
			PersistenceConstraint : true,
			PreferredDirectionConstraint : true,
			ChemotaxisConstraint : true
		}

		if( !otherSupportedConstraints[cName] ){
			this.conversionWarnings.constraints.push(
				"Ignoring unknown constraint of type " + cName + ". Behaviour may change.")
		}

		// Special case for the PersistenceConstraint: warn if
		// protrusion/retraction setting does not correspond.
		if (cName === "PersistenceConstraint" || cName === "PreferredDirectionConstraint" ){
			const protrude = cConf.PROTRUDE
			const retract = cConf.RETRACT
			const lambda = cConf.LAMBDA_DIR

			let warn = false
			for( let k = 0; k < protrude.length; k++ ){

				if( lambda[k] > 0 ) {

					if (!protrude[k]) {
						warn = true
					}
					if (retract[k]) {
						warn = true
					}
					if (warn) {
						this.conversionWarnings.constraints.push(
							"You are trying to set a PersistenceConstraint for cellkind " +
							this.model.getKindName(k) + " with protrusion = " +
							protrude[k] + " and retraction = " + retract[k] + ", but Artistoo " +
							"only supports protrusion = true and retraction = false. " +
							"Reverting to these settings. Behaviour may change slightly; " +
							"if this is important, consider implementing your own constraint."
						)
					}
				}
			}
			delete cConf.PROTRUDE
			delete cConf.RETRACT
		}

		return "this.C.add( new CPM." + cName + "( " + this.objToString( cConf, 1 ) + ") )\n\n\t"

	}

	writeBasicScript(){

		return "\n\n" +
			"function initialize(){ \n" +
			"\t" + "sim = new CPM.Simulation( config, custommethods ) \n" +
			"\t" + "meter = new FPSMeter({left:\"auto\", right:\"5px\"}) \n\n"+
			"\t" + "step() \n" +
			"} \n\n" +
			"function step(){ \n\t" +
			"sim.step() \n\t" +
			"meter.tick() \n\n\t" +
			"if( sim.conf[\"RUNTIME_BROWSER\"] == \"Inf\" | sim.time+1 < sim.conf[\"RUNTIME_BROWSER\"] ){ \n\t" +
			"\t\t" + "requestAnimationFrame( step ) \n" +
			"\t} \n}\n\n" + this.methodDeclarations

	}

	setInitialisation(){
		// Initializers
		let initString = ""
		for( let initConf of this.model.setup.init ){
			initString += this.addInitializer( initConf )
		}
		if( initString !== "" ){
			initString = "" + "this.addGridManipulator()\n\n" + initString
			this.addCustomMethod( "initializeGrid", "", initString )
		}
		return ""
	}

	addInitializer( conf ){

		const kindIndex = conf.kind
		switch( conf.setter ){

		case "circleObject" :
			return "\t" + "this.gm.assignCellPixels( this.gm.makeCircle( [" +
				conf.center.toString() + "], " + conf.radius +  ") , " + kindIndex + " )\n"

		case "boxObject" :
			return "\t" + "this.gm.assignCellPixels( this.gm.makeBox( [" +
				conf.bottomLeft.toString() + "], [" + conf.boxSize.toString() +
				"] ) , " + kindIndex + " )\n"

		case "cellCircle" :
			return "\t" + "this.gm.seedCellsInCircle( " + kindIndex + ", " +
				conf.nCells + ", [" + conf.center.toString() + "], " +
				conf.radius + " )\n"

		case "pixelSet" : {
			let out = "[\n\t\t"
			for (let i = 0; i < conf.pixels.length; i++ ) {
				const p = conf.pixels[i]
				out += "[" + p.toString() + "]"
				if( i < conf.pixels.length - 1 ){ out += "," }
			}
			return "\t" + "this.gm.assignCellPixels( " + out +
				" ], " + kindIndex + ")\n"

		}

		default :
			this.conversionWarnings.init.push( "Unknown initializer "
			+ conf.setter + "; ignoring." )
		}

	}

}

export default ArtistooWriter