elabftw/elabftw

View on GitHub
src/ts/ove.ts

Summary

Maintainability
D
2 days
Test Coverage
/**
 * @author Nicolas CARPi <nico-git@deltablot.email>
 * @author Marcel Bolten
 * @copyright 2012 Nicolas CARPi
 * @see https://www.elabftw.net Official website
 * @license AGPL-3.0
 * @package elabftw
 */
declare global {
  interface Window {
    /* eslint-disable-next-line */
    createVectorEditor: any;
  }
}

import '@teselagen/ove';
import '@teselagen/ove/style.css';
import { anyToJson } from '@teselagen/bio-parsers';
import { notif, reloadElements } from './misc';
import { Action, Model } from './interfaces';
import { Api } from './Apiv2.class';

// DISPLAY Plasmids FILES
export function displayPlasmidViewer(about: DOMStringMap): void {
  /* eslint-disable-next-line */
  const editor: any = {};
  const ApiC = new Api();
  Array.from(document.getElementsByClassName('viewer-ove')).forEach(el => {
    const oveDivDataset = (el as HTMLDivElement).dataset;
    const viewerID = el.id;
    const isSnapGeneFile = (new URL(oveDivDataset.href, window.location.origin)).searchParams.get('f').slice(-4) === '.dna';
    const filename = oveDivDataset.href;
    const realName = oveDivDataset.realName;

    // A Blob() is almost a File(): it's just missing two properties (lastModified and a name)
    // we also add the optional (mime) type attribute
    function blobToFile(theBlob: Blob, fileName: string): File {
      return new File([theBlob], fileName, { lastModified: new Date().getTime(), type: theBlob.type });
    }

    // Save a PNG image of the current plasmid map as a new attachment
    function savePlasmidMapAsImage(opts): void {
      const reader = new FileReader();
      reader.readAsDataURL(opts.pngFile);
      reader.onloadend = function(): void {
        const params = {
          'action': Action.CreateFromString,
          'file_type': 'png',
          'real_name': realName + '.png',
          'content': reader.result,
        };
        ApiC.post(`${about.type}/${about.id}/${Model.Upload}`, params).then(() => reloadElements(['uploadsDiv']));
      };
    }

    async function parseFile(fileContent): Promise<void> {
      const parsedData = await anyToJson(fileContent, {
        fileName: realName,
        guessIfProtein: true,
      });
      // we always return an array of results because some files my contain multiple sequences
      // parsedData[0].success //either true or false
      // parsedData[0].messages //either an array of strings giving any warnings or errors generated during the parsing process
      // Test if fileContent was parsed successfully. if false: show notification
      if (parsedData.length === 0) {
        throw 'Problem with file: ' + realName;
      }

      if (parsedData[0].success === false) {
        const msg = 'Invalid DNA data in file ' + realName;
        notif({res: false, msg: msg});
        throw msg;
      }

      const parsedSequence = parsedData[0].parsedSequence;

      /* eslint-disable-next-line */
      const data: any = {};
      const convertToFeaturedDNASequence = function(openVESequence): void {
        data.sequenceData = {
          features: [],
          sequence: openVESequence.sequence,
        };
        data.registryData = {
          name: openVESequence.name,
        };
        const featureMap = {};

        for (const prop in openVESequence.features) {
          if (!Object.prototype.hasOwnProperty.call(openVESequence.features, prop))
            continue;

          const feature = openVESequence.features[prop];
          const existingFeature = featureMap[feature.id];
          if (existingFeature) {
            existingFeature.locations.push({
              genbankStart: feature.start + 1,
              end: feature.end + 1,
            });
          } else {
            featureMap[feature.id] = {
              id: feature.fid,
              type: feature.type,
              name: feature.name,
              forward: feature.forward,
              notes: [{
                name: 'note',
                value: feature.notes,
              }],
              start: feature.start,
              end: feature.end,
            };
          }
        }
        for (const property in featureMap) {
          if (!Object.prototype.hasOwnProperty.call(featureMap, property))
            continue;
          data.sequenceData.features.push(featureMap[property]);
        }
      };

      const editorProps = {
        editorName: viewerID,
        withPreviewMode: true,
        isFullscreen: false,
        showMenuBar: false,
        withRotateCircularView: false,
        showReadOnly: false,
        disableSetReadOnly: true,
        showGCContentByDefault: true,
        alwaysAllowSave: true,
        generatePng: true,
        handleFullscreenClose: function(): void { // event could be used as parameter
          editor[viewerID].close();
          reloadElements(['uploadsDiv']);
        },
        onCopy: function(event, copiedSequenceData, editorState): void {
          // the copiedSequenceData is the subset of the sequence that has been copied in the teselagen sequence format
          const clipboardData = event.clipboardData;
          clipboardData.setData('text/plain', copiedSequenceData.textToCopy);
          data.selection = editorState.selectionLayer;
          data.openVECopied = copiedSequenceData;
          convertToFeaturedDNASequence(editorState.sequenceData);
          clipboardData.setData(
            'application/json',
            JSON.stringify(data));

          event.preventDefault();
          // in onPaste in your app you can do:
          // e.clipboardData.getData('application/json')
        },
        // repurposed to save an image as new attachment of the current plasmid map
        /* eslint-disable-next-line */
        onSave: function(opts = {} as any): void { // , sequenceDataToSave, editorState, onSuccessCallback could be used as parameter
          savePlasmidMapAsImage(opts);
        },
        allowMultipleFeatureDirections: true,
        PropertiesProps: {
          // the list of tabs shown in the Properties panel
          propertiesList: [
            'general',
            'features',
            'parts',
            'primers',
            'translations',
            'cutsites',
            'orfs',
            'genbank',
          ],
        },
        ToolBarProps: {
          toolList: [
            {
              name: 'saveTool',
              tooltip: 'Save image of plasmid map',
            },
            'downloadTool',
            //'importTool',
            //'undoTool',
            //'redoTool',
            'cutsiteTool',
            'featureTool',
            'partTool',
            //'alignmentTool',
            'oligoTool', // Primers
            'orfTool',
            //'editTool',
            'findTool',
            //'visibilityTool'
          ],
        },
        StatusBarProps: {
          showCircularity: false,
          showReadOnly: true,
          showAvailability: false,
        },
      };

      editor[viewerID] = window.createVectorEditor(document.getElementById(viewerID), editorProps);

      const editorState = {
        // note, sequence data passed here will be coerced to fit the Teselagen data model
        readOnly: true,
        // Open Vector Editor data model
        sequenceData: parsedSequence,
        // clear the sequenceDataHistory if there is any left over from a previous sequence
        sequenceDataHistory: {},
        annotationVisibility: {
          features: true,
        },
        panelsShown: [
          [{
            id: 'circular',
            name: 'Plasmid Map',
            active: true,
          }, {
            id: 'rail',
            name: 'Linear Map',
            active: false,
          }],
          [{
            id: 'sequence',
            name: 'Linear Sequence Map',
            active: true,
          }, {
            id: 'properties',
            name: 'Properties',
            active: false,
          }],
        ],
      };

      // Change layout for linear sequences
      if (!parsedSequence.circular) {
        editorState.panelsShown[0][1].active = true;
        editorState.panelsShown[0].shift();
      }

      editor[viewerID].updateEditor(editorState);

      // change button text from 'Open Editor' to 'Open Viewer'
      const oveButton = document.getElementById(viewerID).firstChild.firstChild.firstChild.firstChild.firstChild.firstChild as HTMLElement;
      oveButton.innerText = 'Open Viewer';
    }

    // load DNA data either as File (.dna files Snapgene) or as String
    fetch(filename).then(response => {
      if (response.ok) {
        if (isSnapGeneFile) {
          return response.blob().then(blob => parseFile(blobToFile(blob, realName)));
        }

        return response.text().then(fileContent => parseFile(fileContent));
      }

      return Promise.reject(response.status);
    }).catch(error => console.error(error));
  });
}