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