Skip to content

Commit

Permalink
Merge pull request #86 from MyersResearchGroup/part-annotation-fix
Browse files Browse the repository at this point in the history
SBOL annotation formatting fix
  • Loading branch information
cjmyers authored Jul 13, 2024
2 parents 4569fb2 + ee9ed07 commit 3c8f887
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 62 deletions.
3 changes: 1 addition & 2 deletions apps/web/src/components/SequenceSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ function Annotations({ colors }) {

const [sequencePartLibrariesSelected, setSequencePartLibrariesSelected] = useState([]);


const AnnotationCheckboxContainer = forwardRef((props, ref) => (
<div ref={ref} {...props}>
<AnnotationCheckbox {...props} />
Expand Down Expand Up @@ -310,8 +311,6 @@ function Annotations({ colors }) {
// mutate the libraries Selected in the store
mutateSequencePartLibrariesSelected(useStore.setState, state => {
if(chosenLibraries.some(item => item.value === 'local_libraries')) {
console.log(true)

state.sequencePartLibrariesSelected = chosenLibraries.filter(item => item.value !== 'local_libraries')
state.sequencePartLibrariesSelected.push(...localLibraries)
}
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/modules/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,31 +116,35 @@ export async function fetchAnnotateSequence({ sbolContent, selectedLibraryFileNa

// make a list of persistentIds to avoid
const originalAnnotations = originalDoc.rootComponentDefinitions[0].sequenceAnnotations
.map(sa => sa.persistentIdentity);
.map(sa => sa.persistentIdentity.slice(0, -2)) //synbict increments a number at the end of the persistent identities, so we cut off the last 2 chars to compare

let annotations = [];
const annDoc = new SBOL2GraphView(new Graph());

await Promise.all(annoLibsAssoc.map(([ sbolAnnotated, partLibrary ]) => {
return (async () => {
// create and load annotated doc
const annDoc = new SBOL2GraphView(new Graph());
// const annDoc = new SBOL2GraphView(new Graph());
await annDoc.loadString(sbolAnnotated);


// concatenate new annotations to result
annotations = annotations.concat(annDoc.rootComponentDefinitions[0].sequenceAnnotations
// filter annotations already in original document
.filter(sa => !originalAnnotations.includes(sa.persistentIdentity))
.filter(sa => !originalAnnotations.includes(sa.persistentIdentity.slice(0, -2)))
// just return the info we need
.map(sa => ({
name: sa.displayName,
id: sa.persistentIdentity,
location: [sa.rangeMin, sa.rangeMax],
componentInstance: sa.component,
featureLibrary: partLibrary,
enabled: true,
})));
})();
}));

return annotations;
return { fetchedAnnotations: annotations, synbictDoc: annDoc };
}

export async function fetchAnnotateText(text) {
Expand Down
100 changes: 75 additions & 25 deletions apps/web/src/modules/sbol.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { set } from "lodash"
import { Graph, S2ComponentDefinition, SBOL2GraphView } from "sbolgraph"
import { Graph, S2ComponentDefinition, S2ComponentInstance, SBOL2GraphView } from "sbolgraph"
import { TextBuffer } from "text-ranger"
import { mutateDocument, useAsyncLoader, useStore } from "./store"

Expand Down Expand Up @@ -123,7 +123,7 @@ Object.defineProperties(S2ComponentDefinition.prototype, {
* @return {SBOL2GraphView}
*/
export async function createSBOLDocument(sbolContent) {
const document = new SBOL2GraphView(new Graph());
let document = new SBOL2GraphView(new Graph());
await document.loadString(sbolContent);

// initialize rich description as regular description if one doesn't exist
Expand All @@ -135,56 +135,106 @@ export async function createSBOLDocument(sbolContent) {
}

/**
* Checks if the passed ComponentDefinition contains the sequence annotation specified
* by the passed annotation ID.
* Return the active status of any annotation in the sequenceAnnotations array
*
* @export
* @param {array} sequenceAnnotations
* @param {string} annotationId
* @return {boolean}
*/
export function hasSequenceAnnotation(sequenceAnnotations, annotationId) {
const anno = sequenceAnnotations.find((sa) => sa.id == annotationId)

return anno.enabled
}

/**
* Returns the index of an annotation with the specified id
*
* @export
* @param {array} sequenceAnnotations
* @param {string} annotationId
* @return {boolean}
*/
export function addSequenceAnnotation(sequenceAnnotations, annotationId) {
if (hasSequenceAnnotation(sequenceAnnotations, annotationId)) return

let annoIndex = sequenceAnnotations.findIndex((sa) => sa.id == annotationId)

return annoIndex
}

/**
* Returns the index of an annotation with the specified id
*
* @export
* @param {array} sequenceAnnotations
* @param {string} annotationId
* @return {boolean}
*/
export function removeSequenceAnnotation(sequenceAnnotations, annotationId) {
if (!hasSequenceAnnotation(sequenceAnnotations, annotationId)) return

let annoIndex = sequenceAnnotations.findIndex((sa) => sa.id == annotationId)

return annoIndex
}

/**
* Return the active status of any annotation in the sequenceAnnotations array
*
* @export
* @param {S2ComponentDefinition} componentDefinition
* @param {string} annotationId
* @return {boolean}
*/
export function hasSequenceAnnotation(componentDefinition, annotationId) {
export function hasSequenceAnnotationSBOL(componentDefinition, annotationId) {
return !!componentDefinition.sequenceAnnotations.find(sa => sa.persistentIdentity == annotationId)
}

/**
* Adds a sequence annotation with the information from the passed annotation object
* to the passed ComponentDefinition.
* Removes any duplicate annotation and its associated component instance from SBOL document
*
* @export
* @param {S2ComponentDefinition} componentDefinition
* @param {{
* id: string,
* name: string,
* location: number[],
* }} annoInfo
* @param {S2ComponentDefinition} componentDefinition
* @param {array} sequenceAnnotations
*/
export function addSequenceAnnotation(componentDefinition, annoInfo) {
if (hasSequenceAnnotation(componentDefinition, annoInfo.id))
export function removeDuplicateComponentAnnotation(componentDefinition, id) {
console.log(id)
if (!hasSequenceAnnotationSBOL(componentDefinition, id))
return

const sa = componentDefinition.annotateRange(annoInfo.location[0], annoInfo.location[1], annoInfo.name)
sa.persistentIdentity = annoInfo.id
sa.name = annoInfo.name

const annotation = componentDefinition.sequenceAnnotations.find(sa => sa.persistentIdentity == id)
const associatedComponent = annotation.component

annotation.destroy()
associatedComponent.destroy()
}

/**
* Removes the sequence annotation matching the passed annotation ID from the passed
* ComponentDefinition.
* Removes annotation, component instance, component definition, and associated sequence from SBOL document
*
* @export
* @param {S2ComponentDefinition} componentDefinition
* @param {{string}} id
* @param {S2ComponentDefinition} componentDefinition
* @param {array} sequenceAnnotations
*/
export function removeSequenceAnnotation(componentDefinition, { id }) {
if (!hasSequenceAnnotation(componentDefinition, id))
export function removeAnnotationWithDefinition(componentDefinition, id) {
console.log(id)
if (!hasSequenceAnnotationSBOL(componentDefinition, id))
return

const annotation = componentDefinition.sequenceAnnotations.find(sa => sa.persistentIdentity == id)
const associatedComponent = annotation.component
const definition = associatedComponent.definition
const sequences = definition.sequences

sequences[0].destroy()
annotation.destroy()
associatedComponent.destroy()
definition.destroy()
}


/**
* Finds existing SequenceAnnotations on a ComponentDefinition and returns
* them in a form suitable for the store.
Expand Down
86 changes: 55 additions & 31 deletions apps/web/src/modules/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _, { remove } from "lodash"
import create from "zustand"
import produce from "immer"
import { getSearchParams, showErrorNotification } from "./util"
import { addSequenceAnnotation, addTextAnnotation, createSBOLDocument, getExistingSequenceAnnotations, hasSequenceAnnotation, hasTextAnnotation, parseTextAnnotations, removeSequenceAnnotation, removeTextAnnotation } from "./sbol"
import { addSequenceAnnotation, addTextAnnotation, createSBOLDocument, getExistingSequenceAnnotations, hasSequenceAnnotation, hasTextAnnotation, parseTextAnnotations, removeAnnotationWithDefinition, removeDuplicateComponentAnnotation, removeSequenceAnnotation, removeTextAnnotation } from "./sbol"
import { fetchAnnotateSequence, fetchAnnotateText, fetchSBOL } from "./api"
import { SBOL2GraphView } from "sbolgraph"
import fileDownload from "js-file-download"
Expand Down Expand Up @@ -146,6 +146,26 @@ export const useStore = create((set, get) => ({
// }
// }),
exportDocument: (download = true) => {
const annotations = get().sequenceAnnotations

//remove duplicate annotation and component instance
for (const rootAnno of get().document.root.sequenceAnnotations) {
console.log(rootAnno.persistentIdentity)
for (const anno of annotations) {
if (rootAnno.persistentIdentity && (anno.id.slice(0, -2) === rootAnno.persistentIdentity.slice(0, -2) && rootAnno.persistentIdentity.slice(-1) >= 2)) { //potential bug: persistentIdentities may match but locations are different. The second instance will be removed if the part appears multiple times in the sequence
console.log("duplicate: " + rootAnno.persistentIdentity)
removeDuplicateComponentAnnotation(get().document.root, rootAnno.persistentIdentity)
}
}
}


//if disabled(in annos array but enabled=false), REMOVE: component & sequence annotation (children of root component definition), component definition, associated sequence
for (const anno of annotations) {
console.log(anno.id + " enabled=" + anno.enabled)
if (!anno.enabled) removeAnnotationWithDefinition(get().document.root, anno.id)
}

const xml = get().document.serializeXML();

if (download) {
Expand Down Expand Up @@ -188,11 +208,13 @@ export const useStore = create((set, get) => ({
set({ loadingSequenceAnnotations: true });

try {
const fetchedAnnotations = await fetchAnnotateSequence({
const result = await fetchAnnotateSequence({
sbolContent: get().document.serializeXML(),
selectedLibraryFileNames: get().sequencePartLibrariesSelected.map(lib => lib.value),
}) ?? [];

let { fetchedAnnotations, synbictDoc } = result;

set({
sequenceAnnotations: produce(get().sequenceAnnotations, draft => {
fetchedAnnotations.forEach(anno => {
Expand All @@ -202,7 +224,8 @@ export const useStore = create((set, get) => ({
}
});
}),
loadingSequenceAnnotations: false
loadingSequenceAnnotations: false,
document: synbictDoc,
});
} catch (err) {
showErrorNotification("Load Error", "Could not load sequence annotations");
Expand Down Expand Up @@ -230,34 +253,34 @@ export const useStore = create((set, get) => ({

sequenceAnnotationActions: createAnnotationActions(set, get, state => state.sequenceAnnotations, {
test: hasSequenceAnnotation,
add: async (...args) => {
addSequenceAnnotation(...args);

let xml = get().document.serializeXML();
let xmlChunks = [];
let matchData = xml.match(/\=\"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//= ]*)"/);

while (matchData) {
xmlChunks.push(xml.slice(0, matchData.index));
const uri = matchData[0];
const validURI = uri.replace(/ /g, '')
xmlChunks.push(validURI);
add: (...args) => {
const index = addSequenceAnnotation(...args);

xml = xml.slice(matchData.index + uri.length);
matchData = xml.match(/\=\"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//= ]*)"/);
}
xml = xmlChunks.concat(xml).join('');
const sequenceAnnotations = get().sequenceAnnotations;

try {
var document = await createSBOLDocument(xml);
} catch (err) {
console.error(err);
throw err;
}
set({ document });
set({
sequenceAnnotations: produce(get().sequenceAnnotations, draft => {
args[0].forEach(anno => {
if (!draft.find(a => a.id == anno.id)) {
draft.push(anno)
}
});
draft[index].enabled = true
}),
});
},
remove: (...args) => {
const index = removeSequenceAnnotation(...args);

set({
sequenceAnnotations: produce(get().sequenceAnnotations, draft => {
args[0].forEach(anno => {
if (!draft.find(a => a.id == anno.id)) {
draft.push(anno)
}
});
draft[index].enabled = false
}),
});
},
remove: removeSequenceAnnotation,
}),


Expand Down Expand Up @@ -470,10 +493,11 @@ function createAnnotationActions(set, get, selector, { test, add, remove } = {})

const getAnnotation = id => selector(get()).find(anno => anno.id == id)

const isActive = id => test(get().document.root, id)
const isActive = id => test(get().sequenceAnnotations, id)
const setActive = (id, value) => {
mutateDocument(set, state => {
(value ? add : remove)(state.document.root, getAnnotation(id))
// (value ? add : remove)(state.document.root, getAnnotation(id))
(value ? add : remove)(get().sequenceAnnotations, id)
})
}

Expand Down

0 comments on commit 3c8f887

Please sign in to comment.