Home Reference Source Test Repository

src/js/ui/sidebar-algorithm.js

import SidebarContent from '../ui/sidebar-content';
import Node from '../data/node/node';
import Edge from '../data/edge/edge';
import Stepper from '../algorithm/stepper';
import scrollToElement from '../util/scroll-to-element';

/**
 * Sidebar that accompanies the AlgorithmTool.
 * @class SidebarAlgorithm
 */
class SidebarAlgorithm extends SidebarContent {

  /**
   * An instance of a Stepper. Used for stepping through algorithm results.
   * @type {Stepper}
   */
  stepper;

  /**
   * Instance of the current algorithm.
   * @type {AbstractAlgorithm}
   */
  curAlgorithm;

  /**
   * Constructor calls the super, creates a new stepper object and then assigns algorithm specific button listeners to the sidebar
   * @param  {Graph} graph - Reference to the master graph object
   * @constructs SidebarAlgorithm
   */
  constructor(graph) {
    super(graph);
    this.graph = graph;
    this.stepper = new Stepper();
    document.getElementById('sidebar').addEventListener('click', (event) => {
      if (event.target.classList.contains('algorithm-next-btn')) {
        this.stepper.stepForward();
        this.updateStepGUI();
      } else if (event.target.classList.contains('algorithm-prev-btn')) {
        this.stepper.stepBackward();
        this.updateStepGUI();
      } else if (event.target.classList.contains('algorithm-play-toggle-btn')) {
        if (this.stepper.interval !== null) {
          this.stepper.pause();
          event.target.textContent = 'Play';
        } else if (this.stepper.interval === null && this.stepper.result !== null) {
          this.stepper.play(() => {
            this.updateStepGUI();
          });
          event.target.textContent = 'Pause';
        }
      } else if (event.target.classList.contains('algorithm-speed-up-btn')) {
        if (this.stepper.speedUp() === true) {
          let speed = this.tabs.getTabContentElement('algorithm').querySelector('.speed-notch--active');
          if (speed) {
            speed.classList.remove('speed-notch--active');
            speed.nextElementSibling.classList.add('speed-notch--active');
          }
        }
      } else if (event.target.classList.contains('algorithm-slow-down-btn')) {
        if (this.stepper.slowDown() === true) {
          let speed = this.tabs.getTabContentElement('algorithm').querySelector('.speed-notch--active');
          if (speed) {
            speed.classList.remove('speed-notch--active');
            speed.previousElementSibling.classList.add('speed-notch--active');
          }
        }
      }
    });

    document.getElementById('sidebar').addEventListener('mouseover', (event) => {
      this.hoverEvent(true);
    });

    document.getElementById('sidebar').addEventListener('mouseout', (event) => {
      this.hoverEvent(false);
    });
  }

  /**
   * Called to check for hovering over a graph link (link that associates text with an object in the Graph).
   * @param  {boolean} bool - Whether or not the graph link is currently being hovered over.
   */
  hoverEvent(bool) {
    if (event.target.classList.contains('graph-link')) {
      let type = event.target.getAttribute('data-type');
      let id = event.target.getAttribute('data-id');
      this.toggleHover(type, parseInt(id, 10), bool);
    }
  }

  /**
   * Runs the current algorithm and displays the results in the sidebar.
   */
  run() {
    this.stepper.resetGraph();
    this.curAlgorithm.reset();
    for (let inputName of Object.keys(this.curAlgorithm.inputs)) {
      this.curAlgorithm[inputName] = this.curAlgorithm.inputs[inputName];
    }

    let hasNextStep = true;
    while (hasNextStep) {
      hasNextStep = this.curAlgorithm.step();
    }

    this.stepper.setResult(this.curAlgorithm.result);

    this.updateStepGUI();

    let stepsHtml = `
      <li class="stepper__step stepper__step--active" data-index="-1">
        <div class="stepper__step__header">
          <div class="stepper__step__header__number"></div><div class="stepper__step__header__description">Initial state</div>
        </div>
        <div class="stepper__step__details">
          <div class="stepper__step__details__spacer"></div>
          <div class="stepper__step__details__content"></div>
        </div>
      </li>
    `;

    for (let stepIndex = 0; stepIndex < this.stepper.result.timeline.length; stepIndex++) {
      stepsHtml += `
        <li class="stepper__step" data-index="${stepIndex}">
          <div class="stepper__step__header">
            <div class="stepper__step__header__number">${stepIndex + 1}</div><div class="stepper__step__header__description">${this.stepper.result.timeline[stepIndex].description}</div>
          </div>
          <div class="stepper__step__details">
            <div class="stepper__step__details__spacer"></div>
            <div class="stepper__step__details__content"></div>
          </div>
        </li>
      `;
    }

    stepsHtml += `
      <li class="stepper__step" data-index="${this.stepper.result.timeline.length}">
        <div class="stepper__step__header">
          <div class="stepper__step__header__number"></div><div class="stepper__step__header__description">Finished</div>
        </div>
        <div class="stepper__step__details">
          <div class="stepper__step__details__spacer"></div>
          <div class="stepper__step__details__content"></div>
        </div>
      </li>
    `;

    let stepper = this.tabs.getTabContentElement('algorithm').querySelector('.stepper');
    stepper.innerHTML = stepsHtml;

    let prevBtn = document.getElementById('sidebar').querySelector('.algorithm-prev-btn');
    let nextBtn = document.getElementById('sidebar').querySelector('.algorithm-next-btn');
    let playBtn = document.getElementById('sidebar').querySelector('.algorithm-play-toggle-btn');
    let fastBtn = document.getElementById('sidebar').querySelector('.algorithm-speed-up-btn');
    let slowBtn = document.getElementById('sidebar').querySelector('.algorithm-slow-down-btn');
    fastBtn.disabled = false;
    slowBtn.disabled = false;
    prevBtn.disabled = true;
    nextBtn.disabled = false;
    playBtn.disabled = false;
  }

  /**
   * Update the state of the buttons in the sidebar.
   */
  updateStepGUI() {
    let prevBtn = document.getElementById('sidebar').querySelector('.algorithm-prev-btn');
    let nextBtn = document.getElementById('sidebar').querySelector('.algorithm-next-btn');
    let stepIndex = this.stepper.result.stepIndex;
    let maxStepIndex = this.stepper.result.timeline.length;
    if (stepIndex === maxStepIndex) {
      nextBtn.disabled = true;
    } else if (stepIndex === -1) {
      prevBtn.disabled = true;
    }
    if (stepIndex > -1) {
      prevBtn.disabled = false;
    }
    if (stepIndex < maxStepIndex) {
      nextBtn.disabled = false;
    }

    let playBtn = document.getElementById('sidebar').querySelector('.algorithm-play-toggle-btn');
    if (stepIndex === maxStepIndex) {
      playBtn.textContent = 'Play';
      playBtn.disabled = true;
    } else if (stepIndex < maxStepIndex) {
      playBtn.disabled = false;
    }

    let activeStep = this.tabs.getTabContentElement('algorithm').querySelector('.stepper__step--active');
    if (activeStep) {
      activeStep.classList.remove('stepper__step--active');
    }

    let step = this.tabs.getTabContentElement('algorithm').querySelector(`.stepper__step[data-index="${this.stepper.result.stepIndex}"]`);
    if (step) {
      step.classList.add('stepper__step--active');
      let resultsContainer = document.getElementById('sidebar').querySelector('.algorithm-results');
      scrollToElement(resultsContainer, step.offsetTop - resultsContainer.offsetTop - 10, 100, 'easeInOutSine');
    }
  }

  /**
   * Toggle the hover state of the associated Node or Edge when a graph link is being hovered.
   * @param  {string}  type - String containing the type of the object.
   * @param  {number}  id - ID of the object.
   * @param  {boolean} isHovering - Whether or not the mouse is hovering over the graph link.
   */
  toggleHover(type, id, isHovering) {
    if (type === 'node') {
      this.graph.forEachNode((node) => {
        if (node.id === id) {
          node.isSelected = isHovering;
        }
      });
    } else if (type === 'edge') {
      this.graph.forEachEdge((edge) => {
        if (edge.id === id) {
          edge.isSelected = isHovering;
        }
      });
    }
  }

  /**
   * Called when the sidebar content is switched to this.
   * @override
   */
  display() {
    this.tabs.replaceTabs({
      algorithm: 'Algorithm'
    });
    this.tabs.setTabScroll('algorithm', false);
    this.tabs.selectTab('algorithm');
  }

  /**
   * Create a graph link element using the specified Node or Edge.
   * @param  {(Node|Edge)} obj - Node or Edge object to create a graph link for.
   * @return {string} - String containing the HTML for the graph link to the specified Node or Edge.
   */
  createLinkElement(obj) {
    let id = obj.id;
    let name;

    let type;
    if (obj instanceof Node) {
      type = 'node';
      name = `Node ${id}`;
    } else if (obj instanceof Edge) {
      type = 'edge';
      name = `Edge ${id}`;
    }

    return `
      <div class="graph-link" data-type="${type}" data-id="${id}">${name}</div>
    `;
  }

  /**
   * Set the current algorithm to a new instance of the given algorithm class.
   * @param {Object} AlgorithmClass - The algorihtm class
   */
  setAlgorithm(AlgorithmClass) {
    this.curAlgorithm = new AlgorithmClass(this.graph);
    this.stepper.reset();

    let html = `
      <div class="algorithm-content">
        <div class="algorithm-controls">
          <div class="algorithm-step-controls">
            <button type="button" class="algorithm-prev-btn btn-raised" disabled>Previous step</button>
            <button type="button" class="algorithm-play-toggle-btn btn-raised" disabled>Play</button>
            <button type="button" class="algorithm-next-btn btn-raised" disabled>Next step</button>
          </div>
          <div class="algorithm-speed">
            <button type="button" class="algorithm-slow-down-btn btn-raised" disabled>Slower</button>
            <div class="speed-notch"><<</div>
            <div class="speed-notch"><</div>
            <div class="speed-notch speed-notch--active">.</div>
            <div class="speed-notch">></div>
            <div class="speed-notch">>></div>
            <button type="button" class="algorithm-speed-up-btn btn-raised" disabled>Faster</button>
          </div>
        </div>
        <div class="algorithm-results">
          <ul class="stepper"></ul>
        </div>
      </div>
    `;
    this.tabs.getTabContentElement('algorithm').innerHTML = html;
    this.tabs.setTabContent('algorithm', html);
  }

  /**
   * Reset the graph. Calls resetGraph() of the Stepper instance.
   */
  resetGraph() {
    this.stepper.resetGraph();
  }

  /**
   * Update does nothing now, at this point might not do anything ever since changing the algorithm type occurs in algorithm-tool and the top-bar.
   */
  update() {}
}

export { SidebarAlgorithm };
export default SidebarAlgorithm;