diff --git a/__version__.py b/__version__.py index a9f7c69a73..b6b2f10e76 100644 --- a/__version__.py +++ b/__version__.py @@ -5,7 +5,7 @@ # @website https://github.com/stochss/stochss # ============================================================================= -__version__ = '2.4.7' +__version__ = '2.4.8' __title__ = 'StochSS' __description__ = 'StochSS is an integrated development environment (IDE) \ for simulation of biochemical networks.' diff --git a/client/app.js b/client/app.js index af22f9612a..786121e86f 100644 --- a/client/app.js +++ b/client/app.js @@ -100,31 +100,45 @@ let registerRenderSubview = (parent, view, hook) => { let getXHR = (endpoint, { always = function (err, response, body) {}, success = function (err, response, body) {}, error = function (err, response, body) {}}={}) => { - xhr({uri: endpoint, json: true}, function (err, response, body) { - if(response.statusCode < 400) { - success(err, response, body); - }else if(response.statusCode < 500) { - error(err, response, body); - }else{ - console.log("Critical Error Detected"); - } - always(err, response, body); - }); + try { + xhr({uri: endpoint, json: true}, function (err, response, body) { + if(response.statusCode < 400) { + success(err, response, body); + }else if(response.statusCode < 500) { + error(err, response, body); + }else{ + console.log("Critical Error Detected"); + } + always(err, response, body); + }); + }catch(exception){ + console.log(exception); + let response = {Reason: "Network Error", Message: exception}; + let body = {response: response, err: exception} + error(exception, response, body); + } }; let postXHR = (endpoint, data, { always = function (err, response, body) {}, success = function (err, response, body) {}, error = function (err, response, body) {}}={}, isJSON) => { - xhr({uri: endpoint, json: isJSON !== undefined ? isJSON : true, method: "post", body: data}, function (err, response, body) { - if(response.statusCode < 400) { - success(err, response, body); - }else if(response.statusCode < 500) { - error(err, response, body); - }else{ - console.log("Critical Error Detected"); - } - always(err, response, body); - }); + try { + xhr({uri: endpoint, json: isJSON !== undefined ? isJSON : true, method: "post", body: data}, function (err, response, body) { + if(response.statusCode < 400) { + success(err, response, body); + }else if(response.statusCode < 500) { + error(err, response, body); + }else{ + console.log("Critical Error Detected"); + } + always(err, response, body); + }); + }catch(exception){ + console.log(exception); + let response = {Reason: "Network Error", Message: exception}; + let body = {response: response, err: exception} + error(exception, response, body); + } }; let getBrowser = () => { diff --git a/client/domain-view/domain-view.js b/client/domain-view/domain-view.js new file mode 100644 index 0000000000..6afabcda6f --- /dev/null +++ b/client/domain-view/domain-view.js @@ -0,0 +1,544 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +let _ = require('underscore'); +//support files +let app = require('../app'); +let Plotly = require('../lib/plotly'); +//models +let Particle = require('../models/particle'); +//views +let View = require('ampersand-view'); +let TypesView = require('./views/types-view'); +let LimitsView = require('./views/limits-view'); +let QuickviewType = require('./views/quickview-type'); +let PropertiesView = require('./views/properties-view'); +let EditParticleView = require('./views/particle-view'); +let ViewParticleView = require('./views/view-particle'); +let FillGeometryView = require('./views/fill-geometry-view'); +let ImportMeshView = require('./views/import-mesh-view'); +let Edit3DDomainView = require('./views/edit-3D-domain-view'); +let TypesDescriptionView = require('./views/types-description-view'); +//templates +let template = require('./domainView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse-domain-particles]' : 'changeCollapseButtonText', + 'click [data-hook=add-new-particle]' : 'handleAddParticle', + 'click [data-hook=save-selected-particle]' : 'handleSaveParticle', + 'click [data-hook=remove-selected-particle]' : 'handleRemoveParticle', + 'click [data-hook=collapse-domain-figure]' : 'changeCollapseButtonText' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.plot = attrs.plot ? attrs.plot : null; + this.elements = attrs.elements ? attrs.elements : null; + this.queryStr = attrs.queryStr; + this.newPart = this.createNewParticle(); + this.actPart = {"part":null, "tn":0, "pn":0}; + this.model.updateValid(); + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + this.renderPropertiesView(); + this.renderLimitsView(); + this.renderTypesView(); + if(this.readOnly) { + $(this.queryByHook('domain-particles-editor')).css('display', 'none'); + $(this.queryByHook('domain-figure-preview')).css('display', 'none'); + this.renderTypesQuickview(); + }else{ + this.updateParticleViews({includeGeometry: true}); + this.renderTypesDescriptionView(); + } + if(!this.elements) { + this.elements = { + figure: this.queryByHook('domain-figure'), + figureEmpty: this.queryByHook('domain-figure-empty') + } + } + let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot") + this.queryStr; + if(this.plot) { + this.displayFigure(); + }else{ + app.getXHR(endpoint, {success: (err, response, body) => { + this.plot = body.fig; + this.traceTemp = body.trace_temp; + this.displayFigure(); + }}); + } + }, + add3DDomain: function (limits, particles) { + let limitsChanged = this.changeDomainLimits(limits, false); + particles.forEach((particle) => { + particle = new Particle(particle); + this.model.particles.addParticle({particle: particle}); + this.addParticle({particle: particle}); + }); + if(limitsChanged) { + this.renderLimitsView(); + } + this.renderTypesView(); + this.resetFigure(); + }, + addMeshDomain: function (limits, particles, types, reset) { + let limitsChanged = this.changeDomainLimits(limits, reset); + if(types) { + this.addMissingTypes(types); + } + particles.forEach((particle) => { + particle = new Particle(particle); + this.model.particles.addParticle({particle: particle}); + this.addParticle({particle: particle}); + }); + if(limitsChanged) { + this.renderLimitsView(); + } + this.renderTypesView(); + if(types) { + this.updateParticleViews(); + } + this.resetFigure(); + }, + addMissingTypes: function (typeIDs) { + typeIDs.forEach((typeID) => { + if(!this.model.types.get(typeID, 'typeID')) { + let name = this.model.types.addType(); + this.addType(name, {update: false}); + } + }); + }, + addParticle: function ({particle=this.newPart}={}) { + this.plot.data[particle.type].ids.push(particle.particle_id); + this.plot.data[particle.type].x.push(particle.point[0]); + this.plot.data[particle.type].y.push(particle.point[1]); + this.plot.data[particle.type].z.push(particle.point[2]); + }, + addType: function (name, {update=true}={}) { + let newTrace = JSON.parse(JSON.stringify(this.traceTemp)); + newTrace.name = name; + this.plot.data.push(newTrace); + if(update) { + this.updateParticleViews(); + } + }, + applyGeometry: function (ids, type) { + let actPart = JSON.parse(JSON.stringify(this.actPart)); + ids.forEach((id) => { + let particle = this.model.particles.get(id, 'particle_id'); + // Set active particle for updating particle in the plot + this.actPart = { + part: particle, + tn: particle.type, + pn: this.plot.data[particle.type].ids.indexOf(particle.particle_id) + } + // Update the particle attributes + particle.type = type.typeID; + particle.mass = type.mass; + particle.volume = type.volume; + particle.rho = type.rho; + particle.nu = type.nu; + particle.c = type.c; + particle.fixed = type.fixed; + // Update the particle in the figure + this.changeParticleType(type.typeID, {update: false}); + }); + if(actPart.part) { + this.actPart = { + part: this.model.particles.get(actPart.part.particle_id, "particle_id"), + tn: type.typeID, + pn: this.plot.data[type.typeID].ids.indexOf(actPart.part.particle_id) + } + if(ids.includes(actPart.part.particle_id)) { + this.renderEditParticleView(); + } + }else{ + this.actPart = actPart + } + this.renderTypesView(); + this.resetFigure(); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e) + }, + changeDomainLimits: function (limits, reset) { + var limitsChanged = false; + if(reset) { + this.model.x_lim = limits.x_lim; + this.model.y_lim = limits.y_lim; + this.model.y_lim = limits.y_lim; + limitsChanged = true; + }else{ + if(this.model.x_lim[0] > limits.x_lim[0]) { + this.model.x_lim[0] = limits.x_lim[0]; + limitsChanged = true; + } + if(this.model.y_lim[0] > limits.y_lim[0]) { + this.model.y_lim[0] = limits.y_lim[0]; + limitsChanged = true; + } + if(this.model.z_lim[0] > limits.z_lim[0]) { + this.model.z_lim[0] = limits.z_lim[0]; + limitsChanged = true; + } + if(this.model.x_lim[1] < limits.x_lim[1]) { + this.model.x_lim[1] = limits.x_lim[1]; + limitsChanged = true; + } + if(this.model.y_lim[1] < limits.y_lim[1]) { + this.model.y_lim[1] = limits.y_lim[1]; + limitsChanged = true; + } + if(this.model.z_lim[1] < limits.z_lim[1]) { + this.model.z_lim[1] = limits.z_lim[1]; + limitsChanged = true; + } + } + return limitsChanged; + }, + changeParticleLocation: function () { + this.plot.data[this.actPart.tn].x[this.actPart.pn] = this.actPart.part.point[0]; + this.plot.data[this.actPart.tn].y[this.actPart.pn] = this.actPart.part.point[1]; + this.plot.data[this.actPart.tn].z[this.actPart.pn] = this.actPart.part.point[2]; + }, + changeParticleType: function (type, {update=true}={}) { + let id = this.plot.data[this.actPart.tn].ids.splice(this.actPart.pn, 1)[0]; + let x = this.plot.data[this.actPart.tn].x.splice(this.actPart.pn, 1)[0]; + let y = this.plot.data[this.actPart.tn].y.splice(this.actPart.pn, 1)[0]; + let z = this.plot.data[this.actPart.tn].z.splice(this.actPart.pn, 1)[0]; + this.plot.data[type].ids.push(id); + this.plot.data[type].x.push(x); + this.plot.data[type].y.push(y); + this.plot.data[type].z.push(z); + if(update) { + this.resetFigure(); + } + }, + createNewParticle: function () { + let type = this.model.types.get(0, 'typeID'); + return new Particle({ + c: type.c, + fixed: type.fixed, + mass: type.mass, + nu: type.nu, + point: [0, 0, 0], + rho: type.rho, + type: type.typeID, + volume: type.volume + }); + }, + completeAction: function (prefix) { + $(this.queryByHook(`${prefix}-in-progress`)).css("display", "none"); + $(this.queryByHook(`${prefix}-complete`)).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook(`${prefix}-complete`)).css('display', 'none'); + }, 5000); + }, + deleteParticle: function () { + this.plot.data[this.actPart.tn].ids.splice(this.actPart.pn, 1); + this.plot.data[this.actPart.tn].x.splice(this.actPart.pn, 1); + this.plot.data[this.actPart.tn].y.splice(this.actPart.pn, 1); + this.plot.data[this.actPart.tn].z.splice(this.actPart.pn, 1); + this.resetFigure(); + }, + deleteType: function (type, {unassign=true}={}) { + if(unassign) { + this.unassignAllParticles(type, {update: false}); + }else{ + if(this.actPart.part && this.actPart.part.type === type) { + this.actPart = {"part":null, "tn":0, "pn":0}; + } + if(this.newPart && this.newPart.type === type) { + this.newPart.type = 0 + } + let particles = this.model.particles.filter((particle) => { + return particle.type === type; + }); + this.model.particles.removeParticles(particles); + } + this.model.realignTypes(type); + this.plot.data.splice(type, 1); + this.renderTypesView(); + this.updateParticleViews({includeGeometry: true}); + this.resetFigure(); + }, + displayFigure: function () { + if(this.model.particles.length > 0) { + $(this.elements.figureEmpty).css('display', 'none'); + $(this.elements.figure).css('display', 'block'); + Plotly.newPlot(this.elements.figure, this.plot); + this.elements.figure.on('plotly_click', _.bind(this.selectParticle, this)); + }else{ + $(this.elements.figureEmpty).css('display', 'block'); + $(this.elements.figure).css('display', 'none'); + } + }, + handleAddParticle: function () { + this.startAction("anp") + this.model.particles.addParticle({particle: this.newPart}); + this.addParticle(); + this.resetFigure(); + this.renderTypesView(); + this.newPart = this.createNewParticle(); + this.renderNewParticleView(); + this.completeAction("anp"); + }, + handleRemoveParticle: function () { + this.startAction("rsp") + this.model.particles.removeParticle(this.actPart.part); + this.deleteParticle(); + this.actPart = {"part":null, "tn":0, "pn":0}; + this.renderTypesView(); + this.renderEditParticleView(); + this.completeAction("rsp") + }, + handleSaveParticle: function () { + this.startAction("esp"); + if(this.editParticleView.origType !== this.actPart.part.type) { + this.changeParticleType(this.actPart.part.type, {update: false}); + this.renderTypesView(); + } + if(!this.actPart.part.comparePoint(this.editParticleView.origPoint)) { + this.changeParticleLocation(); + } + this.resetFigure(); + this.completeAction("esp"); + }, + removeFigure: function () { + try { + this.elements.figure.removeListener('plotly_click', this.selectParticle); + Plotly.purge(this.elements.figure); + }catch (err) { + return + } + }, + renameType: function (index, name) { + this.plot.data[index].name = name; + this.resetFigure(); + }, + renderEdit3DDomainView: function () { + if(this.edit3DDomainView) { + this.edit3DDomainView.remove(); + } + this.edit3DDomainView = new Edit3DDomainView(); + app.registerRenderSubview(this, this.edit3DDomainView, "3d-domain-container"); + }, + renderEditParticleView: function () { + if(this.editParticleView) { + this.editParticleView.remove(); + } + let disable = this.actPart.part == null + this.editParticleView = new EditParticleView({ + model: this.actPart.part ? this.actPart.part : this.createNewParticle(), + defaultType: this.model.types.get(this.actPart.part ? this.actPart.part.type : 0, "typeID"), + viewIndex: 1, + disable: disable + }); + app.registerRenderSubview(this, this.editParticleView, "edit-particle-container"); + $(this.queryByHook("edit-select-message")).css('display', disable ? 'block' : 'none'); + $(this.queryByHook("save-selected-particle")).prop('disabled', disable); + $(this.queryByHook("remove-selected-particle")).prop('disabled', disable); + }, + renderFillGeometryView: function () { + if(this.fillGeometryView) { + this.fillGeometryView.remove(); + } + this.fillGeometryView = new FillGeometryView(); + app.registerRenderSubview(this, this.fillGeometryView, 'fill-geometry-container'); + }, + renderImportMeshView: function () { + if(this.importMeshView) { + this.importMeshView.remove(); + } + this.importMeshView = new ImportMeshView(); + app.registerRenderSubview(this, this.importMeshView, "import-particles-section"); + }, + renderLimitsView: function () { + if(this.limitsView) { + this.limitsView.remove(); + } + this.limitsView = new LimitsView({ + model: this.model, + readOnly: this.readOnly + }); + app.registerRenderSubview(this, this.limitsView, "domain-limits-container"); + }, + renderNewParticleView: function () { + if(this.newParticleView) { + this.newParticleView.remove(); + } + this.newParticleView = new EditParticleView({ + model: this.newPart, + defaultType: this.model.types.get(0, "typeID"), + viewIndex: 0 + }); + app.registerRenderSubview(this, this.newParticleView, "new-particle-container"); + }, + renderPropertiesView: function () { + if(this.propertiesView) { + this.propertiesView.remove(); + } + this.propertiesView = new PropertiesView({ + model: this.model, + readOnly: this.readOnly + }); + app.registerRenderSubview(this, this.propertiesView, "domain-properties-container"); + }, + renderTypesDescriptionView: function () { + if(this.typesDescriptionView) { + this.typesDescriptionView.remove(); + } + this.typesDescriptionView = new TypesDescriptionView(); + app.registerRenderSubview(this, this.typesDescriptionView, "particle-types-container"); + }, + renderTypesQuickview: function () { + if(this.typesQuickviewView) { + this.typesQuickviewView.remove(); + } + this.elements.select.css('display', 'block'); + this.typesQuickviewView = this.renderCollection( + this.model.types, + QuickviewType, + this.elements.type + ); + }, + renderTypesView: function () { + if(this.typesView) { + this.typesView.remove(); + } + let particleCounts = {}; + this.model.particles.forEach((particle) => { + if(particleCounts[particle.type]) { + particleCounts[particle.type] += 1; + }else{ + particleCounts[particle.type] = 1; + } + }); + this.model.types.forEach((dType) => { + if(particleCounts[dType.typeID]) { + dType.numParticles = particleCounts[dType.typeID]; + }else{ + dType.numParticles = 0; + } + }); + this.typesView = new TypesView({ + collection: this.model.types, + readOnly: this.readOnly + }); + app.registerRenderSubview(this, this.typesView, "domain-types-container"); + }, + renderViewParticleView: function () { + if(this.viewParticleView) { + this.viewParticleView.remove(); + } + this.elements.select.css('display', 'none'); + this.viewParticleView = new ViewParticleView({ + model: this.actPart.part + }); + app.registerRenderSubview(this.elements.particle.view, this.viewParticleView, this.elements.particle.hook); + }, + resetFigure: function () { + this.removeFigure(); + this.displayFigure(); + }, + selectParticle: function (data) { + let point = data.points[0]; + this.actPart.part = this.model.particles.get(point.id, 'particle_id'); + this.actPart.tn = point.curveNumber; + this.actPart.pn = point.pointNumber; + if(this.readOnly) { + this.renderViewParticleView(); + }else{ + this.renderEditParticleView(); + } + }, + setParticleTypes: function (typeIDs, types) { + this.addMissingTypes(typeIDs); + let actPart = JSON.parse(JSON.stringify(this.actPart)); + types.forEach((type) => { + let particle = this.model.particles.get(type.particle_id, 'particle_id'); + this.actPart = { + part: particle, + tn: particle.type, + pn: this.plot.data[particle.type].ids.indexOf(particle.particle_id) + } + this.changeParticleType(type.typeID, {update: false}); + particle.type = type.typeID; + }); + if(actPart.part && actPart.part.type === type) { + this.actPart = { + part: this.model.particles.get(actPart.part.particle_id, "particle_id"), + tn: 0, + pn: this.plot.data[0].ids.indexOf(actPart.part.particle_id) + } + this.renderEditParticleView(); + }else{ + this.actPart = actPart + } + this.resetFigure(); + this.updateParticleViews(); + }, + startAction: function (prefix) { + $(this.queryByHook(`${prefix}-complete`)).css('display', 'none'); + $(this.queryByHook(`${prefix}-in-progress`)).css("display", "inline-block"); + }, + unassignAllParticles: function (type, {update=true}={}) { + let actPart = JSON.parse(JSON.stringify(this.actPart)); + this.model.particles.forEach((particle) => { + if(particle.type === type) { + this.actPart = { + part: particle, + tn: type, + pn: this.plot.data[type].ids.indexOf(particle.particle_id) + } + this.changeParticleType(0, {update: false}); + particle.type = 0; + } + }); + if(actPart.part) { + this.actPart = { + part: this.model.particles.get(actPart.part.particle_id, "particle_id"), + tn: 0, + pn: this.plot.data[0].ids.indexOf(actPart.part.particle_id) + } + if(actPart.part.type === type) { + this.renderEditParticleView(); + } + }else{ + this.actPart = actPart + } + if(update) { + this.renderTypesView(); + this.resetFigure(); + } + }, + updateParticleViews: function ({includeGeometry=false}={}) { + this.renderNewParticleView(); + this.renderEditParticleView(); + this.renderEdit3DDomainView(); + this.renderImportMeshView(); + if(includeGeometry) { + this.renderFillGeometryView(); + } + } +}); diff --git a/client/domain-view/domainView.pug b/client/domain-view/domainView.pug new file mode 100644 index 0000000000..646a80baa5 --- /dev/null +++ b/client/domain-view/domainView.pug @@ -0,0 +1,126 @@ +div#domain-view + + div(data-hook="domain-properties-container") + + div(data-hook="domain-limits-container") + + div(data-hook="domain-types-container") + + div#domain-particles-editor.card(data-hook="domain-particles-editor") + + div.card-header.pb-0 + + h3.inline.mr-3 Particles + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="domain-particles-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="new-particle-tab" data-toggle="tab" href="#create-new-particle") Create New + + li.nav-item + + a.nav-link.tab(data-hook="edit-particle-tab" data-toggle="tab" href="#edit-selected-particle") Edit Selected + + li.nav-item + + a.nav-link.tab(data-hook="particle-types-tab" data-toggle="tab" href="#set-particle-types") Set Types from File + + li.nav-item + + a.nav-link.tab(data-hook="3d-domain-tab" data-toggle="tab" href="#create-3d-domain") Create 3D Domain + + li.nav-item + + a.nav-link.tab(data-hook="import-particles-tab" data-toggle="tab" href="#import-particles") Import Mesh + + li.nav-item + + a.nav-link.tab(data-hook="fill-geometry-tab" data-toggle="tab" href="#fill-geometry") Fill Geometry + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-particles-section" data-hook="collapse-domain-particles") - + + div.collapse.show(id="domain-particles-section" data-hook="domain-particles-section") + + div.card-body.tab-content + + div.tab-pane.active(id="create-new-particle" data-hook="new-particle-section") + + div(data-hook="new-particle-container") + + button.btn.btn-outline-primary.box-shadow(data-hook="add-new-particle") Add Particle + + div.mdl-edit-btn.saving-status.inline(data-hook="anp-in-progress") + + div.spinner-grow.mr-2 + + span Adding particle ... + + div.mdl-edit-btn.saved-status.inline(data-hook="anp-complete") + + span Particle Added + + div.tab-pane(id="edit-selected-particle" data-hook="edit-particle-section") + + div.text-info.mb-4(data-hook="edit-select-message" style="display: none") + | Click on a particle in the Domain section to begin editing. + + div(data-hook="edit-particle-container") + + button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="save-selected-particle") Save Particle + + div.mdl-edit-btn.saving-status.inline(data-hook="esp-in-progress") + + div.spinner-grow.mr-2 + + span Saving particle ... + + div.mdl-edit-btn.saved-status.inline(data-hook="esp-complete") + + span Particle Saved + + button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="remove-selected-particle") Remove Particle + + div.mdl-edit-btn.saving-status.inline(data-hook="rsp-in-progress") + + div.spinner-grow.mr-2 + + span Removing particle ... + + div.mdl-edit-btn.saved-status.inline(data-hook="rsp-complete") + + span Particle Removed + + div.tab-pane(id="set-particle-types" data-hook="set-particle-types-section") + + div(data-hook="particle-types-container") + + div.tab-pane(id="create-3d-domain" data-hook="3d-domain-section") + + div(data-hook="3d-domain-container") + + div.tab-pane(id="import-particles" data-hook="import-particles-section") + + div(data-hook="import-particles-container") + + div.tab-pane(id="fill-geometry" data-hook="fill-geometry-section") + + div(data-hook="fill-geometry-container") + + div#domain-figure-preview.card(data-hook="domain-figure-preview") + + div.card-header.pb-0 + + h3.inline.mr-3 Domain + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-figure-container" data-hook="collapse-domain-figure") - + + div.collapse.show(id="domain-figure-container" data-hook="domain-figure-container") + + div.card-body + + div(id="domain-figure" data-hook="domain-figure" style="height: 800px") + + div.text-danger(data-hook="domain-figure-empty") The domain currently has no particles to display \ No newline at end of file diff --git a/client/domain-view/templates/edit3DDomainView.pug b/client/domain-view/templates/edit3DDomainView.pug new file mode 100644 index 0000000000..7e3a60dd29 --- /dev/null +++ b/client/domain-view/templates/edit3DDomainView.pug @@ -0,0 +1,231 @@ +div.mx-1 + + h4.mt-3 Particle Distribution + + div + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.col-sm-2 + + h6.inline X-Plane + + div.col-sm-2 + + h6.inline Y-Plane + + div.col-sm-2 + + h6.inline Z-Plane + + div.col-sm-3 + + h6.inline Total + + div.row + + div.col-sm-3 + + h6.inline Number of Particles + + div.col-sm-2 + + div(data-target="edit-3d-domain-n" data-hook="edit-3d-domain-nx" data-key="nx") + + div.col-sm-2 + + div(data-target="edit-3d-domain-n" data-hook="edit-3d-domain-ny" data-key="ny") + + div.col-sm-2 + + div(data-target="edit-3d-domain-n" data-hook="edit-3d-domain-nz" data-key="nz") + + div.col-sm-3 + + div(data-target="3d-domain-n" data-hook="3d-domain-n") + + h4.mt-3 Domain Limits + + div + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.col-sm-3 + + h6.inline X-Axis + + div.col-sm-3 + + h6.inline Y-Axis + + div.col-sm-3 + + h6.inline Z-Axis + + div.mt-3.mx-1.row + + div.col-sm-3 + + h6.inline Minimum + + div.col-sm-3 + + div(data-hook="edit-3d-domain-x-lim-min" data-target="edit-3d-domain-limitation" data-name="xLim" data-index="0") + + div.col-sm-3 + + div(data-hook="edit-3d-domain-y-lim-min" data-target="edit-3d-domain-limitation" data-name="yLim" data-index="0") + + div.col-sm-3 + + div(data-hook="edit-3d-domain-z-lim-min" data-target="edit-3d-domain-limitation" data-name="zLim" data-index="0") + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Maximum + + div.col-sm-3 + + div(data-hook="edit-3d-domain-x-lim-max" data-target="edit-3d-domain-limitation" data-name="xLim" data-index="1") + + div.col-sm-3 + + div(data-hook="edit-3d-domain-y-lim-max" data-target="edit-3d-domain-limitation" data-name="yLim" data-index="1") + + div.col-sm-3 + + div(data-hook="edit-3d-domain-z-lim-max" data-target="edit-3d-domain-limitation" data-name="zLim" data-index="1") + + div.mt-3 + + h4.inline Advanced + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#edit-3D-domain-advanced" data-hook="collapse-3D-domain-advanced") + + + div.collapse(id="edit-3D-domain-advanced" data-hook="3D-domain-advanced-container") + + div.mt-3 + + h5.inline.mr-2 Type: + + div.inline(data-hook="edit-3d-domain-type-select") + + div.mt-3 + + h5 Type Defaults + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row.pl-3 + + div.col-sm-2 + + div(data-hook="edit-3d-domain-td-mass") + + div.col-sm-2 + + div(data-hook="edit-3d-domain-td-vol") + + div.col-sm-2 + + div(data-hook="edit-3d-domain-td-rho") + + div.col-sm-2 + + div(data-hook="edit-3d-domain-td-nu") + + div.col-sm-2 + + div(data-hook="edit-3d-domain-td-c") + + div.col-sm-2 + + input(type="checkbox" data-hook="edit-3d-domain-td-fixed" disabled) + + div.my-3 + + h5 Particle Transformations + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-4 + + h6.inline X-Axis + + div.col-sm-4 + + h6.inline Y-Axis + + div.col-sm-4 + + h6.inline Z-Axis + + div.row + + div.col-sm-4 + + div(data-target="edit-3d-domain-trans" data-hook="edit-3d-domain-x-trans" data-index="0") + + div.col-sm-4 + + div(data-target="edit-3d-domain-trans" data-hook="edit-3d-domain-y-trans" data-index="1") + + div.col-sm-4 + + div(data-target="edit-3d-domain-trans" data-hook="edit-3d-domain-z-trans" data-index="2") + + div.inline + + button.btn.btn-outline-primary(data-hook="build-3d-domain") Build Domain + + div.mdl-edit-btn.saving-status.inline(data-hook="c3dd-in-progress") + + div.spinner-grow.mr-2 + + span Creating domain ... + + div.mdl-edit-btn.saved-status.inline(data-hook="c3dd-complete") + + span Domain successfully created + + div.mdl-edit-btn.save-error-status(data-hook="c3dd-error") + + span(data-hook="c3dd-action-error") diff --git a/client/domain-view/templates/editParticleView.pug b/client/domain-view/templates/editParticleView.pug new file mode 100644 index 0000000000..0ab7f02d4e --- /dev/null +++ b/client/domain-view/templates/editParticleView.pug @@ -0,0 +1,83 @@ +div.mx-1 + + div + + h4.inline.mr-2 Type: + + div.inline(data-target="type" data-hook=`particle-type-${this.viewIndex}`) + + div + + h4.inline.mr-2 Location: + + div.inline.mr-2 (x: + + div.inline.ml2(data-target=`location-${this.viewIndex}` data-hook=`x-coord-${this.viewIndex}` data-index="0") + + div.inline.mr-2 , y: + + div.inline.ml2(data-target=`location-${this.viewIndex}` data-hook=`y-coord-${this.viewIndex}` data-index="1") + + div.inline.mr-2 , z: + + div.inline.ml2(data-target=`location-${this.viewIndex}` data-hook=`z-coord-${this.viewIndex}` data-index="2") + + div.inline ) + + h4 Particle Properties + + div.pl-2(data-hook="particle-details") + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row + + div.col-sm-2 + + div(data-hook=`particle-mass-${this.viewIndex}`) + + div.col-sm-2 + + div(data-hook=`particle-vol-${this.viewIndex}`) + + div.col-sm-2 + + div(data-hook=`particle-rho-${this.viewIndex}`) + + div.col-sm-2 + + div(data-hook=`particle-nu-${this.viewIndex}`) + + div.col-sm-2 + + div(data-hook=`particle-c-${this.viewIndex}`) + + div.col-sm-2 + + input(type="checkbox" data-hook=`particle-fixed-${this.viewIndex}`) \ No newline at end of file diff --git a/client/domain-view/templates/editType.pug b/client/domain-view/templates/editType.pug new file mode 100644 index 0000000000..0c630182e8 --- /dev/null +++ b/client/domain-view/templates/editType.pug @@ -0,0 +1,128 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 1) + hr + + div.row + + div.col-sm-1 + + div.pl-3 + + input(type="checkbox" data-hook="select" data-toggle="collapse" data-target="#collapse-type-details" + this.model.typeID) + + div.col-sm-3 + + div(data-hook="type-name" data-target=this.model.typeID) + + div.col-sm-3 + + div=this.model.numParticles + + div.col-sm-3 + + button.btn.btn-outline-secondary.box-shadow(data-type=this.model.typeID data-hook='unassign-all') Un-Assign Particles + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow.dropdown-toggle( + id="delete-domain-type", + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + type="button" + ) Delete + + ul.dropdown-menu(aria-labelledby="delete-domain-type") + li.dropdown-item(data-hook="delete-type" data-type=this.model.typeID) Type + li.dropdown-item(data-hook="delete-all" data-type=this.model.typeID) Type and Particles + + div-mx-1.pl-2(data-hook="type-details") + + div.collapse(id="collapse-type-details" + this.model.typeID) + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row + + div.col-sm-2 + + div(data-hook="td-mass" data-target="type-defaults") + + div.col-sm-2 + + div(data-hook="td-vol" data-target="type-defaults") + + div.col-sm-2 + + div(data-hook="td-rho" data-target="type-defaults") + + div.col-sm-2 + + div(data-hook="td-nu" data-target="type-defaults") + + div.col-sm-2 + + div(data-hook="td-c" data-target="type-defaults") + + div.col-sm-2 + + input(type="checkbox" data-hook="td-fixed") + + hr + + div.row.align-items-baseline + + div.col-sm-10.align-items-baseline + + h6.inline Geometry: + + div.tooltip-icon.mr-3(data-html="true" data-toggle="tooltip" title=this.tooltips.geometry) + + div.inline(style="width: 80%") + + div(id="type-geometry" data-hook="type-geometry") + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="apply-geometry") Apply + + div.mdl-edit-btn.saving-status.inline(data-hook="tg-in-progress-" + this.model.typeID) + + div.spinner-grow.mr-2 + + span Applying type to particles ... + + div.mdl-edit-btn.saved-status.inline(data-hook="tg-complete-" + this.model.typeID) + + span Type successfully applied + + div.mdl-edit-btn.save-error-status(data-hook="tg-error-" + this.model.typeID) + + span(data-hook="tg-action-error-" + this.model.typeID) diff --git a/client/domain-view/templates/fillGeometryView.pug b/client/domain-view/templates/fillGeometryView.pug new file mode 100644 index 0000000000..e9f2400326 --- /dev/null +++ b/client/domain-view/templates/fillGeometryView.pug @@ -0,0 +1,167 @@ +div.mx-1 + + div + + h4.inline.mr-2 Type: + + div.inline(data-hook="fill-geometry-type-select") + + div + + h4.inline.mr-2 Geometry: + + div.inline(data-hook="fill-geometry-type-geometry") + + div.mt-3 + + h5 Type Defaults + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row.pl-3 + + div.col-sm-2 + + div(data-hook="fill-geometry-td-mass") + + div.col-sm-2 + + div(data-hook="fill-geometry-td-vol") + + div.col-sm-2 + + div(data-hook="fill-geometry-td-rho") + + div.col-sm-2 + + div(data-hook="fill-geometry-td-nu") + + div.col-sm-2 + + div(data-hook="fill-geometry-td-c") + + div.col-sm-2 + + input(type="checkbox" data-hook="fill-geometry-td-fixed" disabled) + + h4.mt-3 Domain Limits + + div + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.col-sm-3 + + h6.inline X-Axis + + div.col-sm-3 + + h6.inline Y-Axis + + div.col-sm-3 + + h6.inline Z-Axis + + div.mt-3.mx-1.row + + div.col-sm-3 + + h6.inline Minimum + + div.col-sm-3 + + div(data-hook="fill-geometry-x-lim-min" data-target="fill-geometry-limitation" data-name="xmin") + + div.col-sm-3 + + div(data-hook="fill-geometry-y-lim-min" data-target="fill-geometry-limitation" data-name="ymin") + + div.col-sm-3 + + div(data-hook="fill-geometry-z-lim-min" data-target="fill-geometry-limitation" data-name="zmin") + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Maximum + + div.col-sm-3 + + div(data-hook="fill-geometry-x-lim-max" data-target="fill-geometry-limitation" data-name="xmax") + + div.col-sm-3 + + div(data-hook="fill-geometry-y-lim-max" data-target="fill-geometry-limitation" data-name="ymax") + + div.col-sm-3 + + div(data-hook="fill-geometry-z-lim-max" data-target="fill-geometry-limitation" data-name="zmax") + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Spacing + + div.col-sm-3 + + div(data-hook="fill-geometry-deltax" data-target="fill-geometry-delta" data-name="deltax") + + div.col-sm-3 + + div(data-hook="fill-geometry-deltay" data-target="fill-geometry-delta" data-name="deltay") + + div.col-sm-3 + + div(data-hook="fill-geometry-deltaz" data-target="fill-geometry-delta" data-name="deltaz") + + div.inline + + button.btn.btn-outline-primary(data-hook="fill-geometry" disabled) Fill with Particles + + div.mdl-edit-btn.saving-status.inline(data-hook="fg-in-progress") + + div.spinner-grow.mr-2 + + span Creating particles ... + + div.mdl-edit-btn.saved-status.inline(data-hook="fg-complete") + + span Particles successfully created + + div.mdl-edit-btn.save-error-status(data-hook="fg-error") + + span(data-hook="fg-action-error") diff --git a/client/domain-view/templates/importMeshView.pug b/client/domain-view/templates/importMeshView.pug new file mode 100644 index 0000000000..000a38acc3 --- /dev/null +++ b/client/domain-view/templates/importMeshView.pug @@ -0,0 +1,137 @@ +div.mx-1 + + div.my-3 + + span.inline.mr-2(for="meshfile") Please specify a mesh to import: + + input(id="meshfile" type="file" name="meshfile" size="30" accept=".xml" required) + + div.mb-3 + + span.inline.mr-2(for=typefile) Type descriptions (optional): + + input(id="typefile" type="file" name="typefile" size="30" accept=".txt") + + div.mt-3 + + h4.inline Advanced + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#import-mesh-advanced" data-hook="collapse-import-mesh-advanced") + + + div.collapse(id="import-mesh-advanced" data-hook="import-mesh-container") + + div.mt-3 + + h5.inline.mr-2 Type: + + div.inline(data-hook="import-mesh-type-select") + + div.mt-3 + + h5 Type Defaults + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-2 + + h6.inline Mass + + div.col-sm-2 + + h6.inline Volume + + div.col-sm-2 + + h6.inline Density + + div.col-sm-2 + + h6.inline Viscosity + + div.col-sm-2 + + h6.inline Speed of Sound + + div.col-sm-2 + + h6.inline Fixed + + div.row.pl-3 + + div.col-sm-2 + + div(data-hook="import-mesh-td-mass") + + div.col-sm-2 + + div(data-hook="import-mesh-td-vol") + + div.col-sm-2 + + div(data-hook="import-mesh-td-rho") + + div.col-sm-2 + + div(data-hook="import-mesh-td-nu") + + div.col-sm-2 + + div(data-hook="import-mesh-td-c") + + div.col-sm-2 + + input(type="checkbox" data-hook="import-mesh-td-fixed" disabled) + + div.my-3 + + h5 Particle Transformations + + hr + + div.mb-3.mx-1.row.head.align-items-baseline + + div.col-sm-4 + + h6.inline X-Axis + + div.col-sm-4 + + h6.inline Y-Axis + + div.col-sm-4 + + h6.inline Z-Axis + + div.row + + div.col-sm-4 + + div(data-target="import-mesh-trans" data-hook="import-mesh-x-trans" data-index="0") + + div.col-sm-4 + + div(data-target="import-mesh-trans" data-hook="import-mesh-y-trans" data-index="1") + + div.col-sm-4 + + div(data-target="import-mesh-trans" data-hook="import-mesh-z-trans" data-index="2") + + div.inline + + button.btn.btn-outline-primary.box-shadow(data-hook="import-mesh-particles" disabled) Import Mesh + + div.mdl-edit-btn.saving-status.inline(data-hook="imp-in-progress") + + div.spinner-grow.mr-2 + + span Importing mesh ... + + div.mdl-edit-btn.saved-status.inline(data-hook="imp-complete") + + span + + div.mdl-edit-btn.save-error-status(data-hook="imp-error") + + span(data-hook="imp-action-error") \ No newline at end of file diff --git a/client/domain-view/templates/limitsView.pug b/client/domain-view/templates/limitsView.pug new file mode 100644 index 0000000000..3fb1868a60 --- /dev/null +++ b/client/domain-view/templates/limitsView.pug @@ -0,0 +1,175 @@ +div#domain-limits-editor.card + + div.card-header.pb-0 + + h3.inline.mr-3 Limits + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="domain-limits-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="domain-limits-edit-tab" data-toggle="tab" href="#edit-domain-limits") Edit + + li.nav-item + + a.nav-link.tab(data-hook="domain-limits-view-tab" data-toggle="tab" href="#view-domain-limits") View + + button.btn.btn-ouline-collapse(data-toggle="collapse" data-target="#domain-limits-section" data-hook="collapse-domain-limits") - + + div.collapse.show(id="domain-limits-section" data-hook="domain-limits-section") + + div.card-body.tab-content + + div.tab-pane.active(id="edit-domain-limits" data-hook="edit-domain-limits") + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.col-sm-3 + + h6.inline X-Axis + + div.col-sm-3 + + h6.inline Y-Axis + + div.col-sm-3 + + h6.inline Z-Axis + + div.mt-3.mx-1.row + + div.col-sm-3 + + h6.inline Minimum + + div.col-sm-3 + + div(data-hook="x-lim-min" data-target="limitation" data-name="x_lim" data-index="0") + + div.col-sm-3 + + div(data-hook="y-lim-min" data-target="limitation" data-name="y_lim" data-index="0") + + div.col-sm-3 + + div(data-hook="z-lim-min" data-target="limitation" data-name="z_lim" data-index="0") + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Maximum + + div.col-sm-3 + + div(data-hook="x-lim-max" data-target="limitation" data-name="x_lim" data-index="1") + + div.col-sm-3 + + div(data-hook="y-lim-max" data-target="limitation" data-name="y_lim" data-index="1") + + div.col-sm-3 + + div(data-hook="z-lim-max" data-target="limitation" data-name="z_lim" data-index="1") + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Reflect + + div.col-sm-3 + + input(type="checkbox" id="x-reflect" data-hook="reflect_x" data-target="reflect" disabled) + + div.col-sm-3 + + input(type="checkbox" id="y-reflect" data-hook="reflect_y" data-target="reflect" disabled) + + div.col-sm-3 + + input(type="checkbox" id="z-reflect" data-hook="reflect_z" data-target="reflect" disabled) + + div.tab-pane(id="view-domain-limits" data-hook="view-domain-limits") + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + div.col-sm-3 + + h6.inline X-Axis + + div.col-sm-3 + + h6.inline Y-Axis + + div.col-sm-3 + + h6.inline Z-Axis + + div.mt-3.mx-1.row + + div.col-sm-3 + + h6.inline Minimum + + div.col-sm-3 + + div=this.model.x_lim[0] + + div.col-sm-3 + + div=this.model.y_lim[0] + + div.col-sm-3 + + div=this.model.z_lim[0] + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Maximum + + div.col-sm-3 + + div=this.model.x_lim[1] + + div.col-sm-3 + + div=this.model.y_lim[1] + + div.col-sm-3 + + div=this.model.z_lim[1] + + hr + + div.mx-1.row + + div.col-sm-3 + + h6.inline Reflect + + div.col-sm-3 + + input(type="checkbox" id="x-reflect" data-hook="view-reflect_x" data-target="reflect" disabled) + + div.col-sm-3 + + input(type="checkbox" id="y-reflect" data-hook="view-reflect_y" data-target="reflect" disabled) + + div.col-sm-3 + + input(type="checkbox" id="z-reflect" data-hook="view-reflect_z" data-target="reflect" disabled) \ No newline at end of file diff --git a/client/domain-view/templates/propertiesView.pug b/client/domain-view/templates/propertiesView.pug new file mode 100644 index 0000000000..b6bc871a94 --- /dev/null +++ b/client/domain-view/templates/propertiesView.pug @@ -0,0 +1,123 @@ +div#domain-properties-editor.card + + div.card-header.pb-0 + + h3.inline.mr-3 Properties + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="domain-properties-tabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="domain-properties-edit-tab" data-toggle="tab" href="#edit-domain-properties") Edit + + li.nav-item + + a.nav-link.tab(data-hook="domain-properties-view-tab" data-toggle="tab" href="#view-domain-properties") View + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-properties-section" data-hook="collapse-domain-properties") - + + div.collapse.show(id="domain-properties-section" data-hook="domain-properties-section") + + div.card-body.tab-content + + div.tab-pane.active(id="edit-domain-properties" data-hook="edit-domain-properties") + + div + + span.mr-3(for="#static-domain") Static Domain: + + input(type="checkbox" id="static-domain" data-hook="static-domain") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Density + + div.col-sm-3 + + h6.inline Gravity + + div.col-sm-3 + + h6.inline Pressure + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.pressure) + + div.col-sm-3 + + h6.inline Speed of Sound + + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.speed) + + div.mt-3.mx-1.row + + div.col-sm-3 + + div(data-hook="density") + + div.col-sm-3 + + div(data-target="gravity" data-hook="gravity-x" data-index="0") + + div(data-target="gravity" data-hook="gravity-y" data-index="1") + + div(data-target="gravity" data-hook="gravity-z" data-index="2") + + div.col-sm-3 + + div(data-hook="pressure") + + div.col-sm-3 + + div(data-hook="speed") + + div.tab-pane(id="view-domain-properties" data-hook="view-domain-properties") + + div + + span.mr-3(for="#view-static-domain") Static Domain: + + input(type="checkbox" id="view-static-domain" data-hook="view-static-domain" disabled) + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Density + + div.col-sm-3 + + h6.inline Gravity + + div.col-sm-3 + + h6.inline Pressure + + div.col-sm-3 + + h6.inline Speed of Sound + + div.mt-3.mx-1.row + + div.col-sm-3 + + div=this.model.rho_0 + + div.col-sm-3 + + div=`(X: ${this.model.gravity[0]}, Y: ${this.model.gravity[1]}, Z: ${this.model.gravity[2]})` + + div.col-sm-3 + + div=this.model.p_0 + + div.col-sm-3 + + div=this.model.c_0 \ No newline at end of file diff --git a/client/domain-view/templates/quickviewType.pug b/client/domain-view/templates/quickviewType.pug new file mode 100644 index 0000000000..391b93bbc7 --- /dev/null +++ b/client/domain-view/templates/quickviewType.pug @@ -0,0 +1,9 @@ +div + + hr + + div.row.mx-1 + + div.col-sm-6=this.model.name + + div.col-sm-6=this.model.numParticles diff --git a/client/domain-view/templates/typesDescriptionView.pug b/client/domain-view/templates/typesDescriptionView.pug new file mode 100644 index 0000000000..f4f2ab190e --- /dev/null +++ b/client/domain-view/templates/typesDescriptionView.pug @@ -0,0 +1,36 @@ +div.mx-1 + + div.text-info(data-hook="type-location-message" style="display: none") + | There are multiple type files with that name, please select a location + + div + + div.inline.mr-3 + + span.inline.mr-2(for="file-select") Select type description file: + + div.inline(id="file-select" data-hook="file-select") + + div.inline(data-hook="file-location-container" style="display: none") + + span.inlinemr-2(for="file-location-select") Location: + + div.inline(id="file-location-select" data-hook="file-location-select") + + div.my-3 + + button.btn.btn-outline-primary.box-shadow(data-hook="set-particle-types-btn" disabled) Set Types + + div.mdl-edit-btn.saving-status.inline(data-hook="st-in-progress") + + div.spinner-grow.mr-2 + + span Setting types ... + + div.mdl-edit-btn.saved-status.inline(data-hook="st-complete") + + span + + div.mdl-edit-btn.save-error-status(data-hook="st-error") + + span(data-hook="st-error-action") \ No newline at end of file diff --git a/client/domain-view/templates/typesView.pug b/client/domain-view/templates/typesView.pug new file mode 100644 index 0000000000..d51f270214 --- /dev/null +++ b/client/domain-view/templates/typesView.pug @@ -0,0 +1,75 @@ +div#domain-types-editor.card + + div.card-header.pb-0 + + h3.inline.mr-3 Types + + div.inline.mr-3 + + ul.nav.nav-tabs.card-header-tabs(id="domain-types-teabs") + + li.nav-item + + a.nav-link.tab.active(data-hook="domain-types-edit-tab" data-toggle="tab" href="#edit-domain-types") Edit + + li.nav-item + + a.nav-link.tab(data-hook="domain-types-view-tab" data-toggle="tab" href="#view-domain-types") View + + button.btn.btn-outline-collapse(data-toggle="collapse" data-target="#domain-types-section" data-hook="collapse-domain-types") - + + div.collapse.show(id="domain-types-section" data-hook="domain-types-section") + + div.card-body.tab-content + + div + + span.mr-3.inline(for="unassigned-type-count") Number of Un-Assigned Particles: + + div.inline(id="unassigned-type-count" data-hook="unassigned-type-count")=this.collection.models[0].numParticles + + div(data-hook="domain-error") + + p.text-danger A domain cannot have any un-assigned particles. + + div.tab-pane.active(id="edit-domain-types" data-hook="edit-domain-types") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-1 + + h6.inline Edit + + div.col-sm-3 + + h6.inline Name + + div.col-sm-8 + + h6.inline Number of Particles + + div.my-3(data-hook="edit-domain-types-list") + + button.btn.btn-outline-primary.box-shadow(data-hook="add-domain-type") Add Type + + div.tab-pane(id="view-domain-types" data-hook="view-domain-types") + + hr + + div.mx-1.row.head.align-items-baseline + + div.col-sm-3 + + h6.inline Name + + div.col-sm-3 + + h6.inline Numbe of Particles + + div.col-sm-6 + + h6.inline Defaults + + div.my-3(data-hook="view-domain-types-list") \ No newline at end of file diff --git a/client/domain-view/templates/viewParticle.pug b/client/domain-view/templates/viewParticle.pug new file mode 100644 index 0000000000..fd290ef052 --- /dev/null +++ b/client/domain-view/templates/viewParticle.pug @@ -0,0 +1,117 @@ +div + + h5 Properties + + hr.mt-2.mb-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 ID: + + div.col-sm-6 + + div.inline=this.model.particle_id + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Type: + + div.col-sm-6 + + div.inline=this.type + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Location: + + div.col-sm-6 + + div.inline + + div=`X: ${this.model.point[0]}` + + div=`Y: ${this.model.point[1]}` + + div=`Z: ${this.model.point[2]}` + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Mass: + + div.col-sm-6 + + div.inline=this.model.mass + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Volume: + + div.col-sm-6 + + div.inline=this.model.volume + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Density: + + div.col-sm-6 + + div.inline=this.model.rho + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Viscosity: + + div.col-sm-6 + + div.inline=this.model.nu + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Speed of Sound: + + div.col-sm-6 + + div.inline=this.model.c + + hr.my-1 + + div.row.mx-1 + + div.col-sm-6 + + h6 Fixed: + + div.col-sm-6 + + input.inline(type="checkbox" checked=this.model.fixed disabled) diff --git a/client/domain-view/templates/viewType.pug b/client/domain-view/templates/viewType.pug new file mode 100644 index 0000000000..5a0277bf4a --- /dev/null +++ b/client/domain-view/templates/viewType.pug @@ -0,0 +1,48 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 1) + hr + + div.row + + div.col-sm-6 + + div.row + + div.col-sm-6 + + div=this.model.name + + div.col-sm-6 + + div=this.model.numParticles + + hr + + h6.inline.mr-2 Geometry: + + div.inline=this.model.geometry + + div.col-sm-6 + + div.row + + div.col-sm-6 + + div="Mass: " + this.model.mass + + div="Volume: " + this.model.volume + + div="Density: " + this.model.rho + + div.col-sm-6 + + div="Viscosity: " + this.model.nu + + div="Speed of Sound: " + this.model.c + + div + + span.mr-3(for="view-td-fixed") Fixed: + + input(type="checkbox" data-hook="view-td-fixed" disabled) diff --git a/client/domain-view/views/edit-3D-domain-view.js b/client/domain-view/views/edit-3D-domain-view.js new file mode 100644 index 0000000000..cfd4c503b4 --- /dev/null +++ b/client/domain-view/views/edit-3D-domain-view.js @@ -0,0 +1,302 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +//support files +let app = require('../../app'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let template = require('../templates/edit3DDomainView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'change [data-target=edit-3d-domain-n]' : 'handleNumParticlesUpdate', + 'change [data-target=edit-3d-domain-limitation]' : 'handleLimitsUpdate', + 'change [data-hook=edit-3d-domain-type-select]' : 'handleTypeUpdate', + 'change [data-target=edit-3d-domain-trans]' : 'handleTransformationupdate', + 'click [data-hook=collapse-3D-domain-advanced]' : 'changeCollapseButtonText', + 'click [data-hook=build-3d-domain]' : 'handleBuildDomain' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.data = { + "nx":1, "ny":1, "nz":1, + "xLim":[0, 0], "yLim":[0, 0], "zLim":[0, 0], + "type": null, "transformation": null + } + this.transformation = [0, 0, 0]; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + this.type = this.parent.model.types.get(0, "typeID"); + this.updateTotalParticles(); + this.updateTypeDefaults(); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + completeAction: function () { + $(this.queryByHook("c3dd-in-progress")).css("display", "none"); + $(this.queryByHook("c3dd-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("c3dd-complete")).css("display", "none"); + }, 5000); + }, + errorAction: function (action) { + $(this.queryByHook("c3dd-in-progress")).css("display", "none"); + $(this.queryByHook("c3dd-action-error")).text(action); + $(this.queryByHook("c3dd-error")).css("display", "block"); + }, + handleBuildDomain: function () { + this.startAction(); + this.transformation.every((value) => { + if(value !== 0) { + this.data.transformation = this.transformation; + return false; + } + return true; + }); + this.data.domainExists = this.parent.model.particles.length > 0; + let endpoint = path.join(app.getApiPath(), "spatial-model/3d-domain"); + app.postXHR(endpoint, this.data, { + success: (err, response, body) => { + this.parent.add3DDomain(body.limits, body.particles); + this.completeAction(); + $('html, body').animate({ + scrollTop: $("#domain-figure").offset().top + }, 20); + }, + error: (err, response, body) => { + this.errorAction(body.Message); + } + }); + }, + handleLimitsUpdate: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + let index = Number(e.target.parentElement.parentElement.dataset.index); + this.data[key][index] = Number(e.target.value); + }, + handleNumParticlesUpdate: function (e) { + let key = e.target.parentElement.parentElement.dataset.key; + this.data[key] = Number(e.target.value); + this.updateTotalParticles(); + }, + handleTransformationupdate: function (e) { + let index = e.target.parentElement.parentElement.dataset.index; + this.transformation[index] = Number(e.target.value); + }, + handleTypeUpdate: function (e) { + let typeID = Number(e.target.value); + this.type = this.parent.model.types.get(typeID, "typeID"); + this.updateTypeDefaults(); + }, + startAction: function () { + $(this.queryByHook("c3dd-complete")).css("display", "none"); + $(this.queryByHook("c3dd-error")).css("display", "none"); + $(this.queryByHook("c3dd-in-progress")).css("display", "inline-block"); + }, + update: function () {}, + updateValid: function () {}, + updateTotalParticles: function () { + let n = this.data.nx * this.data.ny * this.data.nz; + $(this.queryByHook("3d-domain-n")).text(n); + $(this.queryByHook("build-3d-domain")).prop("disabled", n <= 0); + }, + updateTypeDefaults: function () { + this.data.typeID = this.type.typeID; + this.data.type = { + "type_id": this.type.name, "mass": this.type.mass, "rho": this.type.rho, + "nu": this.type.nu, "c": this.type.c, "fixed": this.type.fixed + } + $(this.queryByHook("edit-3d-domain-td-mass")).text(this.type.mass); + $(this.queryByHook("edit-3d-domain-td-vol")).text(this.type.volume); + $(this.queryByHook("edit-3d-domain-td-rho")).text(this.type.rho); + $(this.queryByHook("edit-3d-domain-td-nu")).text(this.type.nu); + $(this.queryByHook("edit-3d-domain-td-c")).text(this.type.c); + $(this.queryByHook("edit-3d-domain-td-fixed")).prop('checked', this.type.fixed); + }, + subviews: { + nXInputView: { + hook: "edit-3d-domain-nx", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'nx', + valueType: 'number', + tests: tests.valueTests, + value: this.data.nx + }); + } + }, + nYInputView: { + hook: "edit-3d-domain-ny", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'ny', + valueType: 'number', + tests: tests.valueTests, + value: this.data.ny + }); + } + }, + nZInputView: { + hook: "edit-3d-domain-nz", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'nz', + valueType: 'number', + tests: tests.valueTests, + value: this.data.nz + }); + } + }, + xMinLimInputView: { + hook: "edit-3d-domain-x-lim-min", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'x-lim-min', + valueType: 'number', + value: this.data.xLim[0] + }); + } + }, + yMinLimInputView: { + hook: "edit-3d-domain-y-lim-min", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'y-lim-min', + valueType: 'number', + value: this.data.yLim[0] + }); + } + }, + zMinLimInputView: { + hook: "edit-3d-domain-z-lim-min", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'z-lim-min', + valueType: 'number', + value: this.data.zLim[0] + }); + } + }, + xMaxLimInputView: { + hook: "edit-3d-domain-x-lim-max", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'x-lim-max', + valueType: 'number', + value: this.data.xLim[1] + }); + } + }, + yMaxLimInputView: { + hook: "edit-3d-domain-y-lim-max", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'y-lim-max', + valueType: 'number', + value: this.data.yLim[1] + }); + } + }, + zMaxLimInputView: { + hook: "edit-3d-domain-z-lim-max", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'z-lim-max', + valueType: 'number', + value: this.data.zLim[1] + }); + } + }, + typeSelectView: { + hook: "edit-3d-domain-type-select", + prepareView: function (el) { + return new SelectView({ + name: 'type', + required: true, + idAttribute: 'typeID', + textAttribute: 'name', + eagerValidate: true, + options: this.parent.model.types, + value: this.type + }); + } + }, + xTransInputView: { + hook: "edit-3d-domain-x-trans", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'x-transformation', + valueType: 'number', + value: this.transformation[0] + }); + } + }, + yTransInputView: { + hook: "edit-3d-domain-y-trans", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'y-transformation', + valueType: 'number', + value: this.transformation[1] + }); + } + }, + zTransInputView: { + hook: "edit-3d-domain-z-trans", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'z-transformation', + valueType: 'number', + value: this.transformation[2] + }); + } + } + } +}); \ No newline at end of file diff --git a/client/domain-view/views/fill-geometry-view.js b/client/domain-view/views/fill-geometry-view.js new file mode 100644 index 0000000000..f9f507f3a9 --- /dev/null +++ b/client/domain-view/views/fill-geometry-view.js @@ -0,0 +1,267 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +//support files +let app = require('../../app'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let template = require('../templates/fillGeometryView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'change [data-hook=fill-geometry-type-select]' : 'handleTypeUpdate', + 'change [data-target=fill-geometry-limitation]' : 'handleDataUpdate', + 'change [data-target=fill-geometry-delta]' : 'handleDataUpdate', + 'click [data-hook=fill-geometry]' : 'handleFillGeometry' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.data = { + "xmin":0, "ymin":0, "zmin":0, "xmax":0, "ymax":0, + "zmax":0, "deltax": 0, "deltay": 0, "deltaz": 0 + } + this.type = null; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + this.getTypes(); + this.renderTypeSelectView(); + }, + completeAction: function () { + $(this.queryByHook("fg-in-progress")).css("display", "none"); + $(this.queryByHook("fg-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("fg-complete")).css("display", "none"); + }, 5000); + }, + disable: function () { + if(this.data.xmin === this.data.xmax) { return true; } + if(this.data.ymin === this.data.ymax) { return true; } + if(this.data.zmin === this.data.zmax) { return true; } + if(this.data.deltax === 0) { return true; } + if(this.data.deltay === 0) { return true; } + if(this.data.deltaz === 0) { return true; } + if(!this.type) { return true; } + return false; + }, + errorAction: function (action) { + $(this.queryByHook("fg-in-progress")).css("display", "none"); + $(this.queryByHook("fg-action-error")).html(action); + $(this.queryByHook("fg-error")).css("display", "block"); + }, + getTypes: function () { + this.types = this.parent.model.types.filter((type) => { + return Boolean(type.geometry); + }); + }, + handleDataUpdate: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + this.data[key] = Number(e.target.value); + this.toggleFillGeometry(); + }, + handleFillGeometry: function () { + this.startAction(); + let data = {kwargs: this.data, type: this.type} + let endpoint = path.join(app.getApiPath(), 'spatial-model/fill-geometry'); + app.postXHR(endpoint, data, { + success: (err, response, body) => { + this.parent.add3DDomain(body.limits, body.particles); + this.completeAction(); + }, + error: (err, response, body) => { + if(body.Traceback.includes("SyntaxError")) { + var tracePart = body.Traceback.split('\n').slice(6) + tracePart.splice(2, 2) + tracePart[1] = tracePart[1].replace(new RegExp(' ', 'g'), ' ') + var errorBlock = `

${body.Message}
${tracePart.join('
')}

` + }else{ + var errorBlock = body.Message + } + this.errorAction(errorBlock); + } + }); + }, + handleTypeUpdate: function (e) { + if(e.target.value) { + let typeID = Number(e.target.value); + this.type = this.parent.model.types.get(typeID, "typeID"); + }else{ + this.type = null; + } + this.updateTypeDefaults(); + this.toggleFillGeometry(); + }, + renderTypeSelectView: function () { + if(this.typeSelectView) { + this.typeSelectView.remove(); + } + if(this.types) { + var options = this.types.map((type) => { + return [type.typeID, type.name]; + }); + }else{ + var options = []; + } + this.typeSelectView = new SelectView({ + name: 'type', + required: false, + idAttribute: 'typeID', + textAttribute: 'name', + eagerValidate: true, + options: options, + unselectedText: "Select a Type" + }); + app.registerRenderSubview(this, this.typeSelectView, "fill-geometry-type-select"); + }, + startAction: function () { + $(this.queryByHook("fg-complete")).css("display", "none"); + $(this.queryByHook("fg-error")).css("display", "none"); + $(this.queryByHook("fg-in-progress")).css("display", "inline-block"); + }, + toggleFillGeometry: function () { + $(this.queryByHook('fill-geometry')).prop('disabled', this.disable()); + }, + update: function() {}, + updateTypeDefaults: function () { + $(this.queryByHook("fill-geometry-type-geometry")).text(this.type.geometry || ""); + $(this.queryByHook("fill-geometry-td-mass")).text(this.type.mass || ""); + $(this.queryByHook("fill-geometry-td-vol")).text(this.type.volume || ""); + $(this.queryByHook("fill-geometry-td-rho")).text(this.type.rho || ""); + $(this.queryByHook("fill-geometry-td-nu")).text(this.type.nu || "0"); + $(this.queryByHook("fill-geometry-td-c")).text(this.type.c || ""); + $(this.queryByHook("fill-geometry-td-fixed")).prop('checked', this.type.fixed || false); + }, + updateValid: function () {}, + subviews: { + xMinLimInputView: { + hook: "fill-geometry-x-lim-min", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'x-lim-min', + valueType: 'number', + value: this.data.xmin + }); + } + }, + yMinLimInputView: { + hook: "fill-geometry-y-lim-min", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'y-lim-min', + valueType: 'number', + value: this.data.ymin + }); + } + }, + zMinLimInputView: { + hook: "fill-geometry-z-lim-min", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'z-lim-min', + valueType: 'number', + value: this.data.zmin + }); + } + }, + xMaxLimInputView: { + hook: "fill-geometry-x-lim-max", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'x-lim-max', + valueType: 'number', + value: this.data.xmax + }); + } + }, + yMaxLimInputView: { + hook: "fill-geometry-y-lim-max", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'y-lim-max', + valueType: 'number', + value: this.data.ymax + }); + } + }, + zMaxLimInputView: { + hook: "fill-geometry-z-lim-max", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'z-lim-max', + valueType: 'number', + value: this.data.zmax + }); + } + }, + deltaxInputView: { + hook: "fill-geometry-deltax", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'deltax', + valueType: 'number', + value: this.data.deltax + }); + } + }, + deltayInputView: { + hook: "fill-geometry-deltay", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'deltay', + valueType: 'number', + value: this.data.deltay + }); + } + }, + deltazInputView: { + hook: "fill-geometry-deltaz", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'deltaz', + valueType: 'number', + value: this.data.deltaz + }); + } + }, + } +}); \ No newline at end of file diff --git a/client/domain-view/views/import-mesh-view.js b/client/domain-view/views/import-mesh-view.js new file mode 100644 index 0000000000..29bf950f57 --- /dev/null +++ b/client/domain-view/views/import-mesh-view.js @@ -0,0 +1,187 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +//support files +let app = require('../../app'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let template = require('../templates/importMeshView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'change #meshfile' : 'setMeshFile', + 'change #typefile' : 'setTypeFile', + 'change [data-hook=import-mesh-type-select]' : 'handleTypeUpdate', + 'change [data-target=import-mesh-trans]' : 'handleTransformationUpdate', + 'click [data-hook=collapse-import-mesh-advanced]' : 'changeCollapseButtonText', + 'click [data-hook=import-mesh-particles]' : 'handleImportMesh' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.meshFile = null; + this.typeFile = null; + this.data = {"type": null, "transformation": null}; + this.transformation = [0, 0, 0]; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + this.type = this.parent.model.types.get(0, "typeID"); + this.updateTypeDefaults(); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + completeAction: function () { + $(this.queryByHook("imp-in-progress")).css("display", "none"); + $(this.queryByHook("imp-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("imp-complete")).css("display", "none"); + }, 5000); + }, + errorAction: function (action) { + $(this.queryByHook("imp-in-progress")).css("display", "none"); + $(this.queryByHook("imp-action-error")).text(action); + $(this.queryByHook("imp-error")).css("display", "block"); + }, + handleImportMesh: function () { + this.startAction(); + this.transformation.every((value) => { + if(value !== 0) { + this.data.transformation = this.transformation; + return false; + } + return true; + }); + let formData = new FormData(); + formData.append("datafile", this.meshFile); + formData.append("particleData", JSON.stringify(this.data)); + if(this.typeFile) { + formData.append("typefile", this.typeFile); + } + let endpoint = path.join(app.getApiPath(), 'spatial-model/import-mesh'); + app.postXHR(endpoint, formData, { + success: (err, response, body) => { + body = JSON.parse(body); + this.parent.addMeshDomain(body.limits, body.particles, body.types, this.parent.model.particles.length > 0); + this.completeAction(); + $('html, body').animate({ + scrollTop: $("#domain-figure").offset().top + }, 20); + }, + error: (err, response, body) => { + body = JSON.parse(body); + this.errorAction(body.Message); + } + }, false); + }, + handleTransformationUpdate: function (e) { + let index = e.target.parentElement.parentElement.dataset.index; + this.transformation[index] = Number(e.target.value); + }, + handleTypeUpdate: function (e) { + let typeID = Number(e.target.value); + this.type = this.parent.model.types.get(typeID, "typeID"); + this.updateTypeDefaults(); + }, + setMeshFile: function (e) { + this.meshFile = e.target.files[0]; + $(this.queryByHook("import-mesh-particles")).prop('disabled', !this.meshFile); + }, + setTypeFile: function (e) { + this.typeFile = e.target.files[0]; + }, + startAction: function () { + $(this.queryByHook("imp-complete")).css("display", "none"); + $(this.queryByHook("imp-error")).css("display", "none"); + $(this.queryByHook("imp-in-progress")).css("display", "inline-block"); + }, + update: function () {}, + updateValid: function () {}, + updateTypeDefaults: function () { + this.data.typeID = this.type.typeID; + this.data.type = { + "type_id": this.type.name, "mass": this.type.mass, "rho": this.type.rho, + "nu": this.type.nu, "c": this.type.c, "fixed": this.type.fixed + } + $(this.queryByHook("import-mesh-td-mass")).text(this.type.mass); + $(this.queryByHook("import-mesh-td-vol")).text(this.type.volume); + $(this.queryByHook("import-mesh-td-rho")).text(this.type.rho); + $(this.queryByHook("import-mesh-td-nu")).text(this.type.nu); + $(this.queryByHook("import-mesh-td-c")).text(this.type.c); + $(this.queryByHook("import-mesh-td-fixed")).prop('checked', this.type.fixed); + }, + subviews: { + typeSelectView: { + hook: "import-mesh-type-select", + prepareView: function (el) { + return new SelectView({ + name: 'type', + required: true, + idAttribute: 'typeID', + textAttribute: 'name', + eagerValidate: true, + options: this.parent.model.types, + value: this.type + }); + } + }, + xTransInputView: { + hook: "import-mesh-x-trans", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'x-transformation', + valueType: 'number', + value: this.transformation[0] + }); + } + }, + yTransInputView: { + hook: "import-mesh-y-trans", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'y-transformation', + valueType: 'number', + value: this.transformation[1] + }); + } + }, + zTransInputView: { + hook: "import-mesh-z-trans", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'z-transformation', + valueType: 'number', + value: this.transformation[2] + }); + } + } + } +}); \ No newline at end of file diff --git a/client/domain-view/views/limits-view.js b/client/domain-view/views/limits-view.js new file mode 100644 index 0000000000..09f740b160 --- /dev/null +++ b/client/domain-view/views/limits-view.js @@ -0,0 +1,162 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +//templates +let template = require('../templates/limitsView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse-domain-limits]' : 'changeCollapseButtonText', + 'change [data-target=limitation]' : 'setLimitation', + 'change [data-target=reflect]' : 'setBoundaryCondition' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook('domain-limits-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook('domain-limits-view-tab')).tab('show'); + $(this.queryByHook('edit-domain-limits')).removeClass('active'); + $(this.queryByHook('view-domain-limits')).addClass('active'); + }else{ + this.renderXMinLimitInputView(); + this.renderYMinLimitInputView(); + this.renderZMinLimitInputView(); + this.renderXMaxLimitInputView(); + this.renderYMaxLimitInputView(); + this.renderZMaxLimitInputView(); + $(this.queryByHook("reflect_x")).prop("checked", this.model.boundary_condition.reflect_x); + $(this.queryByHook("reflect_y")).prop("checked", this.model.boundary_condition.reflect_y); + $(this.queryByHook("reflect_z")).prop("checked", this.model.boundary_condition.reflect_z); + } + $(this.queryByHook("view-reflect_x")).prop("checked", this.model.boundary_condition.reflect_x); + $(this.queryByHook("view-reflect_y")).prop("checked", this.model.boundary_condition.reflect_y); + $(this.queryByHook("view-reflect_z")).prop("checked", this.model.boundary_condition.reflect_z); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + renderXMinLimitInputView: function () { + if(this.xMinLimitInputView) { + this.xMinLimitInputView.remove(); + } + this.xMinLimitInputView = new InputView({ + parent: this, + required: true, + name: 'x-lim-min', + valueType: 'number', + value: this.model.x_lim[0] || 0 + }); + app.registerRenderSubview(this, this.xMinLimitInputView, "x-lim-min"); + }, + renderYMinLimitInputView: function () { + if(this.yMinLimitInputView) { + this.yMinLimitInputView.remove(); + } + this.yMinLimitInputView = new InputView({ + parent: this, + required: true, + name: 'y-lim-min', + valueType: 'number', + value: this.model.y_lim[0] || 0 + }); + app.registerRenderSubview(this, this.yMinLimitInputView, "y-lim-min"); + }, + renderZMinLimitInputView: function () { + if(this.zMinLimitInputView) { + this.zMinLimitInputView.remove(); + } + this.zMinLimitInputView = new InputView({ + parent: this, + required: true, + name: 'z-lim-min', + valueType: 'number', + value: this.model.z_lim[0] || 0 + }); + app.registerRenderSubview(this, this.zMinLimitInputView, "z-lim-min"); + }, + renderXMaxLimitInputView: function () { + if(this.xMaxLimitInputView) { + this.xMaxLimitInputView.remove(); + } + this.xMaxLimitInputView = new InputView({ + parent: this, + required: true, + name: 'x-lim-max', + valueType: 'number', + value: this.model.x_lim[1] || 0 + }); + app.registerRenderSubview(this, this.xMaxLimitInputView, "x-lim-max"); + }, + renderYMaxLimitInputView: function () { + if(this.yMaxLimitInputView) { + this.yMaxLimitInputView.remove(); + } + this.yMaxLimitInputView = new InputView({ + parent: this, + required: true, + name: 'y-lim-max', + valueType: 'number', + value: this.model.y_lim[1] || 0 + }); + app.registerRenderSubview(this, this.yMaxLimitInputView, "y-lim-max"); + }, + renderZMaxLimitInputView: function () { + if(this.zMaxLimitInputView) { + this.zMaxLimitInputView.remove(); + } + this.zMaxLimitInputView = new InputView({ + parent: this, + required: true, + name: 'z-lim-max', + valueType: 'number', + value: this.model.z_lim[1] || 0 + }); + app.registerRenderSubview(this, this.zMaxLimitInputView, "z-lim-max"); + }, + setBoundaryCondition: function (e) { + let key = e.target.dataset.hook; + this.model.boundary_condition[key] = e.target.checked; + this.updateView(); + }, + setLimitation: function (e) { + let key = e.target.parentElement.parentElement.dataset.name; + let index = Number(e.target.parentElement.parentElement.dataset.index); + this.model[key][index] = Number(e.target.value.trim()); + this.updateView(); + }, + update: function () {}, + updateValid: function () {}, + updateView: function () { + this.parent.renderLimitsView(); + } +}); diff --git a/client/domain-view/views/particle-view.js b/client/domain-view/views/particle-view.js new file mode 100644 index 0000000000..87f1a0b424 --- /dev/null +++ b/client/domain-view/views/particle-view.js @@ -0,0 +1,229 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +//templates +let template = require('../templates/editParticleView.pug'); + +module.exports = View.extend({ + template: template, + events: function () { + let events = {}; + events[`change [data-hook=particle-type-${this.viewIndex}]`] = 'handleSelectType'; + events[`change [data-target=location-${this.viewIndex}]`] = 'handleUpdateLocation'; + events[`change [data-hook=particle-fixed=${this.viewIndex}]`] = 'handleUpdateFixed'; + return events; + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewIndex = attrs.viewIndex; + this.defaultType = attrs.defaultType; + this.disable = attrs.disable ? attrs.disable : false; + this.origType = this.model.type; + this.origPoint = JSON.parse(JSON.stringify(this.model.point)); + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + this.renderTypeSelectView(); + this.renderParticleProperties(); + this.renderXCoordView(); + this.renderYCoordView(); + this.renderZCoordView(); + }, + handleSelectType: function (e) { + this.defaultType = this.parent.model.types.get(this.model.type, "typeID") + this.model.type = Number(e.target.value); + let type = this.parent.model.types.get(this.model.type, "typeID") + if(this.model.mass === this.defaultType.mass) { + this.model.mass = type.mass; + } + if(this.model.volume === this.defaultType.volume) { + this.model.volume = type.volume; + } + if(this.model.rho === this.defaultType.rho) { + this.model.rho = type.rho; + } + if(this.model.nu === this.defaultType.nu) { + this.model.nu = type.nu; + } + if(this.model.c === this.defaultType.c) { + this.model.c = type.c; + } + if(this.model.fixed === this.defaultType.fixed) { + this.model.fixed = type.fixed; + } + this.renderParticleProperties(); + }, + handleUpdateFixed: function (e) { + this.model.fixed = e.target.checked; + }, + handleUpdateLocation: function (e) { + let index = Number(e.target.parentElement.parentElement.dataset.index); + this.model.point[index] = Number(e.target.value); + }, + renderCInputView: function () { + if(this.cInputView) { + this.cInputView.remove(); + } + this.cInputView = new InputView({ + parent: this, + required: true, + name: 'speed-of-sound', + disabled: this.disable, + modelKey: 'c', + valueType: 'number', + value: this.model.c + }); + app.registerRenderSubview(this, this.cInputView, `particle-c-${this.viewIndex}`); + }, + renderParticleProperties: function () { + this.renderCInputView(); + this.renderMassInputView(); + this.renderNuInputView(); + this.renderRhoInputView(); + this.renderVolumeInputView(); + $(this.queryByHook(`particle-fixed-${this.viewIndex}`)).prop('checked', this.model.fixed); + $(this.queryByHook(`particle-fixed-${this.viewIndex}`)).prop('disabled', this.disable); + }, + renderMassInputView: function () { + if(this.massInputView) { + this.massInputView.remove(); + } + this.massInputView = new InputView({ + parent: this, + required: true, + name: 'mass', + disabled: this.disable, + modelKey: 'mass', + valueType: 'number', + value: this.model.mass + }); + app.registerRenderSubview(this, this.massInputView, `particle-mass-${this.viewIndex}`); + }, + renderNuInputView: function () { + if(this.nuInputView) { + this.nuInputView.remove(); + } + this.nuInputView = new InputView({ + parent: this, + required: true, + name: 'viscosity', + disabled: this.disable, + modelKey: 'nu', + valueType: 'number', + value: this.model.nu + }); + app.registerRenderSubview(this, this.nuInputView, `particle-nu-${this.viewIndex}`); + }, + renderRhoInputView: function () { + if(this.rhoInputView) { + this.rhoInputView.remove(); + } + this.rhoInputView = new InputView({ + parent: this, + required: true, + name: 'density', + disabled: this.disable, + modelKey: 'rho', + valueType: 'number', + value: this.model.rho + }); + app.registerRenderSubview(this, this.rhoInputView, `particle-rho-${this.viewIndex}`); + }, + renderTypeSelectView: function () { + if(this.typeSelectView) { + this.typeSelectView.remove(); + } + this.typeSelectView = new SelectView({ + name: 'type', + required: true, + idAttribute: 'typeID', + textAttribute: 'name', + eagerValidate: true, + options: this.parent.model.types, + value: this.parent.model.types.get(this.model.type, "typeID") + }); + app.registerRenderSubview(this, this.typeSelectView, `particle-type-${this.viewIndex}`) + $(this.queryByHook(`particle-type-${this.viewIndex}`)).find('select').prop('disabled', this.disable); + }, + renderVolumeInputView: function () { + if(this.volumeInputView) { + this.volumeInputView.remove(); + } + this.volumeInputView = new InputView({ + parent: this, + required: true, + name: 'volume', + disabled: this.disable, + modelKey: 'volume', + valueType: 'number', + value: this.model.volume + }); + app.registerRenderSubview(this, this.volumeInputView, `particle-vol-${this.viewIndex}`); + }, + renderXCoordView: function () { + if(this.xCoordView) { + this.xCoordView.remove(); + } + this.xCoordView = new InputView({ + parent: this, + required: true, + name: 'x-coord', + disabled: this.disable, + valueType: 'number', + value: this.model.point[0] + }); + app.registerRenderSubview(this, this.xCoordView, `x-coord-${this.viewIndex}`); + }, + renderYCoordView: function () { + if(this.yCoordView) { + this.yCoordView.remove(); + } + this.yCoordView = new InputView({ + parent: this, + required: true, + name: 'y-coord', + disabled: this.disable, + valueType: 'number', + value: this.model.point[1] + }); + app.registerRenderSubview(this, this.yCoordView, `y-coord-${this.viewIndex}`); + }, + renderZCoordView: function () { + if(this.zCoordView) { + this.zCoordView.remove(); + } + this.zCoordView = new InputView({ + parent: this, + required: true, + name: 'z-coord', + disabled: this.disable, + valueType: 'number', + value: this.model.point[2] + }); + app.registerRenderSubview(this, this.zCoordView, `z-coord-${this.viewIndex}`); + }, + update: function (e) {}, + updateValid: function (e) {} +}); \ No newline at end of file diff --git a/client/domain-view/views/properties-view.js b/client/domain-view/views/properties-view.js new file mode 100644 index 0000000000..f45575c57c --- /dev/null +++ b/client/domain-view/views/properties-view.js @@ -0,0 +1,176 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +let Tooltips = require('../../tooltips'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +//templates +let template = require('../templates/propertiesView.pug'); + +module.exports = View.extend({ + events: { + 'click [data-hook=collapse-domain-properties]' : 'changeCollapseButtonText', + 'change [data-hook=static-domain]' : 'setStaticDomain', + 'change [data-hook=density]' : 'setDensity', + 'change [data-target=gravity]' : 'setGravity', + 'change [data-hook=pressure]' : 'setPressure', + 'change [data-hook=speed]' : 'setSpeed' + }, + template: template, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + this.tooltips = Tooltips.domainEditor; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook('domain-properties-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook('domain-properties-view-tab')).tab('show'); + $(this.queryByHook('edit-domain-properties')).removeClass('active'); + $(this.queryByHook('view-domain-properties')).addClass('active'); + }else{ + $(this.queryByHook("static-domain")).prop("checked", this.model.static); + this.renderDensityInputView(); + this.renderXGravityInputView(); + this.renderYGravityInputView(); + this.renderZGravityInputView(); + this.renderPressureInputView(); + this.renderSpeedInputView(); + } + $(this.queryByHook("view-static-domain")).prop("checked", this.model.static); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + renderDensityInputView: function () { + if(this.densityInputView) { + this.densityInputView.remove(); + } + this.densityInputView = new InputView({ + parent: this, + required: true, + name: 'density', + valueType: 'number', + value: this.model.rho_0 || 1 + }); + app.registerRenderSubview(this, this.densityInputView, "density"); + }, + renderPressureInputView: function () { + if(this.pressureInputView) { + this.pressureInputView.remove(); + } + this.pressureInputView = new InputView({ + parent: this, + required: true, + name: 'pressure', + valueType: 'number', + value: this.model.p_0 || 0 + }); + app.registerRenderSubview(this, this.pressureInputView, "pressure"); + }, + renderSpeedInputView: function () { + if(this.speedInputView) { + this.speedInputView.remove(); + } + this.speedInputView = new InputView({ + parent: this, + required: true, + name: 'speed', + valueType: 'number', + value: this.model.c_0 || 0 + }); + app.registerRenderSubview(this, this.speedInputView, "speed"); + }, + renderXGravityInputView: function () { + if(this.xGravityInputView) { + this.xGravityInputView.remove(); + } + this.xGravityInputView = new InputView({ + parent: this, + required: true, + name: 'gravity-x', + valueType: 'number', + value: this.model.gravity[0], + label: "X: " + }); + app.registerRenderSubview(this, this.xGravityInputView, "gravity-x"); + }, + renderYGravityInputView: function () { + if(this.yGravityInputView) { + this.yGravityInputView.remove(); + } + this.yGravityInputView = new InputView({ + parent: this, + required: true, + name: 'gravity-y', + valueType: 'number', + value: this.model.gravity[1], + label: "Y: " + }); + app.registerRenderSubview(this, this.yGravityInputView, "gravity-y"); + }, + renderZGravityInputView: function () { + if(this.zGravityInputView) { + this.zGravityInputView.remove(); + } + this.zGravityInputView = new InputView({ + parent: this, + required: true, + name: 'gravity-z', + valueType: 'number', + value: this.model.gravity[2], + label: "Z: " + }); + app.registerRenderSubview(this, this.zGravityInputView, "gravity-z"); + }, + setDensity: function (e) { + this.model.rho_0 = Number(e.target.value); + this.updateView(); + }, + setGravity: function (e) { + let index = Number(e.target.parentElement.parentElement.dataset.index); + this.model.gravity[index] = Number(e.target.value); + this.updateView(); + }, + setPressure: function (e) { + this.model.p_0 = Number(e.target.value); + this.updateView(); + }, + setSpeed: function (e) { + this.model.c_0 = Number(e.target.value); + this.updateView(); + }, + setStaticDomain: function (e) { + this.model.static = e.target.checked; + this.updateView(); + }, + update: function (e) {}, + updateValid: function (e) {}, + updateView: function (e) { + this.parent.renderPropertiesView(); + } +}); \ No newline at end of file diff --git a/client/views/quickview-domain-types.js b/client/domain-view/views/quickview-type.js similarity index 87% rename from client/views/quickview-domain-types.js rename to client/domain-view/views/quickview-type.js index ee7f17446f..1e0f17bc6c 100644 --- a/client/views/quickview-domain-types.js +++ b/client/domain-view/views/quickview-type.js @@ -17,9 +17,9 @@ along with this program. If not, see . */ //views -var View = require('ampersand-view'); +let View = require('ampersand-view'); //templates -var template = require('../templates/includes/quickviewDomainTypes.pug'); +let template = require('../templates/quickviewType.pug'); module.exports = View.extend({ template: template, diff --git a/client/domain-view/views/type-view.js b/client/domain-view/views/type-view.js new file mode 100644 index 0000000000..ea2ac60f3d --- /dev/null +++ b/client/domain-view/views/type-view.js @@ -0,0 +1,251 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +let _ = require('underscore'); +//support files +let app = require('../../app'); +let Tooltips = require('../../tooltips'); +let tests = require('../../views/tests'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +//templates +let editTemplate = require('../templates/editType.pug'); +let viewTemplate = require('../templates/viewType.pug'); + +module.exports = View.extend({ + bindings: { + 'model.selected' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'select' + } + }, + events: { + 'change [data-hook=type-name]' : 'handleRenameType', + 'change [data-target=type-defaults]' : 'updateView', + 'change [data-hook=td-fixed]' : 'setTDFixed', + 'change [data-hook=type-geometry]' : 'updateView', + 'click [data-hook=select]' : 'selectType', + 'click [data-hook=unassign-all]' : 'handleUnassignParticles', + 'click [data-hook=delete-type]' : 'handleDeleteType', + 'click [data-hook=delete-all]' : 'handleDeleteTypeAndParticle', + 'click [data-hook=apply-geometry]' : 'handleApplyGeometry' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + this.tooltips = Tooltips.domainType; + }, + render: function (attrs, options) { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + if(!this.viewMode) { + if(this.model.selected) { + setTimeout(_.bind(this.openTypeDetails, this), 1); + } + } + $(this.queryByHook('view-td-fixed')).prop('checked', this.model.fixed) + app.documentSetup(); + }, + completeAction: function () { + $(this.queryByHook(`tg-in-progress-${this.model.typeID}`)).css("display", "none"); + $(this.queryByHook(`tg-complete-${this.model.typeID}`)).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook(`tg-complete-${this.model.typeID}`)).css("display", "none"); + }, 5000); + }, + errorAction: function (action) { + $(this.queryByHook(`tg-in-progress-${this.model.typeID}`)).css("display", "none"); + $(this.queryByHook(`tg-action-error-${this.model.typeID}`)).html(action); + $(this.queryByHook(`tg-error-${this.model.typeID}`)).css("display", "block"); + }, + handleApplyGeometry: function (e) { + this.startAction(); + let domain = this.model.collection.parent; + let particles = domain.particles.toJSON(); + let center = [ + (domain.x_lim[1] + domain.x_lim[0]) / 2, + (domain.y_lim[1] + domain.y_lim[0]) / 2, + (domain.z_lim[1] + domain.z_lim[0]) / 2 + ]; + let data = {particles: particles, type: this.model.toJSON(), center: center} + let endpoint = path.join(app.getApiPath(), 'spatial-model/apply-geometry'); + app.postXHR(endpoint, data, { + success: (err, response, body) => { + this.parent.parent.applyGeometry(body.particles, this.model); + this.completeAction(); + }, + error: (err, response, body) => { + if(body.Traceback.includes("SyntaxError")) { + var tracePart = body.Traceback.split('\n').slice(6) + tracePart.splice(2, 2) + tracePart[1] = tracePart[1].replace(new RegExp(' ', 'g'), ' ') + var errorBlock = `

${body.Message}
${tracePart.join('
')}

` + }else{ + var errorBlock = body.Message + } + this.errorAction(errorBlock); + } + }); + }, + handleDeleteType: function (e) { + let type = Number(e.target.dataset.type); + this.model.collection.removeType(this.model); + this.parent.parent.deleteType(this.model.typeID); + }, + handleDeleteTypeAndParticle: function (e) { + let type = Number(e.target.dataset.type); + this.model.collection.removeType(this.model); + this.parent.parent.deleteType(this.model.typeID, {unassign: false}); + }, + handleRenameType: function (e) { + this.updateView(); + let type = Number(e.target.parentElement.parentElement.dataset.target); + let name = e.target.value; + this.parent.parent.renameType(type, name); + }, + handleUnassignParticles: function () { + this.model.numParticles = 0; + this.parent.parent.unassignAllParticles(this.model.typeID); + }, + openTypeDetails: function () { + $("#collapse-type-details" + this.model.typeID).collapse("show"); + }, + selectType: function () { + this.model.selected = !this.model.selected; + }, + setTDFixed: function (e) { + this.model.fixed = e.target.checked; + this.updateView(); + }, + startAction: function () { + $(this.queryByHook(`tg-complete-${this.model.typeID}`)).css("display", "none"); + $(this.queryByHook(`tg-error-${this.model.typeID}`)).css("display", "none"); + $(this.queryByHook(`tg-in-progress-${this.model.typeID}`)).css("display", "inline-block"); + }, + update: function () {}, + updateValid: function () {}, + updateView: function () { + this.parent.renderViewTypeView(); + this.parent.parent.updateParticleViews({includeGeometry: true}); + }, + subviews: { + inputTypeID: { + hook: "type-name", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'name', + modelKey: 'name', + tests: [tests.invalidChar], + valueType: 'string', + value: this.model.name + }); + } + }, + inputMass: { + hook: "td-mass", + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'mass', + modelKey: 'mass', + valueType: 'number', + tests: tests.valueTests, + value: this.model.mass + }); + } + }, + inputVolume: { + hook: 'td-vol', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'volume', + modelKey: 'volume', + valueType: 'number', + tests: tests.valueTests, + value: this.model.volume + }); + } + }, + inputDensity: { + hook: 'td-rho', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'density', + modelKey: 'rho', + valueType: 'number', + tests: tests.valueTests, + value: this.model.rho + }); + } + }, + inputViscosity: { + hook: 'td-nu', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'viscosity', + modelKey: 'nu', + valueType: 'number', + tests: tests.valueTests, + value: this.model.nu + }); + } + }, + inputSpeedOfSound: { + hook: 'td-c', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'speed-of-sound', + modelKey: 'c', + valueType: 'number', + tests: tests.valueTests, + value: this.model.c + }); + } + }, + inputGeometry: { + hook: 'type-geometry', + prepareView: function (el) { + return new InputView({ + parent: this, + required: false, + name: 'geometry', + modelKey: 'geometry', + valueType: 'string', + value: this.model.geometry, + placeholder: "--Expression in terms of 'x', 'y', 'z'--" + }); + } + } + } +}); \ No newline at end of file diff --git a/client/domain-view/views/types-description-view.js b/client/domain-view/views/types-description-view.js new file mode 100644 index 0000000000..2279ff68ee --- /dev/null +++ b/client/domain-view/views/types-description-view.js @@ -0,0 +1,128 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let path = require('path'); +//support files +let app = require('../../app'); +//views +let View = require('ampersand-view'); +let SelectView = require('ampersand-select-view'); +//templates +let template = require('../templates/typesDescriptionView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'change [data-hook=file-select]' : 'selectTypeFile', + 'change [data-hook=file-location-select]' : 'selectFileLocation', + 'click [data-hook=set-particle-types-btn]' : 'getTypesFromFile' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.typeFile = null; + }, + render: function(attrs, options) { + View.prototype.render.apply(this, arguments); + let endpoint = path.join(app.getApiPath(), 'spatial-model/types-list'); + app.getXHR(endpoint, {success: (err, response, body) => { + this.typeFiles = body.files; + this.fileLocations = body.paths; + this.renderFileSelectView(); + }}); + }, + completeAction: function () { + $(this.queryByHook('st-in-progress')).css('display', 'none'); + $(this.queryByHook('st-complete')).css('display', 'inline-block'); + }, + errorAction: function (action) { + $(this.queryByHook('st-in-progress')).css('display', 'none'); + $(this.queryByHook('st-action-error')).text(action); + $(this.queryByHook('st-error')).css('display', 'block'); + }, + getTypesFromFile: function () { + this.startAction(); + let queryStr = `?path=${this.typeFile}`; + let endpoint = path.join(app.getApiPath(), 'spatial-model/particle-types') + queryStr; + app.getXHR(endpoint, { + success: (err, response, body) => { + this.parent.setParticleTypes(body.names, body.types); + this.completeAction(); + }, + error: (err, response, body) => { + this.errorAction(body.Message); + } + }); + }, + renderFileLocationSelectView: function (options) { + if(this.fileLocationSelectView) { + this.fileLocationSelectView.remove(); + } + this.fileLocationSelectView = new SelectView({ + name: 'type-locations', + required: false, + idAttributes: 'cid', + options: options, + unselectedText: "-- Select Location --" + }); + app.registerRenderSubview(this, this.fileLocationSelectView, "file-location-select"); + }, + renderFileSelectView: function () { + if(this.fileSelectView) { + this.fileSelectView.remove(); + } + this.fileSelectView = new SelectView({ + name: 'type-files', + required: false, + idAttributes: 'cid', + options: this.typeFiles, + unselectedText: "-- Select Type File --", + }); + app.registerRenderSubview(this, this.fileSelectView, "file-select"); + }, + selectFileLocation: function (e) { + let value = e.target.value; + this.typeFile = value ? value : null; + $(this.queryByHook("set-particle-types-btn")).prop("disabled", this.typeFile === null); + }, + selectTypeFile: function (e) { + let value = e.target.value; + if(value) { + if(this.fileLocations[value].length > 1) { + $(this.queryByHook("type-location-message")).css('display', "block"); + $(this.queryByHook("file-location-container")).css("display", "inline-block"); + this.renderFileLocationSelectView(this.fileLocations[value]); + this.typeFile = null; + }else{ + $(this.queryByHook("type-location-message")).css('display', "none"); + $(this.queryByHook("file-location-container")).css("display", "none"); + this.typeFile = this.fileLocations[value][0]; + } + }else{ + $(this.queryByHook("type-location-message")).css('display', "none"); + $(this.queryByHook("file-location-container")).css("display", "none"); + this.typeFile = null; + } + $(this.queryByHook("set-particle-types-btn")).prop("disabled", this.typeFile === null); + }, + startAction: function () { + $(this.queryByHook("st-complete")).css('display', 'none'); + $(this.queryByHook("st-error")).css('display', 'none'); + $(this.queryByHook("st-in-progress")).css("display", "inline-block"); + }, +}); \ No newline at end of file diff --git a/client/domain-view/views/types-view.js b/client/domain-view/views/types-view.js new file mode 100644 index 0000000000..b5351ed9e9 --- /dev/null +++ b/client/domain-view/views/types-view.js @@ -0,0 +1,105 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +//support files +let app = require('../../app'); +//views +let View = require('ampersand-view'); +let TypeView = require('./type-view'); +//templates +let template = require('../templates/typesView.pug'); + +module.exports = View.extend({ + template: template, + events: { + 'click [data-hook=collapse-domain-types]' : 'changeCollapseButtonText', + 'click [data-hook=add-domain-type]' : 'handleAddDomainType', + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.readOnly = attrs.readOnly ? attrs.readOnly : false; + }, + render: function (attrs, options) { + View.prototype.render.apply(this, arguments); + if(this.readOnly) { + $(this.queryByHook('domain-types-edit-tab')).addClass("disabled"); + $(".nav .disabled>a").on("click", function(e) { + e.preventDefault(); + return false; + }); + $(this.queryByHook('domain-types-view-tab')).tab('show'); + $(this.queryByHook('edit-domain-types')).removeClass('active'); + $(this.queryByHook('view-domain-types')).addClass('active'); + }else{ + this.renderEditTypeView(); + } + this.toggleDomainError(); + this.renderViewTypeView(); + }, + changeCollapseButtonText: function (e) { + app.changeCollapseButtonText(this, e); + }, + handleAddDomainType: function () { + let name = this.collection.addType(); + this.parent.addType(name); + }, + renderEditTypeView: function () { + if(this.editTypeView) { + this.editTypeView.remove(); + } + let options = { + filter: function (model) { + return model.typeID != 0; + } + } + this.editTypeView = this.renderCollection( + this.collection, + TypeView, + this.queryByHook("edit-domain-types-list"), + options + ); + }, + renderViewTypeView: function () { + if(this.viewTypeView) { + this.viewTypeView.remove(); + } + let options = { + viewOptions: {viewMode: true}, + filter: function (model) { + return model.typeID != 0; + } + } + this.viewTypeView = this.renderCollection( + this.collection, + TypeView, + this.queryByHook("view-domain-types-list"), + options + ); + }, + toggleDomainError: function () { + let errorMsg = $(this.queryByHook('domain-error')) + if(!this.collection.parent.valid) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } + } +}); \ No newline at end of file diff --git a/client/views/view-particle.js b/client/domain-view/views/view-particle.js similarity index 89% rename from client/views/view-particle.js rename to client/domain-view/views/view-particle.js index d832472fda..68cd5c7d7a 100644 --- a/client/views/view-particle.js +++ b/client/domain-view/views/view-particle.js @@ -17,9 +17,9 @@ along with this program. If not, see . */ //views -var View = require('ampersand-view'); +let View = require('ampersand-view'); //templates -var template = require('../templates/includes/viewParticle.pug'); +let template = require('../templates/viewParticle.pug'); module.exports = View.extend({ template: template, diff --git a/client/model-view/model-view.js b/client/model-view/model-view.js index ee749e91a9..8cdd074563 100644 --- a/client/model-view/model-view.js +++ b/client/model-view/model-view.js @@ -46,6 +46,9 @@ module.exports = View.extend({ 'change [data-hook=all-continuous]' : 'setDefaultMode', 'change [data-hook=all-discrete]' : 'setDefaultMode', 'change [data-hook=advanced]' : 'setDefaultMode', + 'change [data-hook=spatial-continuous]' : 'setDefaultMode', + 'change [data-hook=spatial-discrete]' : 'setDefaultMode', + 'change [data-hook=spatial-discrete-concentration]' : 'setDefaultMode', 'change [data-hook=edit-volume]' : 'updateVolumeViewer', 'click [data-hook=collapse-mv-advanced-section]' : 'changeCollapseButtonText', 'click [data-hook=collapse-system-volume]' : 'changeCollapseButtonText' @@ -76,14 +79,27 @@ module.exports = View.extend({ this.setReadOnlyMode("model-mode"); this.setReadOnlyMode("system-volume"); }else { - if(this.model.defaultMode === "" && !this.model.is_spatial){ - this.getInitialDefaultMode(); + if(this.model.defaultMode === ""){ + if(this.model.is_spatial) { + this.model.defaultMode = "discrete"; + $(this.queryByHook("spatial-discrete")).prop('checked', true); + }else{ + this.getInitialDefaultMode(); + } }else { - let dataHooks = {'continuous':'all-continuous', 'discrete':'all-discrete', 'dynamic':'advanced'}; - $(this.queryByHook(dataHooks[this.model.defaultMode])).prop('checked', true); if(this.model.is_spatial) { - $(this.queryByHook("advanced-model-mode")).css("display", "none"); + let dataHooks = { + 'continuous':'spatial-continuous', + 'discrete':'spatial-discrete', + 'discrete-concentration':'spatial-discrete-concentration' + }; + $(this.queryByHook(dataHooks[this.model.defaultMode])).prop('checked', true); + $(this.queryByHook("model-mode-container")).css("display", "none"); $(this.queryByHook("system-volume-container")).css("display", "none"); + }else{ + let dataHooks = {'continuous':'all-continuous', 'discrete':'all-discrete', 'dynamic':'advanced'}; + $(this.queryByHook(dataHooks[this.model.defaultMode])).prop('checked', true); + $(this.queryByHook("spatial-model-mode-container")).css("display", "none"); } } this.model.reactions.on("change", (reactions) => { @@ -170,34 +186,14 @@ module.exports = View.extend({ if(this.domainViewer) { this.domainViewer.remove(); } - if(domainPath && domainPath !== "viewing") { - let queryStr = `?path=${this.model.directory}&domain_path=${domainPath}`; - let endpoint = path.join(app.getApiPath(), "spatial-model/load-domain") + queryStr; - app.getXHR(endpoint, { - always: (err, response, body) => { - let domain = new Domain(body.domain); - this.domainViewer = new DomainViewer({ - parent: this, - model: domain, - domainPath: domainPath, - readOnly: this.readOnly, - domainElements: this.domainElements, - domainPlot: this.domainPlot - }); - app.registerRenderSubview(this, this.domainViewer, 'domain-viewer-container'); - } - }); - }else{ - this.domainViewer = new DomainViewer({ - parent: this, - model: this.model.domain, - domainPath: domainPath, - readOnly: this.readOnly, - domainElements: this.domainElements, - domainPlot: this.domainPlot - }); - app.registerRenderSubview(this, this.domainViewer, 'domain-viewer-container'); - } + this.domainViewer = new DomainViewer({ + parent: this, + model: this.model, + readOnly: this.readOnly, + domainElements: this.domainElements, + domainPlot: this.domainPlot + }); + app.registerRenderSubview(this, this.domainViewer, 'domain-viewer-container'); }, renderEventsView: function () { if(this.model.is_spatial) { return }; @@ -319,8 +315,10 @@ module.exports = View.extend({ let prevMode = this.model.defaultMode; this.model.defaultMode = e.target.dataset.name; this.speciesView.defaultMode = e.target.dataset.name; - this.setAllSpeciesModes(prevMode); - this.toggleVolumeContainer(); + if(!this.model.is_spatial) { + this.setAllSpeciesModes(prevMode); + this.toggleVolumeContainer(); + } }, setInitialDefaultMode: function (modal, mode) { var dataHooks = {'continuous':'all-continuous', 'discrete':'all-discrete', 'dynamic':'advanced'}; diff --git a/client/model-view/modelView.pug b/client/model-view/modelView.pug index 9284a249c5..8fea01cd20 100644 --- a/client/model-view/modelView.pug +++ b/client/model-view/modelView.pug @@ -32,17 +32,28 @@ div#model-view hr - div.row + div.row(data-hook="model-mode-container") div.col-sm-4 input.mr-2(type="radio" name="default-mode" data-hook="all-continuous" data-name="continuous") span Concentration div.col-sm-4 input.mr-2(type="radio" name="default-mode" data-hook="all-discrete" data-name="discrete") span Population - div.col-sm-4(data-hook="advanced-model-mode") + div.col-sm-4 input.mr-2(type="radio" name="default-mode" data-hook="advanced" data-name="dynamic") span Hybrid Concentration/Population + div.row(data-hook="spatial-model-mode-container") + div.col-sm-4 + input.mr-2(type="radio" name="default-mode" data-hook="spatial-continuous" data-name="continuous") + span Concentration + div.col-sm-4 + input.mr-2(type="radio" name="default-mode" data-hook="spatial-discrete" data-name="discrete") + span Population (outputs: absolute value) + div.col-sm-4 + input.mr-2(type="radio" name="default-mode" data-hook="spatial-discrete-concentration" data-name="discrete-concentration") + span Population (outputs: scaled by volume) + div.tab-pane(id="view-model-mode" data-hook="view-model-mode") hr diff --git a/client/model-view/templates/domainViewer.pug b/client/model-view/templates/domainViewer.pug index 41b57b714f..d675aa0174 100644 --- a/client/model-view/templates/domainViewer.pug +++ b/client/model-view/templates/domainViewer.pug @@ -41,101 +41,11 @@ div#domain-viewer.card div.inline(data-hook="select-location") - div.row - - div.col-md-4 - - h4 Domain Properties - - hr.mt-2 - div.row - div.col-sm-6: h6.pl-2 Static Domain - div.col-sm-6: input(type="checkbox" id="static-domain" data-hook="static-domain" checked=this.model.static disabled) - - hr.mt-2 - div.row - div.col-sm-6: h6.pl-2 Density - div.col-sm-6=this.model.rho_0 - - hr.mt-2 - div.row - div.col-sm-6: h6.pl-2 Gravity - div.col-sm-6=this.gravity - - hr.mt-2 - div.row - div.col-sm-6: h6.pl-2 Pressure - div.col-sm-6=this.model.p_0 - - hr.mt-2 - div.row - div.col-sm-6: h6.pl-2 Speed of Sound - div.col-sm-6=this.model.c_0 - - div.col-md-8 - - h4 Domain Limits - - hr.mt-2 - div.row.head.mx-1 - div.col-sm-3 - div.col-sm-3.pb-1: h6 Minimum - div.col-sm-3: h6 Minimum - div.col-sm-3: h6 Reflect - - div.row.mt-3 - div.col-sm-3: h6.pl-2 X-Axis - div.col-sm-3=this.model.x_lim[0] - div.col-sm-3=this.model.x_lim[1] - div.col-sm-3: input(type="checkbox" checked=this.model.boundary_condition.reflect_x disabled) - - hr.mt-2 - div.row - div.col-sm-3: h6.pl-2 Y-Axis - div.col-sm-3=this.model.y_lim[0] - div.col-sm-3=this.model.y_lim[1] - div.col-sm-3: input(type="checkbox" checked=this.model.boundary_condition.reflect_y disabled) - - hr.mt-2 - div.row - div.col-sm-3: h6.pl-2 Z-Axis - div.col-sm-3=this.model.z_lim[0] - div.col-sm-3=this.model.z_lim[1] - div.col-sm-3: input(type="checkbox" checked=this.model.boundary_condition.reflect_z disabled) - - div.mt-3 - - h4 Types - - hr - - h6="Number of Un-Assigned Particles: " + this.model.types.get(0, "typeID").numParticles - - div.component-valid(data-hook="domain-error"): p.text-danger A domain cannot have any un-assigned particles. - - hr - - div.mx-1.row.head.align-items-baseline - - div.col-sm-2: h6 Name - - div.col-sm-2: h6 Number of Particles - - div.col-sm-2: h6 Mass - - div.col-sm-2: h6 Volume - - div.col-sm-2: h6 Viscosity - - div.col-sm-2: h6 Fixed - - div.my-3(data-hook="domain-types-list") + div(data-hook="domain-view-container") div.tab-pane.active(id="edit-domain" data-hook="edit-domain") - hr - - div + div.mt-3 button.btn.btn-outline-primary.box-shadow(data-hook="edit-domain-btn") Edit Domain diff --git a/client/model-view/templates/editCustomStoichSpecie.pug b/client/model-view/templates/editCustomStoichSpecie.pug deleted file mode 100644 index 5f0d1a6d7f..0000000000 --- a/client/model-view/templates/editCustomStoichSpecie.pug +++ /dev/null @@ -1,10 +0,0 @@ -div - div.inline - button.btn.btn-outline-secondary(class='btn-sm', data-hook='decrement') - - span.custom(data-hook="ratio") - div.inline - button.btn.btn-outline-secondary(class='btn-sm', data-hook='increment') + - select(data-hook="select-stoich-specie") - span.message.message-below.message-error(data-hook="message-container") - p(data-hook="message-text") - button.btn.btn-outline-secondary.custom(class='btn-sm', data-hook='remove') X \ No newline at end of file diff --git a/client/model-view/templates/editReaction.pug b/client/model-view/templates/editReaction.pug new file mode 100644 index 0000000000..455489de7d --- /dev/null +++ b/client/model-view/templates/editReaction.pug @@ -0,0 +1,90 @@ +div.mx-1 + + if(this.model.collection.indexOf(this.model) !== 0) + hr + + div.row.align-items-baseline + + div.col-sm-1 + + div.pl-3: input(type="checkbox" data-hook="select" data-toggle="collapse" data-target="#collapse-reaction-details" + this.model.compID) + + div.col-sm-3(data-hook="input-name-container") + + div.col-sm-4.reaction-list-summary(data-hook="summary") + + div.col-sm-2 + + div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation") + + button.btn.btn-outline-secondary.btn-sm.box-shadow(data-hook="edit-annotation-btn") Edit + + div.col-sm-2 + + button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X + + div.mx-1.pl-2(data-hook="reaction-details") + + div.collapse(id="collapse-reaction-details" + this.model.compID) + + hr + + div.mx-1.row.align-items-baseline + + div.col-sm-6 + + div(data-hook="select-reaction-type") + + div + + h5.inline + + span(for="propensity-input") Stochastic Propensity Function: + + div.tooltip-icon(data-html="true" data-hook="rate-parameter-tooltip" data-toggle="tooltip" title=this.parent.tooltips.propensity) + + div + + div(id="propensity-input" data-hook="propensity-input") + + div + + h5.inline + + span(for="ode-propensity-input") ODE Propensity Function: + + div.tooltip-icon(data-html="true" data-hook="rate-parameter-tooltip" data-toggle="tooltip" title=this.parent.tooltips.odePropensity) + + div + + div(id="ode-propensity-input" data-hook="ode-propensity-input") + + div.align-self-center + + input.inline.mr-3(type="checkbox" data-hook="mirror-propensities") + + span.inline Same as Stochastic Propensity Function + + div.col-sm-6 + + div + + span(for="select-rate-parameter") Rate Parameter: + + div.tooltip-icon(data-html="true" data-hook="rate-parameter-tooltip" data-toggle="tooltip" title=this.parent.tooltips.rate) + + div.inline.horizontal-space(id="select-rate-parameter" data-hook="select-rate-parameter") + + div.row + + div.col-sm-6(data-hook="reactants-editor") + + div.col-sm-6(data-hook="products-editor") + + div.col-md-12(data-hook="domain-types-editor") + + div.collapse(data-hook="conflicting-modes-message") + + p.text-warning Warning: This reaction containes Variables with modes of 'Concentration' and 'Population' or 'Hybrid Concentration/Population' and will fire stochastically. + + div(data-hook="custom-reaction-error"): p.text-danger A reaction must have at least one reactant or product diff --git a/client/model-view/templates/editStoichSpecie.pug b/client/model-view/templates/editStoichSpecie.pug deleted file mode 100644 index 34e754e846..0000000000 --- a/client/model-view/templates/editStoichSpecie.pug +++ /dev/null @@ -1,6 +0,0 @@ -div - label.select - span(data-hook="ratio") - select(data-hook="select-stoich-specie") - span.message.message-below.message-error(data-hook="message-container") - p(data-hook="message-text") \ No newline at end of file diff --git a/client/model-view/templates/reactantProduct.pug b/client/model-view/templates/reactantProduct.pug index 72578cf9f3..65d07ac51d 100644 --- a/client/model-view/templates/reactantProduct.pug +++ b/client/model-view/templates/reactantProduct.pug @@ -6,9 +6,12 @@ div(data-hook="reaction-details") div.tooltip-icon(data-hook="field-title-tooltip" data-html="true" data-toggle="tooltip" title="") - div(data-hook="reactants-editor") + div(data-hook=this.hookAnchor + "-editor") - div(data-hook="collapse", class="collapse") - div.row(data-hook="add-specie-container") - div(data-hook="select-specie") - button.btn.btn-outline-secondary(class="btn-sm", data-hook="add-selected-specie") add \ No newline at end of file + div.hidden(data-hook="custom-" + this.hookAnchor) + + div.row(data-hook=this.hookAnchor + "-add-specie-container") + + div(data-hook=this.hookAnchor + "-select-specie") + + button.btn.btn-outline-secondary(class="btn-sm", data-hook=this.hookAnchor + "-add-selected-specie") add diff --git a/client/model-view/templates/reactionDetails.pug b/client/model-view/templates/reactionDetails.pug deleted file mode 100644 index 90ad574574..0000000000 --- a/client/model-view/templates/reactionDetails.pug +++ /dev/null @@ -1,27 +0,0 @@ -div.row(data-hook="reaction-details") - - div.reaction-summary Summary: - - div(data-hook="summary-container", id="reaction-summary") - - div.col-md-12(data-hook="select-reaction-type") - - div.col-md-12.verticle-space - - span(for="select-rate-parameter" data-hook="rate-parameter-label") - - div.tooltip-icon(data-html="true" data-hook="rate-parameter-tooltip" data-toggle="tooltip" title="") - - div.inline.horizontal-space(id="select-rate-parameter" data-hook="select-rate-parameter") - - div.col-md-12(data-hook="domain-types-editor") - - div.col-md-6(data-hook="reactants-editor") - - div.col-md-6(data-hook="products-editor") - - div.collapse(data-hook="conflicting-modes-message") - - p.text-warning Warning: This reaction containes Variables with modes of 'Concentration' and 'Population' or 'Hybrid Concentration/Population' and will fire stochastically. - - div(data-hook="custom-reaction-error"): p.text-danger A reaction must have at least one reactant or product diff --git a/client/model-view/templates/reactionListing.pug b/client/model-view/templates/reactionListing.pug deleted file mode 100644 index 3b7ea5cae3..0000000000 --- a/client/model-view/templates/reactionListing.pug +++ /dev/null @@ -1,24 +0,0 @@ -div.mx-1 - - if(this.model.collection.indexOf(this.model) !== 0) - hr - - div.row.align-items-baseline - - div.col-md-2 - - div.pl-3: input(type="radio" data-hook="select" name="reaction-select") - - div.col-md-2(data-hook="input-name-container") - - div.col-md-3.reaction-list-summary(data-hook="summary") - - div.col-md-3 - - div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation || "Click 'Add' to add an annotation") - - button.btn.btn-outline-secondary.btn-sm.box-shadow(data-hook="edit-annotation-btn") Edit - - div.col-md-2 - - button.btn.btn-outline-secondary.box-shadow(data-hook="remove") X diff --git a/client/model-view/templates/reactionTypes.pug b/client/model-view/templates/reactionRestrictTo.pug similarity index 100% rename from client/model-view/templates/reactionTypes.pug rename to client/model-view/templates/reactionRestrictTo.pug diff --git a/client/model-view/templates/reactionsView.pug b/client/model-view/templates/reactionsView.pug index c3b6e5b7ef..e32bec78cd 100644 --- a/client/model-view/templates/reactionsView.pug +++ b/client/model-view/templates/reactionsView.pug @@ -27,37 +27,31 @@ div#reactions-editor.card div.tab-pane.active(id="edit-reactions" data-hook="edit-reactions") - div.row.mb-3 - - div.col-md-7.container-part - - hr + hr - div.mx-1.row.head.align-items-baseline + div.mx-1.row.head.align-items-baseline + + div.col-sm-1: h6.align-bottom Edit - div.col-md-2: h6.align-bottom Edit - - div.col-md-2 - - h6.inline Name + div.col-sm-3 - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.name) + h6.inline Name - div.col-md-3: h6 Summary + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.name) - div.col-md-3 + div.col-sm-4: h6 Summary - div.inline + div.col-sm-2 - h6 Annotation + div.inline - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.annotation) - - div.col-md-2: h6 Remove + h6 Annotation - div.mt-3(data-hook="edit-reaction-list") + div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.annotation) + + div.col-sm-2: h6 Remove - div.col-md-5.container-part(data-hook="reaction-details-container") + div.mt-3(data-hook="edit-reaction-list") div(data-hook="massaction-message"): p.text-info To add a mass action reaction the model must have at least one parameter @@ -66,7 +60,7 @@ div#reactions-editor.card div.dropdown.inline button.btn.btn-outline-primary.dropdown-toggle.box-shadow#addReactionBtn( - data-hook="add-reaction-full", + data-hook="add-reaction", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false", @@ -74,29 +68,41 @@ div#reactions-editor.card ) Add Reaction ul.dropdown-menu(aria-labelledby="addReactionBtn") - li.dropdown-item(data-hook="creation") Creation Reaction - li.dropdown-item(data-hook="destruction") Destruction Reaction - li.dropdown-item(data-hook="change") Transformation Reaction - li.dropdown-item(data-hook="dimerization") Dimerization Reaction - li.dropdown-item(data-hook="merge") Merge Reaction - li.dropdown-item(data-hook="split") Split Reaction - li.dropdown-item(data-hook="four") Four Reaction + li.dropdown-header Mass Action li.dropdown-divider - li.dropdown-item(data-hook="custom-massaction") Custom mass action - li.dropdown-item(data-hook="custom-propensity") Custom propensity - - div.dropdown.inline - - button.btn.btn-outline-primary.dropdown-toggle.box-shadow#addReactionBtn( - data-hook="add-reaction-partial", - data-toggle="dropdown", - aria-haspopup="true", - aria-expanded="false", - type="button" - ) Add Reaction - - ul.dropdown-menu(aria-labelledby="addReactionBtn") - li.dropdown-item(data-hook="custom-propensity") Custom propensity + li.dropdown-item.rtype(data-hook="creation") + + span.reaction-lb2 Creation Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="destruction") + + span.reaction-lb2 Destruction Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="change") + + span.reaction-lb2 Transformation Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="dimerization") + + span.reaction-lb2 Dimerization Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="merge") + + span.reaction-lb2 Merge Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="split") + + span.reaction-lb2 Split Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="four") + + span.reaction-lb2 Four Reaction + span.reaction-lb3 Parameter Required + li.dropdown-item.rtype(data-hook="custom-massaction") + span Custom Mass Action + span.reaction-lb3 Parameter Required + li.dropdown-divider + li.dropdown-item(data-hook="custom-propensity") Custom Propensity div.tab-pane(id="view-reactions" data-hook="view-reactions") diff --git a/client/model-view/templates/stoichSpecieView.pug b/client/model-view/templates/stoichSpecieView.pug new file mode 100644 index 0000000000..073d00ec0b --- /dev/null +++ b/client/model-view/templates/stoichSpecieView.pug @@ -0,0 +1,21 @@ +div + + label.select + + div.inline(data-hook="custom-decrement") + + button.btn.btn-outline-secondary.btn-sm(data-hook='decrement') - + + span.custom(data-hook="ratio") + + div.inline(data-hook="custom-increment") + + button.btn.btn-outline-secondary.btn-sm(data-hook='increment') + + + select(data-hook="select-stoich-specie") + + span.message.message-below.message-error(data-hook="message-container") + + p(data-hook="message-text") + + button.btn.btn-outline-secondary.custom.btn-sm(data-hook='custom-remove') X diff --git a/client/model-view/templates/viewReaction.pug b/client/model-view/templates/viewReaction.pug index 21be6b3275..6c45bdc68a 100644 --- a/client/model-view/templates/viewReaction.pug +++ b/client/model-view/templates/viewReaction.pug @@ -5,20 +5,38 @@ div.mx-1 div.row.align-items-baseline - div.col-md-2 + div.col-sm-2 div.pl-2=this.model.name - div.col-md-3(data-hook="summary") + div.col-sm-3(data-hook="summary") - div.col-md-3=this.rate + if this.model.massaction + div.col-sm-3 + + div=this.rate + else + div.col-sm-3 + h5 + + span Stochastic Propensity Function: + + div.pl-3=this.model.propensity + + if !this.model.mirrorPropensities + h5 + + span ODE Propensity Function: + + div.pl-3=this.model.odePropensity + if this.model.collection.parent.is_spatial - div.col-md-2(data-hook="reaction-types") + div.col-sm-2(data-hook="reaction-types") div=this.types.join(', ') - div.col-md-2(data-hook="reaction-annotation-header") + div.col-sm-2(data-hook="reaction-annotation-header") if this.model.annotation div.tooltip-icon-large(data-hook="annotation-tooltip" data-html="true" data-toggle="tooltip" title=this.model.annotation) diff --git a/client/model-view/views/domain-viewer.js b/client/model-view/views/domain-viewer.js index 04b83f3fdc..c2a7d99b18 100644 --- a/client/model-view/views/domain-viewer.js +++ b/client/model-view/views/domain-viewer.js @@ -18,48 +18,42 @@ along with this program. If not, see . let $ = require('jquery'); let path = require('path'); -let _ = require('underscore'); //support files let app = require('../../app'); -let Plotly = require('../../lib/plotly'); -let Tooltips = require('../../tooltips'); +//models +let Domain = require('../../models/domain'); //views let View = require('ampersand-view'); let SelectView = require('ampersand-select-view'); -let TypesViewer = require('../../views/edit-domain-type'); -let ParticleViewer = require('../../views/view-particle'); -let QuickviewDomainTypes = require('../../views/quickview-domain-types'); +let DomainView = require('../../domain-view/domain-view'); //templates let template = require('../templates/domainViewer.pug'); module.exports = View.extend({ template: template, events: { - 'click [data-hook=domain-edit-tab]' : 'toggleViewExternalDomainBtn', - 'click [data-hook=domain-view-tab]' : 'toggleViewExternalDomainBtn', + 'change [data-hook=select-domain]' : 'handleSelectDomain', + 'change [data-hook=select-location]' : 'handleSelectLocation', + 'click [data-hook=domain-edit-tab]' : 'handleModeSwitch', + 'click [data-hook=domain-view-tab]' : 'handleModeSwitch', 'click [data-hook=collapse]' : 'changeCollapseButtonText', + 'click [data-hook=select-external-domain]' : 'handleViewExternalDomains', 'click [data-hook=edit-domain-btn]' : 'editDomain', 'click [data-hook=create-domain]' : 'editDomain', - 'click [data-hook=save-to-model]' : 'saveDomainToModel', - 'click [data-hook=select-external-domain]' : 'handleLoadExternalDomain', - 'change [data-hook=select-domain]' : 'handleSelectDomain', - 'change [data-hook=select-location]' : 'handleSelectDomainLocation' + 'click [data-hook=save-to-model]' : 'saveDomainToModel' }, initialize: function (attrs, options) { View.prototype.initialize.apply(this, arguments); this.readOnly = attrs.readOnly ? attrs.readOnly : false; - this.tooltips = Tooltips.domainEditor; - this.domainPath = attrs.domainPath; - this.plot = Boolean(attrs.domainPlot) ? attrs.domainPlot : null; - this.elements = Boolean(attrs.domainElements) ? attrs.domainElements : null; - this.model.particles.forEach((particle) => { - this.model.types.get(particle.type, "typeID").numParticles += 1; - }); - this.gravity = this.getGravityString(); + this.elements = attrs.domainElements ? attrs.domainElements : null; + this.plot = attrs.domainPlot ? attrs.domainPlot : null; + this.domainPath = null; + this.domain = null; + this.files = null; + this.locations = null; }, render: function (attrs, options) { View.prototype.render.apply(this, arguments); - this.queryStr = `?path=${this.parent.model.directory}`; if(this.readOnly) { $(this.queryByHook('domain-edit-tab')).addClass("disabled"); $(".nav .disabled>a").on("click", (e) => { @@ -69,33 +63,25 @@ module.exports = View.extend({ $(this.queryByHook('domain-view-tab')).tab('show'); $(this.queryByHook('edit-domain')).removeClass('active'); $(this.queryByHook('view-domain')).addClass('active'); - this.toggleViewExternalDomainBtn(); - }else { - this.renderDomainSelectView(); - if(this.domainPath) { - $(this.queryByHook("domain-container")).collapse("show"); - $(this.queryByHook("collapse")).text("-"); - if(this.domainPath !== "viewing") { - $(this.queryByHook("save-to-model")).prop("disabled", false); - $(this.queryByHook("external-domain-select")).css("display", "block"); - $(this.queryByHook("select-external-domain")).text("View Model's Domain"); - this.queryStr += `&domain_path=${this.domainPath}`; - } + this.toggleViewExternalDomains(this.readOnly); + } + if(!this.elements) { + $(this.queryByHook("view-domain-plot-container")).css('display', 'block'); + this.elements = { + select: $(this.queryByHook("domain-select-particle")), + particle: {view: this, hook: "domain-particle-viewer"}, + figure: this.queryByHook("view-domain-plot"), + // figureEmpty: this.queryByHook("domain-plot-container-empty"), + type: this.queryByHook("domain-types-quick-view") } - this.toggleDomainError(); } - this.renderTypesViewer(); - this.renderPlotParticleViewer(); + this.renderDomainView(); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); }, - displayDomain: function (domainPreview) { - Plotly.newPlot(domainPreview, this.plot); - domainPreview.on('plotly_click', _.bind(this.selectParticle, this)); - }, editDomain: function (e) { - var queryStr = `?path=${this.parent.model.directory}`; + var queryStr = `?path=${this.model.directory}`; if(e.target.dataset.hook === "create-domain") { queryStr += "&new" }else if(this.domainPath) { @@ -104,45 +90,69 @@ module.exports = View.extend({ let endpoint = path.join(app.getBasePath(), "stochss/domain/edit") + queryStr; window.location.href = endpoint; }, - getDomainSelectValue: function (files) { - if(!this.domainPath || this.domainPath === "viewing") { - return null; - } - let domainFile = this.domainPath.split('/').pop(); - let value = files.filter((file) => { - return file[1] === domainFile; - })[0][0]; - return value; - }, - getGravityString: function () { - var gravity = `(X: ${this.model.gravity[0]}`; - gravity += `, Y: ${this.model.gravity[1]}`; - gravity += `, Z: ${this.model.gravity[2]})`; - return gravity; - }, - handleLoadExternalDomain: function (e) { - let text = e.target.textContent; - if(text === "View External Domain") { - $(this.queryByHook("external-domain-select")).css("display", "block"); - $(this.queryByHook("select-external-domain")).text("View Model's Domain"); - }else{ - this.reloadDomain("viewing"); + handleModeSwitch: function (e) { + if(!e.target.classList.contains('active')) { + this.toggleViewExternalDomains(e.target.text === "View"); } }, handleSelectDomain: function (e) { let value = e.srcElement.value; if(value) { - if(this.externalDomains[value].length <= 1) { - this.reloadDomain(this.externalDomains[value][0]); + if(this.locations[value].length <= 1) { + $(this.queryByHook('select-location-message')).css('display', 'none'); + $(this.queryByHook('select-domain-location')).css('display', 'none'); + this.domainPath = this.locations[value][0]; + this.loadExternalDomainModel(); + }else{ + this.renderDomainLocationSelectView(this.locations[value]); + } + } + }, + handleSelectLocation: function (e) { + this.domainPath = e.srcElement.value; + this.loadExternalDomainModel(); + }, + handleViewExternalDomains: function (e) { + let display = e.target.textContent === "View External Domain"; + if(display) { + $(this.queryByHook('select-external-domain')).text("View Model's Domain"); + if(!this.files) { + this.loadExternalDomainFiles(); }else{ - $(this.queryByHook("select-location-message")).css('display', "block"); - $(this.queryByHook("select-domain-location")).css("display", "inline-block"); - this.renderDomainLocationSelectView(this.externalDomains[value]); + this.renderDomainFileSelectView(); } + }else{ + $(this.queryByHook('select-external-domain')).text("View External Domain"); + this.domain = null; + this.domainPath = null; + $(this.queryByHook('select-location-message')).css('display', 'none'); + $(this.queryByHook('select-domain-location')).css('display', 'none'); + $(this.queryByHook('external-domain-select')).css('display', 'none'); + $(this.queryByHook('save-to-model')).prop('disabled', true); + this.renderDomainView(); } }, - handleSelectDomainLocation: function (e) { - this.reloadDomain(e.srcElement.value); + loadExternalDomainFiles: function () { + let endpoint = path.join(app.getApiPath(), "spatial-model/domain-list"); + app.getXHR(endpoint, { + always: (err, response, body) => { + this.files = body.files; + this.locations = body.paths; + this.renderDomainFileSelectView(); + } + }); + }, + loadExternalDomainModel: function () { + let queryStr = `?path=${this.model.directory}&domain_path=${this.domainPath}`; + let endpoint = path.join(app.getApiPath(), "spatial-model/load-domain") + queryStr; + app.getXHR(endpoint, { + always: (err, response, body) => { + console.log(body) + this.domain = new Domain(body.domain); + this.renderDomainView({modelsDomain: false}); + $(this.queryByHook('save-to-model')).prop('disabled', false); + } + }); }, openSection: function () { if(!$(this.queryByHook("domain-container")).hasClass("show")) { @@ -152,156 +162,60 @@ module.exports = View.extend({ } app.switchToEditTab(this, "domain"); }, - reloadDomain: function (domainPath) { - if(this.domainPath !== domainPath || domainPath === "viewing") { - let el = this.elements === null ? this.queryByHook("view-domain-plot") : this.elements.plot; - el.removeListener('plotly_click', this.selectParticle); - Plotly.purge(el); - this.plot = null; - this.model.types.forEach(function (type) { - type.numParticles = 0; - }); - this.parent.renderDomainViewer(domainPath); + renderDomainFileSelectView: function () { + if(this.domainFileSelectView) { + this.domainFileSelectView.remove(); } + this.domainFileSelectView = new SelectView({ + name: 'domain-files', + required: false, + idAttributes: 'cid', + options: this.files, + unselectedText: '-- Select Domain --' + }); + app.registerRenderSubview(this, this.domainFileSelectView, 'select-domain'); + $(this.queryByHook('external-domain-select')).css('display', 'block'); }, renderDomainLocationSelectView: function (options) { if(this.domainLocationSelectView) { this.domainLocationSelectView.remove(); } this.domainLocationSelectView = new SelectView({ - name: 'locations', + name: 'file-locations', required: false, idAttributes: 'cid', options: options, unselectedText: "-- Select Location --" }); app.registerRenderSubview(this, this.domainLocationSelectView, "select-location"); - }, - renderDomainSelectView: function () { - let endpoint = path.join(app.getApiPath(), "spatial-model/domain-list"); - app.getXHR(endpoint, { - always: (err, response, body) => { - this.externalDomains = body.paths; - let domainSelectView = new SelectView({ - name: 'domains', - required: false, - idAttributes: 'cid', - options: body.files, - unselectedText: "-- Select Domain --", - value: this.getDomainSelectValue(body.files) - }); - app.registerRenderSubview(this, domainSelectView, "select-domain"); - } + $(this.queryByHook('select-location-message')).css('display', 'block'); + $(this.queryByHook('select-domain-location')).css('display', 'inline-block'); + }, + renderDomainView: function ({modelsDomain=true}={}) { + if(this.domainView) { + this.domainView.remove(); + } + let domain = modelsDomain ? this.model.domain : this.domain; + var queryStr = `?path=${this.model.directory}`; + if(!modelsDomain) { + queryStr += `&domain_path=${this.domainPath}`; + } + this.domainView = new DomainView({ + model: domain, + readOnly: true, + elements: this.elements, + plot: this.plot, + queryStr: queryStr }); - }, - renderLocalParticlesViewer: function ({particle=null}) { - if(particle){ - $(this.queryByHook("domain-select-particle")).css("display", "none"); - this.particleViewer = new ParticleViewer({ - model: particle - }); - app.registerRenderSubview(this, this.particleViewer, "domain-particle-viewer"); - }else{ - $(this.queryByHook("domain-select-particle")).css("display", "block"); - this.typeQuickViewer = this.renderCollection( - this.model.types, - QuickviewDomainTypes, - this.queryByHook("domain-types-quick-view") - ); - } - }, - renderMEParticlesViewer: function ({particle=null}) { - if(particle){ - this.elements.select.css("display", "none"); - this.particleViewer = new ParticleViewer({ - model: particle - }); - app.registerRenderSubview(this.elements.particle.view, this.particleViewer, this.elements.particle.hook); - }else{ - this.elements.select.css("display", "block"); - this.typeQuickViewer = this.renderCollection( - this.model.types, - QuickviewDomainTypes, - this.elements.type - ); - } - }, - renderPlot: function (plotElement) { - if(this.plot === null) { - let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot") + this.queryStr; - app.getXHR(endpoint, { - always: (err, response, body) => { - this.plot = body.fig; - this.displayDomain(plotElement); - } - }); - }else{ - this.displayDomain(plotElement); - } - }, - renderPlotParticleViewer: function ({particle=null}={}) { - if(this.particleViewer) { - this.particleViewer.remove(); - } - if(this.typeQuickViewer) { - this.typeQuickViewer.remove(); - } - if(this.elements === null) { - $(this.queryByHook("view-domain-plot-container")).css('display', 'block'); - this.renderLocalParticlesViewer({particle: particle}); - if(particle === null) { - this.renderPlot(this.queryByHook("view-domain-plot")); - } - }else{ - this.renderMEParticlesViewer({particle: particle}); - if(particle === null) { - this.renderPlot(this.elements.plot); - } - } - }, - renderTypesViewer: function () { - if(this.typesViewer) { - this.typesViewer.remove(); - } - this.typesViewer = this.renderCollection( - this.model.types, - TypesViewer, - this.queryByHook("domain-types-list"), - {filter: (model) => { - return model.typeID != 0; - }, viewOptions: {viewMode: true}} - ); + app.registerRenderSubview(this, this.domainView, "domain-view-container"); }, saveDomainToModel: function (e) { - this.parent.model.domain = this.model; - this.parent.modelStateButtons.clickSaveHandler(e); - this.reloadDomain(); - }, - selectParticle: function (data) { - let point = data.points[0]; - let particle = this.model.particles.get(point.id, "particle_id"); - this.renderPlotParticleViewer({particle: particle}); + this.model.domain = this.domain; + this.parent.parent.clickSaveHandler(e); + this.renderDomainView(); }, - toggleDomainError: function () { - let errorMsg = $(this.queryByHook('domain-error')); - this.model.updateValid(); - if(!this.model.valid) { - errorMsg.addClass('component-invalid'); - errorMsg.removeClass('component-valid'); - }else{ - errorMsg.addClass('component-valid'); - errorMsg.removeClass('component-invalid'); - } + toggleViewExternalDomains: function (hide) { + let display = hide ? "none" : "block"; + $(this.queryByHook('external-domains-container')).css('display', display); }, - toggleViewExternalDomainBtn: function (e) { - if(e) { - if(!e.target.classList.contains("active")) { - let display = e.target.text === "View" ? "none" : "block"; - $(this.queryByHook("external-domains-container")).css("display", display); - } - }else{ - let display = this.readOnly ? "none" : "block"; - $(this.queryByHook("external-domains-container")).css("display", display); - } - } }); \ No newline at end of file diff --git a/client/model-view/views/edit-stoich-specie.js b/client/model-view/views/edit-stoich-specie.js deleted file mode 100644 index e4d15bdc24..0000000000 --- a/client/model-view/views/edit-stoich-specie.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -var $ = require('jquery'); -//views -var SelectView = require('ampersand-select-view'); -//templates -var template = require('../templates/editStoichSpecie.pug'); - -module.exports = SelectView.extend({ - // SelectView expects a string template, so pre-render it - template: template(), - bindings: { - 'model.ratio' : { - hook: 'ratio' - } - }, - events: { - 'change select' : 'selectChangeHandler' - }, - initialize: function () { - SelectView.prototype.initialize.apply(this, arguments); - this.value = this.model.specie || null; - }, - render: function() { - SelectView.prototype.render.apply(this, arguments); - }, - update: function () { - }, - selectChangeHandler: function (e) { - var species = this.getSpeciesCollection(); - var reactions = this.getReactionsCollection(); - var specie = species.filter(function (m) { - return m.name === e.target.selectedOptions.item(0).text; - })[0]; - this.model.specie = specie; - this.value = specie; - reactions.trigger("change"); - this.model.collection.parent.trigger('change-reaction') - }, - getReactionsCollection: function () { - return this.model.collection.parent.collection; - }, - getSpeciesCollection: function () { - return this.model.collection.parent.collection.parent.species; - }, -}); \ No newline at end of file diff --git a/client/model-view/views/parameters-view.js b/client/model-view/views/parameters-view.js index 4dc463bb68..c9c95b6e9b 100644 --- a/client/model-view/views/parameters-view.js +++ b/client/model-view/views/parameters-view.js @@ -41,6 +41,9 @@ module.exports = View.extend({ self.collection.parent.reactions.map(function (reaction) { if(reaction.rate && reaction.rate.compID === compID){ reaction.rate = parameter; + if(reaction.reactionType !== 'custom-propensity') { + reaction.trigger('change-reaction'); + } } }); self.collection.parent.eventsCollection.map(function (event) { diff --git a/client/model-view/views/reactant-product.js b/client/model-view/views/reactant-product.js index d10f54923a..f3b82d54b9 100644 --- a/client/model-view/views/reactant-product.js +++ b/client/model-view/views/reactant-product.js @@ -17,68 +17,96 @@ along with this program. If not, see . */ let $ = require('jquery'); -//models -let StoichSpecie = require('../../models/stoich-specie'); +//support files +let app = require('../../app'); //views let View = require('ampersand-view'); let SelectView = require('ampersand-select-view'); -let EditStoichSpecieView = require('./edit-stoich-specie'); -let EditCustomStoichSpecieView = require('./edit-custom-stoich-specie'); +let StoichSpeciesView = require('./stoich-species-view'); //templates let template = require('../templates/reactantProduct.pug'); module.exports = View.extend({ template: template, - events: { - 'change [data-hook=select-specie]' : 'selectSpecie', - 'click [data-hook=add-selected-specie]' : 'addSelectedSpecie' + events: function () { + let events = {}; + events[`change [data-hook=${this.hookAnchor}-select-specie]`] = 'selectSpecie'; + events[`click [data-hook=${this.hookAnchor}-add-selected-specie]`] = 'addSelectedSpecie'; + return events; }, initialize: function (args) { View.prototype.initialize.apply(this, arguments); this.collection = args.collection; this.species = args.species; this.reactionType = args.reactionType; - this.isReactants = args.isReactants + this.custom = args.reactionType.startsWith('custom'); + this.isReactants = args.isReactants; this.unselectedText = 'Pick a species'; - this.fieldTitle = args.fieldTitle; + if(this.isReactants) { + this.fieldTitle = 'Reactants'; + this.hookAnchor = 'reactants'; + }else{ + this.fieldTitle = 'Products'; + this.hookAnchor = 'products'; + } }, render: function () { View.prototype.render.apply(this, arguments); + if(this.isReactants) { + var tooltip = this.parent.parent.tooltips.reactant; + }else{ + var tooltip = this.parent.parent.tooltips.product; + } + $(this.queryByHook('field-title-tooltip')).prop('title', tooltip); + this.renderStoichSpecies(); + if(this.custom) { + $(this.queryByHook(`custom-${this.hookAnchor}`)).css('display', 'block'); + this.renderSelectSpeciesView(); + this.toggleAddSpecieButton(); + } + }, + addSelectedSpecie: function () { + var specieName = this.specieName ? this.specieName : 'Pick a variable'; + if(this.validateAddSpecie()) { + this.collection.addStoichSpecie(specieName); + this.toggleAddSpecieButton(); + this.collection.parent.trigger('change-reaction'); + } + }, + renderSelectSpeciesView: function () { + if(this.selectSpeciesView) { + this.selectSpeciesView.remove(); + } + + this.selectSpeciesView = new SelectView({ + name: 'stoich-specie', + required: false, + textAttribute: 'name', + eagerValidate: false, + idAttribute: 'compID', + options: this.species, + unselectedText: this.unselectedText + }); + app.registerRenderSubview(this, this.selectSpeciesView, `${this.hookAnchor}-select-specie`); + }, + renderStoichSpecies: function () { let args = { viewOptions: { name: 'stoich-specie', required: true, textAttribute: 'name', eagerValidate: true, - idAttribute: 'name', + idAttribute: 'compID', + yieldModel: false, options: this.species } }; - let type = this.reactionType; - let StoichSpeciesView = (type.startsWith('custom')) ? EditCustomStoichSpecieView : EditStoichSpecieView; this.renderCollection( this.collection, StoichSpeciesView, - this.queryByHook('reactants-editor'), + this.queryByHook(`${this.hookAnchor}-editor`), args ); - if(this.reactionType.startsWith('custom')) { - $(this.queryByHook('collapse')).collapse(); - } - this.toggleAddSpecieButton(); - if(this.fieldTitle === "Reactants"){ - $(this.queryByHook('field-title-tooltip')).prop('title', this.parent.parent.tooltips.reactant); - }else{ - $(this.queryByHook('field-title-tooltip')).prop('title', this.parent.parent.tooltips.product); - } - }, - addSelectedSpecie: function () { - var specieName = this.specieName ? this.specieName : 'Pick a variable'; - if(this.validateAddSpecie()) { - this.collection.addStoichSpecie(specieName); - this.toggleAddSpecieButton(); - this.collection.parent.trigger('change-reaction'); - } }, selectSpecie: function (e) { if(this.unselectedText === e.target.selectedOptions.item(0).text){ @@ -90,37 +118,17 @@ module.exports = View.extend({ this.toggleAddSpecieButton(); }, toggleAddSpecieButton: function () { - if(!this.validateAddSpecie()){ - $(this.queryByHook('add-selected-specie')).prop('disabled', true); - }else{ - $(this.queryByHook('add-selected-specie')).prop('disabled', false); - } + $(this.queryByHook(`${this.hookAnchor}-add-selected-specie`)).prop('disabled', !this.validateAddSpecie()); this.parent.toggleCustomReactionError(); }, validateAddSpecie: function () { if(this.hasSelectedSpecie){ if(!this.collection.length) { return true; } if(this.collection.length < 2 && this.collection.at(0).ratio < 2) { return true; } - if(this.reactionType !== 'custom-massaction') { return true; } + if(this.reactionType === 'custom-propensity') { return true; } if(!this.isReactants) { return true; } return false; } return false; }, - subviews: { - selectSpecies: { - hook: 'select-specie', - prepareView: function (el) { - return new SelectView({ - name: 'stoich-specie', - required: false, - textAttribute: 'name', - eagerValidate: false, - idAttribute: 'name', - options: this.species, - unselectedText: this.unselectedText - }); - } - } - } }); \ No newline at end of file diff --git a/client/model-view/views/reaction-details.js b/client/model-view/views/reaction-details.js deleted file mode 100644 index 981b699487..0000000000 --- a/client/model-view/views/reaction-details.js +++ /dev/null @@ -1,239 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let katex = require('katex'); -let _ = require('underscore'); -//support files -let app = require('../../app'); -let ReactionTypes = require('../../reaction-types'); -//models -let StoichSpecie = require('../../models/stoich-specie'); -//views -let InputView = require('../../views/input'); -let View = require('ampersand-view'); -let SelectView = require('ampersand-select-view'); -let ReactionTypesView = require('./reaction-types'); -let ReactantProductView = require('./reactant-product'); -//templates -let template = require('../templates/reactionDetails.pug'); - -module.exports = View.extend({ - template: template, - bindings: { - 'model.propensity': { - type: 'value', - hook: 'select-rate-parameter' - }, - 'model.summary' : { - type: function (el, value, previousValue) { - katex.render(this.model.summary, this.queryByHook('summary-container'), { - displayMode: true, - output: 'html' - }); - }, - hook: 'summary-container', - }, - 'model.hasConflict': { - type: function (el, value, previousValue) { - if(value) { - $(this.queryByHook('conflicting-modes-message')).collapse('show'); - }else{ - $(this.queryByHook('conflicting-modes-message')).collapse('hide'); - } - }, - hook: 'conflicting-modes-message' - } - }, - events: { - 'change [data-hook=select-rate-parameter]' : 'selectRateParam', - 'change [data-hook=select-reaction-type]' : 'selectReactionType' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - let self = this; - this.model.on("change:reaction_type", function (model) { - self.updateStoichSpeciesForReactionType(model.reactionType); - }); - }, - render: function () { - View.prototype.render.apply(this, arguments); - let self = this; - this.renderReactionTypesSelectView(); - if(this.model.reactionType === 'custom-propensity'){ - let propensityView = new InputView({ - parent: this, - required: true, - name: 'rate', - modelKey:'propensity', - valueType: 'string', - value: this.model.propensity, - placeholder: "--No Expression Entered--" - }); - app.registerRenderSubview(this, propensityView, 'select-rate-parameter'); - $(this.queryByHook('rate-parameter-label')).text('Propensity:'); - $(this.queryByHook('rate-parameter-tooltip')).prop('title', this.parent.tooltips.propensity); - }else{ - // make sure the reaction has a rate and that rate exists in the parameters collection - let paramIDs = this.model.collection.parent.parameters.map(function (param) { - return param.compID; - }); - if(!this.model.rate.compID || !paramIDs.includes(this.model.rate.compID)) { - this.model.rate = this.model.collection.getDefaultRate(); - } - let rateParameterView = new SelectView({ - name: 'rate', - required: true, - idAttribute: 'cid', - textAttribute: 'name', - eagerValidate: true, - options: this.model.collection.parent.parameters, - // For new reactions (with no rate.name) just use the first parameter in the Parameters collection - // Else fetch the right Parameter from Parameters based on existing rate - value: this.model.rate.name ? this.getRateFromParameters(this.model.rate.name) : this.model.collection.parent.parameters.at(0) - }); - app.registerRenderSubview(this, rateParameterView, 'select-rate-parameter'); - $(this.queryByHook('rate-parameter-label')).text('Rate Parameter:'); - $(this.queryByHook('rate-parameter-tooltip')).prop('title', this.parent.tooltips.rate); - } - let reactantsView = new ReactantProductView({ - collection: this.model.reactants, - species: this.model.collection.parent.species, - reactionType: this.model.reactionType, - fieldTitle: 'Reactants', - isReactants: true - }); - app.registerRenderSubview(this, reactantsView, 'reactants-editor'); - let productsView = new ReactantProductView({ - collection: this.model.products, - species: this.model.collection.parent.species, - reactionType: this.model.reactionType, - fieldTitle: 'Products', - isReactants: false - }); - app.registerRenderSubview(this, productsView, 'products-editor'); - if(this.model.collection.parent.is_spatial) { - let typesView = new ReactionTypesView({ - model: this.model, - parent: this - }); - app.registerRenderSubview(this, typesView, 'domain-types-editor'); - } - this.totalRatio = this.getTotalReactantRatio(); - app.tooltipSetup(); - this.toggleCustomReactionError(); - }, - getArrayOfDefaultStoichSpecies: function (arr) { - return arr.map(function (params) { - let stoichSpecie = new StoichSpecie(params); - stoichSpecie.specie = this.parent.getDefaultSpecie(); - return stoichSpecie; - }, this); - }, - getRateFromParameters: function (name) { - // Seems like model.rate is not actually part of the Parameters collection - // Get the Parameter from Parameters that matches model.rate - // TODO this is some garbagio, get model.rate into Parameters collection...? - if (!name) { name = this.model.rate.name }; - let rate = this.model.collection.parent.parameters.filter(function (param) { - return param.name === name; - })[0]; - return rate; - }, - getReactionTypeLabels: function () { - return _.map(ReactionTypes, function (val, key) { return val.label; }); - }, - getTotalReactantRatio: function () { - return this.model.reactants.length; - }, - renderReactionTypes: function () { - if(this.model.collection.parent.parameters.length < 1){ return }; - let options = { - displayMode: true, - output: 'html' - } - katex.render(ReactionTypes['creation'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['0'], options); - katex.render(ReactionTypes['destruction'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['1'], options); - katex.render(ReactionTypes['change'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['2'], options); - katex.render(ReactionTypes['dimerization'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['3'], options); - katex.render(ReactionTypes['merge'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['4'], options); - katex.render(ReactionTypes['split'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['5'], options); - katex.render(ReactionTypes['four'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['6'], options); - }, - renderReactionTypesSelectView: function () { - if(this.reactionTypesSelectView) { - this.reactionTypesSelectView.remove(); - } - if(this.model.collection.parent.parameters.length <= 0){ - var options = ["Custom propensity"]; - } - else{ - var options = this.getReactionTypeLabels(); - } - this.reactionTypesSelectView = new SelectView({ - label: 'Reaction Type:', - name: 'reaction-type', - required: true, - idAttribute: 'cid', - options: options, - value: ReactionTypes[this.model.reactionType].label - }); - app.registerRenderSubview(this, this.reactionTypesSelectView, 'select-reaction-type'); - this.renderReactionTypes(); - }, - selectRateParam: function (e) { - if(this.model.reactionType !== 'custom-propensity') { - let val = e.target.selectedOptions.item(0).text; - let param = this.getRateFromParameters(val); - this.model.rate = param || this.model.rate; - this.model.trigger("change"); - this.model.collection.trigger("change"); - } - }, - selectReactionType: function (e) { - let label = e.target.selectedOptions.item(0).value; - let type = _.findKey(ReactionTypes, function (o) { return o.label === label; }); - this.model.reactionType = type; - this.model.summary = label; - this.updateStoichSpeciesForReactionType(type); - this.model.collection.trigger("change"); - this.model.trigger('change-reaction'); - this.render(); - }, - toggleCustomReactionError: function () { - let errorMsg = $(this.queryByHook("custom-reaction-error")); - if(this.model.reactants.length <= 0 && this.model.products.length <= 0) { - errorMsg.addClass('component-invalid'); - errorMsg.removeClass('component-valid'); - }else{ - errorMsg.addClass('component-valid'); - errorMsg.removeClass('component-invalid'); - } - }, - update: function () {}, - updateStoichSpeciesForReactionType: function (type) { - let args = this.parent.getStoichArgsForReactionType(type); - let newReactants = this.getArrayOfDefaultStoichSpecies(args.reactants); - let newProducts = this.getArrayOfDefaultStoichSpecies(args.products); - this.model.reactants.reset(newReactants); - this.model.products.reset(newProducts); - if(type !== 'custom-propensity') - this.model.rate = this.model.collection.getDefaultRate(); - }, - updateValid: function () {} -}); \ No newline at end of file diff --git a/client/model-view/views/reaction-listing.js b/client/model-view/views/reaction-listing.js deleted file mode 100644 index f8f9104774..0000000000 --- a/client/model-view/views/reaction-listing.js +++ /dev/null @@ -1,136 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -let $ = require('jquery'); -let katex = require('katex'); -let _ = require('underscore'); -//support files -let app = require('../../app'); -let tests = require('../../views/tests'); -let modals = require('../../modals'); -//views -let InputView = require('../../views/input'); -let View = require('ampersand-view'); -//templates -let viewTemplate = require('../templates/viewReaction.pug'); -let editTemplate = require('../templates/reactionListing.pug'); - -module.exports = View.extend({ - bindings: { - 'model.name' : { - type: 'value', - hook: 'input-name-container' - }, - 'model.summary' : { - type: function (el, value, previousValue) { - katex.render(this.model.summary, this.queryByHook('summary'), { - displayMode: true, - output: 'html', - maxSize: 5 - }); - }, - hook: 'summary' - }, - 'model.selected' : { - type: function (el, value, previousValue) { - el.checked = value; - }, - hook: 'select' - } - }, - events: { - 'click [data-hook=edit-annotation-btn]' : 'editAnnotation', - 'click [data-hook=select]' : 'selectReaction', - 'click [data-hook=remove]' : 'removeReaction' - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.viewMode = attrs.viewMode ? attrs.viewMode : false; - if(this.viewMode) { - this.rate = this.model.reactionType === "custom-propensity" ? this.model.propensity : this.model.rate.name; - this.types = []; - let self = this; - if(this.model.types) { - this.model.types.forEach(function (index) { - let type = self.model.collection.parent.domain.types.get(index, "typeID"); - self.types.push(type.name); - }); - } - }else{ - this.model.on('change', _.bind(this.updateViewer, this)); - } - }, - render: function () { - this.template = this.viewMode ? viewTemplate : editTemplate; - View.prototype.render.apply(this, arguments); - app.documentSetup(); - if(!this.model.annotation){ - $(this.queryByHook('edit-annotation-btn')).text('Add'); - } - }, - editAnnotation: function () { - if(document.querySelector('#reactionAnnotationModal')) { - document.querySelector('#reactionAnnotationModal').remove(); - } - let self = this; - let name = this.model.name; - let annotation = this.model.annotation; - let modal = $(modals.annotationModalHtml("reaction", name, annotation)).modal(); - let okBtn = document.querySelector('#reactionAnnotationModal .ok-model-btn'); - let input = document.querySelector('#reactionAnnotationModal #reactionAnnotationInput'); - input.addEventListener("keyup", function (event) { - if(event.keyCode === 13){ - event.preventDefault(); - okBtn.click(); - } - }); - okBtn.addEventListener('click', function (e) { - self.model.annotation = input.value.trim(); - self.parent.renderEditReactionListingView(); - modal.modal('hide'); - }); - }, - removeReaction: function (e) { - this.collection.removeReaction(this.model); - this.parent.collection.trigger("change"); - }, - selectReaction: function (e) { - this.model.collection.trigger("select", this.model); - }, - update: function () {}, - updateValid: function () {}, - updateViewer: function () { - this.parent.renderViewReactionView(); - }, - subviews: { - inputName: { - hook: 'input-name-container', - prepareView: function (el) { - return new InputView({ - parent: this, - required: true, - name: 'name', - tests: tests.nameTests, - modelKey: 'name', - valueType: 'string', - value: this.model.name - }); - } - } - } -}); \ No newline at end of file diff --git a/client/model-view/views/reaction-types.js b/client/model-view/views/reaction-restrict-to.js similarity index 79% rename from client/model-view/views/reaction-types.js rename to client/model-view/views/reaction-restrict-to.js index 6cf01751e9..7f48966fc8 100644 --- a/client/model-view/views/reaction-types.js +++ b/client/model-view/views/reaction-restrict-to.js @@ -18,9 +18,9 @@ along with this program. If not, see . //views let View = require('ampersand-view'); -let TypesView = require('./component-types'); +let DomainTypesView = require('./component-types'); //templates -let template = require('../templates/reactionTypes.pug'); +let template = require('../templates/reactionRestrictTo.pug'); module.exports = View.extend({ template: template, @@ -31,15 +31,15 @@ module.exports = View.extend({ }, render: function () { View.prototype.render.apply(this, arguments); - this.renderTypes(); + this.renderDomainTypes(); }, - renderTypes: function () { - if(this.typesView) { - this.typesView.remove(); + renderDomainTypes: function () { + if(this.domainTypesView) { + this.domainTypesView.remove(); } - this.typesView = this.renderCollection( + this.domainTypesView = this.renderCollection( this.baseModel.domain.types, - TypesView, + DomainTypesView, this.queryByHook("reaction-types-container"), {"filter": function (model) { return model.typeID != 0; diff --git a/client/model-view/views/reaction-view.js b/client/model-view/views/reaction-view.js new file mode 100644 index 0000000000..30f8042e32 --- /dev/null +++ b/client/model-view/views/reaction-view.js @@ -0,0 +1,486 @@ +/* +StochSS is a platform for simulating biochemical systems +Copyright (C) 2019-2022 StochSS developers. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +let $ = require('jquery'); +let katex = require('katex'); +let _ = require('underscore'); +//support files +let app = require('../../app'); +let modals = require('../../modals'); +let tests = require('../../views/tests'); +let ReactionTypes = require('../../reaction-types'); +//collections +let StoichSpecies = require('../../models/stoich-species'); +//views +let View = require('ampersand-view'); +let InputView = require('../../views/input'); +let SelectView = require('ampersand-select-view'); +let RestrictToView = require('./reaction-restrict-to'); +let ReactantProductView = require('./reactant-product'); +//templates +let viewTemplate = require('../templates/viewReaction.pug'); +let editTemplate = require('../templates/editReaction.pug'); + +module.exports = View.extend({ + bindings: { + 'model.name' : { + type: 'value', + hook: 'input-name-container' + }, + 'model.summary' : { + type: function (el, value, previousValue) { + katex.render(this.model.summary, this.queryByHook('summary'), { + displayMode: true, + output: 'html', + maxSize: 5 + }); + }, + hook: 'summary' + }, + 'model.selected' : { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'select' + }, + 'model.hasConflict': { + type: function (el, value, previousValue) { + if(value) { + $(this.queryByHook('conflicting-modes-message')).collapse('show'); + }else{ + $(this.queryByHook('conflicting-modes-message')).collapse('hide'); + } + }, + hook: 'conflicting-modes-message' + }, + 'model.mirrorPropensities': { + type: function (el, value, previousValue) { + el.checked = value; + }, + hook: 'mirror-propensities' + } + }, + events: { + 'click [data-hook=select]' : 'selectReaction', + 'click [data-hook=edit-annotation-btn]' : 'editAnnotation', + 'click [data-hook=remove]' : 'removeReaction', + 'click [data-hook=mirror-propensities]' : 'setMirrorPropensities', + 'change [data-hook=select-rate-parameter]' : 'selectRateParam', + 'change [data-hook=propensity-input]' : 'handlePropensityChange', + 'change [data-hook=select-reaction-type]' : 'selectReactionType' + }, + initialize: function (attrs, options) { + View.prototype.initialize.apply(this, arguments); + this.viewMode = attrs.viewMode ? attrs.viewMode : false; + if(this.viewMode) { + this.rate = this.model.reactionType === "custom-propensity" ? this.model.propensity : this.model.rate.name; + this.types = []; + let self = this; + if(this.model.types) { + this.model.types.forEach(function (index) { + let type = self.model.collection.parent.domain.types.get(index, "typeID"); + self.types.push(type.name); + }); + } + }else{ + this.model.on('change', _.bind(this.updateViewer, this)); + this.model.on('change-reaction', () => { + if(this.model.massaction) { + this.renderODEPropensityInputView({override: true}); + this.renderPropensityInputView({override: true}); + } + }); + } + }, + render: function () { + this.template = this.viewMode ? viewTemplate : editTemplate; + View.prototype.render.apply(this, arguments); + if(!this.viewMode){ + if(this.model.selected) { + setTimeout(_.bind(this.openReactionDetails, this), 1); + } + if(!this.model.annotation){ + $(this.queryByHook('edit-annotation-btn')).text('Add'); + } + } + app.documentSetup(); + }, + buildStoichSpecies: function (newSpecs, oldSpecs) { + let specIDs = []; + newSpecs.forEach((stoichSpecies) => { + let index = newSpecs.indexOf(stoichSpecies); + if(oldSpecs.at(index)) { + stoichSpecies.specie = oldSpecs.at(index).specie; + }else{ + stoichSpecies.specie = this.getSpecies(specIDs); + } + specIDs.push(stoichSpecies.specie.compID); + }); + return newSpecs; + }, + editAnnotation: function () { + if(document.querySelector('#reactionAnnotationModal')) { + document.querySelector('#reactionAnnotationModal').remove(); + } + let self = this; + let name = this.model.name; + let annotation = this.model.annotation; + let modal = $(modals.annotationModalHtml("reaction", name, annotation)).modal(); + let okBtn = document.querySelector('#reactionAnnotationModal .ok-model-btn'); + let input = document.querySelector('#reactionAnnotationModal #reactionAnnotationInput'); + input.addEventListener("keyup", function (event) { + if(event.keyCode === 13){ + event.preventDefault(); + okBtn.click(); + } + }); + okBtn.addEventListener('click', function (e) { + modal.modal('hide'); + self.model.annotation = input.value.trim(); + self.parent.renderEditReactionView(); + }); + }, + getReactionTypes: function () { + let disableTypes = this.model.collection.parent.parameters.length == 0; + let options = _.map(ReactionTypes, function (val, key) { + let disabled = disableTypes && key !== "custom-propensity" + return [key, val.label, disabled]; + }); + return options + }, + getSpecies: function (speciesIDs) { + let species = this.model.collection.parent.species.filter((spec) => { + return !speciesIDs.includes(spec.compID); + }); + if(species.length > 0) { return species[0]; } + return this.model.collection.parent.species.at(0); + }, + handlePropensityChange: function () { + if(this.model.mirrorPropensities) { + this.model.odePropensity = this.model.propensity; + this.renderODEPropensityInputView({override: true}); + } + }, + openReactionDetails: function () { + $("#collapse-reaction-details" + this.model.compID).collapse("show"); + this.renderDetailsSection(); + }, + removeReaction: function (e) { + this.collection.removeReaction(this.model); + this.parent.collection.trigger("change"); + }, + renderDetailsSection: function () { + this.renderRateSelectView(); + this.renderPropensityInputView(); + this.renderODEPropensityInputView(); + this.renderReactionTypesSelectView(); + this.toggleCustomReactionError(); + this.renderReactantsView(); + this.renderProductsView(); + if(this.model.collection.parent.is_spatial) { + this.renderRestrictToView() + } + }, + renderODEPropensityInputView: function ({override=false}={}) { + if(override && this.odePropensityInputView) { + this.odePropensityInputView.remove(); + } + + if(!this.odePropensityInputView || override) { + if(this.model.massaction) { + var required = false; + var propensity = this.model.maODEPropensity; + var modelKey = 'maODEPropensity'; + $(this.queryByHook('mirror-propensities')).prop('disabled', true); + }else{ + var required = !this.model.mirrorPropensities; + var propensity = this.model.odePropensity; + var modelKey = 'odePropensity'; + $(this.queryByHook('mirror-propensities')).prop('disabled', false); + } + + this.odePropensityInputView = new InputView({ + parent: this, + required: required, + disabled: this.model.massaction || this.model.mirrorPropensities, + name: 'ode-propensity', + modelKey: modelKey, + valueType: 'string', + value: propensity, + placeholder: "--No Expression Entered--" + }); + app.registerRenderSubview(this, this.odePropensityInputView, 'ode-propensity-input'); + } + }, + renderProductsView: function ({override=false}={}) { + if(override && this.productsView) { + this.productsView.remove(); + } + + if(!this.productsView || override) { + this.productsView = new ReactantProductView({ + collection: this.model.products, + species: this.model.collection.parent.species, + reactionType: this.model.reactionType, + isReactants: false + }); + app.registerRenderSubview(this, this.productsView, 'products-editor'); + } + }, + renderPropensityInputView: function ({override=false}={}) { + if(override && this.propensityInputView) { + this.propensityInputView.remove(); + } + + if(!this.propensityInputView || override) { + if(this.model.massaction) { + var required = false; + var propensity = this.model.maPropensity + var modelKey = 'maPropensity' + }else{ + var required = !this.model.odePropensity; + var propensity = this.model.propensity + var modelKey = 'propensity' + } + + this.propensityInputView = new InputView({ + parent: this, + required: required, + disabled: this.model.massaction, + name: 'propensity', + modelKey: modelKey, + valueType: 'string', + value: propensity, + placeholder: "--No Expression Entered--" + }); + app.registerRenderSubview(this, this.propensityInputView, 'propensity-input'); + } + }, + renderRateSelectView: function ({override=false}={}) { + if(override && this.rateParameterView) { + this.rateParameterView.remove(); + } + + if(!this.rateParameterView || override) { + let propensity = this.model.reactionType === 'custom-propensity'; + viewOptions = { + name: 'rate', + required: !propensity, + idAttribute: 'compID', + textAttribute: 'name', + eagerValidate: !propensity + } + if(propensity) { + viewOptions['options'] = []; + viewOptions['unselectedText'] = "N/A"; + }else{ + // make sure the reaction has a rate and that rate exists in the parameters collection + let paramIDs = this.model.collection.parent.parameters.map(function (param) { + return param.compID; + }); + if(!this.model.rate.compID || !paramIDs.includes(this.model.rate.compID)) { + this.model.rate = this.model.collection.getDefaultRate(); + } + viewOptions['options'] = this.model.collection.parent.parameters; + viewOptions['value'] = this.model.rate.compID; + } + + this.rateParameterView = new SelectView(viewOptions); + app.registerRenderSubview(this, this.rateParameterView, 'select-rate-parameter'); + $(this.queryByHook('select-rate-parameter').firstChild.children[1]).prop('disabled', propensity); + } + }, + renderReactantsView: function ({override=false}={}) { + if(override && this.reactantsView) { + this.reactantsView.remove(); + } + + if(!this.reactantsView || override) { + this.reactantsView = new ReactantProductView({ + collection: this.model.reactants, + species: this.model.collection.parent.species, + reactionType: this.model.reactionType, + isReactants: true + }); + app.registerRenderSubview(this, this.reactantsView, 'reactants-editor'); + } + }, + renderReactionTypes: function () { + let options = { + displayMode: true, + output: 'html' + } + katex.render(ReactionTypes['creation'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['0'], options); + katex.render(ReactionTypes['destruction'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['1'], options); + katex.render(ReactionTypes['change'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['2'], options); + katex.render(ReactionTypes['dimerization'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['3'], options); + katex.render(ReactionTypes['merge'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['4'], options); + katex.render(ReactionTypes['split'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['5'], options); + katex.render(ReactionTypes['four'].label, this.queryByHook('select-reaction-type').firstChild.children[1]['6'], options); + }, + renderReactionTypesSelectView: function ({override=false}={}) { + if(override && this.reactionTypesSelectView) { + this.reactionTypesSelectView.remove(); + } + + if(!this.reactionTypesSelectView || override) { + let options = this.getReactionTypes(); + + this.reactionTypesSelectView = new SelectView({ + label: 'Reaction Type:', + name: 'reaction-type', + required: true, + idAttribute: 'cid', + options: options, + value: this.model.reactionType + }); + app.registerRenderSubview(this, this.reactionTypesSelectView, 'select-reaction-type'); + this.renderReactionTypes(); + } + }, + renderRestrictToView: function ({override=true}={}) { + if(override && this.restrictToView) { + this.restrictToView.remove(); + } + + if(!this.restrictToView || override) { + this.restrictToView = new RestrictToView({ + model: this.model, + parent: this + }); + app.registerRenderSubview(this, this.restrictToView, 'domain-types-editor'); + } + }, + selectRateParam: function (e) { + let val = e.target.selectedOptions.item(0).value; + let param = this.model.collection.parent.parameters.get(val, 'compID'); + if(param) { + this.model.rate = param; + this.updateViewer(); + this.model.trigger('change-reaction'); + this.model.collection.trigger("change"); + } + }, + selectReaction: function (e) { + this.model.selected = !this.model.selected; + this.renderDetailsSection(); + }, + selectReactionType: function (e) { + let oldReactionType = this.model.reactionType; + let newReactionType = e.target.selectedOptions.item(0).value; + if(newReactionType === 'custom-propensity') { + this.model.reactionType = newReactionType; + this.model.massaction = false; + }else if(newReactionType === 'custom-massaction') { + if(oldReactionType === 'custom-propensity') { + this.switchCustoms(); + }else{ + this.model.reactionType = newReactionType; + } + }else{ + this.switchToFormula(newReactionType); + } + this.model.trigger('change-reaction'); + if(newReactionType === 'custom-propensity' || oldReactionType === 'custom-propensity') { + this.renderRateSelectView({override: true}); + this.renderPropensityInputView({override: true}); + this.renderODEPropensityInputView({override: true}); + } + this.renderReactantsView({override: true}); + this.renderProductsView({override: true}); + }, + setMirrorPropensities: function () { + this.model.mirrorPropensities = !this.model.mirrorPropensities; + if(this.model.mirrorPropensities) { + this.prevODEProp = this.model.odePropensity; + this.model.odePropensity = this.model.propensity; + }else{ + this.model.odePropensity = Boolean(this.prevODEProp) ? this.prevODEProp : "" + } + this.renderODEPropensityInputView({override: true}); + }, + switchCustoms: function () { + let reactants = new StoichSpecies([]); + reactants.parent = this.model; + var totalRatio = 0; + this.model.reactants.forEach((stoichSpecies) => { + if(totalRatio < 2) { + let ratio = totalRatio + stoichSpecies.ratio > 2 ? 2 - totalRatio : stoichSpecies.ratio; + reactants.addStoichSpecie(stoichSpecies.specie.name, {ratio: ratio}); + totalRatio += ratio; + } + }); + this.model.reactionType = 'custom-massaction'; + this.model.massaction = true; + this.model.reactants = reactants; + if(!this.model.rate.compID) { + this.model.rate = this.model.collection.getDefaultRate(); + } + }, + switchToFormula: function (reactionType) { + let formula = ReactionTypes[reactionType]; + let reactants = this.buildStoichSpecies( + (new StoichSpecies(formula.reactants)), this.model.reactants + ); + reactants.parent = this.model; + let products = this.buildStoichSpecies( + (new StoichSpecies(formula.products)), this.model.products + ); + products.parent = this.model; + this.model.reactionType = reactionType; + this.model.massaction = true; + this.model.reactants = reactants; + this.model.products = products; + if(!this.model.rate.compID) { + this.model.rate = this.model.collection.getDefaultRate(); + } + }, + toggleCustomReactionError: function () { + let errorMsg = $(this.queryByHook("custom-reaction-error")); + if(this.model.reactants.length <= 0 && this.model.products.length <= 0) { + errorMsg.addClass('component-invalid'); + errorMsg.removeClass('component-valid'); + }else{ + errorMsg.addClass('component-valid'); + errorMsg.removeClass('component-invalid'); + } + }, + update: function () {}, + updateValid: function () {}, + updateViewer: function (event) { + if(!event || !("selected" in event._changed)){ + this.parent.renderViewReactionView(); + } + }, + subviews: { + inputName: { + hook: 'input-name-container', + prepareView: function (el) { + return new InputView({ + parent: this, + required: true, + name: 'name', + tests: tests.nameTests, + modelKey: 'name', + valueType: 'string', + value: this.model.name + }); + } + } + } +}); \ No newline at end of file diff --git a/client/model-view/views/reactions-view.js b/client/model-view/views/reactions-view.js index 38a00ef305..f0ffd3c0c1 100644 --- a/client/model-view/views/reactions-view.js +++ b/client/model-view/views/reactions-view.js @@ -18,33 +18,28 @@ along with this program. If not, see . let $ = require('jquery'); let katex = require('katex'); -let _ = require('underscore'); //support files let app = require('../../app'); let Tooltips = require('../../tooltips'); let ReactionTypes = require('../../reaction-types'); -//models -let StoichSpeciesCollection = require('../../models/stoich-species'); //views let View = require('ampersand-view'); -let ViewSwitcher = require('ampersand-view-switcher'); -let ReactionListingView = require('./reaction-listing'); -let ReactionDetailsView = require('./reaction-details'); +let ReactionView = require('./reaction-view'); //templates let template = require('../templates/reactionsView.pug'); module.exports = View.extend({ template: template, events: { - 'click [data-hook=creation]' : 'handleAddReactionClick', - 'click [data-hook=destruction]' : 'handleAddReactionClick', - 'click [data-hook=change]' : 'handleAddReactionClick', - 'click [data-hook=dimerization]' : 'handleAddReactionClick', - 'click [data-hook=merge]' : 'handleAddReactionClick', - 'click [data-hook=split]' : 'handleAddReactionClick', - 'click [data-hook=four]' : 'handleAddReactionClick', - 'click [data-hook=custom-massaction]' : 'handleAddReactionClick', - 'click [data-hook=custom-propensity]' : 'handleAddReactionClick', + 'click [data-hook=creation]' : 'handleAddReactionClick', + 'click [data-hook=destruction]' : 'handleAddReactionClick', + 'click [data-hook=change]' : 'handleAddReactionClick', + 'click [data-hook=dimerization]' : 'handleAddReactionClick', + 'click [data-hook=merge]' : 'handleAddReactionClick', + 'click [data-hook=split]' : 'handleAddReactionClick', + 'click [data-hook=four]' : 'handleAddReactionClick', + 'click [data-hook=custom-massaction]' : 'handleAddReactionClick', + 'click [data-hook=custom-propensity]' : 'handleAddReactionClick', 'click [data-hook=collapse]' : 'changeCollapseButtonText' }, initialize: function (attrs, options) { @@ -52,31 +47,14 @@ module.exports = View.extend({ this.tooltips = Tooltips.reactionsEditor; this.readOnly = attrs.readOnly ? attrs.readOnly : false; if(!this.readOnly) { - this.collection.on("select", function (reaction) { - this.setSelectedReaction(reaction); - this.setDetailsView(reaction); - }, this); - this.collection.on("remove", function (reaction) { - // Select the last reaction by default - // But only if there are other reactions other than the one we're removing - if (reaction.detailsView){ - reaction.detailsView.remove(); - } - this.collection.removeReaction(reaction); - if (this.collection.length) { - let selected = this.collection.at(this.collection.length-1); - this.collection.trigger("select", selected); - } - }, this); this.collection.parent.species.on('add remove', this.toggleAddReactionButton, this); - this.collection.parent.parameters.on('add remove', this.toggleReactionTypes, this); + this.collection.parent.parameters.on('add remove', this.updateMAState, this); this.collection.parent.on('change', this.toggleProcessError, this); } }, render: function () { View.prototype.render.apply(this, arguments); if(this.readOnly) { - this.renderViewReactionView(); $(this.queryByHook('reactions-edit-tab')).addClass("disabled"); $(".nav .disabled>a").on("click", function(e) { e.preventDefault(); @@ -86,41 +64,34 @@ module.exports = View.extend({ $(this.queryByHook('edit-reactions')).removeClass('active'); $(this.queryByHook('view-reactions')).addClass('active'); }else{ - this.renderReactionListingViews(); - if (this.collection.length) { - this.setSelectedReaction(this.collection.at(0)); - this.collection.trigger("select", this.selectedReaction); - } + this.renderEditReactionView(); this.toggleAddReactionButton(); - if(this.collection.parent.parameters.length > 0){ - $(this.queryByHook('add-reaction-partial')).prop('hidden', true); - }else { - $(this.queryByHook('add-reaction-full')).prop('hidden', true); - } + this.updateMAState(); this.renderReactionTypes(); - katex.render("\\emptyset", this.queryByHook('emptyset'), { - displayMode: false, - output: 'html', - }); this.toggleProcessError(); - $(this.queryByHook('massaction-message')).prop('hidden', this.collection.parent.parameters.length > 0); } + katex.render("\\emptyset", this.queryByHook('emptyset'), { + displayMode: false, + output: 'html', + }); + this.renderViewReactionView(); }, changeCollapseButtonText: function (e) { app.changeCollapseButtonText(this, e); }, - getAnnotation: function (type) { - return ReactionTypes[type].label; - }, - getDefaultSpecie: function () { - return this.collection.parent.species.models[0]; - }, getStoichArgsForReactionType: function(type) { return ReactionTypes[type]; }, handleAddReactionClick: function (e) { + let disableTypes = this.collection.parent.parameters.length == 0; + let maTypes = [ + "creation", "destruction", "change", "dimerization", + "merge", "split", "four", "custom-massaction" + ] let reactionType = e.delegateTarget.dataset.hook; - let stoichArgs = this.getStoichArgsForReactionType(reactionType); + if(disableTypes && maTypes.includes(reactionType)) { + return + } if(this.parent.model.domain.types) { var types = this.parent.model.domain.types.map(function (type) { return type.typeID; @@ -129,20 +100,10 @@ module.exports = View.extend({ }else{ var types = []; } + let stoichArgs = this.getStoichArgsForReactionType(reactionType); let reaction = this.collection.addReaction(reactionType, stoichArgs, types); - reaction.detailsView = this.newDetailsView(reaction); - this.collection.trigger("select", reaction); app.tooltipSetup(); - }, - newDetailsView: function (reaction) { - let detailsView = new ReactionDetailsView({ model: reaction }); - detailsView.parent = this; - return detailsView; - }, - openReactionsContainer: function () { - $(this.queryByHook('reactions-list-container')).collapse('show'); - let collapseBtn = $(this.queryByHook('collapse')); - collapseBtn.trigger('click'); + this.collection.trigger('change'); }, openSection: function (error, {isCollection=false}={}) { if(!$(this.queryByHook("reactions-list-container")).hasClass("show")) { @@ -152,34 +113,24 @@ module.exports = View.extend({ } app.switchToEditTab(this, "reactions"); if(error.type !== "process") { - let reaction = this.collection.filter((react) => { - return react.compID === error.id; + let reactionView = this.editReactionView.views.filter((reactView) => { + return reactView.model.compID === error.id; })[0]; - this.collection.trigger("select", reaction); + reactionView.model.selected = true; + reactionView.openReactionDetails(); } }, - renderEditReactionListingView: function () { - if(this.editReactionListingView){ - this.editReactionListingView.remove(); + renderEditReactionView: function () { + if(this.editReactionView){ + this.editReactionView.remove(); } - if(this.collection.parent.parameters.length <= 0) { - for(var i = 0; i < this.collection.length; i++) { - if(this.collection.models[i].reactionType !== "custom-propensity"){ - this.collection.models[i].reactionType = "custom-propensity"; - } - } - } - this.editReactionListingView = this.renderCollection( + this.editReactionView = this.renderCollection( this.collection, - ReactionListingView, + ReactionView, this.queryByHook('edit-reaction-list') ); app.tooltipSetup(); }, - renderReactionListingViews: function () { - this.renderEditReactionListingView(); - this.renderViewReactionView(); - }, renderReactionTypes: function () { let options = { displayMode: false, @@ -194,8 +145,8 @@ module.exports = View.extend({ katex.render(ReactionTypes['four'].label, this.queryByHook('four-lb1'), options); }, renderViewReactionView: function () { - if(this.viewReactionListingView){ - this.viewReactionListingView.remove(); + if(this.viewReactionView){ + this.viewReactionView.remove(); } if(!this.collection.parent.is_spatial){ $(this.queryByHook("reaction-types-header")).css("display", "none"); @@ -207,25 +158,15 @@ module.exports = View.extend({ $(this.queryByHook("reaction-annotation-header")).css("display", "block"); } let options = {viewOptions: {viewMode: true, hasAnnotations: this.containsMdlWithAnn}} - this.viewReactionListingView = this.renderCollection( + this.viewReactionView = this.renderCollection( this.collection, - ReactionListingView, + ReactionView, this.queryByHook('view-reaction-list'), options ); }, - setDetailsView: function (reaction) { - reaction.detailsView = this.newDetailsView(reaction); - this.detailsViewSwitcher.set(reaction.detailsView); - }, - setSelectedReaction: function (reaction) { - this.collection.each(function (m) { m.selected = false; }); - reaction.selected = true; - this.selectedReaction = reaction; - }, toggleAddReactionButton: function () { - $(this.queryByHook('add-reaction-full')).prop('disabled', (this.collection.parent.species.length <= 0)); - $(this.queryByHook('add-reaction-partial')).prop('disabled', (this.collection.parent.species.length <= 0)); + $(this.queryByHook('add-reaction')).prop('disabled', (this.collection.parent.species.length <= 0)); }, toggleProcessError: function () { let model = this.collection.parent; @@ -239,30 +180,21 @@ module.exports = View.extend({ errorMsg.removeClass('component-invalid'); } }, - toggleReactionTypes: function (e, prev, curr) { - if(curr && curr.add && this.collection.parent.parameters.length === 1){ - $(this.queryByHook('massaction-message')).prop('hidden', true); - $(this.queryByHook('add-reaction-full')).prop('hidden', false); - $(this.queryByHook('add-reaction-partial')).prop('hidden', true); - }else if(curr && !curr.add && this.collection.parent.parameters.length === 0){ - $(this.queryByHook('massaction-message')).prop('hidden', false); - $(this.queryByHook('add-reaction-full')).prop('hidden', true); - $(this.queryByHook('add-reaction-partial')).prop('hidden', false); - } - if(this.selectedReaction){ - this.selectedReaction.detailsView.renderReactionTypesSelectView(); - } - }, update: function () {}, - updateValid: function () {}, - subviews: { - detailsViewSwitcher: { - hook: "reaction-details-container", - prepareView: function (el) { - return new ViewSwitcher({ - el: el - }); + updateMAState: function () { + let disableTypes = this.collection.parent.parameters.length == 0; + let maTypes = [ + "creation", "destruction", "change", "dimerization", + "merge", "split", "four", "custom-massaction" + ] + for(var i = 0; i < maTypes.length; i++) { + if(disableTypes) { + $(this.queryByHook(maTypes[i])).addClass("disabled") + }else{ + $(this.queryByHook(maTypes[i])).removeClass("disabled") } } - } + $(this.queryByHook('massaction-message')).prop('hidden', !disableTypes); + }, + updateValid: function () {}, }); \ No newline at end of file diff --git a/client/model-view/views/species-view.js b/client/model-view/views/species-view.js index f9fa717277..a00e004cdb 100644 --- a/client/model-view/views/species-view.js +++ b/client/model-view/views/species-view.js @@ -42,23 +42,21 @@ module.exports = View.extend({ let self = this; this.collection.on('update-species', function (compID, specie, isNameUpdate, isDefaultMode) { self.collection.parent.reactions.forEach(function (reaction) { + var changedReaction = false; reaction.reactants.forEach(function (reactant) { if(reactant.specie.compID === compID) { reactant.specie = specie; + changedReaction = true; } }); reaction.products.forEach(function (product) { if(product.specie.compID === compID) { product.specie = specie; + changedReaction = true; } }); - if(isNameUpdate) { - reaction.buildSummary(); - if(reaction.selected) { - self.parent.reactionsView.setDetailsView(reaction); - } - }else if(!isDefaultMode || specie.compID === self.collection.models[self.collection.length-1].compID){ - reaction.checkModes(); + if(changedReaction) { + reaction.trigger('change-reaction'); } }); self.collection.parent.eventsCollection.forEach(function (event) { diff --git a/client/model-view/views/edit-custom-stoich-specie.js b/client/model-view/views/stoich-species-view.js similarity index 51% rename from client/model-view/views/edit-custom-stoich-specie.js rename to client/model-view/views/stoich-species-view.js index dbab386e20..01df300036 100644 --- a/client/model-view/views/edit-custom-stoich-specie.js +++ b/client/model-view/views/stoich-species-view.js @@ -16,14 +16,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -var $ = require('jquery'); +let $ = require('jquery'); //views -var SelectView = require('ampersand-select-view'); +let SelectView = require('ampersand-select-view'); //templates -var template = require('../templates/editCustomStoichSpecie.pug'); +let template = require('../templates/stoichSpecieView.pug'); module.exports = SelectView.extend({ - // SelectView expects a string template, so pre-render it template: template(), bindings: { 'model.ratio' : { @@ -32,47 +31,82 @@ module.exports = SelectView.extend({ }, events: { 'change select' : 'selectChangeHandler', - 'click [data-hook=increment]' : 'handleIncrement', 'click [data-hook=decrement]' : 'handleDecrement', - 'click [data-hook=remove]' : 'deleteSpecie' + 'click [data-hook=increment]' : 'handleIncrement', + 'click [data-hook=custom-remove]' : 'deleteSpecie' }, - initialize: function (args) { - var self = this; + initialize: function () { SelectView.prototype.initialize.apply(this, arguments); - this.value = this.model.specie || null; - this.isReactants = this.parent.parent.isReactants; - this.reactionType = this.parent.parent.reactionType; - this.stoichSpecies = this.parent.parent.collection; - this.stoichSpecies.on('add', function () { - self.toggleIncrementButton(); - }); - this.stoichSpecies.on('remove', function () { - self.toggleIncrementButton(); + this.value = this.model.specie.compID; + if(this.parent.parent.custom) { + this.isReactants = this.parent.parent.isReactants; + this.reactionType = this.parent.parent.reactionType; + this.stoichSpecies = this.parent.parent.collection; + this.stoichSpecies.on('add', () => { + this.toggleIncrementButton(); + }); + this.stoichSpecies.on('remove', () => { + this.toggleIncrementButton(); + }); + } + }, + render: function() { + SelectView.prototype.render.apply(this, arguments); + if(this.parent.parent.custom) { + this.toggleDecrementButton(); + this.toggleIncrementButton(); + }else{ + $(this.queryByHook('custom-decrement')).css('display', 'none'); + $(this.queryByHook('custom-increment')).css('display', 'none'); + $(this.queryByHook('custom-remove')).css('display', 'none'); + } + this.model.collection.parent.on('change-reaction', () => { + var setValueFirstModel = () => { + if(!this.options.length) { + return; + } + if(this.yieldModel) { + this.setValue(this.options.models[0]); + } + else { + this.setValue(this.options.models[0][this.idAttribute]); + } + }; + + this.renderOptions(); + if (this.hasOptionByValue(this.value)) { + this.updateSelectedOption(); + } + else { + setValueFirstModel(); + } }); }, - render: function () { - SelectView.prototype.render.apply(this); - this.toggleIncrementButton(); - this.toggleDecrementButton(); + deleteSpecie: function () { + if(!this.model.collection.parent.reactionType.startsWith('custom')) { return } + let reaction = this.model.collection.parent; + this.collection.removeStoichSpecie(this.model); + reaction.trigger('change-reaction'); + this.parent.parent.toggleAddSpecieButton(); }, - selectChangeHandler: function (e) { - var species = this.getSpeciesCollection(); - var reactions = this.getReactionsCollection(); - var specie = species.filter(function (m) { - return m.name === e.target.selectedOptions.item(0).text; - })[0]; - this.model.specie = specie; - this.value = specie; - reactions.trigger("change"); - this.model.collection.parent.trigger('change-reaction') + getReactionsCollection: function () { + return this.model.collection.parent.collection; }, getSpeciesCollection: function () { return this.model.collection.parent.collection.parent.species; }, - getReactionsCollection: function () { - return this.model.collection.parent.collection; + handleDecrement: function () { + if(!this.model.collection.parent.reactionType.startsWith('custom')) { return } + this.model.ratio--; + this.model.collection.parent.trigger('change-reaction') + this.toggleDecrementButton(); + if(this.validateRatioIncrement()){ + this.toggleIncrementButton(); + this.parent.parent.toggleAddSpecieButton(); + } }, handleIncrement: function () { + if(!this.model.collection.parent.reactionType.startsWith('custom')) { return } if(this.validateRatioIncrement()){ this.model.ratio++; this.toggleIncrementButton(); @@ -81,41 +115,29 @@ module.exports = SelectView.extend({ } this.toggleDecrementButton(); }, + selectChangeHandler: function (e) { + let species = this.getSpeciesCollection(); + let reactions = this.getReactionsCollection(); + let specie = species.get(e.target.selectedOptions.item(0).value, 'compID'); + this.model.specie = specie; + this.value = specie.compID; + reactions.trigger("change"); + this.model.collection.parent.trigger('change-reaction') + }, + toggleDecrementButton: function () { + $(this.queryByHook('decrement')).prop('disabled', this.model.ratio <= 1); + }, + toggleIncrementButton: function () { + $(this.queryByHook('increment')).prop('disabled', !this.validateRatioIncrement()); + }, + update: function () {}, validateRatioIncrement: function () { if(this.stoichSpecies.length < 2 && this.model.ratio < 2) return true; - if(this.reactionType !== 'custom-massaction') + if(this.reactionType === 'custom-propensity') return true; if(!this.isReactants) return true; return false; - }, - toggleIncrementButton: function () { - if(!this.validateRatioIncrement()){ - $(this.queryByHook('increment')).prop('disabled', true); - }else{ - $(this.queryByHook('increment')).prop('disabled', false); - } - }, - toggleDecrementButton: function () { - if(this.model.ratio <= 1) - $(this.queryByHook('decrement')).prop('disabled', true); - else - $(this.queryByHook('decrement')).prop('disabled', false); - }, - handleDecrement: function () { - this.model.ratio--; - this.model.collection.parent.trigger('change-reaction') - this.toggleDecrementButton(); - if(this.validateRatioIncrement()){ - this.toggleIncrementButton(); - this.parent.parent.toggleAddSpecieButton(); - } - }, - deleteSpecie: function () { - var reaction = this.model.collection.parent - this.collection.removeStoichSpecie(this.model); - reaction.trigger('change-reaction') - this.parent.parent.toggleAddSpecieButton(); - }, + } }); \ No newline at end of file diff --git a/client/models/domain-type.js b/client/models/domain-type.js index 930f6a197f..16584666e4 100644 --- a/client/models/domain-type.js +++ b/client/models/domain-type.js @@ -21,15 +21,22 @@ var State = require('ampersand-state'); module.exports = State.extend({ props: { + c: 'number', fixed: 'boolean', + geometry: 'string', mass: 'number', name: 'string', nu: 'number', + rho: 'number', typeID: 'number', volume: 'number' }, session: { - numParticles: 'number' + numParticles: 'number', + selected: { + type: 'boolean', + default: false + } }, initialize: function (attrs, options) { State.prototype.initialize.apply(this, arguments) diff --git a/client/models/domain-types.js b/client/models/domain-types.js index 3d557a1dde..9ec4e5ac59 100644 --- a/client/models/domain-types.js +++ b/client/models/domain-types.js @@ -24,19 +24,21 @@ var Collection = require('ampersand-collection'); module.exports = Collection.extend({ model: Type, indexes: ['typeID'], - addType: function (vol, mass, nu, fixed, id=null) { - if(!id) { - id = this.parent.getDefaultTypeID(); - } + addType: function () { + let id = this.parent.getDefaultTypeID(); let name = String(id); - var type = new Type({ - fixed: fixed, - mass: mass, - nu: nu, - typeID: id, + let type = new Type({ + c: 10, + fixed: false, + geometry: "", + mass: 1.0, name: name, - volume: vol + nu: 0.0, + rho: 1.0, + typeID: id, + volume: 1.0 }); + type.selected = true; this.add(type); return name; }, diff --git a/client/models/domain.js b/client/models/domain.js index 738ebbe28d..137e9c38f4 100644 --- a/client/models/domain.js +++ b/client/models/domain.js @@ -33,7 +33,8 @@ module.exports = State.extend({ x_lim: 'object', y_lim: 'object', z_lim: 'object', - static: 'boolean' + static: 'boolean', + template_version: 'number' }, collections: { types: Types, @@ -72,6 +73,23 @@ module.exports = State.extend({ this.def_type_id += 1; return id; }, + realignTypes: function (oldType) { + this.def_type_id -= 1; + this.types.forEach((type) => { + if(type.typeID > oldType) { + let id = type.typeID - 1; + if(type.name === type.typeID.toString()) { + type.name = id.toString(); + } + type.typeID = id; + } + }); + this.particles.forEach((particle) => { + if(particle.type > oldType) { + particle.type -= 1; + } + }); + }, validateModel: function () { if(!this.particles.validateCollection()) return false; return true; diff --git a/client/models/model.js b/client/models/model.js index 076612620c..c993e86c91 100644 --- a/client/models/model.js +++ b/client/models/model.js @@ -43,7 +43,8 @@ module.exports = Model.extend({ defaultID: 'number', defaultMode: 'string', annotation: 'string', - volume: 'any' + volume: 'any', + template_version: 'number' }, collections: { species: Species, diff --git a/client/models/parameters.js b/client/models/parameters.js index 520418a72d..2279bc59ee 100644 --- a/client/models/parameters.js +++ b/client/models/parameters.js @@ -24,6 +24,7 @@ var Collection = require('ampersand-collection'); module.exports = Collection.extend({ model: Parameter, + indexes: ['compID'], addParameter: function () { var id = this.parent.getDefaultID(); var name = this.getDefaultName(); diff --git a/client/models/particle.js b/client/models/particle.js index 1871d36f2c..0a685631bf 100644 --- a/client/models/particle.js +++ b/client/models/particle.js @@ -21,21 +21,26 @@ var State = require('ampersand-state'); module.exports = State.extend({ props: { + c: 'number', fixed: 'boolean', mass: 'number', nu: 'number', particle_id: 'number', point: 'object', + rho: 'number', type: 'number', volume: 'number' }, - session: { - pointChanged: 'boolean', - typeChanged: 'boolean' - }, initialize: function (attrs, options) { State.prototype.initialize.apply(this, arguments) }, + comparePoint(point) { + if(this.point.length !== point.length) { return false; } + for(var i = 0; i < this.point.length; i++) { + if(this.point[i] !== point[i]) { return false; } + } + return true; + }, validate: function () { return true; } diff --git a/client/models/particles.js b/client/models/particles.js index 937f0000a7..968f1f6ff5 100644 --- a/client/models/particles.js +++ b/client/models/particles.js @@ -24,17 +24,23 @@ var Collection = require('ampersand-collection'); module.exports = Collection.extend({ model: Particle, indexes: ['particle_id'], - addParticle: function (point, vol, mass, type, nu, fixed) { + addParticle: function ({particle=null, point=[0,0,0], vol=1, mass=1, type=0, nu=0, fixed=false, c=10, rho=1}={}) { let id = this.parent.getDefaultID(); - var particle = new Particle({ - fixed: fixed, - mass: mass, - nu: nu, - particle_id: id, - point: point, - type: type, - volume: vol - }); + if(particle) { + particle.particle_id = id; + }else{ + particle = new Particle({ + c: c, + fixed: fixed, + mass: mass, + nu: nu, + particle_id: id, + point: point, + rho: rho, + type: type, + volume: vol + }); + } this.add(particle); }, removeParticle: function (particle) { diff --git a/client/models/reaction.js b/client/models/reaction.js index 3edcbe8941..262103d508 100644 --- a/client/models/reaction.js +++ b/client/models/reaction.js @@ -27,9 +27,8 @@ module.exports = State.extend({ props: { compID: 'number', name: 'string', - reactionType: 'string', - summary: 'string', massaction: 'boolean', + odePropensity: 'string', propensity: 'string', annotation: 'string', types: 'object' @@ -42,29 +41,58 @@ module.exports = State.extend({ products: StoichSpecies }, session: { - selected: { + hasConflict: { type: 'boolean', - default: true, + default: false, }, - hasConflict: { + maODEPropensity: 'string', + maPropensity: 'string', + mirrorPropensities: 'boolean', + reactionType: 'string', + selected: { type: 'boolean', default: false, }, + summary: 'string' }, initialize: function (attrs, options) { var self = this; State.prototype.initialize.apply(this, arguments); - if(!this.reactionType.startsWith('custom')) { - let reactionType = this.updateReactionType(); - if(this.reactionType !== reactionType){ - this.reactionType = reactionType - this.buildSummary() + this.reactionType = this.updateReactionType(); + this.mirrorPropensities = this.propensity === this.odePropensity; + this.buildSummary(); + this.buildMAPropensities(); + this.checkModes(); + this.on('change-reaction', () => { + this.buildSummary(); + this.buildMAPropensities(); + this.checkModes(); + }); + }, + buildMAPropensities: function () { + if(this.reactionType === "custom-propensity") { return } + var odePropensity = this.rate.name; + var propensity = this.rate.name; + + this.reactants.forEach((stoichSpecies) => { + let name = stoichSpecies.specie.name; + if(stoichSpecies.ratio == 2) { + odePropensity += ` * ${name} * ${name}`; + propensity = `0.5 * ${propensity} * ${name} * (${name} - 1) / vol`; + }else{ + odePropensity += ` * ${name}`; + propensity += ` * ${name}`; } + }) + + let order = this.reactants.length; + if(order == 2) { + propensity += " / vol"; + }else if(order == 0) { + propensity += " * vol" } - this.on('change-reaction', function () { - self.buildSummary(); - self.checkModes(); - }); + this.maODEPropensity = odePropensity; + this.maPropensity = propensity; }, buildSummary: function () { var summary = ""; @@ -137,6 +165,9 @@ module.exports = State.extend({ this.hasConflict = Boolean(hasContinuous && (hasDynamic || hasDiscrete)) }, updateReactionType: function () { + if(!this.massaction) { + return "custom-propensity" + } let numReactants = this.reactants.length let numProducts = this.products.length let prodRatio1 = numProducts > 0 ? this.products.models[0].ratio : 0 diff --git a/client/models/reactions.js b/client/models/reactions.js index eb4e1d6861..f609f339c5 100644 --- a/client/models/reactions.js +++ b/client/models/reactions.js @@ -31,13 +31,13 @@ Reactions = Collection.extend({ addReaction: function (reactionType, stoichArgs, types) { var id = this.parent.getDefaultID(); var name = this.getDefaultName(); - var massaction = reactionType === 'custom-massaction'; + var massaction = reactionType !== 'custom-propensity'; var reaction = new Reaction({ compID: id, name: name, - reactionType: reactionType, massaction: massaction, propensity: '', + odePropensity: '', annotation: '', types: types, reactants: stoichArgs.reactants, @@ -47,9 +47,12 @@ Reactions = Collection.extend({ this.setDefaultSpecieForStoichSpecies(reaction.products); if(reactionType !== 'custom-propensity') reaction.rate = this.getDefaultRate(); - reaction.buildSummary() + reaction.buildSummary(); + reaction.buildMAPropensities(); + reaction.reactionType = reactionType; + reaction.selected = true; this.add(reaction); - this.parent.updateValid() + this.parent.updateValid(); return reaction; }, getDefaultName: function () { @@ -64,11 +67,14 @@ Reactions = Collection.extend({ }, setDefaultSpecieForStoichSpecies: function (stoichSpecies) { stoichSpecies.forEach(function (stoichSpecie) { - stoichSpecie.specie = this.getDefaultSpecie(); + stoichSpecie.specie = this.getDefaultSpecie(stoichSpecies.indexOf(stoichSpecie)); }, this); }, - getDefaultSpecie: function () { - var specie = this.parent.species.at(0); + getDefaultSpecie: function (index) { + if(this.parent.species.length <= index) { + index -= this.parent.species.length; + } + var specie = this.parent.species.at(index); return specie; }, getDefaultRate: function () { diff --git a/client/models/species.js b/client/models/species.js index 2e822cfc5b..dcba8eadc2 100644 --- a/client/models/species.js +++ b/client/models/species.js @@ -24,6 +24,7 @@ var Collection = require('ampersand-collection'); module.exports = Collection.extend({ model: Specie, + indexes: ['compID'], addSpecie: function (types) { var id = this.parent.getDefaultID(); var name = this.getDefaultName(); diff --git a/client/models/stoich-species.js b/client/models/stoich-species.js index 70cff4ecb5..4fc9006f68 100644 --- a/client/models/stoich-species.js +++ b/client/models/stoich-species.js @@ -32,12 +32,12 @@ module.exports = Collection.extend({ this.baseModel = this.parent.collection.parent; this.baseModel.species.trigger('stoich-species-change'); }, - addStoichSpecie: function (specieName) { + addStoichSpecie: function (specieName, {ratio=1}={}) { var specie = this.parent.collection.parent.species.filter(function (specie) { return specie.name === specieName; })[0]; var stoichSpecie = new StoichSpecie({ - ratio: 1 + ratio: ratio }); stoichSpecie.specie = specie; this.add(stoichSpecie); diff --git a/client/pages/domain-editor.js b/client/pages/domain-editor.js index acbe54f347..9a489a61e9 100644 --- a/client/pages/domain-editor.js +++ b/client/pages/domain-editor.js @@ -16,223 +16,35 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -var $ = require('jquery'); +let $ = require('jquery'); let path = require('path'); -var _ = require('underscore'); //support files -var app = require('../app'); -var tests = require('../views/tests'); -var Plotly = require('../lib/plotly'); -var Tooltips = require('../tooltips'); +let app = require('../app'); let modals = require('../modals'); -//views -var PageView = require('../pages/base'); -var InputView = require('../views/input'); -var SelectView = require('ampersand-select-view'); -var ParticleView = require('../views/edit-particle'); -var EditDomainTypeView = require('../views/edit-domain-type'); -var Create3DDomainView = require('../views/edit-3D-domain'); -//collections -var Collection = require('ampersand-collection'); //models -var Model = require('../models/model'); -var Domain = require('../models/domain'); -var Particle = require('../models/particle'); -var TypeModel = require('../models/domain-type'); +let Model = require('../models/model'); +let Domain = require('../models/domain'); +//views +let DomainView = require('../domain-view/domain-view'); +let PageView = require('../pages/base'); //templates -var template = require('../templates/pages/domainEditor.pug'); +let template = require('../templates/pages/domainEditor.pug'); import initPage from './page.js'; let DomainEditor = PageView.extend({ template: template, events: { - 'click [data-toggle=collapse]' : 'changeCollapseButtonText', - 'click [data-hook=add-domain-type]' : 'handleAddDomainType', - 'click [data-hook=set-type-defaults]' : 'handleSetDefaults', 'click [data-hook=save-to-model]' : 'handleSaveToModel', 'click [data-hook=save-to-file]' : 'handleSaveToFile', - 'click [data-hook=import-particles-btn]' : 'handleImportMesh', - 'click [data-hook=set-particle-types-btn]' : 'getTypesFromFile', - 'change [data-hook=static-domain]' : 'setStaticDomain', - 'change [data-hook=density]' : 'setDensity', - 'change [data-target=gravity]' : 'setGravity', - 'change [data-hook=pressure]' : 'setPressure', - 'change [data-hook=speed]' : 'setSpeed', - 'change [data-name=limitation]' : 'setLimitation', - 'change [data-target=reflect]' : 'setBoundaryCondition', - 'change #meshfile' : 'updateImportBtn', - 'change [data-hook=mesh-type-select]' : 'updateMeshTypeAndDefaults', - 'change [data-hook=types-file-select]' : 'handleSelectTypeFile', - 'change [data-hook=types-file-location-select]' : 'handleSelectTypeLocation' - }, - handleAddDomainType: function () { - this.selectedType = "new"; - this.renderEditTypeDefaults(); - }, - handleImportMesh: function (e) { - this.startAction("Importing mesh ...", "im") - let data = {"type":null, "transformation":null} - let id = Number($(this.queryByHook("mesh-type-select")).find('select')[0].value) - if(id > 0) { - data.type = this.domain.types.get(id, "typeID").toJSON(); - } - let xTrans = Number($(this.queryByHook("mesh-x-trans")).find('input')[0].value) - let yTrans = Number($(this.queryByHook("mesh-y-trans")).find('input')[0].value) - let zTrans = Number($(this.queryByHook("mesh-z-trans")).find('input')[0].value) - if(xTrans !== 0 || yTrans !== 0 || zTrans !== 0) { - data.transformation = [xTrans, yTrans, zTrans]; - } - this.importMesh(data) - }, - importMesh: function (data) { - let self = this; - let file = $("#meshfile").prop("files")[0]; - let typeFiles = $("#typefile").prop("files") - let formData = new FormData(); - formData.append("datafile", file); - formData.append("particleData", JSON.stringify(data)); - if(typeFiles.length) { - formData.append("typefile", typeFiles[0]); - } - let endpoint = path.join(app.getApiPath(), 'spatial-model/import-mesh'); - app.postXHR(endpoint, formData, { - success: function (err, response, body) { - body = JSON.parse(body); - if(body.types) { - self.addMissingTypes(body.types) - } - self.addParticles(body.particles); - if(self.domain.x_lim[0] > body.limits.x_lim[0]) { - self.domain.x_lim[0] = body.limits.x_lim[0] - } - if(self.domain.y_lim[0] > body.limits.y_lim[0]) { - self.domain.y_lim[0] = body.limits.y_lim[0] - } - if(self.domain.z_lim[0] > body.limits.z_lim[0]) { - self.domain.z_lim[0] = body.limits.z_lim[0] - } - if(self.domain.x_lim[1] < body.limits.x_lim[1]) { - self.domain.x_lim[1] = body.limits.x_lim[1] - } - if(self.domain.y_lim[1] < body.limits.y_lim[1]) { - self.domain.y_lim[1] = body.limits.y_lim[1] - } - if(self.domain.z_lim[1] < body.limits.z_lim[1]) { - self.domain.z_lim[1] = body.limits.z_lim[1] - } - self.renderDomainLimitations(); - self.completeAction("Mesh successfully imported", "im") - $('html, body').animate({ - scrollTop: $("#domain-plot").offset().top - }, 20); - }, - error: function (err, response, body) { - body = JSON.parse(body); - self.errorAction(body.Message, "im") - } - }, false) - }, - handleSetDefaults: function (e) { - let mass = Number($(this.queryByHook("td-mass")).find('input')[0].value); - let vol = Number($(this.queryByHook("td-vol")).find('input')[0].value); - let nu = Number($(this.queryByHook("td-nu")).find('input')[0].value); - let fixed = $(this.queryByHook("td-fixed")).prop("checked"); - if(this.selectedType === "new") { - let name = this.domain.types.addType(vol, mass, nu, fixed) - this.addType(name); - }else{ - let type = this.domain.types.get(this.selectedType, "typeID") - type.mass = mass; - type.volume = vol; - type.nu = nu; - type.fixed = fixed; - } - this.renderDomainTypes(); - $(this.queryByHook("edit-defaults")).css("display", "none"); - }, - handleSaveToModel: function () { - if(this.model) { - this.startAction("Saving domain to model ...", "sd"); - this.model.domain = this.domain; - this.model.saveModel(_.bind(function () { - this.completeAction("Domain saved to model", "sd"); - }, this)); - window.location.replace(this.getBreadcrumbData().model.href); - } - }, - handleSaveToFile: function () { - this.startAction("Saving the domain to file (.domn) ...", "sd") - if(this.domain.directory && !this.domain.dirname) { - this.saveDomain() - }else{ - var self = this - if(document.querySelector('#newDomainModal')) { - document.querySelector('#newDomainModal').remove() - } - let modal = $(modals.createDomainHtml()).modal(); - let okBtn = document.querySelector('#newDomainModal .ok-model-btn'); - let input = document.querySelector('#newDomainModal #domainNameInput'); - input.addEventListener("keyup", function (event) { - if(event.keyCode === 13){ - event.preventDefault(); - okBtn.click(); - } - }); - input.addEventListener("input", function (e) { - var endErrMsg = document.querySelector('#newDomainModal #domainNameInputEndCharError') - var charErrMsg = document.querySelector('#newDomainModal #domainNameInputSpecCharError') - let error = app.validateName(input.value) - okBtn.disabled = error !== "" || input.value.trim() === "" - charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none" - endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none" - }); - okBtn.addEventListener('click', function (e) { - if (Boolean(input.value)) { - modal.modal('hide') - let name = input.value.trim() - self.saveDomain(name); - } - }); - } - }, - handleSelectTypeFile: function (e) { - let value = e.srcElement.value; - if(value) { - if(this.typeDescriptions[value].length <= 1) { - $(this.queryByHook("type-location-message")).css('display', "none"); - $(this.queryByHook("type-location-container")).css("display", "none"); - this.typeDescriptionsFile = this.typeDescriptions[value][0]; - var disabled = false; - }else{ - $(this.queryByHook("type-location-message")).css('display', "block"); - $(this.queryByHook("type-location-container")).css("display", "inline-block"); - this.renderTypesLocationSelect(this.typeDescriptions[value]); - var disabled = true; - } - }else{ - $(this.queryByHook("type-location-message")).css('display', "none"); - $(this.queryByHook("type-location-container")).css("display", "none"); - var disabled = true; - } - $(this.queryByHook("set-particle-types-btn")).prop("disabled", disabled); - }, - handleSelectTypeLocation: function (e) { - this.typeDescriptionsFile = e.srcElement.value; - $(this.queryByHook("set-particle-types-btn")).prop("disabled", false); - }, - updateImportBtn: function (e) { - $(this.queryByHook("import-particles-btn")).prop("disabled", !Boolean(e.target.files.length)) }, initialize: function (attrs, options) { PageView.prototype.initialize.apply(this, arguments); - this.tooltips = Tooltips.domainEditor; - var self = this; let urlParams = new URLSearchParams(window.location.search) let modelPath = urlParams.has("path") ? urlParams.get("path") : null; let domainPath = urlParams.has("domainPath") ? urlParams.get("domainPath") : null; let newDomain = urlParams.has("new") ? true : false; - this.queryStr = modelPath ? "?path=" + modelPath : "?" + this.queryStr = modelPath ? `?path=${modelPath}` : "?" if(newDomain) { if(modelPath) { this.queryStr += "&" @@ -246,183 +58,40 @@ let DomainEditor = PageView.extend({ } let endpoint = path.join(app.getApiPath(), "spatial-model/load-domain") + this.queryStr app.getXHR(endpoint, { - success: function (err, response, body) { - self.domain = new Domain(body.domain); - self.domain.directory = domainPath - self.domain.dirname = newDomain && !modelPath ? domainPath : null - self.model = self.buildModel(body.model, modelPath); - self.actPart = {"part":null, "tn":0, "pn":0}; - self.renderSubviews(); + success: (err, response, body) => { + this.domain = new Domain(body.domain); + this.domain.directory = domainPath + this.domain.dirname = newDomain && !modelPath ? domainPath : null + this.model = this.buildModel(body.model, modelPath); + this.renderSubviews(); } }); - $(document).on('shown.bs.modal', function (e) { - $('[autofocus]', e.target).focus(); - }); - }, - addParticle: function (newPart, {fromImport=false, cb=null}={}) { - this.domain.particles.addParticle(newPart.point, newPart.volume, - newPart.mass, newPart.type, - newPart.nu, newPart.fixed); - let numPart = this.domain.particles.models.length - let particle = this.domain.particles.models[numPart-1] - this.plot.data[particle.type].ids.push(particle.particle_id); - this.plot.data[particle.type].x.push(particle.point[0]); - this.plot.data[particle.type].y.push(particle.point[1]); - this.plot.data[particle.type].z.push(particle.point[2]); - if(!fromImport) { - this.renderDomainTypes(); - this.updatePlot(); - } - if(cb) { - cb(); - } - }, - addParticles: function (particles) { - let self = this; - particles.forEach(function (particle) { - self.addParticle(particle, {fromImport: true}); - }); - this.renderDomainTypes(); - this.updatePlot(); - }, - addMissingTypes: function (types) { - let self = this; - let defaultType = self.domain.types.get(0, "typeID"); - types.forEach(function (type) { - let domainType = self.domain.types.get(type, "typeID"); - if(!domainType) { - self.domain.types.addType(defaultType.volume, defaultType.mass, - defaultType.nu, defaultType.fixed, type); - self.addType(String(type)); - } - }); - }, - addType: function (name) { - this.renderEditParticle(); - this.renderNewParticle(); - var trace = {"ids":[], "x":[], "y":[], "z":[], "name":name}; - trace['marker'] = this.plot.data[0].marker; - trace['mode'] = this.plot.data[0].mode; - trace['type'] = this.plot.data[0].type; - this.plot.data.push(trace) - this.updatePlot() }, buildModel: function (modelData, modelPath) { if(!modelPath) { return null; } - var model = new Model(modelData); + let model = new Model(modelData); model.for = "domain"; model.isPreview = false; model.directory = modelPath; return model; }, - changeCollapseButtonText: function (e) { - app.changeCollapseButtonText(this, e); - }, - changeParticleLocation: function (x, y, z) { - this.domain.particles.get(this.actPart.part.particle_id, "particle_id").point = [x, y, z]; - this.plot.data[this.actPart.tn].x[this.actPart.pn] = x; - this.plot.data[this.actPart.tn].y[this.actPart.pn] = y; - this.plot.data[this.actPart.tn].z[this.actPart.pn] = z; - this.actPart.part.pointChanged = false; - }, - changeParticleTypes: function (types) { - let self = this; - types.forEach(function (type) { - let particle = self.domain.particles.get(type.particle_id, "particle_id"); - self.actPart.part = particle; - for(var i = 0; i < self.plot.data.length; i++) { - let trace = self.plot.data[i]; - if(trace.ids.includes(String(type.particle_id))){ - self.actPart.tn = self.plot.data.indexOf(trace); - self.actPart.pn = trace.ids.indexOf(String(type.particle_id)); - break; - } - }; - self.changeParticleType(type.typeID) - }); - this.renderDomainTypes(); - this.updatePlot(); - }, - changeParticleType: function (type) { - this.domain.particles.get(this.actPart.part.particle_id, "particle_id").type = type - let id = this.plot.data[this.actPart.tn].ids.splice(this.actPart.pn, 1)[0]; - let x = this.plot.data[this.actPart.tn].x.splice(this.actPart.pn, 1)[0]; - let y = this.plot.data[this.actPart.tn].y.splice(this.actPart.pn, 1)[0]; - let z = this.plot.data[this.actPart.tn].z.splice(this.actPart.pn, 1)[0]; - this.plot.data[type].ids.push(id); - this.plot.data[type].x.push(x); - this.plot.data[type].y.push(y); - this.plot.data[type].z.push(z); - this.actPart.part.typeChanged = false; - }, - completeAction: function (action, src) { - $(this.queryByHook(src + "-in-progress")).css("display", "none"); - $(this.queryByHook(src + "-action-complete")).text(action); - $(this.queryByHook(src + "-complete")).css("display", "inline-block"); - console.log(action) - let self = this - setTimeout(function () { - $(self.queryByHook(src + "-complete")).css("display", "none"); + completeAction: function (action) { + $(this.queryByHook("sd-in-progress")).css("display", "none"); + $(this.queryByHook("sd-action-complete")).text(action); + $(this.queryByHook("sd-complete")).css("display", "inline-block"); + setTimeout(() => { + $(this.queryByHook("sd-complete")).css("display", "none"); }, 5000); }, - deleteParticle: function () { - this.domain.particles.removeParticle(this.actPart.part); - this.plot.data[this.actPart.tn].ids.splice(this.actPart.pn, 1); - this.plot.data[this.actPart.tn].x.splice(this.actPart.pn, 1); - this.plot.data[this.actPart.tn].y.splice(this.actPart.pn, 1); - this.plot.data[this.actPart.tn].z.splice(this.actPart.pn, 1); - this.actPart.part = null; - this.updatePlot(); - this.renderEditParticle(); - this.renderDomainTypes(); - $('html, body').animate({ - scrollTop: $("#domain-plot").offset().top - }, 20); - }, - deleteType: function (typeID) { - if(this.actPart.part && this.actPart.part.type >= typeID) { - this.actPart.part = null; - } - this.unassignAllParticles(typeID, false); - let type = this.domain.types.get(typeID, "typeID"); - this.domain.types.removeType(type); - this.renderEditParticle(); - this.renderNewParticle(); - this.plot.data.splice(typeID, 1); - this.updatePlot(); - }, - deleteTypeAndParticles: function (typeID) { - if(this.actPart.part && this.actPart.part.type >= typeID) { - this.actPart.part = null; - } - let self = this; - let particles = this.domain.particles.filter(function (particle) { - return particle.type === typeID; - }); - this.domain.particles.removeParticles(particles); - let type = this.domain.types.get(typeID, "typeID"); - this.domain.types.removeType(type); - this.renderEditParticle(); - this.renderNewParticle(); - this.plot.data.splice(typeID, 1); - this.updatePlot(); - }, - displayDomain: function () { - let self = this; - var el = this.queryByHook("domain-plot"); - Plotly.newPlot(el, this.plot); - el.on('plotly_click', _.bind(this.selectParticle, this)); - }, - errorAction: function (action, src) { - $(this.queryByHook(src + "-in-progress")).css("display", "none"); - $(this.queryByHook(src + "-action-error")).text(action); - $(this.queryByHook(src + "-error")).css("display", "block"); - console.log(action) + errorAction: function (action) { + $(this.queryByHook("sd-in-progress")).css("display", "none"); + $(this.queryByHook("sd-action-error")).text(action); + $(this.queryByHook("sd-error")).css("display", "block"); }, getBreadcrumbData: function () { - var data = {"project":null, "model":null}; + let data = {"project":null, "model":null}; var projEP = "stochss/project/manager?path=" var mdlEP = "stochss/models/edit?path=" if(this.model) { @@ -450,217 +119,58 @@ let DomainEditor = PageView.extend({ } return data }, - getNewParticle: function () { - var particle = new Particle({ - point: [0, 0, 0], - mass: 1.0, - volume: 1.0, - nu: 0.0, - fixed: false, - type: 0 - }); - return particle; - }, - getTypesFromFile: function (typePath) { - this.startAction("Setting types ...", "st") - let self = this; - let queryStr = "?path=" + this.typeDescriptionsFile; - let endpoint = path.join(app.getApiPath(), "spatial-model/particle-types") + queryStr; - app.getXHR(endpoint, { - success: function (err, response, body) { - self.addMissingTypes(body.names); - self.changeParticleTypes(body.types); - self.completeAction("Types set", "st"); - }, - error: function (err, response, body) { - self.errorAction(body.Message, "st"); - console.log(err); - } - }); - }, - renameType: function (index, newName) { - this.domain.types.get(index, "typeID").name = newName; - this.renderEditParticle(); - this.renderNewParticle(); - this.renderTypeSelectView(); - this.renderCreate3DDomain(); - this.plot.data[index].name = newName; - this.updatePlot(); - }, - renderCreate3DDomain: function () { - if(this.create3DDomainView) { - this.create3DDomainView.remove() - } - this.create3DDomainView = new Create3DDomainView({ - parent: this, - model: this.domain - }); - app.registerRenderSubview(this, this.create3DDomainView, "add-3d-domain"); - }, - renderDomainLimitations: function () { - if(this.xLimMinView) { - this.xLimMinView.remove(); - this.yLimMinView.remove(); - this.zLimMinView.remove(); - this.xLimMaxView.remove(); - this.yLimMaxView.remove(); - this.zLimMaxView.remove(); - } - this.xLimMinView = new InputView({parent: this, required: true, - name: 'x-lim-min', valueType: 'number', - value: this.domain.x_lim[0] || 0}); - app.registerRenderSubview(this, this.xLimMinView, "x_lim-min"); - this.yLimMinView = new InputView({parent: this, required: true, - name: 'y-lim-min', valueType: 'number', - value: this.domain.y_lim[0] || 0}); - app.registerRenderSubview(this, this.yLimMinView, "y_lim-min"); - this.zLimMinView = new InputView({parent: this, required: true, - name: 'z-lim-min', valueType: 'number', - value: this.domain.z_lim[0] || 0}); - app.registerRenderSubview(this, this.zLimMinView, "z_lim-min"); - this.xLimMaxView = new InputView({parent: this, required: true, - name: 'x-lim-max', valueType: 'number', - value: this.domain.x_lim[1] || 0}); - app.registerRenderSubview(this, this.xLimMaxView, "x_lim-max"); - this.yLimMaxView = new InputView({parent: this, required: true, - name: 'y-lim-max', valueType: 'number', - value: this.domain.y_lim[1] || 0}); - app.registerRenderSubview(this, this.yLimMaxView, "y_lim-max"); - this.zLimMaxView = new InputView({parent: this, required: true, - name: 'z-lim-max', valueType: 'number', - value: this.domain.z_lim[1] || 0}); - app.registerRenderSubview(this, this.zLimMaxView, "z_lim-max"); - }, - renderDomainProperties: function () { - $(this.queryByHook("static-domain")).prop("checked", this.domain.static); - let densityView = new InputView({parent: this, required: true, - name: 'density', valueType: 'number', - value: this.domain.rho_0 || 1}); - app.registerRenderSubview(this, densityView, "density"); - let gravityXView = new InputView({parent: this, required: true, - name: 'gravity-x', valueType: 'number', - value: this.domain.gravity[0], label: "X: "}); - app.registerRenderSubview(this, gravityXView, "gravity-x"); - let gravityYView = new InputView({parent: this, required: true, - name: 'gravity-y', valueType: 'number', - value: this.domain.gravity[1], label: "Y: "}); - app.registerRenderSubview(this, gravityYView, "gravity-y"); - let gravityZView = new InputView({parent: this, required: true, - name: 'gravity-z', valueType: 'number', - value: this.domain.gravity[2], label: "Z: "}); - app.registerRenderSubview(this, gravityZView, "gravity-z"); - let pressureView = new InputView({parent: this, required: true, - name: 'pressure', valueType: 'number', - value: this.domain.p_0 || 0}); - app.registerRenderSubview(this, pressureView, "pressure"); - let speedView = new InputView({parent: this, required: true, - name: 'speed', valueType: 'number', - value: this.domain.c_0 || 0}); - app.registerRenderSubview(this, speedView, "speed"); - }, - renderDomainTypes: function () { - if(this.domainTypesView) { - this.domainTypesView.remove(); - this.domain.types.forEach(function (type) { - type.numParticles = 0; + handleSaveToModel: function () { + if(this.model) { + this.startAction("Saving domain to model ..."); + this.model.domain = this.domain; + this.model.saveModel(() => { + this.completeAction("Domain saved to model"); }); } - let self = this; - this.domain.particles.forEach(function (particle) { - self.domain.types.get(particle.type, "typeID").numParticles += 1; - }); - let unaPart = "Number of Un-Assigned Particles: " + this.domain.types.get(0, "typeID").numParticles; - $(this.queryByHook("unassigned-particles")).text(unaPart); - this.domainTypesView = this.renderCollection( - this.domain.types, - EditDomainTypeView, - this.queryByHook("domain-types-list"), - {"filter": function (model) { - return model.typeID != 0; - }} - ); - this.toggleDomainError() }, - renderEditTypeDefaults: function () { - if(this.massView) { - this.massView.remove(); - } - if(this.volView) { - this.volView.remove(); - } - if(this.nuView) { - this.nuView.remove(); - } - var title = "Defaults for "; - if(this.selectedType === "new"){ - var type = this.domain.types.get(0, "typeID") - title += "New Type"; - }else{ - var type = this.domain.types.get(this.selectedType, "typeID") - title += type.name - } - $(this.queryByHook("set-type-defaults-header")).text(title) - this.massView = new InputView({parent: this, required: true, - name: 'mass', valueType: 'number', - value: type.mass}); - app.registerRenderSubview(this, this.massView, "td-mass"); - this.volView = new InputView({parent: this, required: true, - name: 'volume', valueType: 'number', - value: type.volume}); - app.registerRenderSubview(this, this.volView, "td-vol"); - this.nuView = new InputView({parent: this, required: true, - name: 'viscosity', valueType: 'number', - value: type.nu}); - app.registerRenderSubview(this, this.nuView, "td-nu"); - $(this.queryByHook("td-fixed")).prop("checked", type.fixed); - $(this.queryByHook("edit-defaults")).css("display", "block"); - }, - renderEditParticle: function () { - if(this.editParticleView) { - this.editParticleView.remove(); - } - if(this.actPart.part) { - var particle = this.actPart.part; + handleSaveToFile: function () { + this.startAction("Saving the domain to file (.domn) ...") + if(this.domain.directory && !this.domain.dirname) { + this.saveDomain() }else{ - var particle = this.getNewParticle(); + if(document.querySelector('#newDomainModal')) { + document.querySelector('#newDomainModal').remove() + } + let modal = $(modals.createDomainHtml()).modal(); + let okBtn = document.querySelector('#newDomainModal .ok-model-btn'); + let input = document.querySelector('#newDomainModal #domainNameInput'); + input.addEventListener("keyup", (event) => { + if(event.keyCode === 13){ + event.preventDefault(); + okBtn.click(); + } + }); + input.addEventListener("input", (e) => { + let endErrMsg = document.querySelector('#newDomainModal #domainNameInputEndCharError') + let charErrMsg = document.querySelector('#newDomainModal #domainNameInputSpecCharError') + let error = app.validateName(input.value) + okBtn.disabled = error !== "" || input.value.trim() === "" + charErrMsg.style.display = error === "both" || error === "special" ? "block" : "none" + endErrMsg.style.display = error === "both" || error === "forward" ? "block" : "none" + }); + okBtn.addEventListener('click', (e) => { + if (Boolean(input.value)) { + modal.modal('hide') + let name = input.value.trim() + this.saveDomain({name: name}); + } + }); } - this.editParticleView = new ParticleView({ - model: particle, - newParticle: false, - }); - app.registerRenderSubview(this, this.editParticleView, "edit-particle"); - }, - renderMeshTransformations: function () { - let xtrans = new InputView({parent: this, required: true, - name: 'x-transformation', valueType: 'number', - value: 0}); - app.registerRenderSubview(this, xtrans, "mesh-x-trans"); - let ytrans = new InputView({parent: this, required: true, - name: 'y-transformation', valueType: 'number', - value: 0}); - app.registerRenderSubview(this, ytrans, "mesh-y-trans"); - let ztrans = new InputView({parent: this, required: true, - name: 'z-transformation', valueType: 'number', - value: 0}); - app.registerRenderSubview(this, ztrans, "mesh-z-trans"); }, - renderMeshTypeDefaults: function (id) { - let type = this.domain.types.get(id, "typeID") - $(this.queryByHook("mesh-mass")).text(type.mass); - $(this.queryByHook("mesh-volume")).text(type.volume); - $(this.queryByHook("mesh-nu")).text(type.nu); - $(this.queryByHook("mesh-fixed")).prop("checked", type.fixed); - }, - renderNewParticle: function () { - if(this.newParticleView) { - this.newParticleView.remove(); + renderDomainView: function () { + if(this.doaminView) { + this.domainView.remove(); } - var particle = this.getNewParticle(); - this.newParticleView = new ParticleView({ - model: particle, - newParticle: true, + this.domainView = new DomainView({ + model: this.domain, + queryStr: this.queryStr }); - app.registerRenderSubview(this, this.newParticleView, "new-particle"); + app.registerRenderSubview(this, this.domainView, "domain-view-container"); }, renderSubviews: function () { let breadData = this.getBreadcrumbData(); @@ -692,216 +202,37 @@ let DomainEditor = PageView.extend({ $(this.queryByHook("return-to-model")).css("display", "none"); $(this.queryByHook("return-to-project")).css("display", "none"); } - this.renderDomainProperties(); - this.renderDomainLimitations(); - $(this.queryByHook("reflect_x")).prop("checked", this.domain.boundary_condition.reflect_x); - $(this.queryByHook("reflect_y")).prop("checked", this.domain.boundary_condition.reflect_y); - $(this.queryByHook("reflect_z")).prop("checked", this.domain.boundary_condition.reflect_z); - this.renderDomainTypes(); - this.renderNewParticle(); - this.renderEditParticle(); - this.renderTypeSelectView(); - this.renderMeshTypeDefaults(0); - this.renderMeshTransformations(); - this.renderTypesFileSelect(); - this.renderCreate3DDomain(); - let self = this; - let endpoint = path.join(app.getApiPath(), "spatial-model/domain-plot") + this.queryStr; - app.getXHR(endpoint, { - success: function (err, response, body) { - self.plot = body.fig; - self.displayDomain(); - } - }); - $(document).ready(function () { - $('[data-toggle="tooltip"]').tooltip(); - $('[data-toggle="tooltip"]').click(function () { - $('[data-toggle="tooltip"]').tooltip("hide"); - }); - }); - this.domain.updateValid(); - this.toggleDomainError(); + this.renderDomainView(); + app.documentSetup(); if(!this.model) { $(this.queryByHook("save-to-model")).addClass("disabled") } }, - renderTypesFileSelect: function () { - let self = this; - let endpoint = path.join(app.getApiPath(), "spatial-model/types-list"); - app.getXHR(endpoint, { - success: function (err, response, body) { - self.typeDescriptions = body.paths; - var typesSelectView = new SelectView({ - label: '', - name: 'type-files', - required: false, - idAttributes: 'cid', - options: body.files, - unselectedText: "-- Select Type File --", - }); - app.registerRenderSubview(self, typesSelectView, "types-file-select"); - } - }); - }, - renderTypesLocationSelect: function (options) { - if(this.typesLocationSelectView) { - this.typesLocationSelectView.remove(); - } - this.typesLocationSelectView = new SelectView({ - label: '', - name: 'type-locations', - required: false, - idAttributes: 'cid', - options: options, - unselectedText: "-- Select Location --" - }); - app.registerRenderSubview(this, this.typesLocationSelectView, "types-file-location-select") - }, - renderTypeSelectView: function () { - if(this.typeView) { - this.typeView.remove() - } - this.typeView = new SelectView({ - label: 'Type: ', - name: 'type', - required: true, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: this.domain.types, - value: this.domain.types.get(0, "typeID") - }); - app.registerRenderSubview(this, this.typeView, "mesh-type-select") - }, - saveDomain: function (name=null) { + saveDomain: function ({name=null}={}) { let domain = this.domain.toJSON(); if(name) { - let file = name + ".domn"; + let file = `${name}.domn`; var dirname = this.model ? path.dirname(this.model.directory) : this.domain.dirname; var domainPath = dirname === "/" ? file : path.join(dirname, file); }else{ var domainPath = this.domain.directory; } - let self = this; - let endpoint = path.join(app.getApiPath(), "file/json-data") + "?path=" + domainPath; + let endpoint = path.join(app.getApiPath(), "file/json-data") + `?path=${domainPath}`; app.postXHR(endpoint, domain, { - success: function (err, response, body) { - self.completeAction("Domain save to file (.domn)", "sd"); + success: (err, response, body) => { + this.completeAction("Domain save to file (.domn)"); }, - error: function (err, response, body) { - self.errorAction(body.Message, "sd"); - console.log(body.message); + error: (err, response, body) => { + this.errorAction(body.Message); } }); }, - selectParticle: function (data) { - let point = data.points[0]; - this.actPart.part = this.domain.particles.get(point.id, "particle_id"); - this.actPart.tn = point.curveNumber; - this.actPart.pn = point.pointNumber; - this.renderEditParticle(); - }, - setBoundaryCondition: function (e) { - let key = e.target.dataset.hook; - let value = e.target.checked; - this.domain.boundary_condition[key] = value; - }, - setLimitation: function (e) { - let data = e.target.parentElement.parentElement.dataset.hook.split('-'); - let index = data[1] === "min" ? 0 : 1; - let value = Number(e.target.value.trim()) - this.domain[data[0]][index] = value; - }, - setDensity: function (e) { - let value = Number(e.target.value) - this.domain.rho_0 = value; - }, - setGravity: function (e) { - let value = Number(e.target.value) - let hook = e.target.parentElement.parentElement.dataset.hook; - if(hook.endsWith("x")){ - this.domain.gravity[0] = value; - }else if(hook.endsWith("y")) { - this.domain.gravity[1] = value; - }else { - this.domain.gravity[2] = value; - } - }, - setPressure: function (e) { - let value = Number(e.target.value) - this.domain.p_0 = value; - }, - setSpeed: function (e) { - let value = Number(e.target.value) - this.domain.c_0 = value; - }, - setStaticDomain: function (e) { - let value = e.target.checked; - this.domain.statis = value; - }, - startAction: function (action, src) { - $(this.queryByHook(src + "-complete")).css("display", "none"); - $(this.queryByHook(src + "-action-in-progress")).text(action); - $(this.queryByHook(src + "-in-progress")).css("display", "inline-block"); - console.log(action) - }, - unassignAllParticles: function (type, update=true) { - let self = this; - this.domain.particles.forEach(function (particle) { - if(particle.type === type) { - self.actPart.part = particle; - self.actPart.tn = type; - self.actPart.pn = self.plot.data[type].ids.indexOf(particle.particle_id) - self.changeParticleType(0); - } - }); - if(this.actPart.part && this.actPart.part.type == type) { - this.actPart.part = null; - this.renderEditParticle(); - } - if(update) { - this.updatePlot(); - } - }, - updatePlot: function () { - var el = this.queryByHook("domain-plot"); - el.removeListener('plotly_click', this.selectParticle); - Plotly.purge(el); - this.displayDomain(); - }, - update: function () {}, - updateMeshTypeAndDefaults: function (e) { - let id = Number(e.target.selectedOptions.item(0).value); - this.renderMeshTypeDefaults(id); - }, - updateParticle: function ({cb=null}={}) { - if(this.actPart.part.pointChanged) { - let x = this.actPart.part.point[0]; - let y = this.actPart.part.point[1]; - let z = this.actPart.part.point[2]; - this.changeParticleLocation(x, y, z) - } - if(this.actPart.part.typeChanged) { - let type = this.actPart.part.type - this.changeParticleType(type); - this.renderDomainTypes(); - } - this.updatePlot(); - if(cb) { - cb() - } - }, - updateValid: function () {}, - toggleDomainError: function () { - let errorMsg = $(this.queryByHook('domain-error')) - if(!this.domain.valid) { - errorMsg.addClass('component-invalid') - errorMsg.removeClass('component-valid') - }else{ - errorMsg.addClass('component-valid') - errorMsg.removeClass('component-invalid') - } - }, + startAction: function (action) { + $(this.queryByHook("sd-complete")).css("display", "none"); + $(this.queryByHook("sd-error")).css("display", "none"); + $(this.queryByHook("sd-action-in-progress")).text(action); + $(this.queryByHook("sd-in-progress")).css("display", "inline-block"); + } }); initPage(DomainEditor); diff --git a/client/pages/model-editor.js b/client/pages/model-editor.js index fe16d3b4dc..63e41c697b 100644 --- a/client/pages/model-editor.js +++ b/client/pages/model-editor.js @@ -330,7 +330,8 @@ let ModelEditor = PageView.extend({ let domainElements = { select: $(this.queryByHook("me-select-particle")), particle: {view: this, hook: "me-particle-viewer"}, - plot: this.queryByHook("domain-plot-container"), + figure: this.queryByHook("domain-plot-container"), + figureEmpty: this.queryByHook("domain-plot-container-empty"), type: this.queryByHook("me-types-quick-view") } this.modelView = new ModelView({ diff --git a/client/reaction-types.js b/client/reaction-types.js index 4ff9516b32..f8a2e3f8e4 100644 --- a/client/reaction-types.js +++ b/client/reaction-types.js @@ -55,11 +55,11 @@ module.exports = { 'custom-massaction': { reactants: [ { ratio: 1 } ], products: [ { ratio: 1 } ], - label: 'Custom mass action' + label: 'Custom Mass Action' }, 'custom-propensity': { reactants: [ { ratio: 1 } ], products: [ { ratio: 1 } ], - label: 'Custom propensity' + label: 'Custom Propensity' } } diff --git a/client/styles/styles.css b/client/styles/styles.css index e5c6744d2d..6ddb695ddc 100644 --- a/client/styles/styles.css +++ b/client/styles/styles.css @@ -130,8 +130,8 @@ label { } span.custom { - padding-right: 0.25rem; - padding-left: 0.25rem; + padding-right: 0.5rem; + padding-left: 0.5rem; } button.custom { @@ -302,7 +302,6 @@ input[type="file"]::-ms-browse { .reaction-list-summary { width: auto !important; - text-align: center; } .container-part { @@ -649,13 +648,32 @@ span.checkbox { height: auto; } +.dropdown-item.rtype.disabled { + pointer-events: auto !important; +} + .reaction-lb2 { display: none; } -.dropdown-item:hover .reaction-lb2 { +.dropdown-item:not(.disabled):hover .reaction-lb2 { position: absolute; - left: 195px; + left: 196px; + display: inline-block; + background-color: black; + color: white; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + border: .5px solid rgb(228, 229, 230) !important; + padding: 0px 5px; +} + +.reaction-lb3 { + display: none; +} + +.dropdown-item.disabled:hover .reaction-lb3 { + position: absolute; + left: 196px; display: inline-block; background-color: black; color: white; @@ -727,3 +745,7 @@ span.checkbox { margin-top: 64px; padding: 20px 16px; } + +.hidden { + display: none; +} diff --git a/client/templates/includes/edit3DDomain.pug b/client/templates/includes/edit3DDomain.pug deleted file mode 100644 index 4a6e55b090..0000000000 --- a/client/templates/includes/edit3DDomain.pug +++ /dev/null @@ -1,103 +0,0 @@ -div.card.card-body - - div - - h4.mt-3 Particles Distribution - - table.table - thead - tr - th(scope="col") - th(scope="col") X-Plane - th(scope="col") Y-Plane - th(scope="col") Z-Plane - th(scope="col") Total - tbody - tr - th(scope="row") Number of Particles - td: div(data-target="num-particles" data-hook="nx") - td: div(data-target="num-particles" data-hook="ny") - td: div(data-target="num-particles" data-hook="nz") - td: div(data-hook="total") - - div - - h4 Domain Limits - - table.table - thead - tr - th(scope="col") - th(scope="col") X-Axis - th(scope="col") Y-Axis - th(scope="col") Z-Axis - tbody - tr - th(scope="row") Minimum - td: div(data-target="limits" data-hook="xLim-min") - td: div(data-target="limits" data-hook="yLim-min") - td: div(data-target="limits" data-hook="zLim-min") - tr - th(scope="row") Maximum - td: div(data-target="limits" data-hook="xLim-max") - td: div(data-target="limits" data-hook="yLim-max") - td: div(data-target="limits" data-hook="zLim-max") - - div - - h4.inline Advanced - - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#create-3D-domain-advanced", data-hook="collapse") + - - div.collapse(id="create-3D-domain-advanced" data-hook="3D-domain-advanced-container") - - div(data-hook="type-select") - - h5 Type Defaults - - table.table - thead - tr - th(scope="col") Mass - th(scope="col") Volume - th(scope="col") Viscosity - th(scope="col") Fixed - - tbody - tr - td: div(data-hook="mass") - td: div(data-hook="volume") - td: div(data-hook="nu") - td: input(type="checkbox" data-hook="fixed" disabled) - - h5 Particle Transformations - - table.table - thead - tr - th(scope="col") X-Axis - th(scope="col") Y-Axis - th(scope="col") Z-Axis - tbody - tr - td: div(data-hook="domain-x-trans") - td: div(data-hook="domain-y-trans") - td: div(data-hook="domain-z-trans") - - div.inline - - button.btn.btn-outline-primary(data-hook="build-domain") Build Domain - - div.mdl-edit-btn.saving-status.inline(data-hook="cd-in-progress") - - div.spinner-grow.mr-2 - - span(data-hook="cd-action-in-progress") - - div.mdl-edit-btn.saved-status.inline(data-hook="cd-complete") - - span(data-hook="cd-action-complete") - - div.mdl-edit-btn.save-error-status(data-hook="cd-error") - - span(data-hook="cd-action-error") diff --git a/client/templates/includes/editParticle.pug b/client/templates/includes/editParticle.pug deleted file mode 100644 index c4b5d34153..0000000000 --- a/client/templates/includes/editParticle.pug +++ /dev/null @@ -1,63 +0,0 @@ -div.card.card-body - - div.text-info.mb-4(data-hook="select-message-" + this.viewIndex style="display: none") - | Click on a particle in the Domain section to begin editing. - - div - - h4.inline.mr-2 Location: - - div.inline.mr-2 (x: - - div.inline.ml2(data-target="location" data-hook="x-coord-" + this.viewIndex) - - div.inline.mr-2 , y: - - div.inline.ml2(data-target="location" data-hook="y-coord-" + this.viewIndex) - - div.inline.mr-2 , z: - - div.inline.ml2(data-target="location" data-hook="z-coord-" + this.viewIndex) - - div.inline ) - - h4 Particle Properties - - table.table - thead - tr - th(scope="row") Type - th(scope="row") Mass - th(scope="row") Volume - th(scope="row") Viscosity - th(scope="row") Fixed - - tbody - tr - td: div(data-target="type" data-hook="type-" + this.viewIndex) - td: div(data-target="mass" data-hook="mass-" + this.viewIndex) - td: div(data-target="volume" data-hook="volume-" + this.viewIndex) - td: div(data-target="nu" data-hook="nu-" + this.viewIndex) - td: input(type="checkbox" data-target="fixed" data-hook="fixed-" + this.viewIndex) - - div - - div.inline(data-hook="new-particle-btns-" + this.viewIndex) - - button.btn.btn-outline-primary.box-shadow(data-hook="add-particle") Add Particle - - div.inline(data-hook="edit-particle-btns-" + this.viewIndex) - - button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="save-particle") Save Particle - - button.btn.btn-outline-primary.box-shadow.ml-2(data-hook="remove-particle") Remove Particle - - div.mdl-edit-btn.saving-status.inline(data-hook="ep-in-progress") - - div.spinner-grow.mr-2 - - span(data-hook="ep-action-in-progress") - - div.mdl-edit-btn.saved-status.inline(data-hook="ep-complete") - - span(data-hook="ep-action-complete") diff --git a/client/templates/includes/jstreeView.pug b/client/templates/includes/jstreeView.pug index 64c3109a8f..e2f0075e83 100644 --- a/client/templates/includes/jstreeView.pug +++ b/client/templates/includes/jstreeView.pug @@ -23,8 +23,8 @@ div li.dropdown-item(id="fb-new-directory" data-hook="fb-new-directory") Create Directory li.dropdown-item(id="fb-new-project" data-hook="fb-new-project") Create Project li.dropdown-item(id="fb-new-model" data-hook="fb-new-model" data-type="model") Create Model - li.dropdown-item(id="fb-new-model" data-hook="fb-new-model" data-type="spatial") Create Spatial Model (beta) - li.dropdown-item(id="fb-new-model" data-hook="fb-new-domain") Create Domain (beta) + li.dropdown-item(id="fb-new-model" data-hook="fb-new-model" data-type="spatial") Create Spatial Model + li.dropdown-item(id="fb-new-model" data-hook="fb-new-domain") Create Domain li.dropdown-divider(data-hook="fb-proj-seperator") li.dropdown-item(id="fb-import-model" data-hook="fb-import-model") Add Existing Model li.dropdown-divider diff --git a/client/templates/includes/meshEditor.pug b/client/templates/includes/meshEditor.pug deleted file mode 100644 index 4b478b473a..0000000000 --- a/client/templates/includes/meshEditor.pug +++ /dev/null @@ -1,13 +0,0 @@ -div#mesh-editor.card.card-body - - div - - h3.inline Mesh Editor - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#collapse-mesh", data-hook="collapse") - - - div.collapse(class="show", id="collapse-mesh") - - table.table - tbody - tr - td: div(data-hook="num-subdomains-container") \ No newline at end of file diff --git a/client/templates/includes/quickviewDomainTypes.pug b/client/templates/includes/quickviewDomainTypes.pug deleted file mode 100644 index 80ad38e1f0..0000000000 --- a/client/templates/includes/quickviewDomainTypes.pug +++ /dev/null @@ -1,5 +0,0 @@ -tr - - td=this.model.name - - td=this.model.numParticles \ No newline at end of file diff --git a/client/templates/includes/viewParticle.pug b/client/templates/includes/viewParticle.pug deleted file mode 100644 index 7049a0fb5d..0000000000 --- a/client/templates/includes/viewParticle.pug +++ /dev/null @@ -1,30 +0,0 @@ -div - - h5 Properties - - table.table - tbody - tr - th(scope="row") ID - td=this.model.particle_id - tr - th(scope="row") Type - td=this.type - tr - th(scope="row") Location - td - div="x: " + this.model.point[0] - div="y: " + this.model.point[1] - div="z: " + this.model.point[2] - tr - th(scope="row") Mass - td=this.model.mass - tr - th(scope="row") Volume - td=this.model.volume - tr - th(scope="row") Viscosity - td=this.model.nu - tr - th(scope="row") Fixed - td: input(type="checkbox" checked=this.model.fixed disabled) \ No newline at end of file diff --git a/client/templates/pages/domainEditor.pug b/client/templates/pages/domainEditor.pug index 578b0d59bb..d97d85b6c2 100644 --- a/client/templates/pages/domainEditor.pug +++ b/client/templates/pages/domainEditor.pug @@ -18,285 +18,7 @@ section.page li.breadcrumb-item a.active-link(data-hook="parent-one-breadcrumb") Parent - div.card.card-body - - div - - h3.inline Properties - - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#domain-properties", data-hook="collapse-domain-properties") - - - div.collapse.show(id="domain-properties" data-hook="domain-properties") - - table.table - thead - tr - th(scope="col") Static Domain - th(scope="col") Density - th(scope="col") Gravity - th(scope="col") - div - div.inline Pressure - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.pressure) - - th(scope="col") - div - div.inline Speed of Sound - - div.tooltip-icon(data-html="true" data-toggle="tooltip" title=this.tooltips.speed) - - tbody - tr - td: input(type="checkbox", id="static-domain", data-hook="static-domain") - td: div(data-hook="density") - td - div(data-target="gravity" data-hook="gravity-x") - div(data-target="gravity" data-hook="gravity-y") - div(data-target="gravity" data-hook="gravity-z") - td: div(data-hook="pressure") - td: div(data-hook="speed") - - h4 Domain Limits - - table.table - thead - tr - th(scope="col") - th(scope="col") X-Axis - th(scope="col") Y-Axis - th(scope="col") Z-Axis - - tbody - tr - th(scope="row") Minimum - td: div(data-hook="x_lim-min" data-name="limitation") - td: div(data-hook="y_lim-min" data-name="limitation") - td: div(data-hook="z_lim-min" data-name="limitation") - tr - th(scope="row") Maximum - td: div(data-hook="x_lim-max" data-name="limitation") - td: div(data-hook="y_lim-max" data-name="limitation") - td: div(data-hook="z_lim-max" data-name="limitation") - tr - th(scope="row") Reflect - td: input(type="checkbox", id="x-reflect", data-hook="reflect_x" data-target="reflect" disabled) - td: input(type="checkbox", id="y-reflect", data-hook="reflect_y" data-target="reflect" disabled) - td: input(type="checkbox", id="z-reflect", data-hook="reflect_z" data-target="reflect" disabled) - - div.card.card-body - - div - - h3.inline Types - - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#domain-types", data-hook="collapse-domain-types") - - - div.collapse.show(id="domain-types" data-hook="domain-types") - - div - - h6(data-hook="unassigned-particles") - - div(data-hook="domain-error"): p.text-danger A domain cannot have any un-assigned particles. - - table.table - thead - tr - th(scope="col") Name - th(scope="col") Number of Particles - th(scope="col" colspan="4") Defaults - - tbody(data-hook="domain-types-list") - - button.btn.btn-outline-primary.box-shadow(data-hook="add-domain-type") Add Type - - div.card.card-body(data-hook="edit-defaults" style="display: none") - - h4(data-hook="set-type-defaults-header") - - table.table - thead - tr - th(scope="col") Mass - th(scope="col") Volume - th(scope="col") Viscosity - th(scope="col") Fixed - - tbody - tr - td: div(data-hook="td-mass") - td: div(data-hook="td-vol") - td: div(data-hook="td-nu") - td: input(type="checkbox" data-hook="td-fixed") - - button.btn.btn-outline-primary.box-shadow(data-hook="set-type-defaults") Submit - - div.card.card-body - - div - - h3.inline Particles - - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#domain-particles", data-hook="collapse-domain-particle") - - - div.collapse.show(id="domain-particles" data-hook="domain-particles") - - ul.nav.nav-tabs - - li.nav-item - - a.nav-link.tab.active(data-toggle="tab" href="#new-particle") Create New - - li.nav-item - - a.nav-link.tab(data-toggle="tab" href="#edit-particle") Edit Selected - - li.nav-item - - a.nav-link.tab(data-toggle="tab" href="#set-particle-types") Set Particle Types - - li.nav-item - - a.nav-link.tab(data-toggle="tab" href="#add-3d-domain") Create 3D Domain - - li.nav-item - - a.nav-link.tab(data-toggle="tab" href="#import-particles") Import Mesh - - div.tab-content - - div.tab-pane.active(id="new-particle" data-hook="new-particle") - - div.tab-pane(id="edit-particle" data-hook="edit-particle") - - div.tab-pane(id="add-3d-domain" data-hook="add-3d-domain") - - div.tab-pane(id="set-particle-types" data-hook="set-particle-types") - - div.card.card-body - - div.my-3 - - div.text-info(data-hook="type-location-message" style="display: none") - | There are multiple type files with that name, please select a location - - div.inline.mr-3 - - span.inline.mr-2(for="types-file-select") Select type description file: - - div.inline(id="types-file-select" data-hook="types-file-select") - - div.inline(data-hook="type-location-container" style="display: none") - - span.inlinemr-2(for="types-file-location-select") Location: - - div.inline(id="types-file-location-select" data-hook="types-file-location-select") - - div.inline - - button.btn.btn-outline-primary.box-shadow(data-hook="set-particle-types-btn" disabled) Set Types - - div.mdl-edit-btn.saving-status.inline(data-hook="st-in-progress") - - div.spinner-grow.mr-2 - - span(data-hook="st-action-in-progress") - - div.mdl-edit-btn.saved-status.inline(data-hook="st-complete") - - span(data-hook="st-action-complete") - - div.mdl-edit-btn.save-error-status(data-hook="st-error") - - span(data-hook="st-action-error") - - div.tab-pane(id="import-particles") - - div.card.card-body - - div.my-3 - - span.inline.mr-2(for="meshfile") Please specify a mesh to import: - - input(id="meshfile" type="file" name="meshfile" size="30" accept=".xml" required) - - div.mb-3 - - span.inline.mr-2(for=typefile) Type descriptions (optional): - - input(id="typefile" type="file" name="typefile" size="30" accept=".txt") - - div - - h4.inline Advanced - - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#mesh-advanced", data-hook="collapse-mesh-advanced") + - - div.collapse(id="mesh-advanced" data-hook="mesh-advanced-container") - - div(data-hook="mesh-type-select") - - h5 Type Defaults - - table.table - thead - tr - th(scope="col") Mass - th(scope="col") Volume - th(scope="col") Viscosity - th(scope="col") Fixed - - tbody - tr - td: div(data-hook="mesh-mass") - td: div(data-hook="mesh-volume") - td: div(data-hook="mesh-nu") - td: input(type="checkbox" data-hook="mesh-fixed" disabled) - - h5 Particle Transformations - - table.table - thead - tr - th(scope="col") X-Axis - th(scope="col") Y-Axis - th(scope="col") Z-Axis - tbody - tr - td: div(data-hook="mesh-x-trans") - td: div(data-hook="mesh-y-trans") - td: div(data-hook="mesh-z-trans") - - div.inline - - button.btn.btn-outline-primary.box-shadow(data-hook="import-particles-btn" disabled) Import Mesh - - div.mdl-edit-btn.saving-status.inline(data-hook="im-in-progress") - - div.spinner-grow.mr-2 - - span(data-hook="im-action-in-progress") - - div.mdl-edit-btn.saved-status.inline(data-hook="im-complete") - - span(data-hook="im-action-complete") - - div.mdl-edit-btn.save-error-status(data-hook="im-error") - - span(data-hook="im-action-error") - - div.card.card-body - - div - - h3.inline Domain - - button.btn.btn-outline-collapse(data-toggle="collapse", data-target="#domain-plot-container", data-hook="collapse-domain-plot") - - - div.collapse.show(id="domain-plot-container" data-hook="domain-plot-container") - - div(id="domain-plot" data-hook="domain-plot" style="height: 800px") + div(data-hook="domain-view-container") div.inline diff --git a/client/templates/pages/modelEditor.pug b/client/templates/pages/modelEditor.pug index 5f5f5f9fc9..306bb6e24b 100644 --- a/client/templates/pages/modelEditor.pug +++ b/client/templates/pages/modelEditor.pug @@ -37,6 +37,8 @@ section.page div(data-hook="domain-plot-container") + div.text-danger(data-hook="domain-plot-container-empty") The domain currently has no particles to display + div.col-sm-4.overflow-auto(style="max-height:450px;") div.mt-2.particle-view(data-hook="me-particle-viewer") @@ -50,8 +52,7 @@ section.page h4 Particles per Type - table.table - tbody(data-hook="me-types-quick-view") + div(data-hook="me-types-quick-view") div(data-hook="model-run-container") diff --git a/client/templates/pages/projectManager.pug b/client/templates/pages/projectManager.pug index 59bd4742bb..05476cf6c0 100644 --- a/client/templates/pages/projectManager.pug +++ b/client/templates/pages/projectManager.pug @@ -51,7 +51,7 @@ section.page ul.dropdown-menu(aria-labelledby="project-manager-add-model-btn") li.dropdown-item(id="new-model" data-hook="new-model" data-type="model") New Model - li.dropdown-item(id="new-model" data-hook="new-model" data-type="spatial") New Spatial Model (beta) + li.dropdown-item(id="new-model" data-hook="new-model" data-type="spatial") New Spatial Model li.dropdown-divider li.dropdown-item(id="existing-model" data-hook="existing-model") Existing Model li.dropdown-divider diff --git a/client/tooltips.js b/client/tooltips.js index 1049db9302..067224de3f 100644 --- a/client/tooltips.js +++ b/client/tooltips.js @@ -53,6 +53,8 @@ module.exports = { rate: "The rate of the mass-action reaction.", propensity: "The custom propensity expression for the reaction.", + + odePropensity: "The custom ode propensity expression for the reaction.", reactant: "The reactants that are consumed in the reaction, with stoichiometry.", @@ -176,6 +178,11 @@ module.exports = { speed: "Approximate or artificial speed of sound" }, + domainType: { + geometry: "The geometry expression can be any mathematical expression which evaluates to a boolean value in a python environment (i.e. x==5). This "+ + "expression is evaluable within the a limited namespace, and only lower case variables (x, y, z), particles location, and (cx, cy, cz), "+ + "center of the geometry, can be referenced in the expression." + }, boundaryConditionsEditor: { annotation: "An optional note about a boundary condition." } diff --git a/client/views/edit-3D-domain.js b/client/views/edit-3D-domain.js deleted file mode 100644 index 03c5afff1a..0000000000 --- a/client/views/edit-3D-domain.js +++ /dev/null @@ -1,219 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -var $ = require('jquery'); -var path = require('path'); -//support files -var app = require('../app'); -var tests = require('./tests'); -//views -var View = require('ampersand-view'); -var InputView = require('./input'); -var SelectView = require('ampersand-select-view'); -//templates -var template = require('../templates/includes/edit3DDomain.pug'); - -module.exports = View.extend({ - template: template, - events: { - 'change [data-target=num-particles]' : 'handleNumParticlesUpdate', - 'change [data-target=limits]' : 'handleLimitsUpdate', - 'change [data-hook=type-select]' : 'updateTypeAndDefaults', - 'click [data-hook=build-domain]' : 'handleBuildDomain' - }, - handleBuildDomain: function (e) { - this.startAction("Creating domain ...") - this.data.type = {"type_id":this.type.typeID, "mass":this.type.mass, "nu":this.type.nu, "fixed":this.type.fixed}; - this.data.volume = this.type.volume; - let xtrans = Number($(this.queryByHook("domain-x-trans")).find('input')[0].value); - let ytrans = Number($(this.queryByHook("domain-y-trans")).find('input')[0].value); - let ztrans = Number($(this.queryByHook("domain-z-trans")).find('input')[0].value); - if(xtrans !== 0 || ytrans !== 0 || ztrans !== 0) { - this.data.transformation = [xtrans, ytrans, ztrans]; - } - let self = this; - let endpoint = path.join(app.getApiPath(), "spatial-model/3d-domain"); - app.postXHR(endpoint, this.data, { - success: function (err, response, body) { - self.parent.addParticles(body.particles); - if(self.parent.domain.x_lim[0] > body.limits.x_lim[0]) { - self.parent.domain.x_lim[0] = body.limits.x_lim[0]; - } - if(self.parent.domain.y_lim[0] > body.limits.y_lim[0]) { - self.parent.domain.y_lim[0] = body.limits.y_lim[0]; - } - if(self.parent.domain.z_lim[0] > body.limits.z_lim[0]) { - self.parent.domain.z_lim[0] = body.limits.z_lim[0]; - } - if(self.parent.domain.x_lim[1] < body.limits.x_lim[1]) { - self.parent.domain.x_lim[1] = body.limits.x_lim[1]; - } - if(self.parent.domain.y_lim[1] < body.limits.y_lim[1]) { - self.parent.domain.y_lim[1] = body.limits.y_lim[1]; - } - if(self.parent.domain.z_lim[1] < body.limits.z_lim[1]) { - self.parent.domain.z_lim[1] = body.limits.z_lim[1]; - } - self.parent.renderDomainLimitations(); - self.completeAction("Domain successfully created"); - $('html, body').animate({ - scrollTop: $("#domain-plot").offset().top - }, 20); - }, - error: function (err, response, body) { - self.errorAction(body.Message); - } - }); - }, - handleLimitsUpdate: function (e) { - let data = e.target.parentElement.parentElement.dataset.hook.split("-"); - let index = data[1] === "min" ? 0 : 1; - let value = Number(e.target.value); - this.data[data[0]][index] = value; - }, - handleNumParticlesUpdate: function (e) { - let hook = e.target.parentElement.parentElement.dataset.hook; - let value = Number(e.target.value); - this.data[hook] = value; - this.updateTotalParticles(); - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.type = this.model.types.get(0, "typeID"); - this.data = {"nx":1, "ny":1, "nz":1, "xLim":[0, 0], - "yLim":[0, 0], "zLim":[0, 0], "type":0, "volume":1, - "transformation": null} - }, - render: function (attrs, options) { - View.prototype.render.apply(this, arguments); - this.renderNumberOfParticles(); - this.updateTotalParticles(); - this.renderParticleLimits(); - this.renderType(); - this.renderTypeDefaults(); - this.renderDomainTransformations(); - }, - completeAction: function (action) { - $(this.queryByHook("cd-in-progress")).css("display", "none"); - $(this.queryByHook("cd-action-complete")).text(action); - $(this.queryByHook("cd-complete")).css("display", "inline-block"); - let self = this - setTimeout(function () { - $(self.queryByHook("cd-complete")).css("display", "none"); - }, 5000); - }, - errorAction: function (action) { - $(this.queryByHook("cd-in-progress")).css("display", "none"); - $(this.queryByHook("cd-action-error")).text(action); - $(this.queryByHook("cd-error")).css("display", "block"); - console.log(action) - }, - renderDomainTransformations: function () { - let xtrans = new InputView({parent: this, required: true, - name: 'x-transformation', valueType: 'number', - value: 0}); - app.registerRenderSubview(this, xtrans, "domain-x-trans"); - let ytrans = new InputView({parent: this, required: true, - name: 'y-transformation', valueType: 'number', - value: 0}); - app.registerRenderSubview(this, ytrans, "domain-y-trans"); - let ztrans = new InputView({parent: this, required: true, - name: 'z-transformation', valueType: 'number', - value: 0}); - app.registerRenderSubview(this, ztrans, "domain-z-trans"); - }, - renderNumberOfParticles: function () { - let nxView = new InputView({parent: this, required: true, - name: 'nx', valueType: 'number', - tests: tests.valueTests, - value: this.data.nx}); - app.registerRenderSubview(this, nxView, "nx"); - let nyView = new InputView({parent: this, required: true, - name: 'ny', valueType: 'number', - tests: tests.valueTests, - value: this.data.ny}); - app.registerRenderSubview(this, nyView, "ny"); - let nzView = new InputView({parent: this, required: true, - name: 'nz', valueType: 'number', - tests: tests.valueTests, - value: this.data.nz}); - app.registerRenderSubview(this, nzView, "nz"); - }, - renderParticleLimits: function () { - let xLimMinView = new InputView({parent: this, required: true, - name: 'x-lim-min', valueType: 'number', - value: this.data.xLim[0]}); - app.registerRenderSubview(this, xLimMinView, "xLim-min"); - let yLimMinView = new InputView({parent: this, required: true, - name: 'y-lim-min', valueType: 'number', - value: this.data.yLim[0]}); - app.registerRenderSubview(this, yLimMinView, "yLim-min"); - let zLimMinView = new InputView({parent: this, required: true, - name: 'z-lim-min', valueType: 'number', - value: this.data.zLim[0]}); - app.registerRenderSubview(this, zLimMinView, "zLim-min"); - let xLimMaxView = new InputView({parent: this, required: true, - name: 'x-lim-max', valueType: 'number', - value: this.data.xLim[1]}); - app.registerRenderSubview(this, xLimMaxView, "xLim-max"); - let yLimMaxView = new InputView({parent: this, required: true, - name: 'y-lim-max', valueType: 'number', - value: this.data.yLim[1]}); - app.registerRenderSubview(this, yLimMaxView, "yLim-max"); - let zLimMaxView = new InputView({parent: this, required: true, - name: 'z-lim-max', valueType: 'number', - value: this.data.zLim[1]}); - app.registerRenderSubview(this, zLimMaxView, "zLim-max"); - }, - renderType: function () { - var typeView = new SelectView({ - label: 'Type: ', - name: 'type', - required: true, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: this.model.types, - value: this.type - }); - app.registerRenderSubview(this, typeView, "type-select") - }, - renderTypeDefaults: function () { - $(this.queryByHook("mass")).text(this.type.mass) - $(this.queryByHook("volume")).text(this.type.volume) - $(this.queryByHook("nu")).text(this.type.nu) - $(this.queryByHook("fixed")).prop("checked", this.type.fixed) - }, - startAction: function (action) { - $(this.queryByHook("cd-complete")).css("display", "none"); - $(this.queryByHook("cd-action-in-progress")).text(action); - $(this.queryByHook("cd-in-progress")).css("display", "inline-block"); - }, - update: function (e) {}, - updateTotalParticles: function (e) { - let total = this.data.nx * this.data.ny * this.data.nz; - $(this.queryByHook("total")).text(total) - $(this.queryByHook("build-domain")).prop("disabled", total <= 0) - }, - updateTypeAndDefaults: function (e) { - let id = Number(e.target.selectedOptions.item(0).value); - this.type = this.model.types.get(id, "typeID"); - this.renderTypeDefaults(); - }, - updateValid: function (e) {} -}); \ No newline at end of file diff --git a/client/views/edit-particle.js b/client/views/edit-particle.js deleted file mode 100644 index e28682ac34..0000000000 --- a/client/views/edit-particle.js +++ /dev/null @@ -1,210 +0,0 @@ -/* -StochSS is a platform for simulating biochemical systems -Copyright (C) 2019-2022 StochSS developers. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -var $ = require('jquery'); -var _ = require('underscore'); -//support files -let app = require('../app'); -var tests = require('../views/tests'); -//views -var View = require('ampersand-view'); -var InputView = require('./input'); -var SelectView = require('ampersand-select-view'); -//templates -var template = require('../templates/includes/editParticle.pug'); - -module.exports = View.extend({ - template: template, - events: { - 'change [data-target=location]' : 'handleUpdateLocation', - 'change [data-target=mass]' : 'handleUpdateMass', - 'change [data-target=volume]' : 'handleUpdateVolume', - 'change [data-target=nu]' : 'handleUpdateNu', - 'change [data-target=fixed]' : 'handleUpdateFixed', - 'change [data-target=type]' : 'handleUpdateType', - 'click [data-hook=add-particle]' : 'handleAddParticle', - 'click [data-hook=save-particle]' : 'handleSaveParticle', - 'click [data-hook=remove-particle]' : 'handleRemoveParticle' - }, - handleAddParticle: function (e) { - this.startAction("Adding particle ...") - this.parent.addParticle(this.model, {cb: _.bind(function () { - this.completeAction("Particle Added") - }, this)}); - }, - handleSaveParticle: function (e) { - this.startAction("Saving particle ...") - this.parent.updateParticle({cb: _.bind(function () { - this.completeAction("Particle Saved") - }, this)}); - }, - handleRemoveParticle: function (e) { - this.parent.deleteParticle(); - }, - handleUpdateFixed: function (e) { - let value = e.target.checked; - this.model.fixed = value; - }, - handleUpdateLocation: function (e) { - let hook = e.target.parentElement.parentElement.dataset.hook; - let value = Number(e.target.value); - if(hook.startsWith('x')) { - this.model.point[0] = value; - }else if(hook.startsWith('y')) { - this.model.point[1] = value; - }else{ - this.model.point[2] = value; - } - this.model.pointChanged = true; - }, - handleUpdateMass: function (e) { - let value = Number(e.target.value); - this.model.mass = value; - }, - handleUpdateNu: function (e) { - let value = Number(e.target.value); - this.model.nu = value; - }, - handleUpdateType: function (e) { - let id = Number(e.target.selectedOptions.item(0).value); - let oldType = this.parent.domain.types.get(this.model.type, "typeID"); - let newType = this.parent.domain.types.get(id, "typeID"); - if(this.model.mass === oldType.mass) { - this.model.mass = newType.mass; - } - if(this.model.volume === oldType.volume) { - this.model.volume = newType.volume; - } - if(this.model.nu === oldType.nu) { - this.model.nu = newType.nu; - } - if(this.model.fixed === oldType.fixed) { - this.model.fixed = newType.fixed; - } - this.renderProperties(); - this.model.type = id; - this.model.typeChanged = true; - }, - handleUpdateVolume: function (e) { - let value = Number(e.target.value); - this.model.volume = value; - }, - initialize: function (attrs, options) { - View.prototype.initialize.apply(this, arguments); - this.newParticle = attrs.newParticle; - this.viewIndex = this.newParticle ? 0 : 1; - }, - completeAction: function (action) { - $(this.queryByHook("ep-in-progress")).css("display", "none"); - $(this.queryByHook("ep-action-complete")).text(action); - $(this.queryByHook("ep-complete")).css("display", "inline-block"); - console.log(action) - let self = this - setTimeout(function () { - $(self.queryByHook("ep-complete")).css("display", "none"); - }, 5000); - }, - disableAll: function () { - $(this.queryByHook("x-coord-" + this.viewIndex)).find('input').prop('disabled', true); - $(this.queryByHook("y-coord-" + this.viewIndex)).find('input').prop('disabled', true); - $(this.queryByHook("z-coord-" + this.viewIndex)).find('input').prop('disabled', true); - $(this.queryByHook("type-" + this.viewIndex)).find('select').prop('disabled', true); - $(this.queryByHook("mass-" + this.viewIndex)).find('input').prop('disabled', true); - $(this.queryByHook("volume-" + this.viewIndex)).find('input').prop('disabled', true); - $(this.queryByHook("nu-" + this.viewIndex)).find('input').prop('disabled', true); - $(this.queryByHook("fixed-" + this.viewIndex)).prop('disabled', true); - $(this.queryByHook("save-particle")).prop('disabled', true); - $(this.queryByHook("remove-particle")).prop('disabled', true); - }, - render: function (attrs, options) { - View.prototype.render.apply(this, arguments); - this.type = this.parent.domain.types.get(this.model.type, "typeID"); - if(this.newParticle) { - $(this.queryByHook("edit-particle-btns-"+this.viewIndex)).css("display", "none"); - }else{ - $(this.queryByHook("new-particle-btns-"+this.viewIndex)).css("display", "none"); - } - this.renderLocation(); - this.renderProperties(); - this.renderType(); - if(!this.newParticle && !this.parent.actPart.part) { - $(this.queryByHook("select-message-" + this.viewIndex)).css('display', 'block') - this.disableAll(); - } - }, - renderLocation: function () { - var xCoord = new InputView({parent: this, required: true, - name: 'x-coord', valueType: 'number', - value: this.model.point[0] || 0}); - app.registerRenderSubview(this, xCoord, "x-coord-" + this.viewIndex); - var yCoord = new InputView({parent: this, required: true, - name: 'y-coord', valueType: 'number', - value: this.model.point[1] || 0}); - app.registerRenderSubview(this, yCoord, "y-coord-" + this.viewIndex); - var zCoord = new InputView({parent: this, required: true, - name: 'z-coord', valueType: 'number', - value: this.model.point[2] || 0}); - app.registerRenderSubview(this, zCoord, "z-coord-" + this.viewIndex); - }, - renderProperties: function () { - if(this.massView) { - this.massView.remove(); - } - if(this.volView) { - this.volView.remove(); - } - if(this.nuView) { - this.nuView.remove(); - } - this.massView = new InputView({parent: this, required: true, - name: 'mass', valueType: 'number', - value: this.model.mass || this.type.mass}); - app.registerRenderSubview(this, this.massView, "mass-" + this.viewIndex); - this.volView = new InputView({parent: this, required: true, - name: 'volume', valueType: 'number', - value: this.model.volume || this.type.volume}); - app.registerRenderSubview(this, this.volView, "volume-" + this.viewIndex); - this.nuView = new InputView({parent: this, required: true, - name: 'viscosity', valueType: 'number', - value: this.model.nu || this.type.nu}); - app.registerRenderSubview(this, this.nuView, "nu-" + this.viewIndex); - let fixed = this.model.fixed || this.type.fixed; - $(this.queryByHook("fixed-" + this.viewIndex)).prop("checked", fixed); - }, - renderType: function () { - var typeView = new SelectView({ - label: '', - name: 'type', - required: true, - idAttribute: 'typeID', - textAttribute: 'name', - eagerValidate: true, - options: this.parent.domain.types, - value: this.parent.domain.types.get(this.model.type, "typeID") - }); - app.registerRenderSubview(this, typeView, "type-" + this.viewIndex) - }, - startAction: function (action) { - $(this.queryByHook("ep-complete")).css("display", "none"); - $(this.queryByHook("ep-action-in-progress")).text(action); - $(this.queryByHook("ep-in-progress")).css("display", "inline-block"); - console.log(action) - }, - update: function (e) {}, - updateValid: function (e) {} -}); \ No newline at end of file diff --git a/client/views/input.js b/client/views/input.js index e4531e0732..3e2ba786cc 100644 --- a/client/views/input.js +++ b/client/views/input.js @@ -24,6 +24,7 @@ module.exports = AmpersandInputView.extend({ label: 'string', modelKey: 'string', valueType: 'string', + disabled: 'boolean' }, events: { 'input input' : 'changeInputHandler', @@ -32,11 +33,12 @@ module.exports = AmpersandInputView.extend({ AmpersandInputView.prototype.initialize.apply(this, arguments); }, render: function () { + let disabled = this.disabled ? "disabled" : ""; if(this.label) { this.template = [ '