Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 91 additions & 110 deletions SBOLCanvasBackend/src/utils/MxToSBML.java

Large diffs are not rendered by default.

62 changes: 46 additions & 16 deletions SBOLCanvasBackend/src/utils/MxToSBOL.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,23 @@ public SBOLDocument setupDocument(InputStream graphStream) throws IOException, U
.toArray(mxCell[]::new);

for(mxCell glyph: glyphs){
createComponentDefinition(document, graph, model, glyph);
try {
createComponentDefinition(document, graph, model, glyph);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped glyph: " + e.getMessage());
}
}

if (layoutHelper.getGraphicalLayout(URI.create((String) circuitContainer.getValue())) != null){
continue;
}

// Create Component Definition for the container itself
createComponentDefinition(document, graph, model, circuitContainer);
try {
createComponentDefinition(document, graph, model, circuitContainer);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped container: " + e.getMessage());
}
}
}

Expand All @@ -190,12 +198,15 @@ public SBOLDocument setupDocument(InputStream graphStream) throws IOException, U
mxCell[] molecularSpecies = Arrays.stream(mxGraphModel.filterCells(viewChildren, molecularSpeciesFilter))
.toArray(mxCell[]::new);

if (viewCell.getStyle().equals(STYLE_MODULE_VIEW) || circuitContainers.length > 1 || molecularSpecies.length > 0) {
// module definitions
createModuleDefinition(document, graph, model, viewCell);
} else {
// component definitions
attachTextBoxAnnotation(model, viewCell, URI.create(viewCell.getId()));
try {
if (STYLE_MODULE_VIEW.equals(viewCell.getStyle()) || circuitContainers.length > 1 || molecularSpecies.length > 0) {
createModuleDefinition(document, graph, model, viewCell);
} else {
// component definitions
attachTextBoxAnnotation(model, viewCell, URI.create(viewCell.getId()));
}
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped view cell: " + e.getMessage());
}
}

Expand All @@ -209,7 +220,11 @@ public SBOLDocument setupDocument(InputStream graphStream) throws IOException, U
for (mxCell cell : cells) {
if (handledContainers.contains((String) cell.getValue()))
continue;
linkComponentDefinition(document, graph, model, cell);
try {
linkComponentDefinition(document, graph, model, cell);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped link: " + e.getMessage());
}
handledContainers.add((String) cell.getValue());
}
}
Expand All @@ -221,24 +236,39 @@ public SBOLDocument setupDocument(InputStream graphStream) throws IOException, U
.toArray(mxCell[]::new);
mxCell[] molecularSpecies = Arrays.stream(mxGraphModel.filterCells(viewChildren, molecularSpeciesFilter))
.toArray(mxCell[]::new);
if (viewCell.getStyle().equals(STYLE_MODULE_VIEW) || circuitContainers.length > 1 || molecularSpecies.length > 0) {
// module definitions
linkModuleDefinition(document, graph, model, viewCell);
if (STYLE_MODULE_VIEW.equals(viewCell.getStyle()) || circuitContainers.length > 1 || molecularSpecies.length > 0) {
try {
linkModuleDefinition(document, graph, model, viewCell);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped link: " + e.getMessage());
}
}
}

// create the combinatorials
for (CombinatorialInfo info : combinatorialDict.values()) {
createCombinatorial(document, graph, model, info);
try {
createCombinatorial(document, graph, model, info);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped combinatorial: " + e.getMessage());
}
}

// link the combinatorials
for (CombinatorialInfo info : combinatorialDict.values()) {
linkCombinatorial(document, graph, model, info);
try {
linkCombinatorial(document, graph, model, info);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped combinatorial link: " + e.getMessage());
}
}

// write events as GenericTopLevel objects
writeEvents(document, graph);
try {
writeEvents(document, graph);
} catch (Exception e) {
System.err.println("Warning: SBOL export skipped events: " + e.getMessage());
}

return document;
}
Expand Down Expand Up @@ -370,7 +400,7 @@ private void createComponentDefinition(SBOLDocument document, mxGraph graph, mxG
// store extra mxGraph information
URI identity = URI.create(glyphInfo.getFullURI());
layoutHelper.createGraphicalLayout(identity, glyphInfo.getDisplayID() + "_Layout");
if(circuitContainer.getStyle().equals(STYLE_CIRCUIT_CONTAINER)){
if(STYLE_CIRCUIT_CONTAINER.equals(circuitContainer.getStyle())){

Object[] containerChildren = mxGraphModel.getChildCells(model, circuitContainer, true, false);
mxCell backboneCell = (mxCell) mxGraphModel.filterCells(containerChildren, backboneFilter)[0];
Expand Down
4 changes: 2 additions & 2 deletions SBOLCanvasFrontend/src/app/graph-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2073,7 +2073,7 @@ export class GraphHelpers extends GraphBase {
this.graph.getModel().execute(new GraphEdits.infoEdit(cell0, eventInfo, null, GraphBase.EVENT_DICT_INDEX))
}

protected getFromEventDict(eventURI: string): EventInfo {
public getFromEventDict(eventURI: string): EventInfo {
const cell0 = this.graph.getModel().getCell(0)
if (!cell0.value[GraphBase.EVENT_DICT_INDEX]) {
return null
Expand Down Expand Up @@ -2142,7 +2142,7 @@ export class GraphHelpers extends GraphBase {
this.graph.getModel().execute(new GraphEdits.infoEdit(cell0, info, null, GraphBase.INTERACTION_DICT_INDEX))
}

protected getFromInteractionDict(interactionURI: string): InteractionInfo {
public getFromInteractionDict(interactionURI: string): InteractionInfo {
const cell0 = this.graph.getModel().getCell(0)
return cell0.value[GraphBase.INTERACTION_DICT_INDEX][interactionURI]
}
Expand Down
12 changes: 2 additions & 10 deletions SBOLCanvasFrontend/src/app/graph.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,14 @@ export class GraphService extends GraphHelpers {
},
error: err => {
console.error('[GraphService] SBOL export failed:', err)
const message = typeof err.error === 'string' ? err.error : err.message || 'SBOL export failed'
embeddedService.postMessage({
error: { type: 'sbol-export', message: message }
})
}
})
}
})

// SBML auto-export pipeline (2000ms debounce)
// SBML auto-export pipeline (1000ms debounce)
modelChange$
.pipe(debounceTime(2000))
.pipe(debounceTime(1000))
.subscribe(graphXml => {
if (embeddedService.isAppEmbedded()) {
console.debug('[GraphService] Model changed. Sending SBML to parent.')
Expand All @@ -91,10 +87,6 @@ export class GraphService extends GraphHelpers {
},
error: err => {
console.error('[GraphService] SBML export failed:', err)
const message = typeof err.error === 'string' ? err.error : err.message || 'SBML export failed'
embeddedService.postMessage({
error: { type: 'sbml-export', message: message }
})
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion SBOLCanvasFrontend/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class HomeComponent implements OnInit, ComponentCanDeactivate {
leftBarOpened = true;

constructor(private graphService: GraphService, private titleService: Title, private embeddedService: EmbeddedService) {
this.titleService.setTitle('SBOL Canvas');
this.titleService.setTitle('SBOLCanvas');
}

ngOnInit() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
Biologists often face the challenge of effectively communicating DNA sequences and their behaviors. An increasingly popular solution is SBOL, the Synthetic Biology Open Language. It is a diagram syntax that models genetic systems, allowing for both abstract visualizations and detailed data. Unfortunately, the complexity of creating SBOL documents is limiting this powerful language’s growth. SBOL, while characterized by graphic models, is lacking tools for graphical editing. Until now.
</p>
<p class="about-text">
SBOL Canvas is an open source web-based graphical editor that opens synthetic biology design to anyone from students to advanced researchers. Users will enjoy a simple interface for creating diagrams that are both artistic and scientific. By interfacing with existing SBOL databases, SBOL Canvas lets users share their designs and easily reference the research of others. This editor significantly lowers SBOL’s barrier to entry, helping it carry synthetic biology to new heights.
SBOLCanvas is an open source web-based graphical editor that opens synthetic biology design to anyone from students to advanced researchers. Users will enjoy a simple interface for creating diagrams that are both artistic and scientific. By interfacing with existing SBOL databases, SBOLCanvas lets users share their designs and easily reference the research of others. This editor significantly lowers SBOL’s barrier to entry, helping it carry synthetic biology to new heights.
</p>
<p class="about-text">
SBOL Canvas is supported in part by the <a href="https://sbolstandard.org/sbol-industrial/" target="_blank">SBOL Industrial Consortium</a>.
SBOLCanvas is supported in part by the <a href="https://sbolstandard.org/sbol-industrial/" target="_blank">SBOL Industrial Consortium</a>.
It is currently being developed by those in the Genetic Logic Lab led by Dr. Chris Myers and is part of the Synthetic Biology Data Exchange Group.
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class LandingPageComponent implements OnInit {
version = "Development";

constructor(private titleService: Title) {
this.titleService.setTitle("SBOL Canvas About");
this.titleService.setTitle("SBOLCanvas About");
this.hash = versions.revision;
this.version = versions.version;
}
Expand Down
117 changes: 113 additions & 4 deletions SBOLCanvasFrontend/src/app/problems/problems.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component } from '@angular/core'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { GraphService } from '../graph.service'


Expand All @@ -8,18 +8,23 @@ import { GraphService } from '../graph.service'
styleUrls: ['./problems.component.css']
})

export class ProblemsComponent {
export class ProblemsComponent implements OnInit, OnDestroy {

warnings: string[]
errors: string[]
private intervalId: number

constructor(private graphService: GraphService) { }

ngOnInit() {
this.warnings = []
this.errors = []

setInterval(this.validate.bind(this), 750)
this.intervalId = window.setInterval(this.validate.bind(this), 750)
}

ngOnDestroy() {
clearInterval(this.intervalId)
}

validate() {
Expand All @@ -29,6 +34,8 @@ export class ProblemsComponent {
// Validation functions
this.validateCurrentView(warnings, errors)
this.validateComponents(warnings, errors)
this.validateBackbones(warnings)
this.validateInteractionsAndEvents(warnings)
// more here...

// Transpose to separate errors and warnings
Expand All @@ -37,7 +44,6 @@ export class ProblemsComponent {
}

validateCurrentView(warnings: string[], errors: string[]) {

const currentView = this.graphService.getCurrentRoot()
const children = currentView.children || []

Expand All @@ -63,6 +69,7 @@ export class ProblemsComponent {

validateComponent(component, warnings: string[], errors: string[]) {
const info = this.graphService.lookupInfo(component.value)
if (!info) return
const sequence = (info.sequence || '').toUpperCase()
const version = (info.version || '')

Expand Down Expand Up @@ -92,4 +99,106 @@ export class ProblemsComponent {
version && !compliant &&
warnings.push(`Component '${info.displayID}' has incompliant version: ${version}`)
}

validateBackbones(warnings: string[]) {
const currentView = this.graphService.getCurrentRoot()
const containers = (currentView.children || []).filter(c => c.isCircuitContainer())

for (const container of containers) {
const children = container.children || []
const glyphs = children.filter(c => c.isSequenceFeatureGlyph && c.isSequenceFeatureGlyph())

if (glyphs.length > 0 && !glyphs.some(g => {
const info = this.graphService.lookupInfo(g.value)
return info && info.partRole && info.partRole.includes('Promoter')
})) {
const containerInfo = this.graphService.lookupInfo(container.value)
const name = (containerInfo && containerInfo.displayID) || 'unnamed'
warnings.push(`Backbone '${name}': no promoter (skipped in SBML)`)
}
}
}

validateInteractionsAndEvents(warnings: string[]) {
const currentView = this.graphService.getCurrentRoot()
const directChildren = currentView.children || []
const nestedChildren = directChildren.map(c => c.children || []).flat()
const allCells = [...directChildren, ...nestedChildren]

// Disconnected interaction edges
const interactions = allCells.filter(c => c.isInteraction && c.isInteraction())
const regulationByTarget: { [key: string]: { inhibition: boolean, stimulation: boolean } } = {}

for (const edge of interactions) {
const info = this.graphService.getFromInteractionDict(edge.value)
if (!info) continue

const type = info.interactionType
const isDegradation = type === 'Degradation'

// Degradation edges naturally have no target (species degrades into nothing)
if (!edge.source || (!edge.target && !isDegradation)) {
const connectedEnd = edge.source || edge.target
const endInfo = connectedEnd ? this.graphService.lookupInfo(connectedEnd.value) : null
const endName = (endInfo && (endInfo.name || endInfo.displayID)) || 'unknown'
const missing = !edge.source ? 'no source' : 'no target'
warnings.push(`${type} edge on '${endName}': ${missing}`)
continue
}
if (type === 'Inhibition' || type === 'Stimulation') {
const targetId = edge.target.value || 'unknown'
if (!regulationByTarget[targetId])
regulationByTarget[targetId] = { inhibition: false, stimulation: false }
if (type === 'Inhibition') regulationByTarget[targetId].inhibition = true
if (type === 'Stimulation') regulationByTarget[targetId].stimulation = true
}
}

for (const [targetId, reg] of Object.entries(regulationByTarget)) {
if (reg.inhibition && reg.stimulation) {
const info = this.graphService.lookupInfo(targetId)
const name = (info && info.name) || (info && info.displayID) || 'unknown'
warnings.push(`Promoter '${name}': mixed regulation (skipped in SBML)`)
}
}

// Complex formation nodes
const interactionNodes = allCells.filter(c => c.isInteractionNode && c.isInteractionNode())
for (const node of interactionNodes) {
const info = this.graphService.getFromInteractionDict(node.value)
if (!info) continue
const type = info.interactionType
if (type !== 'Biochemical Reaction' && type !== 'Non-Covalent Binding') continue

const edges = this.graphService.graph.getModel().getEdges(node) || []
const incoming = edges.filter(e => e.target === node)
const outgoing = edges.filter(e => e.source === node)

const reactantNames = incoming
.filter(e => e.source)
.map(e => {
const ri = this.graphService.lookupInfo(e.source.value)
return (ri && (ri.name || ri.displayID)) || '?'
})
.join(', ') || 'none'

if (outgoing.length === 0 || !outgoing[0].target)
warnings.push(`Complex formation (${reactantNames}): no product connection`)

for (const inEdge of incoming) {
if (!inEdge.source)
warnings.push(`Complex formation (${reactantNames}): disconnected reactant edge`)
}
}

// Events without target species
const events = allCells.filter(c => c.isEvent && c.isEvent())
for (const event of events) {
const eventInfo = this.graphService.getFromEventDict(event.value)
if (!eventInfo) continue
const targetSpecies = (eventInfo.simulationData || {})['targetSpecies']
if (!targetSpecies)
warnings.push(`Event '${eventInfo.displayID || 'unnamed'}': no target species`)
}
}
}
4 changes: 2 additions & 2 deletions SBOLCanvasFrontend/src/app/toolbar/toolbar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@
<!-- Help menu-->
<button mat-stroked-button class="toolbar-row-button" [matMenuTriggerFor]="helpMenu">Help</button>
<mat-menu #helpMenu="matMenu">
<button mat-menu-item class="toolbar-row-button" [matTooltip]="'Help using SBOL Canvas'"
<button mat-menu-item class="toolbar-row-button" [matTooltip]="'Help using SBOLCanvas'"
onClick="window.open('./tutorial')">Tutorial</button>

<button mat-menu-item class="toolbar-row-button" [matTooltip]="'About SBOL Canavs and its creators'"
<button mat-menu-item class="toolbar-row-button" [matTooltip]="'About SBOLCanvas and its creators'"
onClick="window.open('./about')">About</button>

<button mat-menu-item class="toolbar-row-button" [matTooltip]="'Links to Github issue tracker.'"
Expand Down
Loading
Loading