Draw.loadPlugin(function(ui) {

  const model = ui.editor.graph.model;

  const defaultConfig = {
    // When making an activity wider, the x-position of ports will be scaled proportionally to the new width, and vice-versa with height and y-position. This may be annoying when simply resizing an activity to add an extra port.
    movePortsOnResizeActivity: false,
    // The new algorithm probably gives better results under all circumstances.
    newPortSnappingAlgorithm: true,

    // Edge styles.
    edgeStyleCtrlFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;strokeWidth=2;fontSize=14;strokeColor=#004C99;jumpStyle=gap;",
    edgeStyleDataFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=open;rounded=0;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;fillColor=#d5e8d4;strokeColor=#6D9656;endFill=0;jumpStyle=gap;",
    edgeStyleTypedBy: "edgeStyle=0;endArrow=blockThin;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;dashed=1;endFill=1;endSize=6;strokeColor=#999999;",
    edgeStyleParentVersion: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;fontSize=11;fontColor=#000000;fillColor=#fff2cc;strokeColor=#d6b656;rounded=0;jumpStyle=gap;",
    edgeStyleSSLink: "edgeStyle=0;endArrow=blockThin;html=1;fontSize=11;strokeColor=#694B2E;rounded=0;dashed=1;dashPattern=1 4;endFill=1;",
    edgeStyleComment: "edgeStyle=0;endArrow=none;dashed=1;html=1;",


    portLabelOffsetEdgeDirection: 22,
    portLabelOffsetPerpendicularToEdgeDirection: 24,
  };

  let currentConfig;

  const existingConfig = window.localStorage.getItem('ftgpmConfig');
  if (existingConfig === null) { // unlike Object and Map, localStorage returns 'null' when item doesn't exist...
    currentConfig = {};
  } else {
    console.log("Have existing ftgpm config...", existingConfig)
    currentConfig = JSON.parse(existingConfig);
  }

  // Configuration dialog window:
  function createFtgpmConfigWindow() { // only called once - this is only a function in order not to pollute namespace

    // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
    // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
    const wndDiv = document.createElement('div');
      wndDiv.style.userSelect = 'none';
      wndDiv.style.overflow = 'hidden';
      wndDiv.style.padding = '10px';
      wndDiv.style.height = '100%';
    const wnd = new mxWindow("FTG+PM Plugin Configuration",
      wndDiv, 100, 100, 640, 570, true, true);
      wnd.destroyOnClose = false;
      wnd.setMaximizable(false);
      wnd.setResizable(false);
      wnd.setClosable(true);
      wnd.addListener('show', mxUtils.bind(this, function() {
        statusText.innerHTML = "";
      }));

    function createConfig(labelText, json, readonly) {
      const columnDiv = document.createElement('div');
      columnDiv.style.display = 'inline-block';

      const label = document.createElement('label');
      const textarea = document.createElement('textarea');
      label.setAttribute('for', textarea);
      label.innerHTML = labelText;
      textarea.setAttribute('wrap', 'off');
      textarea.setAttribute('spellcheck', 'false');
      textarea.setAttribute('autocorrect', 'off');
      textarea.setAttribute('autocomplete', 'off');
      textarea.setAttribute('autocapitalize', 'off');
      textarea.setAttribute('wrap', 'hard');
      if (readonly) {
        textarea.setAttribute('readonly', 'true');
      }
      textarea.value = JSON.stringify(json, null, 2);
      textarea.style.overflow = 'auto';
      textarea.style.resize = 'none';
      textarea.style.width = '300px';
      textarea.style.height = '360px';
      textarea.style.marginBottom = '16px';

      columnDiv.appendChild(label);
      mxUtils.br(columnDiv);
      columnDiv.appendChild(textarea);
      return [columnDiv, textarea];
    }

    const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
    const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
    wndDiv.appendChild(defaultColumn);
    wndDiv.appendChild(customColumn);

    mxUtils.br(wndDiv);

    const buttonsDiv = document.createElement('div');
    buttonsDiv.style.textAlign = 'right';
    const statusText = document.createElement('div');
    statusText.style.display = 'inline-block';

    // attempts to save user configuration and displays status text as a side effect
    // returns true if all went well
    function saveConfigurationAndDisplayStatus() {
      let parsed;
      try {
        parsed = JSON.parse(customTextArea.value); // may throw
      } catch (parseErr) {
        statusText.innerHTML = "Parse error: " + parseErr.toString();
        statusText.style.color = 'red';
        return false;
      }
      if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
        statusText.innerHTML = "JSON value is not an object";
        statusText.style.color = 'red';
        return false;
      }
      currentConfig = parsed;
      window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
      customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
      statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)";
        statusText.style.color = 'green';
      return true;
    }

    const applyButton =  mxUtils.button("Apply", saveConfigurationAndDisplayStatus);
    applyButton.className = 'geBtn gePrimaryBtn';
    applyButton.style.float = 'none'; // override geBtn style

    const resetButton = mxUtils.button("Reset", function() {
      customTextArea.value = JSON.stringify(currentConfig, null, 2);
    });
    resetButton.className = 'geBtn';
    resetButton.style.float = 'none'; // override geBtn style

    buttonsDiv.appendChild(statusText);
    buttonsDiv.appendChild(applyButton);
    buttonsDiv.appendChild(resetButton);

    // hacks buttons:
    const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
      if (saveConfigurationAndDisplayStatus()) {
        model.beginUpdate();
        for (const [cellId, cell] of Object.entries(model.cells)) {
          applyCellStyleFromConfig(cell);
        }
        model.endUpdate();
      }
    });
    const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
      if (saveConfigurationAndDisplayStatus()) {
        model.beginUpdate();
        for (const [cellId, cell] of Object.entries(model.cells)) {
          if (cell.geometry) {
            // we'll force a geometry update:
            const newGeometry = cell.geometry.clone();
            snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
            model.setGeometry(cell, newGeometry);
          }
        }
        model.endUpdate();
      }
    })
    forceEdgeStyleButton.className = 'geBtn';
    forceEdgeStyleButton.style.float = 'none'; // override geBtn style
    forcePortSnapButton.className = 'geBtn';
    forcePortSnapButton.style.float = 'none'; // override geBtn style

    hacksDiv = document.createElement('div');
    hacksDiv.appendChild(document.createTextNode("Hacks: (use undo if you fuck things up)"));
    mxUtils.br(hacksDiv);
    hacksDiv.appendChild(forceEdgeStyleButton);
    mxUtils.br(hacksDiv);
    hacksDiv.appendChild(forcePortSnapButton);

    wndDiv.appendChild(buttonsDiv);
    mxUtils.br(wndDiv);
    wndDiv.appendChild(hacksDiv);

    return wnd;
  }

  const ftgpmConfigWindow = createFtgpmConfigWindow();

  function getFromConfig(parameter) {
    if (currentConfig.hasOwnProperty(parameter)) {
      return currentConfig[parameter];
    } else {
      return defaultConfig[parameter];
    }
  }


  // // Force style window:
  // const forceStyleDiv = document.createElement('div');

  // const forceStyleWindow = new mxWindow("FTG+PM: Force Style Rules",
  //   forceStyleDiv, 200, 200, 400, 90, true, true);
  // forceStyleWindow.destroyOnClose = false;
  // forceStyleWindow.setMaximizable(false);
  // forceStyleWindow.setResizable(false);
  // forceStyleWindow.setClosable(true);

  // const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
  //   for (const [cellId, cell] of Object.entries(model.cells)) {
  //     applyCellStyleFromConfig(cell);
  //   }
  // });
  // const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
  //   for (const [cellId, cell] of Object.entries(model.cells)) {
  //     if (cell.geometry) {
  //       // we'll force a geometry update:
  //       const newGeometry = cell.geometry.clone();
  //       snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
  //       model.setGeometry(cell, newGeometry);
  //     }
  //   }
  // })
  // forceEdgeStyleButton.className = 'geBtn';
  // forcePortSnapButton.className = 'geBtn';
  // forceStyleDiv.appendChild(forceEdgeStyleButton);
  // mxUtils.br(forceStyleDiv);
  // forceStyleDiv.appendChild(forcePortSnapButton);

  window.onkeyup = function(e) {
    // Shortcut to show FTG+PM config:
    if (e.ctrlKey && e.key === '.') {
      ftgpmConfigWindow.show();
    }
    // // Shortcut to apply style from config to all edges:
    // if (e.ctrlKey && e.key === 'Enter') {
    //   forceStyleWindow.show();
    // }
  }


  // For auto-updating port orientation
  // Note: this is only the 'shape', not the full style
  const encodedPortShapes = {
    in: {
      l: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JRHBAX/++Y0c3zXC2btzO+4wHfFEm81CqBpjgJZM7JoRc4wPHO42C06hCA0Wk7Ka8VkcDREL0roK7PsXOoG0JXseWyj3jW9Y6OJN54axFiXY2jMiAo0xpi2v5g2Td9s9uWnWna3CHGiJ4yjNKmTgsFmf/iGWOSep1ZH5URXXx7mpP01VJdnYevoAeN6rtdwJ6XLsbJFuba6cXGG0Hgg9Dtun7+UUydshFhsK4ADNXiXmiCWxQG0NfY+rKJh2/U/oD3sEL)`,
      r: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JQnBAVv++o0eXH9ls3bid9xkP8qJMpiFXJTDBcyZ3TAi5xgeOdxoFp1GFErJIWaW8VkcDREL07gp3fYqtQdscvI41lXvGt6x2cCbTzFmLEu1sGJAeR5nSFtfyB8na7Z/ttGrfrsQdCojgKU8oZeKwWJz8I5YpJnPHkelRZdeLdzd7mq6aZWfn4QPocKnqfiegw4Wr4H24cW3f6ukMRtueYShINl0/vzhGErlIkRkX4MtdYj5TBVaojaHPce7OJiU3Kf0CTfAC)`,
      t: `stencil(nZPRDoIgGIWfhtuGsNZ1s3oP0r9kIjggrbfv1183S23VDds5Z3zAAZhMQ6FqYIIXTB6YEILjgLIlKbckVaghi+Q1ymt1NkBJiN6V0Oo8DgRtC/A6dqk8Mr5nHZIzmWbOWoRoZ8NLMskRprTFufxOML4ZNvAYNKkaV6gggic3IZeJ09/g5BewTNFZO45Mzyorr97dbD6ftZpdnIeFYIxr1fU7C8a4cg1MDrfU2XI7I8BoOwHIxdZ/ISS7t4K/g2TGBfhwleivNIENamPoNa5d2azj3qUf0BtP)`,
      b: `stencil(nZNRD4IgFIV/Da8NYa3nZvU/EG/JQnBAWv++q1c3m9pWL27nnPHJPQCTeaxUA0zwiskTE0Jw/KDsSMo9SRUb0Im8VgWjCguUxBT8HTpTppFgXAXBpD6VZ8aPrEdyJnPtnUOI8S5+JLMcYco4XMufBOO7cQOvUZNq8A81JAjkZuQycfkbnP0Cljk6W+PIvFD6fgv+4crlqs3s6gOsBFPcqL7fRTDFtW9hNtzHaJNcr2ciWONmhOywXvwvDLn/Yxva+ghfzhL9jSqwQmMtXcetM1uUPLj0BAbjDQ==)`,
    }
  };
  encodedPortShapes.out = {
    r: encodedPortShapes.in.l,
    l: encodedPortShapes.in.r,
    t: encodedPortShapes.in.b,
    b: encodedPortShapes.in.t,
  }

  // Styles for our different edge types
  // Mapping from 'pmRole' attribute to key in config.
  const edgeStyles = {
    ctrl_flow: 'edgeStyleCtrlFlow',
    data_flow: 'edgeStyleDataFlow',
    typed_by: 'edgeStyleTypedBy',
    parent_version: 'edgeStyleParentVersion',
    comment_edge: 'edgeStyleComment',
  };

  function applyCellStyleFromConfig(cell) {
    const ftgpmType = cell.getAttribute(TYPE_ATTR);
    if (ftgpmType) {
      if (edgeStyles.hasOwnProperty(ftgpmType)) {
        setEdgeStyle(cell, ftgpmType);
      }
      // TODO: do the same on nodes.
    }
  }


  const TYPE_ATTR = "pmRole"; // cell attribute holding the ftgpm type

  function parseStyle(str) {
    const pairs = str.split(';').filter(s => s !== '');
    const map = new Map();
    pairs.map(pair => {
      const [key,value] = pair.split('=');
      map.set(key, value);
    });
    return map;
  }

  function unparseStyle(map) {
    let str = "";
    for (const [key, value] of map.entries()) {
      if (value === undefined) {
        str += key + ';';
      } else {
        str += key + '=' + value + ';';
      }
    }
    return str;
  }

  // copy selected keys (=array) from fromMap to toMap
  function mapAssign(toMap, fromMap, keys) {
    for (const key of keys) {
      const value = fromMap.get(key);
      if (value !== undefined) {
        toMap.set(key, value);
      }
    }
  }

  function reverseEdge(edge, sourceCell, targetCell) {
    // Reverse source and target
    ui.editor.graph.model.setTerminals(edge, targetCell, sourceCell);

    // Reverse 'waypoints'
    if (edge.geometry.points) {
      const newGeometry = edge.geometry.clone();
      newGeometry.points.reverse();
      ui.editor.graph.model.setGeometry(edge, newGeometry);
    }

    // Reverse entry and exit points
    const oldStyle = parseStyle(model.getStyle(edge));
    const newStyle = new Map(oldStyle);
    // entry becomes exit
    newStyle.set('entryX', oldStyle.get('exitX'));
    newStyle.set('entryY', oldStyle.get('exitY'));
    // exit becomes entry
    newStyle.set('exitX', oldStyle.get('entryX'));
    newStyle.set('exitY', oldStyle.get('entryY'));
    // entryD becomes exitD
    newStyle.set('entryDx', oldStyle.get('exitDx'));
    newStyle.set('entryDy', oldStyle.get('exitDy'));
    // exitD becomes entryD
    newStyle.set('exitDx', oldStyle.get('entryDx'));
    newStyle.set('exitDy', oldStyle.get('entryDy'));
    model.setStyle(edge, unparseStyle(newStyle));
  }

  // Simply sets the data attribute TYPE_ATTR to ftgpm_type.
  function setFtgpmType(cell, ftgpm_type) {
    // Set type attribute
    let value = model.getValue(cell);
    if (!value) {
      // Workaround: 'value' must be an XML element
      value = mxUtils.createXmlDocument().createElement('object');
      value.setAttribute('label', '');
    }
    value.setAttribute(TYPE_ATTR, ftgpm_type);
    model.setValue(cell, value);    
  }

  function setEdgeStyle(edge, style_type) {
    // Update style
  
    // Workaround: don't overwrite connection points
    const oldstyle = parseStyle(model.getStyle(edge));
    const newstyle = new Map(parseStyle(getFromConfig(edgeStyles[style_type])));
    mapAssign(newstyle, oldstyle, [
      // retain these properties from oldstyle:
      "entryX", "entryY", "entryDx", "entryDy",
      "exitX",  "exitY",  "exitDx",  "exitDy"
    ]);
    model.setStyle(edge, unparseStyle(newstyle));
  }

  function isArtifact(type) {
    return type === "artifact";
  }
  function isFormalism(type) {
    return type === "formalism";
  }
  function isTransformation(type) {
    return type === "auto_transformation" || type === "transformation" || type === "comp_transformation";
  }
  function isActivityNode(type) {
    return type === "autom_activity" || type === "activity" || type === "comp_activity";
  }
  function isControlFlowPort(type) {
    return type === "ctrl_in" || type === "ctrl_out";
  }
  function isDataPort(type) {
    return type === "data_in" || type === "data_out";
  }
  function isInport(type) {
    return type === "data_in" || type === "ctrl_in";
  }
  function isOutport(type) {
    return type === "data_out" || type === "ctrl_out";
  }
  function isPort(type) {
    return ["data_in", "data_out", "ctrl_in", "ctrl_out"].includes(type);
  }
  function isControlFlowNode(type) {
    return isControlFlowPort(type) || type === "initial" || type === "final" || type === "fork_join";
  }
  function isDataFlowNode(type) {
    return isDataPort(type) || isArtifact(type) || isFormalism(type);
  }
  function isTraceEvent(type) {
    return type === "traceevent_begin" || type === "traceevent_end";
  }
  function isArtifactVersion(type) {
    return type === "artifact_version";
  }
  function getFlowType(type) {
    if (isDataFlowNode(type)) {
      return "data";
    }
    if (isControlFlowNode(type)) {
      return "ctrl";
    }
    // throw new Error("unknown flow type");
    console.log("unknown flow type");
  }

  // [ condition, flow-type ]
  const edgeTypeRules = [
    // PM control flow
    [ (src, tgt) => isControlFlowNode(src) && isControlFlowNode(tgt), "ctrl_flow" ],

    // PM data flow
    [ (src, tgt) => isDataFlowNode(src) && isDataFlowNode(tgt), "data_flow" ],

    // FTG data flow
    [ (src, tgt) => isTransformation(src) && isFormalism(tgt), "data_flow" ],
    [ (src, tgt) => isFormalism(src) && isTransformation(tgt), "data_flow" ],

    [ (src,tgt) => isActivityNode(src) && isTransformation(tgt), "typed_by" ],
    [ (src,tgt) => isArtifact(src) && isFormalism(tgt), "typed_by" ],

    [ (src,tgt) => isTraceEvent(src) && isArtifactVersion(tgt), "data_flow"],
    [ (src,tgt) => isArtifactVersion(src) && isTraceEvent(tgt), "data_flow"],
    [ (src,tgt) => isTraceEvent(src) && isTraceEvent(tgt), "ctrl_flow"],

    [ (src,tgt) => src === "traceevent_begin" && tgt === "ctrl_in", "typed_by"],
    [ (src,tgt) => src === "traceevent_end" && tgt === "ctrl_out", "typed_by"],
    [ (src,tgt) => isArtifactVersion(src) && isArtifact(tgt), "typed_by"],

    [ (src,tgt) => isArtifactVersion(src) && isArtifactVersion(tgt), "parent_version"],

    [ (src,tgt) => isArtifactVersion(src) && tgt === "storage", "ss_link"],
    [ (src,tgt) => isArtifactVersion(src) && tgt === "real_object", "ss_link"],
    [ (src,tgt) => src === "traceevent_begin" && tgt === "service", "ss_link"],

    [ (src,tgt) => src === "comment" || tgt === "comment", "comment_edge"],
  ];

  // Very specific: This function is used to determine the correct direction of an arrow automatically
  // An activity or artifact at the highest level (= a direct child of a layer) has level 0
  // Every time something is nested in an activity, the level increases by 1.
  // Ports are special: they can only be a child of an activity, but they have the same level as the activity.
  function getNestedLevel(cell) {
    function getLvlRecursive(cell) {
      if (!cell.parent) {
        throw Error("getNestedLevel called with parentless cell (i.e. cell is root, or is deleted)");
      }
      if (model.isLayer(cell)) {
        return 0;
        // throw Error("getNestedLevel called with layer cell");
      }
      if (cell.parent === model.isLayer(cell.parent)) {
        return 0;
      }
      else if (isActivityNode(cell.parent.getAttribute(TYPE_ATTR))) {
        return getLvlRecursive(cell.parent) + 1;
      }
      else {
        return getLvlRecursive(cell.parent);
      }
    }
    const baseLevel = getLvlRecursive(cell);
    if (isPort(cell.getAttribute(TYPE_ATTR))) {
      return baseLevel - 1;
    } else {
      return baseLevel;
    }
  }

  function isDirectionless(type) {
    return isArtifact(type) || type === "fork_join";
  }

  ui.editor.graph.addListener(mxEvent.CELL_CONNECTED, (_, eventObj) => {
    // Happens whenever an edge is (dis)connected.

    const edge = eventObj.properties.edge;
    const sourceCell = edge.source;
    const targetCell = edge.getTerminal();

    // This will change the edge style WITHIN the transaction of the edit operation.
    // The terminal-change and style-change will be one edit operation from point of view of undo manager.

    const sourceType = sourceCell ? sourceCell.getAttribute(TYPE_ATTR) : null;
    const targetType = targetCell ? targetCell.getAttribute(TYPE_ATTR) : null;

    // Update style if necessary
    for (const [cond, linkType] of edgeTypeRules) {
      if (cond(sourceType, targetType)) {
        setFtgpmType(edge, linkType);
        setEdgeStyle(edge, linkType);
      }
    }

    if (sourceCell && targetCell) {
      // Auto-correct edge direction in certain cases.
      // We call 'reverseEdge' if we have an illegal situation, but reversing the arrow gives a legal situation.
      const sourceLvl = getNestedLevel(sourceCell);
      const targetLvl = getNestedLevel(targetCell);

      // We have 2 flow types: ctrl and data. They remain segregated.
      if (getFlowType(sourceType) === getFlowType(targetType)) {
        if (sourceLvl === targetLvl) {
          // Equally nested ...
          if (isInport(sourceType) && isOutport(targetType)
           || isInport(sourceType) && isDirectionless(targetType)
           || isDirectionless(sourceType) && isOutport(targetType)) {
            reverseEdge(edge, sourceCell, targetCell);
          }
        }
        else if (sourceLvl === targetLvl + 1) {
          // Source of arrow is nested deeper ...
          if (isInport(sourceType) && isInport(targetType)
           || isDirectionless(sourceType) && isInport(targetType)) {
            // Should flow from shallowly nested input port to deeply nested input port:
            reverseEdge(edge, sourceCell, targetCell);
          }
        }
        else if (sourceLvl + 1 === targetLvl) {
          // Target of arrow is nested deeper ...
          if (isOutport(sourceType) && isOutport(targetType)
           || isOutport(sourceType) && isDirectionless(targetType)) {
            // Should flow from deeply nested output port to shallowly nested output port
            reverseEdge(edge, sourceCell, targetCell);
          }
        }
      }
    }
  });

  function outsideOffset(wOrH) {
    return 0;
  }
  function centerOffset(wOrH) {
    return -wOrH / 2;
  }
  function insideOffset(wOrH) {
    return -wOrH;
  }

  // Change this to position ports outside, inside or centered at the edge.
  const borderOffset = centerOffset;

  function rightOrBottomBorder(wOrH, parentWorH) {
    return parentWorH + borderOffset(wOrH);
  }
  function leftOrTopBorder(wOrH) {
    return -wOrH - borderOffset(wOrH);
  }

  function cellCenter(cellGeometry) {
    // Coordinates are relative to topleft corner of parent shape
    const {x,y, width, height} = cellGeometry;
    // Center of cell
    return {
      x: x + width/2,
      y: y + height/2,
    };
  }

  // Deprecated snapping algorithm
  function closestBorder(cellGeometry, parentGeometry) {
    // Cell center is relative to parent: (0,0) is topleft
    const c = cellCenter(cellGeometry);

    // We draw two imaginary diagonals through the parent shape, to determine whether the child shape is more close to the top, right, bottom or left side.
    //
    //           2
    //          /
    //    +----+
    //    |\ t/|
    //    |l\/ |
    //    | /\r|
    //    |/b \|
    //    +----+
    //          \
    //           1
    const slope = parentGeometry.height / parentGeometry.width;
    const above1 = c.y < slope * c.x;
    const above2 = c.y < parentGeometry.height - slope * c.x;

    if (above1)
      if (above2)
        return 't';
      else
        return 'r';
    else
      if (above2)
        return 'l';
      else
        return 'b';
  }

  // Improved snapping algorithm
  function closestBorder2(cellGeometry, parentGeometry) {
    // Cell center is relative to parent: (0,0) is topleft
    const c = cellCenter(cellGeometry);

    const slope = 1; // fixed slope, 45 degrees

    const above1 = c.y < slope * c.x;
    const above3 = c.y < (-slope) * c.x + parentGeometry.height ;

    const above4 = c.y < slope * (c.x - parentGeometry.width) + parentGeometry.height;
    const above2 = c.y < (-slope) * (c.x - parentGeometry.width);

    if (parentGeometry.width >= parentGeometry.height) {
      // Parent's width > height
      //
      //  1          2
      //   \        /
      //    +------+      --> x
      //    |\ t  /|
      //    |l\__/ |__ 5
      //    | /  \r|
      //    |/ b  \|
      //    +------+
      //   /        \
      //  3          4
      //    |
      //    V
      //    y

      const above5 = c.y < parentGeometry.height / 2;

      if (!above1 && above3) {
        return 'l';
      }
      if (!above2 && above4) {
        return 'r';
      }
      if (above1 && above2 && above5) {
        return 't';
      }
      if (!above3 && !above4 && !above5) {
        return 'b';
      }
    }

    else {
      // Parent's height > width
      //  1        2
      //   \      /
      //    +----+
      //    |\ t/|
      //    | \/ |
      //    |l | |
      //    |  | |
      //    | /\r|
      //    |/b \|
      //    +----+
      //   /   |  \
      //  3    5   4

      const leftOf5 = c.x < parentGeometry.width / 2;

      if (above1 && above2) {
        return 't';
      }
      if (!above3 && !above4) {
        return 'b';
      }
      if (above3 && !above1 && leftOf5) {
        return 'l';
      }
      if (above4 && !above2 && !leftOf5) {
        return 'r';
      }
    }
  }

  // Update port shape (so it points in the right direction), move port (and port label) to border of activity.
  function snapPortToBorder(cell, cellGeometry, parentGeometry, border) {
    const style = parseStyle(model.getStyle(cell));
    const type = cell.getAttribute(TYPE_ATTR);
    const inOrOut = isInport(type) ? "in" : "out";
    style.set('shape', encodedPortShapes[inOrOut][border]);

    switch (border) {
      case 't':
        cellGeometry.width = 35;
        cellGeometry.height = 20;
        cellGeometry.y = leftOrTopBorder(cellGeometry.height);

        style.set('align', 'right');
        style.set('verticalAlign', 'bottom');

        cellGeometry.offset = new mxPoint(
          -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
          -getFromConfig('portLabelOffsetEdgeDirection'));
        break;
      case 'r':
        cellGeometry.width = 20;
        cellGeometry.height = 35;
        cellGeometry.x = rightOrBottomBorder(cellGeometry.width, parentGeometry.width);

        style.set('align', 'left');
        style.set('verticalAlign', 'bottom');

        cellGeometry.offset = new mxPoint(
          getFromConfig('portLabelOffsetEdgeDirection'),
          -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
        break;
      case 'l':
        cellGeometry.width = 20;
        cellGeometry.height = 35;
        cellGeometry.x = leftOrTopBorder(cellGeometry.width);

        style.set('align', 'right');
        style.set('verticalAlign', 'top');

        cellGeometry.offset = new mxPoint(
          -getFromConfig('portLabelOffsetEdgeDirection'),
          getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
        break;
      case 'b':
        cellGeometry.width = 35;
        cellGeometry.height = 20;
        cellGeometry.y = rightOrBottomBorder(cellGeometry.height, parentGeometry.height);

        style.set('align', 'left');
        style.set('verticalAlign', 'top');

        cellGeometry.offset = new mxPoint(
          getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
          getFromConfig('portLabelOffsetEdgeDirection'));
        break;
    }

    model.setStyle(cell, unparseStyle(style));
  }

  function snapToBorderIfCellIsPortAndParentIsActivity(cell, geometry) {
    const type = cell.getAttribute(TYPE_ATTR);
    if (isPort(type)) {
      // Port was moved
      if (cell.parent) {
        const parentType = cell.parent.getAttribute(TYPE_ATTR);
        if (isActivityNode(parentType) || isTransformation(parentType)) {
          // Snap port to activity border
          const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
          // border will be one of: 'l', 'r', 't', 'b'
          const border = portSnappingAlgorithm(geometry, cell.parent.geometry);
          snapPortToBorder(cell, geometry, cell.parent.geometry, border);
        }
      }
    }    
  }

  ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
    // High-level event: Happens when the user releases dragged shape(s)
    for (const cell of eventObj.properties.cells) {
      snapToBorderIfCellIsPortAndParentIsActivity(cell, cell.geometry);
    }
  });

  ui.editor.graph.addListener(mxEvent.RESIZE_CELLS, (_, eventObj) => {
    // High-level event: Happens when the user resized cell(s)
    for (let i=0; i<eventObj.properties.cells.length; i++) {
      const cell = eventObj.properties.cells[i];
      const type = cell.getAttribute(TYPE_ATTR);
      if (isActivityNode(type) || isTransformation(type)) {
        // Activity was resized...

        // In drawio, 'arcSize' of a rounded rectangle is relative to size of shape.
        // Retain apparent arc size of activity:
        const style = parseStyle(model.getStyle(cell));
        const newArcSize = 1000 / Math.sqrt(cell.geometry.width * cell.geometry.height);
        style.set("arcSize", newArcSize);
        model.setStyle(cell, unparseStyle(style));          

        if (cell.children) {
          // Need this for moving contained ports:
          const prevGeometry = eventObj.properties.previous[i];
          const scaleW = cell.geometry.width / prevGeometry.width;
          const scaleH = cell.geometry.height / prevGeometry.height;

          for (const child of cell.children) {
            const childType = child.getAttribute(TYPE_ATTR);
            // Keep activity icon in place
            if (childType === "activityIcon") {
              newIconGeometry = child.geometry.clone();
              newIconGeometry.x = cell.geometry.width - 28;
              newIconGeometry.y = 4;
              model.setGeometry(child, newIconGeometry);
            }
            else if (isPort(childType)) {
              // Move contained ports
              const border = closestBorder2(child.geometry, prevGeometry); // < what was the border in the old geometry?
              const newGeometry = child.geometry.clone();
              snapPortToBorder(child, newGeometry, cell.geometry, border);
              if (getFromConfig('movePortsOnResizeActivity')) {
                // Scale position
                switch (border) {
                  case 't':
                    newGeometry.x = child.geometry.x * scaleW;
                    break;
                  case 'l':
                    newGeometry.y = child.geometry.y * scaleH;
                    break;
                  case 'b':
                    newGeometry.x = child.geometry.x * scaleW;
                    break;
                  case 'r':
                    newGeometry.y = child.geometry.y * scaleH;
                    break;
                }
              }
              model.setGeometry(child, newGeometry);
            }
          }            
        }
      }
    }
  });

  // Hardcoded primitive shape library.
  // To alter, make changes to the library in the drawio webapp, then 'save' (this downloads the library as XML), and copy+paste the contents of the XML file below:
  const encodedLibCommon = `<mxlibrary>[
  {"xml":"dVLNToQwEH6aXk1L0Q1Hl9U9mRj34NF0YRaqhdm0o4JPb6GUhegeSGbm+xvaMpk33d6qc/2EJRgmH5jMLSKFqulyMIYlXJdM7liScP+x5PEKKkaUn5WFlv4R4PEdCvIMo45D2G4ER0nzggbCpCBr3k4GvwMUzUfmOtdRH0VQVnCYWs7kFtry3lrvIXeFUc7pwg9raoZU4UtHFj/gVZdU+4k33p6wpYP+GQwEnxk5GrRjhOQ8zbPMI2GvIfHqb4vFsnvABsj2nmLBKNJfa51yoa1m3ix9Rt0O59UHRsrXCoeftoCJtLyXqOui7oZzeZtkqRCbu3SzdiFlK6A/Lr5Y7H4ZjWcf2+lGY3t5OUG9fFi/","w":40.0035294117647,"h":40,"aspect":"fixed","title":"Control Flow"},
  {"xml":"dZJNb4MwDIZ/Te6BULQdN1h7mjSthx2ntHFptoBR8DbYr18+gILWIiH5431sJw4TRd3vrGzPz6jAMPHERGERKVp1X4AxLOVaMVGyNOXuZ+n2RjYJWd5KCw1dAfDwAUdyCiMPvlkZkgGpX9FAjChJ8v1k8CempuJBue7b0TBBoCrYjy5n4hEa9WCtqyFKbKFxkTPVvmXizI4sfsKbVnQeIydsaK9/PZ1ko1+gQRuqCx4+H9fGLOJqA3cqmysuMnl5n2/yOMjWQeNY8Uh+2Js3lizOuQOsgezgJBaMJP295mQX3WrWzegL6sZf9RAVGV8THX7ZI4yi5Uonrr/OkbQV0D/OGYtpL6GwqMkd1z+5l2cW6eUr/AM=","w":40,"h":40,"aspect":"fixed","title":"Data Flow"},
  {"xml":"dVJNT8MwDP01uafNhMQROrYTEmJIHFHamDZbWlepgZZfT5qkX2KLFMnP9vNzHDOR1f3RyrZ6RgWGiScmMotIwar7DIxhKdeKiT1LU+4uSw83oomP8lZaaOgKAfMzFOQyjMxHsb0Pekr9igaCh4YW1Ec+hMhU2yduZTsaJg6oEk4RciYeoVEP1uKPg7nB4vJW6ca5K6pH2cSZHVm8wLtWVEXPJzZ00r9jiWQXcYYGrZcQ3B/nV7KrQEWSEzpoYxYUK9zNEqsS9/64SHja2PTNwSWr9x4BayDrJsItGEn6e8uTXYDlnDdTX1A348SHkLHjW0aHX7aAmLT+2YnXX+eRtCXQP54zVt0uLv9hE4xbMMFl2wJ7vYx/","w":40,"h":40,"aspect":"fixed","title":"Typed-by"},
  {"xml":"dVRtc6IwEP41zNx96Ex4EY+PyptFi56tBf1yEyBCLBAuiQL99ZcotHbmOqMz++zus3mS3UXR7arzKWyKJ5KhUtFdRbcpIfxmVZ2NylLRAM4U3VE0DYi/onnfRNVrFDSQopr/h0CSE0q5yChhIg9zrsFJQyivYYWEORQYHdeMFNeDu4QpKkiZIcq+HlhtSTlmc1r+GRmjME3Cr5oZ70cKK2AjTcZRneLyR3MIM8d4zL3Hg5DuJGm4hzo9XHYC9e402C7ms1heSpuL3x6k+ntsawl/XwtotIuj51a/VPvvvDnlweFpGpBtarSbxdrQbGAW9gxEb9Ml/PX6ljpbdwUoWU43LF4THwQxQ1F/0k+RtTfXQTgxYOfheK+dnvCMgK7B2mWSay+ZxaxzVEc11O2FjwOjP4XL6iVnXnUUyrAfrZ9Rox4Wk902jrtp1WyA+hpk9R5dyAyFJuVZG5jxiiWBd06gmca+DxZ5bjn8XE/FPXa7PmOFV7ClMXPCGMcVtqPutw/C416E6TIoZGPlmxBHZ+7qp6LP2wJz9NyINonnbMVYCV/BK9lqVZjioTPSCgAEoIjhd5hcm3DD6ZkyfEFbGRi9FbnInNUwMLJKhTh0M8zHopySN2STktBrO3UADNuyPiIRznghIpo8g5zrDGVD7SMuyzueY7iqN5E8cQFc5y9EyHceTOFJSYXTUSfhkN/pvk3aBVGOum/XQL0bQB8RcQXai5R20CZH9LYpoEA4LwaWPuwDHMY9/2B+FNsQXMuNGo5+0KwbpR+wan4tQY5Hhvj9UgrjTtOn67onIxw2d4SfX4gb+/4D8g8=","w":20,"h":35,"aspect":"fixed","title":"Control Input Port"},
  {"xml":"dVRbc6IwFP41zOw+dCZc1PKo3CxadG0t6MtOgAixQNgkCvTXN1GwdmY7AzPnO9cvOedE0a2y9Sis82eSokLRHUW3KCH8KpWthYpC0QBOFd1WNA2IX9HcH6zqxQpqSFHF/xNA4iNKuPAoYCyL2RfjqCaUV7BEQuwTDIqLR0JOvNcXMEE5KVJE2feK5YYUgzunxd9byEBNk/A7a8a7IYblsJYi46hKcPGr3gepbTxl7tNekLfjJNhBne7PW4E6Z+Jv5rNpJI+lzcS3A4n+EVlazD9WAhrN/OA65aNq/ZvVx8zfP098skmMZj1fGZoFxrk1BeH7ZAEf394Te+MsASWLyZpFK+IBP2Io7I76MTR345UfjAzYujjaacdnPCWgrbF2HmXaa2oy8xRWYQV1a+5h3+iOwaJ8zZhbHgQz7IWrF1Sr+/lou4midlLWa6C++Wm1Q2cyRcGY8rTxx9GSxb57iuE4iTwPzLPMtPmpmohzbLddynI3ZwtjagcRjkpshe0fDwSHnTDThZ/L1so7IbbOnOVvRZ81OebopRZ9EtfZiMESupyXstmqEMVFp6QRAAhAEcMfML404YqTE2X4jDbSMGhLcpY+y35kZJYSceikmA9JOSXvyCIFoZd26gAYlmneLCFOeS4smqxBTlWK0j73ARfFXZxtOKo7knHiALjKXomgbz+MhSYhJU4GnoRDfsf7OmlnRDlqf1wE9W4APUTEEWgnXJqemxzR666AHOEs76P0fiNgP+/ZLfKWbE1wJXeqLz3U7q7wQTW/ZyCHA0P8fiuFcEfpS3VZkwH2qzvAryfiGn3/gnwC","w":20,"h":35,"aspect":"fixed","title":"Control Output Port"},
  {"xml":"dVRtk6IwDP41zNx98KZQRP2ovLnoqueuC/rlpkCBukC5torur78isLoztyPO5EnS5EmTVIFmcXEZqrJnGuNcgbYCTUapaKXiYuI8VzRAYgVaiqYB+Vc05xurerOCCjFciv8coOERR0J65Chsklk347CiTJSowFLsAvSKm0dMyk6dowhnNI8x418TFlua995IoD/9iZ6Y1sCvnLm49kd4hqpG5AKXEcl/VIdVbOlPqfN0kNStMFrtEWSH806iqz3ytvPZNGiK0mby24MIfgSmFoqPtYR6PU8cuxir5t9ZdUy9w/PIo9tIrzfzta6ZwMjMKfDfRws0fnuPrK29BIwuRhserKkLvIBj/3qER3+yN9beaqiji0OCvXZ8JlMKLhXRzsNUe40nfHLyS79E0Jy7xNOvx9WieE25UySSGXH99Quu1MN8uNsGwWVUVBugvnlxucdnOsUrg4m49oxgyUPPOYXIiALXBfM0nVjiVI5kHbvdNeaZk/GFPrVWAQkKYvqX3y5YJXtpZgsvaxrb3Am1ILeXPxU4qzMi8Esl2ySvs5ZjJXWZKJpWq1KUFx3TWgIgAcOcfKDw1oQWRyfGyRlvG0OvLei58Vl2A9NEKbBAdkxEH1Qw+o5NmlN2ayc0rIkxND4tPolF1vkyeipjHHexuSRKyvSVSprWoDkR0YJEnTWhpXhpiaja3VvCB7TEiej9SZ4/sLChM7K1W04hR/JeZzuZZ8wEvny7NurDwLqYypLZVbpcW+vwlzHWoa6OxwYYy5+GB6reBqm7apuhb3cPZJikWRcXdhuGugVKP2N/pttQUjY72pEbwC5Ml3vQ0+1D0CThWDyuuRQeWN9Vt83rYfcW9PD+5rSnH5+kfw==","w":20,"h":35,"aspect":"fixed","title":"Data Input Port"},
  {"xml":"dVRtb6MwDP41SHcfJvFWWD+2vHW0o71uHbRfTgECpAPCJWmB/folLXSddJNA8mP7cezYjqRZVecR0BTPOIWlpDmSZhGM2VWqOguWpaTKKJU0W1JVmf+S6v5gVS5WuQEE1uw/BBwfYcK4RwlicZh9MU4aTFgNKsjFIcCouHik+MQGfQkSWOAyhYR+P7Ha4nJ0Bwz8vVHG1FQBv2dNWT9yaAEaIVIG6wSVv5pDkNr6U+4+HXjydpwEe6CRw3nHUe+Y/nYxn0WiLHXOv72caB+RpcbsY82h3i4y16keFevfvDnm/uHZ9PE20dvNYq2rlmwU1kwO380leHx7T+yts5IJXpobGq2xJ/sRhWF/1I7hdG+s/WCig85F0V49PqMZlrsGqedJrr6mUzo9hXVYA81aeMjX+2OwrF5z6lYZzwx54foFNsphMdlto6gzq2YjK29+Wu/hGc9gYBCWtr4RrWjsu6cYGEnkefIiz6c2O9Umr2O361NauAVd6jM7iFBUISvs/nhykO25mSz9QrRW3Am2NeqsfkvavC0Qgy8N7xO/zpYPFtcVrBLNVrjILzrFLQcyBwRS9AHiSxOuODkRis5wKwyjtsJn4bMaRkZEqSADTorYGJQR/A4tXGJyaadm2FNjYtwsIUpZMfgSfKpTmA6xM1SWdzxHc01HFTxeAKrzV8zTtx9EpARXKBlYg5WjO98VzNhYB2Z8Br/quk7iGRIGux8XRbkbUA9iXiLpuUs75C5G+LpLcgFRXgwsbdgYMOxDfmPegm0wqsXODUePZ/dX+KA8fo+As4xCdr+1XLhL6Ut1WaMRDqs9wq8n5Mq+f2E+AQ==","w":20,"h":35,"aspect":"fixed","title":"Data Output Port"},
  {"xml":"dVLbboMwDP2avE4BtGqvK9v6VFVavyAEj2TNBQW3wL5+DgnrKrUPKD7H59ixA6tqO+2C6NXet2BY9c6qOniPKbJTDcawkuuWVW+sLDl9rPx4kC2WLO9FAId3DL75BomkMKKJzaJAemujOjntpzdwJ7F2KCO8bT7gvFoGJfoYOo90bEelEY69kJEbaUjiFNrYuKCwEfLUBX927eGMRjvIfCvC6UAujTMx/Ik/EymM7hxBA1802fYCAbUU5jXT6GPxQf8sRWKVdO+og+nhdop/4+zAW8Awk2TULaqkeEkL5Ap0p7Jrs0mcGBLu/pzXVVOQN7TCvPwVXh95kd78A78=","w":80,"h":66,"aspect":"fixed","title":"Comment"},
  {"xml":"dZLdbsMgDIWfhvv8qA+wpluvKk19gYoGKzABrhx3Sd5+BEKbaO1FJB/b3zE4iLpx45HkTZ9QgRX1p6gbQuQUubEBa0VVGCXqg6iqInyi+npTLWO1uEkCzy8AvP5Ay6HDyus87BCLEXFntJAyLToX+AuoDlI1+8fm7eiep8yBVx9EOATl0YfkXslew8yWQWh2dgmTa/R/d+5yNeoI6IBpCi2DUaxTxy5drdBgOs3bHIGVbH639rJPsnvYPSZ8o/HzXqatTSZ6vFMLS9N6/5kbX3MsqQP+x4VgdalnKq40y+VfZfl8E4leP5k/","w":50,"h":50,"aspect":"fixed","title":"Comment Edge"}]</mxlibrary>`;

  const encodedLibFTG = `<mxlibrary>[
  {"xml":"dVPbkqIwEP0aqnbfuKzKPsrNwXHUGVFX3wK0EAkkGyKIXz/h4jpW7RSVok+fTqfTfaIYdn6dccTSNxoDUQxXMWxOqeit/GoDIYqu4lgxHEXXVbkU3fuG1TpWZYhDIf6zgYZniISMIChsD3M6clSgHORv2Jx/UAI9JzgqyhPlORKYFgNPUAQpJTHw8unULkvnmEYCV1g0QcOg5+4F6i18rr0Uzf24MkWsNUsBRYTJj/PRC53ST1x396e+yVvYdnbbnfbTCdlm86VvMSNUzb95drWspT0qvCadZO/GOWNHbL9tSr9KF27AwF0purVbZHVpvEtLq6PosE3JSDeO1PesiTmn82OiRtGlus7HTlabxi/LqGJ3vHl9+ZhQx0z8IEaXZnWRg9EtIOeb5i4Dflhc6NI7IF/8Rt709Xg9xXtZ5rrJNajetcN4H9D4pQrXpTXbIbFkN5Pu/e1ZJvFWe2bWB3nbn4ph1SkWsGGys9JRSzlIXyrydkSaNDmU+IbCrk9qi6lA4gvOQSA3xmIIjzllAeIJiIGPaCEQLoAPWDY6pvUAGHAsE3SkHAlmJawfLuuECbEpobybkeE67Sf9/WAr4AKu36pP+zLvGVCZkzcypMaxSPuISS9QNQWcpOLZhwaBJf92PqQsjUFCdziI+w4fj6gLfXpjnw==","w":70,"h":70,"aspect":"fixed","title":"(FTG) ActivityType"},
  {"xml":"pVZZc6pIFP41Vs283IJGE31k05BrtxK3wBsCaVabQWT79XO6wcTcWWqqxsSyz/b1WT48ThQ9b1elV0SYBWE2UcyJopeMVcMpb/UwyyZIioOJYkwQkuA9Qct/sMrCKhVeGV6qvwlg5yT0K/DIvDO/zBDG2cXLQ/gYg/M3loWDrSq9y/WDlblXxewy2jPPDyOWBWF5/XarQBEK1a/iOq66fVeEg81necGucfWJexsN98wRF78Xda26ex7XyCv48VqFFz/Ofkvc5dm4WtQ0j+9ND+XpetofP07qc3ZIX4mlFcpZmv+Rp62mEX12WXbRc2orSVq4sY53V6uO1ua+CM3NBGnHddpcFRtOcuP7ziHKZkhxmbXUnuev7NWlku/f6vb1yUibuTLVlDown3Y/X96emTGn1j7wbt3mBhNDWpglvWySfemsb4wsHc+qFt5S/em2H8EJ0tx2uRzWtuw8nfYseKnP26u2OnoVKfo5O1mHBECWm1Mxbxyo9veJojURNG1XQMtB0QBPQBdVOZ+dDMcyvMa9dxZ9krjMKq96kPOw8swgrkb3oGTF3itpWI12n10qL76E5ShDowPWjEIRljEACCOMJC6u4fZLpX3EWaazjJViRopp8D/QD4Otw7IK23+kpfww71XIALPswKWJgyoaPJ4H5kpRGNOo+q7zRubRz8gvjsNhpNBdHFn/L0/Ar7z3RgJb/p31d54q/4mnce5R+NR4E2Lfy9b8si1/APhjpBhnVlUsBweRheb5KS3Z7RI8tPNDvB4w1CymPLZinAPetRA1GR9xG0JumrhSvWuluwbOATBioqiDiJbXmgJTW6AQ0rcvBLmdNj2f2pvfS7H38ib5BqvXSqAE3UzB3az2c7/GidpgfdEHuR9bL27hvgf6WaELK1Ep7i1K9j6yDFva6BYNUJYGK247KCS2qLc6Fi6KpO3OkohB423SROudFFmmW58vNrXfg8xHi8rdabafzymJp80acJ1LSu0V93mLAnTsz0jOLF2iIscX/LTuFvkZzSTnM36WnJFE96sscU+z3t2plbWKpOBF6zfxvHbzxS1YLRX3ndTn0+HmQMwatYWDjjcfHehPk+vlzM9J4ZyaBxzIAy2a8P1V1GEZVoMTi74JvAzunPVrXXs95/gG+VXbpG2c9zdmreyFlUodNijdGJoBPZxaxqFfJ/YMMLrNbipbBu7gLJNY7XFKe5yAX2+jdXIQfcW98xCD0UZXp3jptMR4MwabqWBd4EjQN/g0Z5hj8bns025n4BZnTof7V4sYjmT39ozoU35/D36y8IM5rRMTZmg1oJ9ulg7PwwJ5avcHmQz4HdZVmfQO5GS3PL9h5pDT0pEA3xg5IG+E/z3OnvI4nFAKs292ho2IicHfv5GdKm0MFfTmQ5zdQi843onXB/lyuQcf4T/Gifp4PSIvk0qE+yeU+ytQV7cx8I3jkITeiM7lu781Bf8e7zm+BfnweAz4zifu0FfIcZgP73lPkuhEwNfuhSyRPRX+Y5yog+OJek3akb0L87Zmok9QN+fMQ5zgBMc7JoBnYiHf+3qPG/BGbsA8cOJIx0Tk30L+Bt6rMDeYR8LxzWaoU9TdYsPm+ingd1/+ePqFayKsf/a1JWPdxwQqTLGQiXEY/Me4oQ6zhf5CvcAPI4IaId5IR9tDjP44Q/WhtsH/3qtxfjIBjm4Aj+ydTz5uIDds+CDzZ0TcK3g89AF4nn7vM+f72OeW9Opnn/HeBn/ahLFVw3dP7SvuZUv5lyz/1+BHV3jfmHxF+hm7hA/yrxvVyzLWqGXJmut95/6yg3NWP0iwQS7wnfy4o0P/Vl7jOnzjgXftf9qa6O+35hiAlB/zp+ni6zUAPOxUhH6g+V/Xqvz8Q5H/72YF8et3q3D99rP2Tw==","w":70,"h":70,"aspect":"fixed","title":"(FTG) ActivityType (Composite)"},
  {"xml":"dVPJkqMwDP0aqmZuLJOlj4FAmnQ6+zLJzcEKOBjsMQZCvn4MgUmnavpAIb0nPdmWpFlOcpsIxKNPhoFqlqtZjmBMPqzk5gClmqkTrFljzTR19Wmm9w1rNKzOkYBU/ieBna8QSBVB0bkuNm7IXooSUL82OVkzCg9OCpRmFyYSJAlLW56iACJGMYjspWqj0gCjQJKCyGpbcXhwKJdMiQDudPOW6E5u1u7rpTJZdefIIsRrM5OQBoT+uJ688zjzQ9fd/y7v6nqOE9/3l8NoQHfxdO7b3Drrwz9JfLPtudNLvSoaxCvrGvMTcT43mV9EM3fLwV1opr2fxWVmrZRllEFw3EW0Z1on5nv2YDhl01OoB0Fe3Kb9cVwOrV+2VWC3v/l4Xw/YeBj6W4zyapGrjpk20OvdcOdbcZzlbO4dkS/fkDf6ON0u+KCOuawSA4qVcewftgy/F+dlZk/2SM75fcgO/u6qRLzFgQ/Lo7rtT82yy4hI2HD15Aoo1ZwoLJJJ3TtDmQIyckfn5p302mcSyS9+AhK5mMg2HAvGt0iEIFs+YKlEJAXR5UOQi4wUsK6FO5UChCQBoiNKwlRhCcG4LmGrvmBWtlEcBFH1Gi3VQcIzWD4h+0IodRhlommp5XnOuP+m8Mcc1CXg9u0UG1/GYwJMaYpKhZQEy+gRMXgMuh4BCSP5iqF2UMN/mc+VUEY7cZ3bLknnPpexCX3Z1b8=","w":70,"h":70,"aspect":"fixed","title":"(FTG) ActivityType (Automated)"},
  {"xml":"pVbZcqs4EP0aV8283GKxE/uRzY5yLWy8Bt4wELFaDGC2r5+WwImTuXdqqsaVFOru00e9CTGRtaxdFW4eYuoH6UQ2JrJWUFoNq6zVgjSdSELkT2R9IkkC/E+k5W+sIrcKuVsE1+oXDvQSB14FiNS9sM10bpxd3SyAx+ic7WgaDLaqcK/lOy0yt4rodbSnrheENPWDovyyK2fhCsWrojqqukOXB4PNo1lOy6j64L2NBvdWUWAP/O+Ge0oSE79mW1bdPcAydHO2LKvg6kXpH7GzvOglIoZxemt6yFvTkv70flae02PyaiI1ly/C/K8saVXV1GbXZRc+J5YcJ7kTaXhfojpcG4c8MDYTST2tk6aULViJjefZxzCdSbJD0VJ9nr/SV4cInner29cnPWnm8lSVa9942v982T1TfU7QwXdv3eYGrZTUII170TAPhb2+UXNpu6hauEvlp9O++2cIc9tlYlBbov10PlD/pb5sS3V1cisz7+f0jI4xkCw353ze2JDtnxNZbUKo5j6HXoCigQECXVhlrKkiLIugjHr3wuskMJlWbvUgZ0HlGn5UjXC/oPnBLUhQjXaPXis3ugbFKEOhfdqMQh4UERBwI7Qkystg+6lS36M01WhKC94jebnU9KcF6IfG1kFRBe1v51V86PcqoMBZdABpIr8KB8TzMNJCGEQkrL7q3HEkyYfn5/DDYhyhuzgeh385Gt8PhDtONvLux+E+p/J/mtMocwk8VVaEyHPTNdtsy04GO1+yfqEVnAcA8ChU10tIQW9X/6Gc7/z3wKGkEWG+FWUz4JY5z0l/j1p2qlS+pXLXCncNrH2YiImsDKK0LGsCk9rCCEna9sWUnE6dXs7tzeuFyH3ZCZ5O67Xsy343k3E3q73Mq3GsNFhb9H7mRejFyZ03X7vIZIFiheAeEfPgSUi3hI2GiC+lib9itqNsRoi4q1PuSKGw3SPB1Em0jZtwvRdCZDj15WoR681PPWlROXvV8rI5MaNpswZe+5oQa8Uwu9CXTv1FElOkCYTH+IKf1t0iu0gzwf7wn8UXSSCHVRo751nv7JUKrULBf1H7TTSvnWxx81dL2Xkz68v5eLPBZy21uS2dbp50JD8NphdTLzNz+9w88EAc0qIJ3l55HkhHDY4R2XG+FPac9WtNfb1k+AbxVdu4bey3HUUra4ESocM6IRtd1aGGU6Qf+3VszYCj2+ynItJxB2vRjJQeJ6THMeB6S1rHR15X3NsPPljaaMoUL+3W1Hf6YDNkrHEeAeoGT2OGGRfryyHp9jpucWp3uH9Fpm4LVm/NTG3K9u8BJ3Ic9GkdG9BD1IB+ulnaLA4E8tTqj6I58HdYU0SztyEmq2XxDT2HmJa2APz6OAPihuPvftaU+eGYEOh9s9ctyTQw4L2buVeEja6A3njws1qoBeM7s/wgXib3gOH40Y/nx/LhcRlEMBk+JgwvQ17dRsc3xmPG5GZqTL7j0RTwPT4wfgTxMH8M/PYH71BXiHHoD6t5b8bh2QSs1XNZMA+E40c/ngfj4/kapDMPDvQbzXidIG82Mw9+fCYY3ykGPgNz+V7Xu9/AN84G9APHtnCKefwtxK/jgwJ9g37EjN9ohjx53i3WLaafAn/3icfTT15DwtpHXVtzzPsUQ4YJ5rKpHwf86DfkYbRQX8gX5kMPIUfw15PR9uCjPfZQechtwN9rNfZPNGFGN8BnHuyPedxAbFj3QGZnhO/L53ioA8x58rXObN7HOrdmr3zUGR8swJMmiFAN757ak53rlrCXLPtT4WssuN+Y7Ir0UnoNHuTvN6qbprRRioI25f3O/XYHZ7R+kOAGucI7+fGODrxbUUZ1sGOOd+1/ujWlX9+ao4Mk/5g/TRefv4Hg4U6VpB/S/J/Xqvj8Qxb/780K4ucHLYd++d79Gw==","w":70,"h":70,"aspect":"fixed","title":"(FTG) ActivityType (Composite, Automated)"},
  {"xml":"dVNNj5swEP01ltpLBSZE22MC6V62qrS7Us8GJuCu8bCDE5L++o6NCRupK0Kw35sZv/mwyIr+8khq6H5iA0ZkB5EVhOjmVX8pwBghE92IrBRSJvwK+eMTNg1sMigC6/7jgNUfqB1bGFX5w8pAbg3b7sdBWWZGdzUQmO37ycvY12iQRLZjktrqCwfkX8H/H1ZfV3NetfM3t6oH/iwnVLRyM8LC5mMXOKjvnzFKkI6UHY9IvXIa7cyHqIHd1U6ftbu+XgeIvkbV0KFpgMa7kiwVkn57X7w1Y0l4sg1405TlTJ128DJwRAYm7hFjnetNpEdH+Aa/deO6iNRondIW6LY3Rg2jrkL8hJGGcHhV1IKLJorqF/3X0w/f8s2GSym3m/Qh2yaetfgUG+WdCeoTjfoMzzDOPh6FC1ewiZtK1W9tyOLXyRmWEnFWZrnzalbiQ/fg1KHRiw5CpxbaOwyorQslzPciL326nWpwivSRM426U+nTMAanHRFOYwwYLGJlk1u1inmWSote2v6ojVkgIbND6Z/bIJyBHFw+nez0QysfATkhurLJFBviLfJ5+pMOdNtFt+8RU3FA2pvrek94Ecdj2cabs2zXGxpM7y7wPw==","w":150,"h":90,"aspect":"fixed","title":"(FTG) ActivityType w/ ports"},
  {"xml":"dVNtj5swDP41kbZvFNRK97HA9b5smnR3fyAQF7ILMTOhtPv1c0JSrtJOAmL78RuPY1FUw/WF5Nj/RAVGFM+iqAjRrdJwrcAYkWdaiaIWeZ7xK/LTF+guoNkoCaz7TwA2v6F17GFk44vVATwY9i2nUVpGJnczEJDDn9m3UbZokERxZJC65hsn5Kfi7yfp++bOUreeeysH4CNVaGjDVgs3tpZN5tD98IqxhdyRtNMZaZBOo13xkDWgx9bpi3a399sIMdbIFno0Cmh6oETODjkJqJR3jhGJutyrj6xuVORLrx28jZyc9YXHxd32bvAU7gJF1kltge66MXKcdBPiM7YowvFdUgcuulj8EWfgYYJ2pklf4BUm/TcFwZXJUVFpZPvREc5W/Zqd4WLRzrUtD1WutXzqAZx8VjpVInQywT5gRG1dYGdfin3tR99LhUuEz/wvb2sPO2aklMbgciTCZbon5CYCk16bHOEHVOslqS36xsqzNiaZRF6cTlV9eLpP+ALk4Prlld19GsULIP8O3dhl0cr10WO/XuusB931Mewp2mScfHcP3RaAhTjepMaVSOq2esH1YTP/AQ==","w":150,"h":90,"aspect":"fixed","title":"(FTG) ActivityType w/ ports (Automated)"},
  {"xml":"pVZbc6s2EP41nmlfzoCANH7EiDicWjjEzgW/cYsAA6IYm8uv70oIx+k5p9OZJk7Q3j7tfrtCXmhW2a+boE4Ji5NiodkLzWoYa6dV2VtJUSyQksULDS8QUuBvgR5+YVWFVamDJqnanwSwME+iFjyKIOSbYWG8K8B3daqDCiyndigSYbn768zTWEWsYM1CM8HY0PA3AISPBf9vVr9/usOKTk+jCsoEHvMOYfNpmzSQ2LTtrBbZl89MpoDaJqhOH6wpgzZj1WQXqMJqRm12ydphP9SJjC2CKElZESfN6QslEStrdsraK+5ZRszUIS5+ZfWTCtSwcxUn3FWFPLsUgHY1bAWKDpoHurQtC2k+tQ07Jm9Z3KZSE7GqDbIqaa5yUQT1KQsFvgKauGH1Pmho0kqXoIl22cjN998MXQeO0Z2u3mt3CrdWbCM7yIObJDo3p+ySPCenKYZrkx6ojaUQBtGRiiq257aAVKQeMqtgJIIpEw5dJm1gx9mcR8PaYDbzgJplVSu4NVYLA/Ny0yBmnTR/QKUybxXxMoqCdWbTsO4kAYWHZFa5smVNQ4YrxlNbfWRFMasWSLMx/71OyCVp2qT/5cirN61cJwwKagZw6WRDuIcxHQslTTKayrCl1AVycug19PMAwUKOxyzKI/Uvx+ufMx3IqXWieaLnGdT+dQaB55ovszKgnCPOQhYFhZiEJz7c/IhoOGRty0pwEFmsro2/4fND/NxgmEVGeWzL+CwHp1rUhD+yng/9Smxpzlpl1sA6huGAd8Mkwmm+0AVa9XAUkPX06KLDsNLDt/4cjUoWPD4rEWaXjRZr8WBoZDAuURldSG52xFqOcRllzuOhPrzHVqjRpZOblIwOdfcRcrCnbC2Hxqg4xmtue9HczKHB+rU+oFR52jmKi2n2lHfpZqekjn24hJVHvfe4iNCyPexWXlTeUzfTuw3g+tWRemvu85zG6HUMkVo4lkJFjo/kbjMsyxAZin+NN/IQKXS/LvLDmzEedmbrrFMlflyN2+z+ciiX53j9oB3e3Uv49nL2IWaD+tpHr+cIvdA/ba5Xi6h0a/+tu8GBPNCyS96/izoc7HQkd+izwCtgT2PcWKvvYUnOkF/7lPed//7MnLW3dI7KQDClW7zCwKHu4Jdxk3sGYAzbna46mAywVt3MHMmRjiQHv9FDm/xF8EpG/yaGoK1l6uTB7138jCebrRFL4CjAGzxtg3As3pf9cdhh0pPCH8j43XGxr3ijZ7iWzvcfwU8VftCnTW5DD50O9Pr2wed5OCDr3viiuhP+QCxTdUcfcvJ6nt/Uc8jpwVcAH8sZULfCf47zdB5Hckqh990Oe8i1CfhHZ3dnKltsgt6+ifN64ILjvfH6IF8uj+Aj/GWcqI/XI/KyqeJy/5xyfw3qGraYnDmOm9Oza3F59nd08B/JnuM7kA+PJ4DvX3EnXiHHqT+c89HN0zcXfL1RyIq7p8Jfxok6OJ6o16aDuz9Avx1D8AR185m5iRMzwfFec8CziZBnXue4CU/OBvSD5L7ymov8e8gfk70JfYN+5Bzf7qY6Rd09wR7X64A/fPoT/RPXRsS68tq7su7XHCo8EiG7+GXyl3FTHXYP/EK9MB84hRohHh+l7SbGuu2heVPb5D9zJfunujCjW8Bz9/51HreQG8ERyPyMiH3FHE88wJwfv/LM513y3LujeeWZ7D3wp12SORd491wi7VA9Uf6S5Z8fLs+SXW6kht/WX+SfXeNfb9Af72zlv16L6OfX4hwwX1XDJOuTdHNpIvQN3f94bap/fNPU/3tzgvj5pVe4fvlO/Dc=","w":150,"h":90,"aspect":"fixed","title":"(FTG) ActivityType (Composite)"},
  {"xml":"pVZZc6pIFP41Vs283IJGMtdHpNFwx8bgkgTf2NKAQDMIsvz6Od2AmrvVVE3KaJ/tO9uH7UzRs3ZdukVEWBCmM8WYKXrJWDWcslYP03SGpDiYKXiGkAT/M7T6hVUWVqlwyzCvfhLAvCT0K/BIXY8nw8L4lILv8lK4OVguVZeGwvL0T83LWPosZeVM0cBYUu8PAISXDu8Ppz/v7nCiw6eau1kIH1MGr7zbBg0UNqSd1KL6bMfGElBVuvnlg5WZW8UsH+wCVVg1v4qvcdUduiIcY1PXDyOWBmF5+TQSn2UFu8TVDbceI9y6YoAeBt8bppkiLn4e931GqGR1HohgGRpoIsiwL6AGUDSwVdBFVZaOZp/llRvnYXmT09QtLrEn0CTQBCUrDm5Jw2p0ydlmXBU3l6Ffl5f4Gu7CS9xPQWELMwxGwXP9MxVVbesqhWSjHnLnsHt3yMWhs7ByjSCeMpWsciczDyhYnFdiiOpypmLOkMgNWDOaP6CX/VCDDPNZumnKGq0sWXMZAS9Vyc6hPrAH54yXsvyI03RSzZCyWun4aXFb/TUsq7D9JZflh1WsQwYNlB24NHFQRaOHOvBdisKYRmPYYtS5IyXoLfT+ZMBhXO8kjs/Kb56b78nqjnQ0/YmqE4eU33II5lrwY5y5lM+ITyH23VRs/oWzlnNfwR6rgKvgIKpY3hb9MM8P8feAoaUx5bEV41x0L4XoCX/ELSftUqTUJq00aeAcABngoR9EeEyvdIaWLVAZ6S/PFjp1y7n31tZ+L8Xu807yMbtulEAJOlUhnXr1M/9KEq0h+qIPMj82n0/F6T3QPYUuzESjpDepdfCRiW1pq5s0QOk5WHPbUbFik7rr1+KEIullb0oWpvFL0kSbvRSZxunq5Ta134PUR4vqtF/afvaVWvG82QCuk5+pveY+uyhAr72H5NTUJSpqfCZPm26ReUiVnFu8mnhIood1mpze1P601ypzHUnB87Lfxl+vp2xRB+uVcnq3rt7bsXYgZoPawkGvtY+O9G+D6+XUz6zCeWsecKAOtGjC92+iDxObDUlMuhN4KeRU+42+/OZlpIb6qpekbZz3HTPX9sI8Sx3BlG7xEsMM5yY+9pvEVgGj2+7nsolJB2fZirWenGlPEvDrbbRJjmKupHceYgja6tqcrJzWwjs82AyF6AJHgrnBp6ESjsX3cjh3e0xakjod6b+ZFnYku7dVS5/z/D34ycIP9rRJDNih2YB+vl05vA4T5LndH2VrwO+IrslW70BNdsvrG3YONa0cCfDxyAF5K/ynOHvO40hCKey+2WMbWQYBf7+29pq0xRrojYc4u4VZcLw33h/Uy+UefIT/GCf64/2IugwqWdw/odxfgb66LSY1x7ESWls6lyd/cw7+PTlwfBPq4fEE8J0b7jBXqHHYD595byXRmwW+di9kyTpQ4T/GiT44nujXoJ11OMG+TVXMCfrmnHmIE5zgeK8J4BlEyNNcp7gBb+QG7IMkjvSaiPpbqB+TgwZ7g30kHN9ohj5F3y3BNtfPAb+7+5P5HddARL/NtbXGvl8T6PBMhGzh4+A/xg19GC3MF/oFfuAIeoR4fB5tDzH64w61h94G/2lW4/5kCzi6BTzr4Nz4uIXaCPZB5s+IyCt4PMwBeH7+PGfO93HOrdVrtzmTgw3+tAlj8wrfPVdfOeUvlH/J8tcPl2XGrg9SyW/nT/LPru3PN+aPd7T0X69F9PNrcQqYrqpukOeD9HBpIvQFff3x2pT/+qLI//fmBPH+a1a4fvqx+y8=","w":150,"h":90,"aspect":"fixed","title":"(FTG) ActivityType (Composite, Automated)"},
  {"xml":"dVPBboMwDP2aXCcgotuuUNrTLmulnVNwIWsSoyQd7b5+JoS2TBsC4ff8nNixw3ipL1sr+u4NG1CMV4yXFtFPlr6UoBTLEtkwvmZZltDHss0/3jR4k15YMP6PADx8Qu1JocRh3GwdnLkRGugXg/U7Kph8R7RaKOn05Aq64Nhfe4hyJWroUDVg3SKJOadshMt0nb/OO1g8mwZGacJ4MXTSw66nFYkY6FSI67weM03JdJ1ocIjaVgnnou28xRN8yMZ3UXpE40tUaMM2PAkP8VRNa4ir6YDARuEu5jMKwskUoj61IbV5DYOGFEWNWtZRKc4enfyeAx2lLU27x35JFOg96iVHaEXIAsWLQ9h7TFqDF1Uj/VyDVOqhhopvnqvsVu2DZ7V+XeXjgj1K40Mb8oJe6n2ZPOUsJ1E5YrKJ+YXTG07venp5MTXyC6yHy78Dlj70dwtINdgrSYbYDPK+TDOYdCDbLkbxyIk4NO0t8j6tZMSRmWGc3xne70mQLq7RDw==","w":80,"h":30,"aspect":"fixed","title":"(FTG) Type"}]</mxlibrary>`

  const encodedLibPM = `<mxlibrary>[
  {"xml":"dVPbkppAEP0aqpKHTQ2MAX3k4mXjEpWNRn0DaWFkuDgzgPr1O4DEtSr7QNF9Tp/u6sso2E4vU+YXsZuHQBU8VrDN8lx0VnqxgVJFQyRUsKNoGpKfok2+YNWWRYXPIBP/EeTBCQ5CRlA/aIo5LdlKUi+n0CEkI4L4tCP61G3cc1Uurr2Ex37RmFxAdiD0W7ifeA5/jcZjb1nfZH3bXt82cDU5J4llFqe/GirjYxIXq1Owhz/2frsi0h1b9nwxHVoiNsa/irO/OZZUT0znFa/BnZ95UnhNNicirLLKyPXcOvi5DW2maNZ2EVZRUm2XEzNQ32bvM35Y6ccV1yv992gx28iQ0C3RyXHNZWSsd+gUqFryVrnYkFSZDWQjk91+Hs2nhpfyWWnvJG7NxVn+Bu75aMrune8KtuqYCHgv/EPTci2XJ7FYpM1AVWkeCaV2TnPWzgYjNLBHI4lzwfIEeibLMym3GHBy84N2kEj6EBJxd5tcLBe++EQzOJSMkwq8RtejBTCSgoAmr9wNKTgsH5DVbbICJuDy5aGonxY8hVxq2VWGXDv2Rf2hYn2oD7FhaKqu6gjDi4q7NDUJRdzF4e7eUAwkisUz5vPOj/5lf1ymNO531bv3W+3dx5toQ5+ezAc=","w":30,"h":30,"aspect":"fixed","title":"(PM) Initial"},
  {"xml":"dVNRj6IwEP41JHcPe6F0ZeFRQI2onGIOlbeiFeoVim0R3F+/BfRckzMpycz3zTdDZ6YadPNmwlGZLdgBUw2ONOhyxmRv5Y2LKdUMnRw06GmGoatPM8YvWNCxeok4LuR/BCw54b1UERQlbTGvIztJHjKKe+RICkR7+J64i3quKeT1LhAZKltTSFzsCf1xjOZzT0zT0fKKLuvlCHtxpP7BXK75ppmx4Xhh2tbcWdn2buRP4xRNYESHwdgE09ovXLEKfntWM77gaVmDSil36TWez4pJIhea4ajjidQJAAvJ6g9OmT/e7tMInvIkHE1NvdF3E2GFYqwCZ4tqScXg4jd2bgxUqrPJAagmjuUHIWpK2yxYtY0Hn8HAAnOH7tdpePY2CydT4ncbxeCkjG3AotpPomgY10U8DY7Hs6VLUcHVJigUP8qOqftTg06dEYnXJdq37ajVWBWWybxtNVDmkVDqMsp41zeo6++ubStcSM7+4tfMhhxkphg1A4djQT5R0vVeVz4+EHlz2xKcSSS/0RzvKy7IBYet7o6WmJMcS9yWU+MkpcDLB+T0w79gLnHzcrPAt52YYKa0/KpCrj37Bn4BaFqmBT8+DGACU4f4DcA+TX27j/Jgv6B6hkmayWcMid5P/2V/rLIybqt4d2/LfXcfj6gLfXpjXw==","w":30,"h":30,"aspect":"fixed","title":"(PM) Final"},
  {"xml":"dVK7csIwEPwatRnZjgvKYBIqGihSZoR1YIGs88jila/PnSSHMBMKzdzt7r0lqqa/Lr0auhVqsKJ6F1XjEUOy+msD1opSGi2qhShLSU+UH0/YIrJyUB5c+CcAtwdoAyms2nKxRSRjSL9GCwnZoT9+HdC4RE3Jo/Kx7hhuU5DHk9PAUimq+aUzATaDapm90HiEdaHnkgWZO3RhY76ZLFg+Bo9H+DQ6dASVrDDWNmjRx+yVlK/NbEa4smbvCGtpQCByfgYfTKvsWyZ6ozX3lHNOORw6BgeaKoycs56LekEIaBPUNk7BnXloT340Z1jDmBrk/tIiuBRcn266+LOfJWAPwd9IcslTsULKlzoFdmD2XQ7MN5JqTP7+N/h+OTLy1ic333Jy738mSh++1A8=","w":100.5,"h":20,"aspect":"fixed","title":"(PM) Fork / Join"},
  {"xml":"dVPbbptAEP0apPYhEewGHB5tnOQlVaUkUh+rBaawzcKSYTF2v76zF4wtJcKYnTkzZ+ca8aI7PqEY2h+6BhXxh4gXqLXxp+5YgFIRi2Ud8X3EWExvxB6/QBOHxoNA6M0nDrr8C5UhCyVKe9negZki291E6tGcFDh19jHZGHaVVhojviUQm/IbsdGvoP+L0/fVnE6N/6a96IA+C32JZ8yxEWROw6UFRTmt/i6P7kWHeJiojDxIc/KII7/S//b3WdDxOnAbwDd3k6NUooJWqxpwvKrZUkJmxevqrlVhqKe+BmuaUJRzKw28DsRIipmaSLrWdCrAo0H9Dr9kbdqgqXRvhOwBz7JSYhhl6fhj0tSohzeBDZhgIrB6lf8svLm9S3ma5jm7y/OcE9br59BH64pQTTjKA7zA6D2sFo6D6OsglKJ6b1wOPyejKJCgp7h6Ggzh47DXdmDEQy2XKFAbscDWYdCyN66A6S5K9zbZVtR6DvAfyjNEnTArS6UKP0lURf64tY9NTik9bxH1PAZP34wDoIHjlzOdXPToCTTFijQX8cmjN8ltwrP77J5vNizJkizmcJNwTzOHbliW1O9G3IJs2oU6LEwswng0Z/51jegQhmMRw2It4rrAzvRqv/8D","w":150,"h":120.00000000000001,"aspect":"fixed","title":"(PM) Activity"},
  {"xml":"dVNtb5tADP41SNuHVnAUwj4mpOmXTZPaSvs4HeDBrQdHjQnJfv18xxESqZV4OfuxH7+cHcR5e3pC2Tc/TAU6iB+DOEdjaD61pxy0DkSoqiDeB0KE/Abi8AkaOTTsJUJHHziY4i+UxBZaFjbY3oGpZtvdyOqBzhqcOn0fbQ670miDQbxlEOviC7Pxk/P36vR1NedTPf+TTrbAv4W+wAvm2Biic39twVmOq7+ro302Ph8hS1JHRecZceQ3+t9zPAs6XgduPfjqIjlKLUtojK4Ah5ueyZFMKwl8JwlH77H0Vljxtu1ruwSasaucc8TpT40ieOk5FCsmvl3WNdRqDw+E5g1+qYoarylNR1J1gBdZa9kPqnD8IWsqNP2rxBrIm0gsX9Q/C2/ukyh9SEORhfyKLGO0M9/9FVtnhHLEQR3hGYbZx2rh1Muu8kIhy7faVfFzJM2peD1n1vHMyDkTG7gFko+VWvJAQ3KBrUNvVEeut8kuSPa23EZWZvLwH67U5x0JW4bWZtoimmlYLJTW+Tx23Nn4cMj36bfLTBwBCU6fjnt0dUtPYDhX5JEJzzN6F91HcZqlWbzZiCiN0jCGuyieaSZ/H5YlmdcmbEDVzULtdymUfnLqC/+6YXzw47GIfucWcd1tZ3qz+v8B","w":150,"h":120.00000000000001,"aspect":"fixed","title":"(PM) Activity (Automated)"},
  {"xml":"pVbbkqJIEP0aI3YfpqMoxNZHBLSZtbDx0ja+bHCbAgRxEeTy9ZtVgGLM9MRGbLdKZebJU3kry5GoJNUysy8BST0/HonaSFSyNM3bVVIpfhyPMAq9kaiOMEbwHuHFF1aBW9HFzvxz/guH1Il8NwdEbDtsM5UbJzFg5wWor3kd+1w9+adgMczdNE6zkSiDMaPOH8AGLwU+B6s/H3BY0fYpne3Eh0dP72R3G2cDU15fhgiIsnj48zySTdrFg203D29hXrcWTv6k/7vdjxk5LzfKnXHHd+KUse36QRp7fnZ9qpmbJpf0GuadZ54VnUdfW8zE57I/yoWztDh7PoMKEH4ZANH2AluBooTugi7Ik7gzX/MsPfmH0MuDTuOm59wOz352l+PYvlxDh/Mj0HhZetnZGfXzDmJn7jZsmPn1RRIm4wnCUwRvPJ2C9ZyuuhYz58x3i+wa3vyNf219mNavLvbZ6wTHdk+UZ7Eu8hhC6fQQ2Rlmxm4jYRsnfm5rXtjHkaW53ZuZwyUNzzmvrTQfSSpLN7C9tOzMPyDTLm4BszTiOC3lLEvLa48I41hpxw4qKy5k9n+fiZuf5X715bgLgy4t/RRizWBkUN1avwkvgjiZTqbi6ysWJsIEif43QWxpyq4fjEVqjw0K/JAGPXV3lpDdTQ698z9OGCy68ejF7sz95vx9Neo61P55BsXfziDUmQ9+mNgUnnNWqtC1Yz4J72y4QyAUVSfN8zQBAI9ifm/8oOg/+N+AQ45DynzzlM2yfb3wnNQfYcWGfs63lHst6jWw9mA44MS3Il5cb3SE5xUcBay8vxn4WM/HzqEq3AaF9tsGuWp6W4me6NWSSGrp5ibujURySZRZ4yVuqL8dL8dPT3FEOtMjmZJGp8bOxbpqorWiUw/HJ2/JbHvRCHVqLz8uRxyg962ODJWG71EZrLYo0LXjzTmb1Pz0YhfP8uN2brrJlBrhuFwBr3U+UXPJMJvAwx+Ng4VYVxDlMb6RyaqeJQ6WkHX3lyIHI7pbxtHxIDXHrZzrywB5b/NmHU5vx2RWeMuFePw0bs5hX1jgs8LVxcIfhYv39C+N6YXYTYyLdSgHPBAHnpX+53eeh67qJYl0uuF8MewpNStl/t1JSAHx5e9RVVqfm1RfmjP9hGqiUrpW5yrUcKyr+2YVmRJw1OvtWNBVUsNaMEK5ISfakAhwjYlX0Z7XlTTWwIfgtSKPycKqDHWjtjZNJArnQVA3eGoSYVysL7tTvVVJRWKrJs133VAtZDamZChjtn8DOIHjoE+rSIMe6iXox+uFxeLQQR6bzV4wWv6aKLJgNBbEZFYsvrbnENPCQsCvdjMgrDm+9zPHzI9ElELvy61qYkMjgHcLYyujtSqDXhv4mRXUgvEdWH4QL5MbwHB858fzY/nwuDSKDIaPKMOLkFe9VknBeIyIFobC5B6vjwHfkB3j1yEe5k+A37rztnWFGNv+sJo3RhQcDMCaDZeRsaMc3/nxPBgfz1ejtbE7Qr91idcJ8mYzM/DjM8H4PiLg0wiX+7r2fi1fNxvQDxJZ6CPi8VcQv0p2MvQN+hExfq1s8+R5V0Q1mX4M/PUDT8YPXg0T5V7Xyujy/oggwxPhsqHuW3zn1+ahVVBfyBfmQw0gR/BXT51t4KMMeygPcmvxfa26/gkGzOga+IyddZ/HNcRGVBdkdkb4vnyO2zrAnJ+e68zmvatzZTTyvc5kZwKeln6o3+C75+aKx/M7ZV+y7PXT5Qk/Af2hnLH7eiAn6W0gPd/NX1/0P9+x/+kuxb++S3uH/urq7tbxT7coxi94+ot79PVFFP7vTQri41cyhz79iP4X","w":150,"h":120.00000000000001,"aspect":"fixed","title":"(PM) Activity (Composite)"},
  {"xml":"pVZbk6o4EP41Vu0+nKkQxNFHJOhw1uCgzgVftridAIK4CHL59dsJ4DB7zmxt1U7pkO7++kvfQpzIWlqvc+cS0swPkomsT2Qtz7KiW6W1FiTJBKPIn8hkgjGC7wSvvrBKwoouTh6ci184ZG4ceAUgEsflmxFhnCWAXZagvhZNEgj17K+Sx7D0siTLJ7IKxpy5vwEbfDT4P1r9/gGHFeueytlJA3gM9G5+twk2MBXNZYyAKMsPf5FHusv6eLDjFdEtKprOIsg/6f/s9uNGwSuMam88iJ0EZeJ4QZglfpBfP9XMy9JLdo2K3rPIy97DKYssdYrA/6dhKDrm4ud+fNQR51l59oWzBHlVIeywv0AMoKig7aALizTpzdciz07BW+QXYa/xsnPhROcgv8tJ4lyukSv4EWj8PLscnJwFRQ9xcm8ftdz8+KBIs+kM4TmCL57PwXrONn3vuXMeeGV+jW7BLrh2Plwb1Bfn7PeC63gnJrLYlkUCofR6iOwMw+R0kfCN06BwdD8a4sizwhnM3OGSRedCFF1ZThTC0w0dP6t68w/ItI9bwjyNJMkqNc+z6jogoiTRunmEysqrlUZmi/uw3IK8COovz4E06tI6yCDWHGYJNZ31m/QgybP5bC4/PmJpJs2QHHyT5I6m6vvBWZTuPKEwiFg4UPeHDDn9SLE7/8fRg0U/HoPYH8Z/OZhfnQEDav95BuV/nUGoszgRUeoweC55qSLPScQkPPOpj4BQJm5WwKwDQESxvDd+VPQf4m/EoSYR475FxmfZuV5ETuRHVPOhX4ot1UGLBg2sfRgOeBV0Il5db2yClzUcBaw9P5n42Cyn7ltdei2KnKcd8kh228i+7DeKTBvl5qXejcZqRbVF66deZDwdL8d3X3NltjBildHWYObBwwax0FYzmI+Tk7/mthfZjAzmrF8vRxyi572BTMKi57gKN3sUGvrx5p4tZr37iYcXxXG/tLx0zsxoWm2A1z6fmLXmmF3o49fWxVJiaIiJGJ/obNMsUhcryL77K7GLETusk/j4prTHvVoY6xD5T8t2G81vx3RR+uuVfHw3b+7bS2mDzwbXFxu/lh5+YX/oXC8lXmpe7LdqxANx4EUVvH8XeRjEqGhssJ3gS2BPpd1oy+9uSkuIr3iO68p+32XG2loYJ9RQwtiWLAnUcGqQl3YTWwpwNNv9VDIIbWAtmZHa0hNraQy41sKb+EXUlbb2yIfiraZO6cquTbIjnU2XqSZ4ENQNnrpCORfvy+HU7AmtaWI3tP1umMRGVmsppjbl+7eAkwQO+rSJdeihUYF+ul3ZPA4D5KnVvkhmx99QTZXM1oaYrJrH1/UcYlrZCPhJPwPSVuAHP2vK/WjMGPS+2hMLmzoFvFeaexVtiQp6feRn1VALzvfG84N4udwCRuB7P5Efz0fEpTNkcnzMOF6GvJotoSXnMWNWmhqXB7wxBXxLD5zfgHi4PwV++87b1RVi7PrDa96acfhmAtZqhYzMAxP43k/kwflEvjprzMMR+m0ook6QN5+ZkZ+YCc73GgOfToU81HXw6/j62YB+0NhGr7GIv4b4CT2o0DfoR8z59arLU+RdU2Jx/RT4mw88nX7w6phq97rWZp/3awwZnqiQTfLS4Xu/Lg+9hvpCvjAfJIQcwZ+cetvIRxv3UB3l1uGHWvX9k0yY0S3wmQf7Po9biI0SD2R+RsS+Yo67OsCcnz7Xmc97X+fabNV7nenBAjyrgsi4wbvn5snH8zPjL1n++enyhN+GwVjO+X09ktPsNpI+381fX/Q/37H/6S7Fv75LB4fh6urv1ulPtyjGD3j+i3v08UGW/u9NCuLHz2cB/fTr+m8=","w":150,"h":120.00000000000001,"aspect":"fixed","title":"(PM) Activity (Composite, Automated)"},
  {"xml":"dVPJcoMwDP0aXzssJZ0eC1lOvTSZ6dkBBdwYizFKk/TrKxuTQCedYZGenqRnWxZp0V42VnbNO1agRboSaWERabDaSwFaiyRSlUiXIkkifkWy/ica+2jUSQuGHiTg/gtKYoaWe9ds6YOZkS3wj3GRvrlvktG1GyBfr/1ADQNdWlIH6Yr4iJYlNKgrsP1Mgi/5IMHX9fjOd0gm6hPnzhfW03VsbPFkKnBUVpmfG0Ww7bg7A2feP8Yaat2aYjb7RlZ4Dtxay74Pdk8Wj/CpKmoC9aC0LlCj9W3SVbp+WbGMXGpVG8ZK3kqwjoiGtkHPM/t+D3NZHmsvbaxh0DAjL7FVZWgqT4S9+oFRA8tWpmZvwZ4FDsm9LxvfFE4ULZavi8wxWyC5qhQFYofKkN/1LOeHT62InjKRcU7hfLYZ+ePHNz++8/lhHUiSgo6Jyh12cyBHImwDNhzgN1iCy78jGE/OdQPIy7BXppzDIThGNIxp1ICqm5CWBkyGyapvqfeBZiPMyuiGER/d+1Xy1NlN+wU=","w":100,"h":30,"aspect":"fixed","title":"(PM) Artifact"}]</mxlibrary>`;

  const encodedLibTrace = `<mxlibrary>[
  {"xml":"jVNLb9swDP41ug7yK12OjZP2smFAO2DHQrZYW6ssGbQSJ/v1o2TJTYZ12MGwSH58fHywoh7OjyjG/quVoFlxYEWN1rrlNZxr0JrlXElW7Fmec/pY/vCBNQtWPgoE4/7iYJuf0DpCaNH4ZB7QQKcMK+5ZvhHDyIqdaSb/I1lTjF1Dj84FRSVap07KXV6MGIDkhKEEV7Doh0kzWnT/mcBD/x088BuerIalfIeiBTgR35eFyYLQpO2tloDTTWtuGQTLH6SC+1pGQLQpbOpz7sXbEUzukkqae+XgeaQKSJ5ptlR47wbf74yek0P7Bj+UdD1pKM6utcYJZQAjorVai3FSTYjISSPRjt8FduAixNgvcYTejNAecVIneIJJ/UpOcB6FkVFoRPvWoT0a+e3oNCWLesptaCfEksuHHsCJg1QpE1onkpmHaSrjQlOrHav2nlAvpJ2j+ZW4PC81ZJ6b0NrO94h2nmJAge0K+LTdlPyuLO8yXhb5503IRzWCjOBXpXVttcXQ2mJfHrKHam3ilYXzst5u1xU5ATo4f3gV2dUAH8ESZbwQZI5D8Yh4ObwH1fXRrajiFsWl6lbX9xujR1yKJMarS+L7dQfozfH/Bg==","w":120,"h":35,"aspect":"fixed","title":"(Trace) Begin-event"},
  {"xml":"jVNLb9swDP41vg7yK12OjZP2smFAO2DHQrZZW6tsGjQTJ/v1o2TZTYZ12MGwyO/jm4zSojs/kh7ar1iDjdJDlBaEyPOrOxdgbZQoU0fpPkoSJV+UPHyAxh5Vgybo+S8GWP6EioVhdemCOQL0YnwfJRvdDVG668vR/US24mFXyqNhr8h1xeZk+PLS6w5EXjji/ooW7GjRDEj8nwEc9d/OfXXdE1qYk2fSFcBJqn1xdcy4FV2LtgYab9pym79H/ijJm69JeEaFR56BpcmJE2/7P/JlyWhqDcPzICmIPMlgJe+WO9fsWJ4jE77BD1NzKxrxs6uwZ216oMCo0Fo9jKb0HpVoasLhu6YGOFB6/BLm52CC6kijOcETjObXYgTnQbvJeqHU1VtDeOzrb0e2EizoJXYvC6HnWM51B6wPtVkiEbJeYOWHaXr2Xc13Ub53BbW6xinAr1LL85xD7GrT1uJ0T4TTGBxqqlbCp+0mU3dZdherLE0+b3w8yRHqQH411hZokXxr0312iB/ytYlXiFJZsd2uG3ICYjh/eBLx1QAfAaVkughlCkNxjHA2qgXTtMEszcMaha1qVtP3A5NHWIpFDCe3iO+n7ak3l/8b","w":120,"h":35,"aspect":"fixed","title":"(Trace) End-event"},
  {"xml":"dVRdd6o6EP01rHXOGx9iy6Mg9lgrpfW7L3cFCBANCZIg6K8/E0Rt17rnQZzZ2XtmkkxGs7yifalQmc95gqlm+ZrlVZzLq1W0HqZUM3WSaNZYM00dfpo5+ceq0a3qJaowk/8j4NEexxIYFEUq2bhbHFLgujUYmTI002aowPAHRM0aqa9py3N5ha5sCPlNoJJ2mi7iVa2wTtRhy07eFUdRjHNOE1yJn1UXn5z2dFRJkqJY/ncCFuHsyrht01TuzxMQ8nzTihx1WYXELCb0l/zaJns+z/zpJs2lCBYnROjAitIPzXSPwt6ssuzlWENQtStu5ktU62ETL3YX9zIopi3L38aeUTXuQb4j4QFvYbcFZHXnaFAPCjtCrh5Kep6CPscZxWKuIk0Z2sdn1zu6JIirp2nC1zLWL6ATs7f1wnMnyFx5tp7/ecnjmeME6bitQ1Bmx1yVkh3i4m0yZeet7YDotNs/O0YT0Jmzi3bHtDo4aDfO2zVDljn4eh+JP0LMLt75q2jXT9t8EH6WzjpIQGocQ4OPLOwvNvYgNPXL7us1rPmKza0FJAraYOSx1aQOm4lO/Pp5WX/M/I9S7jbs+Dx7t7aZ4PIziMVHMconYxycT020ens6hFuPMCeNRvrrHso34FAmXmO+po26C2v8G9qjyYnEixJuHYAGWh2wXBaq/QwwIxQfsorXLHmvJSUM93gHYXXj0IQuXGrCm97JKBKit0tOmMSVf4KOv2FCVvyAPU551bWENRw7Q3sIKymh9BvuW5Mn31QRK5QQiHBbY1zV4SJKMgZujFUOAKAdJYkRHfULBUkS1Xduyplc9D34KGFDEpn3+xFwAoRl4A0f3pKXN8EVcLmUvOixmBckvttMIjidqveTipdLVGVY3gmUolKQ6F5DheMaXs8Jf2JBLneUSyTRg1VgifyEyL7M60tT+8TtP4eK8e0BvmAOIaozUJp+u4phXAePnmOS5b3M6kOh/uFnd+ljRIHRP+qb2w+tm/sYjh31x+z8Cw==","w":110,"h":31,"aspect":"fixed","title":"(Trace) Versioned Artifact"},
  {"xml":"dVRbd6o6EP41rLX3Gxex5VFQu62V2nrvy1kBAkRDgkkQ9NfvCaK2a539IM58880lycwYTlA0LwKV+YwnmBrOyHACwbm6SkUTYEoN2ySJ4QwN2zbhZ9jjf1it1mqWSGCm/seBR3scK2BQFOlkw9bYp8D1KxAyLRi2y1CB4Q+IhjPQX9tV5/IKXdmReNCvCCT5HuKEhSSctS66pjZkm/AaXGNtzBZbttHb2imKcc5pAv4/D1V8ctrRkVAkRbH6755FM25KSzldD36/GlurP29NqvMtoMxRW4pUmMWE/lJf22TPZ9losklzJcPFCRHac6L0w7D9o3Q3qyx7OVYQVJ+b2/kSVea8jhe7i3/pFZOG5W/DwBK1f1DvSAbAW7hNAVn9GepVvcKNkG/OFT1PwD/HGcVypiNNGNrHZz84+iSMxdMk4WsVmxfwk9O39SLwx8heBa6Z/3nJ46nnhemwqebgmR1zXUp2iIu38YSdt64HTqfd/tmz6pBOvV20O6bi4KHdMG/WDDl27+t9IP9IOb0E56+iWT9t8978s/TWYQKu1nFu8YGDR4uN25vb5mX39Tqv+IrNnAUkCptwELDVuJrXY5OMqudl9TEdfZRqt2HH5+m7s80kV59hLD+KQT4e4vB8qqPV29Nhvg0I89JoYL7uoXwLLmUc1PZrWuu3cIa/oYHqnCi8KKEVAKhhPADLVaFb1tLdh+JDJnjFkvdKUcJwh7cQ1i8OjevDoya87pSMIik7ueSEKSxGJ5iSGyaV4AcccMpF2xJOf+j13T5YUkLpN3zkjJ9Gto4oUEIgws3GuK7DR5RkugdjrHMAAG2pSIzooDMUJEl03/kpZ2rR9eCjhA1JVN6dR8INEJaB1n9oS17eHK6Az5XiRYfFvCDxXWYKwe2ITk8EL5dIZFjdCZSiUpLoXoPgCin0TcdxBVN1wp9YkssNLbBCo4Sorsz7+Cnc/HMRWd8G8AVzCCHOQKm742qG1c1sjkmWd25OFwp12yC7uz7WGgjdUN/UbtHd1MdCbak/9u1f","w":110,"h":31,"aspect":"fixed","title":"(Trace) Versioned Artifact (w/ version)"},
  {"xml":"dZLdTsMwDIWfJvdpA72HDnY1CbEHQFnjdoG0npxstDw9+Wm3VLBKlXLs88VOHCbqftySPB13qMAw8cJETYgurfqxBmNYybViYsPKkvufla93skXM8pMkGNw/AB4+oXHeYeQhFNvEZET6dzSQIgn/uABZjUPKLxWifV3cumkhQXWwnyVn4hkG9USE3142RlqrGx88uj6ULvyyxcHt9U+wF4uu0SDF7QSPX4hrY7J427ZlE7ayjvALsoyqDtVj5TOE50GBmvtIZwjd3b2nIjvYFrAHR5O3EBjp9GXNSZtkd/Vd0TfUQ7jgKTke+JqweKYGZlM+yIUbZ06sOSepA/eH84us21soTmaR89AXeXtcic7f3i8=","w":43,"h":40,"aspect":"fixed","title":"(Trace) Parent Version"}]</mxlibrary>`;


  const encodedLibSS = `<mxlibrary>[
  {"xml":" ","w":160,"h":40,"aspect":"fixed","title":"(S/S) Service"},
  {"xml":" ","w":160,"h":40,"aspect":"fixed","title":"(S/S) Storage"},
  {"xml":"jVTBbtswDP0aXwdHSrr02LhdLx06dLsXsk3b2mTTkOQ42dePkuVERp2hBgyIj3wkRT4o4Vl7etaib75jCSrhTwnPNKKdTu0pA6USlsoy4Y8JYyn9Cft2w7vx3rQXGjq7QsD8NxSWIpTIXbFH79xN8HsJptCytxI7AqdUKy7Peg2ZfDklCmhQlaDNso/2DRVMkAah3jFizT0zZy6vY+x5pplG9O5YnJXsqABP+GFspIWfPRUlx0izI6yxrbvPho45DhRZvuQXQBR/au3Q18FSGgi4kX/dcftlf8/3+5Sle07fV+eqsLMZKtS+C175j/BSY/9L6BrcdFMCCgoUlFJfbKVEb2TuL+CQFo+CrJcwcQdpKAZt5BHeIPTgUbTCiogIVjyV0oZuK6lU1NLd/fbAaGqHaZpH0BZON1WwiYb8DEip9ZlCRlnaJkTcTUpJG5B1E2jbgImw1/pCvWT7gbJzippr84lxnky+5GNVGbCxIukQdXSFvBJmM8hmXfOhxFGoAWZFs6WIInGsaWfWmDSYDTmwG5JxO6GQh672eTe7T29SO6f4IImrhDq6XoQIpXB80BpHE5BPLZmtLzkQdovFzHuKFMBWBMD+I4DFChf7ur5f03rj5+0f","w":160,"h":40,"aspect":"fixed","title":"(S/S) Real-world object"},
  {"xml":"dVJNbwIhEP01XBs+thqPda2empjae4MydaksYwBb7a8vC4vupvVAMm/mvXkDAxF1e145eWxeUIEh4pmI2iGGHLXnGowhnGpFxIJwTuMhfHmnylKVHqUDG/4R4PYTdiEyjNx2ZotUTJL2FQ3kjPfvRttDLpTWiTd29eFSJKD2sOkhJWIOVj05h98Rbg3uDm+NtjHdhLZzZTH8QBs2+qcTsA774PAANRp0qaWYzKo5j35zhyerQPWdlfRNAqwHaxkCOJsynFbZfKlNMcrX6Aa8+0ZscLcVYAvBXSLFgZFBf4110me4v/Ku0jVq2z3uJTMqOlZ4PLkd9KThEovuXHQPlIpHPqsYm06q6bhLkG4P4U+XGAxmv6XSqgrs11/g7Ztl9fAX/gI=","w":40.0035294117647,"h":40,"aspect":"fixed","title":"Trace - S/S Link"}]</mxlibrary>`;

  // plugin loaded last will be on top
  ui.loadLibrary(new LocalLibrary(ui, encodedLibSS, "FTG+PM - S/S"));
  ui.loadLibrary(new LocalLibrary(ui, encodedLibTrace, "FTG+PM - Trace"));
  ui.loadLibrary(new LocalLibrary(ui, encodedLibPM, "FTG+PM - PM"));
  ui.loadLibrary(new LocalLibrary(ui, encodedLibFTG, "FTG+PM - FTG"));
  ui.loadLibrary(new LocalLibrary(ui, encodedLibCommon, "FTG+PM - Common"));

  // For debugging only
  window.ui = ui;
});
