<commit_before>import $ from '../../utils/dom';
import Utils from '../../utils/utils';

function isEventSupported() {
  const eventName = 'onwheel';
  let isSupported = eventName in document;

  if (!isSupported) {
    const element = document.createElement('div');
    element.setAttribute(eventName, 'return;');
    isSupported = typeof element[eventName] === 'function';
  }

  if (!isSupported &&
    document.implementation &&
    document.implementation.hasFeature &&
    // always returns true in newer browsers as per the standard.
    // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature
    document.implementation.hasFeature('', '') !== true
  ) {
    // This is the only way to test support for the `wheel` event in IE9+.
    isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
  }

  return isSupported;
}
const Mousewheel = {
  bound: false,
  lastScrollTime: Utils.now(),
  event: (function getEvent() {
    if (window.navigator.userAgent.indexOf('firefox') > -1) return 'DOMMouseScroll';
    return isEventSupported() ? 'wheel' : 'mousewheel';
  }()),
  normalize(e) {
    // Reasonable defaults
    const PIXEL_STEP = 10;
    const LINE_HEIGHT = 40;
    const PAGE_HEIGHT = 800;

    let sX = 0;
    let sY = 0; // spinX, spinY
    let pX = 0;
    let pY = 0; // pixelX, pixelY

    // Legacy
    if ('detail' in e) {
      sY = e.detail;
    }
    if ('wheelDelta' in e) {
      sY = -e.wheelDelta / 120;
    }
    if ('wheelDeltaY' in e) {
      sY = -e.wheelDeltaY / 120;
    }
    if ('wheelDeltaX' in e) {
      sX = -e.wheelDeltaX / 120;
    }

    // side scrolling on FF with DOMMouseScroll
    if ('axis' in e && e.axis === e.HORIZONTAL_AXIS) {
      sX = sY;
      sY = 0;
    }

    pX = sX * PIXEL_STEP;
    pY = sY * PIXEL_STEP;

    if ('deltaY' in e) {
      pY = e.deltaY;
    }
    if ('deltaX' in e) {
      pX = e.deltaX;
    }

    if ((pX || pY) && e.deltaMode) {
      if (e.deltaMode === 1) { // delta in LINE units
        pX *= LINE_HEIGHT;
        pY *= LINE_HEIGHT;
      } else { // delta in PAGE units
        pX *= PAGE_HEIGHT;
        pY *= PAGE_HEIGHT;
      }
    }

    // Fall-back if spin cannot be determined
    if (pX && !sX) {
      sX = (pX < 1) ? -1 : 1;
    }
    if (pY && !sY) {
      sY = (pY < 1) ? -1 : 1;
    }

    return {
      spinX: sX,
      spinY: sY,
      pixelX: pX,
      pixelY: pY,
    };
  },
  handle(event) {
    let e = event;
    const swiper = this;
    const params = swiper.params.mousewheel;
    if (e.originalEvent) e = e.originalEvent; // jquery fix
    let delta = 0;
    const rtlFactor = swiper.rtl ? -1 : 1;

    const data = Mousewheel.normalize(e);

    if (params.forceToAxis) {
      if (swiper.isHorizontal()) {
        if (Math.abs(data.pixelX) > Math.abs(data.pixelY)) delta = data.pixelX * rtlFactor;
        else return;
      } else if (Math.abs(data.pixelY) > Math.abs(data.pixelX)) delta = data.pixelY;
      else return;
    } else {
      delta = Math.abs(data.pixelX) > Math.abs(data.pixelY) ? -data.pixelX * rtlFactor : -data.pixelY;
    }

    if (delta === 0) return;

    if (params.invert) delta = -delta;

    if (!swiper.params.freeMode) {
      if (Utils.now() - Mousewheel.lastScrollTime > 60) {
        if (delta < 0) {
          if ((!swiper.isEnd || swiper.params.loop) && !swiper.animating) {
            swiper.slideNext();
            swiper.emit('scroll', e);
          } else if (params.releaseOnEdges) return true;
        } else if ((!swiper.isBeginning || swiper.params.loop) && !swiper.animating) {
          swiper.slidePrev();
          swiper.emit('scroll', e);
        } else if (params.releaseOnEdges) return true;
      }
      Mousewheel.lastScrollTime = (new window.Date()).getTime();
    } else {
      // Freemode or scrollContainer:
      let position = swiper.getWrapperTranslate() + (delta * params.sensitivity);
      const wasBeginning = swiper.isBeginning;
      const wasEnd = swiper.isEnd;

      if (position >= swiper.minTranslate()) position = swiper.minTranslate();
      if (position <= swiper.maxTranslate()) position = swiper.maxTranslate();

      swiper.setWrapperTransition(0);
      swiper.setWrapperTranslate(position);
      swiper.updateProgress();
      swiper.updateActiveIndex();

      if ((!wasBeginning && swiper.isBeginning) || (!wasEnd && swiper.isEnd)) {
        swiper.updateClasses();
      }

      if (swiper.params.freeModeSticky) {
        clearTimeout(Mousewheel.timeout);
        Mousewheel.timeout = setTimeout(() => {
          swiper.slideReset();
        }, 300);
      } else if (swiper.params.lazyLoading && swiper.lazy) {
        swiper.lazy.load();
      }
      // Emit event
      swiper.emit('scroll', e);

      // Stop autoplay
      if (swiper.params.autoplay && swiper.params.autoplayDisableOnInteraction) swiper.stopAutoplay();

      // Return page scroll on edge positions
      if (position === 0 || position === swiper.maxTranslate()) return;
    }

    if (e.preventDefault) e.preventDefault();
    else e.returnValue = false;
    return false;
  },
  enable() {
    const swiper = this;
    if (!Mousewheel.event) return false;
    if (swiper.mousewheel.enabled) return false;
    let target = swiper.$el;
    if (swiper.params.mousewheel.eventsTarged !== 'container') {
      target = $(swiper.params.mousewheel.eventsTarged);
    }
    if (!Mousewheel.bound) {
      Mousewheel.bound = true;
      Mousewheel.handle = Mousewheel.handle.bind(swiper);
    }
    target.on(Mousewheel.event, Mousewheel.handle);
    swiper.mousewheel.enabled = true;
    return true;
  },
  disable() {
    const swiper = this;
    if (!Mousewheel.event) return false;
    if (!swiper.mousewheel.enabled) return false;
    let target = swiper.$el;
    if (swiper.params.mousewheel.eventsTarged !== 'container') {
      target = $(swiper.params.mousewheel.eventsTarged);
    }
    target.off(Mousewheel.event, Mousewheel.handle);
    swiper.mousewheel.enabled = false;
    return true;
  },
};

export default {
  name: 'mousewheel',
  params: {
    mousewheel: {
      enabled: false,
      releaseOnEdges: false,
      invert: false,
      forceToAxis: false,
      sensitivity: 1,
      eventsTarged: 'container',
    },
  },
  create() {
    const swiper = this;
    Utils.extend(swiper, {
      mousewheel: {
        enabled: false,
        enable: Mousewheel.enable.bind(swiper),
        disable: Mousewheel.disable.bind(swiper),
      },
    });
  },
  on: {
    init() {
      const swiper = this;
      if (swiper.params.mousewheel.enabled) swiper.mousewheel.enable();
    },
    destroy() {
      const swiper = this;
      if (swiper.mousewheel.enabled) swiper.mousewheel.disable();
    },
  },
};
<commit_msg>Disable lazy<commit_after>import $ from '../../utils/dom';
import Utils from '../../utils/utils';

function isEventSupported() {
  const eventName = 'onwheel';
  let isSupported = eventName in document;

  if (!isSupported) {
    const element = document.createElement('div');
    element.setAttribute(eventName, 'return;');
    isSupported = typeof element[eventName] === 'function';
  }

  if (!isSupported &&
    document.implementation &&
    document.implementation.hasFeature &&
    // always returns true in newer browsers as per the standard.
    // @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature
    document.implementation.hasFeature('', '') !== true
  ) {
    // This is the only way to test support for the `wheel` event in IE9+.
    isSupported = document.implementation.hasFeature('Events.wheel', '3.0');
  }

  return isSupported;
}
const Mousewheel = {
  bound: false,
  lastScrollTime: Utils.now(),
  event: (function getEvent() {
    if (window.navigator.userAgent.indexOf('firefox') > -1) return 'DOMMouseScroll';
    return isEventSupported() ? 'wheel' : 'mousewheel';
  }()),
  normalize(e) {
    // Reasonable defaults
    const PIXEL_STEP = 10;
    const LINE_HEIGHT = 40;
    const PAGE_HEIGHT = 800;

    let sX = 0;
    let sY = 0; // spinX, spinY
    let pX = 0;
    let pY = 0; // pixelX, pixelY

    // Legacy
    if ('detail' in e) {
      sY = e.detail;
    }
    if ('wheelDelta' in e) {
      sY = -e.wheelDelta / 120;
    }
    if ('wheelDeltaY' in e) {
      sY = -e.wheelDeltaY / 120;
    }
    if ('wheelDeltaX' in e) {
      sX = -e.wheelDeltaX / 120;
    }

    // side scrolling on FF with DOMMouseScroll
    if ('axis' in e && e.axis === e.HORIZONTAL_AXIS) {
      sX = sY;
      sY = 0;
    }

    pX = sX * PIXEL_STEP;
    pY = sY * PIXEL_STEP;

    if ('deltaY' in e) {
      pY = e.deltaY;
    }
    if ('deltaX' in e) {
      pX = e.deltaX;
    }

    if ((pX || pY) && e.deltaMode) {
      if (e.deltaMode === 1) { // delta in LINE units
        pX *= LINE_HEIGHT;
        pY *= LINE_HEIGHT;
      } else { // delta in PAGE units
        pX *= PAGE_HEIGHT;
        pY *= PAGE_HEIGHT;
      }
    }

    // Fall-back if spin cannot be determined
    if (pX && !sX) {
      sX = (pX < 1) ? -1 : 1;
    }
    if (pY && !sY) {
      sY = (pY < 1) ? -1 : 1;
    }

    return {
      spinX: sX,
      spinY: sY,
      pixelX: pX,
      pixelY: pY,
    };
  },
  handle(event) {
    let e = event;
    const swiper = this;
    const params = swiper.params.mousewheel;
    if (e.originalEvent) e = e.originalEvent; // jquery fix
    let delta = 0;
    const rtlFactor = swiper.rtl ? -1 : 1;

    const data = Mousewheel.normalize(e);

    if (params.forceToAxis) {
      if (swiper.isHorizontal()) {
        if (Math.abs(data.pixelX) > Math.abs(data.pixelY)) delta = data.pixelX * rtlFactor;
        else return;
      } else if (Math.abs(data.pixelY) > Math.abs(data.pixelX)) delta = data.pixelY;
      else return;
    } else {
      delta = Math.abs(data.pixelX) > Math.abs(data.pixelY) ? -data.pixelX * rtlFactor : -data.pixelY;
    }

    if (delta === 0) return;

    if (params.invert) delta = -delta;

    if (!swiper.params.freeMode) {
      if (Utils.now() - Mousewheel.lastScrollTime > 60) {
        if (delta < 0) {
          if ((!swiper.isEnd || swiper.params.loop) && !swiper.animating) {
            swiper.slideNext();
            swiper.emit('scroll', e);
          } else if (params.releaseOnEdges) return true;
        } else if ((!swiper.isBeginning || swiper.params.loop) && !swiper.animating) {
          swiper.slidePrev();
          swiper.emit('scroll', e);
        } else if (params.releaseOnEdges) return true;
      }
      Mousewheel.lastScrollTime = (new window.Date()).getTime();
    } else {
      // Freemode or scrollContainer:
      let position = swiper.getWrapperTranslate() + (delta * params.sensitivity);
      const wasBeginning = swiper.isBeginning;
      const wasEnd = swiper.isEnd;

      if (position >= swiper.minTranslate()) position = swiper.minTranslate();
      if (position <= swiper.maxTranslate()) position = swiper.maxTranslate();

      swiper.setWrapperTransition(0);
      swiper.setWrapperTranslate(position);
      swiper.updateProgress();
      swiper.updateActiveIndex();

      if ((!wasBeginning && swiper.isBeginning) || (!wasEnd && swiper.isEnd)) {
        swiper.updateClasses();
      }

      if (swiper.params.freeModeSticky) {
        clearTimeout(Mousewheel.timeout);
        Mousewheel.timeout = setTimeout(() => {
          swiper.slideReset();
        }, 300);
      }
      /* else if (swiper.params.lazyLoading && swiper.lazy) {
        swiper.lazy.load();
      } */
      // Emit event
      swiper.emit('scroll', e);

      // Stop autoplay
      if (swiper.params.autoplay && swiper.params.autoplayDisableOnInteraction) swiper.stopAutoplay();

      // Return page scroll on edge positions
      if (position === 0 || position === swiper.maxTranslate()) return;
    }

    if (e.preventDefault) e.preventDefault();
    else e.returnValue = false;
    return false;
  },
  enable() {
    const swiper = this;
    if (!Mousewheel.event) return false;
    if (swiper.mousewheel.enabled) return false;
    let target = swiper.$el;
    if (swiper.params.mousewheel.eventsTarged !== 'container') {
      target = $(swiper.params.mousewheel.eventsTarged);
    }
    if (!Mousewheel.bound) {
      Mousewheel.bound = true;
      Mousewheel.handle = Mousewheel.handle.bind(swiper);
    }
    target.on(Mousewheel.event, Mousewheel.handle);
    swiper.mousewheel.enabled = true;
    return true;
  },
  disable() {
    const swiper = this;
    if (!Mousewheel.event) return false;
    if (!swiper.mousewheel.enabled) return false;
    let target = swiper.$el;
    if (swiper.params.mousewheel.eventsTarged !== 'container') {
      target = $(swiper.params.mousewheel.eventsTarged);
    }
    target.off(Mousewheel.event, Mousewheel.handle);
    swiper.mousewheel.enabled = false;
    return true;
  },
};

export default {
  name: 'mousewheel',
  params: {
    mousewheel: {
      enabled: false,
      releaseOnEdges: false,
      invert: false,
      forceToAxis: false,
      sensitivity: 1,
      eventsTarged: 'container',
    },
  },
  create() {
    const swiper = this;
    Utils.extend(swiper, {
      mousewheel: {
        enabled: false,
        enable: Mousewheel.enable.bind(swiper),
        disable: Mousewheel.disable.bind(swiper),
      },
    });
  },
  on: {
    init() {
      const swiper = this;
      if (swiper.params.mousewheel.enabled) swiper.mousewheel.enable();
    },
    destroy() {
      const swiper = this;
      if (swiper.mousewheel.enabled) swiper.mousewheel.disable();
    },
  },
};
<|endoftext|>


<commit_before>import angular from 'angular';
import {grid} from 'egrid-core';
import Graph from 'eg-graph/lib/graph';
import SugiyamaLayouter from 'eg-graph/lib/layouter/sugiyama';
import angularfireModule from 'angularfire';
import shinsekaiModule from 'shinsekai';
import commonModules from '../common-modules';
import openAddConstructDialogModule from '../services/open-add-construct-dialog';
import rootRefModule from '../services/root-ref';

const startFrom = ([x, y]) => {
  return `M${x} ${y}`;
};

const lineTo = ([x, y]) => {
  return ` L ${x} ${y}`;
};

const curveTo = ([x1, y1], [x2, y2], ltor) => {
  const dx = x2 - x1,
        dy = y2 - y1;
  return ltor
    ? ` q ${dx / 4} ${0}, ${dx / 2} ${dy / 2} q ${dx / 4} ${dy / 2}, ${dx / 2} ${dy / 2}`
    : ` q ${0} ${dy / 4}, ${dx / 2} ${dy / 2} q ${dx / 2} ${dy / 4}, ${dx / 2} ${dy / 2}`;
};

const svgPath = (points, ltor) => {
  let d = `${startFrom(points[0])}${lineTo(points[1])}`;
  for (let i = 3; i < points.length; i += 2) {
    d += curveTo(points[i - 2], points[i - 1], ltor) + lineTo(points[i]);
  }
  return d;
};

const load = (graph, constructs) => {
  for (const {u, d} of constructs.vertices) {
    graph.addVertex(u, d);
  }
  for (const {u, v, d} of constructs.edges) {
    graph.addEdge(u, v, d);
  }
};

const layout = (constructs, oldLayout) => {
  const graph = new Graph(),
        layouter = new SugiyamaLayouter();
  load(graph, constructs);
  layouter
    .layerMargin(200)
    .vertexMargin(50)
    .vertexWidth(() => 10)
    .vertexHeight(() => 10)
    .edgeWidth(() => 1);
  const positions = layouter.layout(graph);
  const vertices = graph.vertices().map((u) => {
    const d = positions.vertices[u];
    return {
      u: u,
      x: d.x,
      y: d.y,
      height: d.height,
      width: d.width,
      text: graph.vertex(u).text
    };
  });
  const edges = graph.edges().map(([u, v]) => {
    const points = positions.edges[u][v] ? positions.edges[u][v].points : positions.edges[v][u].points;
    let oldPoints;
    if (oldLayout) {
      if (oldLayout.positions.edges[u] && oldLayout.positions.edges[u][v]) {
        oldPoints = Array.from(oldLayout.positions.edges[u][v].points);
        if (points.length > oldPoints.length) {
          while (points.length > oldPoints.length) {
            oldPoints.push(oldPoints[oldPoints.length - 1]);
          }
        }
      } else {
        const x1 = oldLayout.positions.vertices[u] ? oldLayout.positions.vertices[u].x : 0,
              y1 = oldLayout.positions.vertices[u] ? oldLayout.positions.vertices[u].y : 0,
              x2 = oldLayout.positions.vertices[v] ? oldLayout.positions.vertices[v].x : 0,
              y2 = oldLayout.positions.vertices[v] ? oldLayout.positions.vertices[v].y : 0;
        oldPoints = [
          [x1, y1],
          [x1, y1],
          [x2, y2],
          [x2, y2]
        ];
      }
    } else {
      oldPoints = points.map(() => [0, 0]);
    }
    return {
      u: u,
      v: v,
      reverse: !positions.edges[u][v],
      pathBefore: svgPath(oldPoints, true),
      path: svgPath(points, true)
    };
  });
  return {vertices, edges, positions};
};

const template = `

  <svg
      width="100%"
      height="100%"
      ng-mousemove="participantInterview.onMouseMove($event)"
      ng-mousedown="participantInterview.onMouseDown($event)"
      style="cursor: move">
    <marker
        id="m_ar"
        viewBox="0 0 10 10"
        refX="10"
        refY="5"
        markerUnits="strokeWidth"
        preserveAspectRatio="none"
        markerWidth="8"
        markerHeight="12"
        orient="auto-start-reverse">
      <polygon points="0,0 0,10 10,5" fill="#888"/>
    </marker>
    <g ng-attr-transform="{{participantInterview.svgTranslate}}">
      <g>
        <g
            ng-repeat="edge in participantInterview.layout.edges track by edge.u + ':' + edge.v">
          <path
              fill="none"
              stroke="#ccc"
              ng-attr-stroke-dasharray="{{edge.reverse ? '5' : 'none'}}"
              ss-d="edge.path"
              ss-d-update="edge.pathBefore"
              ss-dur="participantInterview.duration"/>
        </g>
      </g>
      <g>
        <g
            class="vertex"
            ss-transform="participantInterview.translate(vertex.x, vertex.y)"
            ss-transform-enter="'translate(0,0)'"
            ss-dur="participantInterview.duration"
            ng-repeat="vertex in participantInterview.layout.vertices track by vertex.u">
          <circle
              style="cursor: pointer;"
              r="5"
              ss-fill="participantInterview.selected[vertex.u] ? participantInterview.selectedColor : '#000'"
              ng-click="participantInterview.toggleSelected(vertex.u)"/>
          <text
              style="cursor: pointer;"
              x="7"
              y="5"
              ss-fill="participantInterview.selected[vertex.u] ? participantInterview.selectedColor : '#000'"
              ng-click="participantInterview.toggleSelected(vertex.u)">
            {{vertex.text}}
          </text>
          <foreignObject width="260" height="60" x="-130" transform="translate(0,10)scale(0.5)">

              <md-button class="md-icon-button" ng-click="participantInterview.ladderUp($event, vertex.u)">
                <md-icon>arrow_back</md-icon>
              </md-button>
              <md-button class="md-icon-button" ng-click="participantInterview.editText($event, vertex.u)">
                <md-icon>edit</md-icon>
              </md-button>
              <md-button class="md-icon-button" ng-click="participantInterview.removeConstruct($event, vertex.u)">
                <md-icon>delete</md-icon>
              </md-button>
              <md-button class="md-icon-button" ng-click="participantInterview.ladderDown($event, vertex.u)">
                <md-icon>arrow_forward</md-icon>
              </md-button>

          </foreignObject>
        </g>
      </g>
    </g>
  </svg>

  <md-button class="md-fab" ui-sref="app.projects.detail">
    <md-icon>arrow_back</md-icon>
  </md-button>

  <md-button class="md-fab" ng-click="participantInterview.addConstruct($event)">
    <md-icon>add</md-icon>
  </md-button>

`;

const modName = 'egrid.components.participant-interview';

angular.module(modName, [
  angularfireModule,
  shinsekaiModule,
  commonModules,
  openAddConstructDialogModule,
  rootRefModule
]);

angular.module(modName).factory('ParticipantInterviewController', ($firebaseObject, openConstructFormDialog) => {
  const initialValue = '{"vertices": [], "edges": []}';
  const privates = new WeakMap();

  return class ParticipantInterviewController {
    constructor($scope, participant, constructs) {
      const graph = grid();
      const {vertices, edges} = JSON.parse(constructs.$value || initialValue);
      for (const {u, d} of vertices) {
        graph.graph().addVertex(u, d);
      }
      for (const {u, v, d} of edges) {
        graph.graph().addEdge(u, v, d);
      }

      const x0 = 100,
            y0 = 100;
      privates.set(this, {
        graph: graph,
        constructs: constructs,
        svgX: x0,
        svgY: y0,
        svgX0: x0,
        svgY0: y0
      });
      this.delay = 0.2;
      this.selectedColor = '#dc143c';
      this.svgTranslate = this.translate(x0, y0);
      this.participant = participant;
      this.selected = {};

      constructs.$loaded(() => {
        this.layout = layout(JSON.parse(constructs.$value || initialValue), this.layout);
      });
      constructs.$watch((event) => {
        this.layout = layout(JSON.parse(constructs.$value || initialValue), this.layout);
      });
    }

    addConstruct($event) {
      openConstructFormDialog($event)
        .then(({text}) => {
          const constructs = privates.get(this).constructs,
                graph = grid();
          load(graph.graph(), JSON.parse(constructs.$value || initialValue));
          graph.addConstruct(text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    ladderDown($event, fromConstruct) {
      openConstructFormDialog($event)
        .then(({text}) => {
          const constructs = privates.get(this).constructs,
                graph = grid();
          load(graph.graph(), JSON.parse(constructs.$value || initialValue));
          graph.ladderDown(fromConstruct, text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    ladderUp($event, fromConstruct) {
      openConstructFormDialog($event)
        .then(({text}) => {
          const constructs = privates.get(this).constructs,
                graph = grid();
          load(graph.graph(), JSON.parse(constructs.$value || initialValue));
          graph.ladderUp(fromConstruct, text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    editText($event, u) {
      const constructs = privates.get(this).constructs,
            graph = grid();
      load(graph.graph(), JSON.parse(constructs.$value || initialValue));
      openConstructFormDialog($event, graph.graph().vertex(u).text)
        .then(({text}) => {
          graph.updateConstruct(u, 'text', text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    removeConstruct($event, u) {
      const constructs = privates.get(this).constructs,
            graph = grid();
      load(graph.graph(), JSON.parse(constructs.$value || initialValue));
      graph.removeConstruct(u);
      constructs.$value = graph.graph().toString();
      constructs.$save();
    }

    onMouseDown($event) {
      const attrs = privates.get(this);
      attrs.svgX0 = $event.clientX;
      attrs.svgY0 = $event.clientY;
    }

    onMouseMove($event) {
      if ($event.buttons > 0) {
        const attrs = privates.get(this);
        attrs.svgX += $event.clientX - attrs.svgX0;
        attrs.svgY += $event.clientY - attrs.svgY0;
        attrs.svgX0 = $event.clientX;
        attrs.svgY0 = $event.clientY;
        this.svgTranslate = this.translate(attrs.svgX, attrs.svgY);
      }
    }

    translate(x, y) {
      return `translate(${x},${y})`;
    }

    toggleSelected(u) {
      this.selected[u] = !this.selected[u];
    }
  };
});

angular.module(modName).config(($stateProvider) => {
  $stateProvider.state('app.projects.detail.participant.interview', {
    url: '/interview',
    views: {
      'base@app': {
        controllerAs: 'participantInterview',
        controllerProvider: (ParticipantInterviewController) => ParticipantInterviewController,
        template: template
      }
    },
    resolve: {
      constructs: ($stateParams, $firebaseObject, rootRef) => {
        const ref = rootRef
          .child('graph')
          .child($stateParams.projectId)
          .child($stateParams.participantId);
        return $firebaseObject(ref).$loaded((data) => data);
      }
    }
  });
});

export default modName;
<commit_msg>Replace removing construct buttons with removing selected construct button<commit_after>import angular from 'angular';
import {grid} from 'egrid-core';
import Graph from 'eg-graph/lib/graph';
import SugiyamaLayouter from 'eg-graph/lib/layouter/sugiyama';
import angularfireModule from 'angularfire';
import shinsekaiModule from 'shinsekai';
import commonModules from '../common-modules';
import openAddConstructDialogModule from '../services/open-add-construct-dialog';
import rootRefModule from '../services/root-ref';

const startFrom = ([x, y]) => {
  return `M${x} ${y}`;
};

const lineTo = ([x, y]) => {
  return ` L ${x} ${y}`;
};

const curveTo = ([x1, y1], [x2, y2], ltor) => {
  const dx = x2 - x1,
        dy = y2 - y1;
  return ltor
    ? ` q ${dx / 4} ${0}, ${dx / 2} ${dy / 2} q ${dx / 4} ${dy / 2}, ${dx / 2} ${dy / 2}`
    : ` q ${0} ${dy / 4}, ${dx / 2} ${dy / 2} q ${dx / 2} ${dy / 4}, ${dx / 2} ${dy / 2}`;
};

const svgPath = (points, ltor) => {
  let d = `${startFrom(points[0])}${lineTo(points[1])}`;
  for (let i = 3; i < points.length; i += 2) {
    d += curveTo(points[i - 2], points[i - 1], ltor) + lineTo(points[i]);
  }
  return d;
};

const load = (graph, constructs) => {
  for (const {u, d} of constructs.vertices) {
    graph.addVertex(u, d);
  }
  for (const {u, v, d} of constructs.edges) {
    graph.addEdge(u, v, d);
  }
};

const layout = (constructs, oldLayout) => {
  const graph = new Graph(),
        layouter = new SugiyamaLayouter();
  load(graph, constructs);
  layouter
    .layerMargin(200)
    .vertexMargin(80)
    .vertexWidth(() => 10)
    .vertexHeight(() => 10)
    .edgeWidth(() => 1);
  const positions = layouter.layout(graph);
  const vertices = graph.vertices().map((u) => {
    const d = positions.vertices[u];
    return {
      u: u,
      x: d.x,
      y: d.y,
      height: d.height,
      width: d.width,
      text: graph.vertex(u).text
    };
  });
  const edges = graph.edges().map(([u, v]) => {
    const points = positions.edges[u][v] ? positions.edges[u][v].points : positions.edges[v][u].points;
    let oldPoints;
    if (oldLayout) {
      if (oldLayout.positions.edges[u] && oldLayout.positions.edges[u][v]) {
        oldPoints = Array.from(oldLayout.positions.edges[u][v].points);
        if (points.length > oldPoints.length) {
          while (points.length > oldPoints.length) {
            oldPoints.push(oldPoints[oldPoints.length - 1]);
          }
        }
      } else {
        const x1 = oldLayout.positions.vertices[u] ? oldLayout.positions.vertices[u].x : 0,
              y1 = oldLayout.positions.vertices[u] ? oldLayout.positions.vertices[u].y : 0,
              x2 = oldLayout.positions.vertices[v] ? oldLayout.positions.vertices[v].x : 0,
              y2 = oldLayout.positions.vertices[v] ? oldLayout.positions.vertices[v].y : 0;
        oldPoints = [
          [x1, y1],
          [x1, y1],
          [x2, y2],
          [x2, y2]
        ];
      }
    } else {
      oldPoints = points.map(() => [0, 0]);
    }
    return {
      u: u,
      v: v,
      reverse: !positions.edges[u][v],
      pathBefore: svgPath(oldPoints, true),
      path: svgPath(points, true)
    };
  });
  return {vertices, edges, positions};
};

const template = `

  <svg
      width="100%"
      height="100%"
      ng-mousemove="participantInterview.onMouseMove($event)"
      ng-mousedown="participantInterview.onMouseDown($event)"
      style="cursor: move">
    <marker
        id="m_ar"
        viewBox="0 0 10 10"
        refX="10"
        refY="5"
        markerUnits="strokeWidth"
        preserveAspectRatio="none"
        markerWidth="8"
        markerHeight="12"
        orient="auto-start-reverse">
      <polygon points="0,0 0,10 10,5" fill="#888"/>
    </marker>
    <g ng-attr-transform="{{participantInterview.svgTranslate}}">
      <g>
        <g
            ng-repeat="edge in participantInterview.layout.edges track by edge.u + ':' + edge.v">
          <path
              fill="none"
              stroke="#ccc"
              ng-attr-stroke-dasharray="{{edge.reverse ? '5' : 'none'}}"
              ss-d="edge.path"
              ss-d-update="edge.pathBefore"
              ss-dur="participantInterview.duration"/>
        </g>
      </g>
      <g>
        <g
            class="vertex"
            ss-transform="participantInterview.translate(vertex.x, vertex.y)"
            ss-transform-enter="'translate(0,0)'"
            ss-dur="participantInterview.duration"
            ng-repeat="vertex in participantInterview.layout.vertices track by vertex.u">
          <circle
              style="cursor: pointer;"
              r="5"
              ss-fill="participantInterview.selected[vertex.u] ? participantInterview.selectedColor : '#000'"
              ng-click="participantInterview.toggleSelected(vertex.u)"/>
          <text
              style="cursor: pointer;"
              x="7"
              y="5"
              ss-fill="participantInterview.selected[vertex.u] ? participantInterview.selectedColor : '#000'"
              ng-click="participantInterview.toggleSelected(vertex.u)">
            {{vertex.text}}
          </text>
          <foreignObject width="195" height="60" x="-90" transform="translate(0,10)scale(0.75)">

              <md-button class="md-icon-button" ng-click="participantInterview.ladderUp($event, vertex.u)">
                <md-icon>arrow_back</md-icon>
              </md-button>
              <md-button class="md-icon-button" ng-click="participantInterview.editText($event, vertex.u)">
                <md-icon>edit</md-icon>
              </md-button>
              <md-button class="md-icon-button" ng-click="participantInterview.ladderDown($event, vertex.u)">
                <md-icon>arrow_forward</md-icon>
              </md-button>

          </foreignObject>
        </g>
      </g>
    </g>
  </svg>

  <md-button class="md-fab" ui-sref="app.projects.detail">
    <md-icon>arrow_back</md-icon>
  </md-button>

  <md-button class="md-fab" ng-click="participantInterview.addConstruct($event)">
    <md-icon>add</md-icon>
  </md-button>

  <md-button class="md-fab" ng-click="participantInterview.removeSelectedConstruct($event)">
    <md-icon>delete</md-icon>
  </md-button>

      <md-button class="md-fab" ng-click="participantInterview.selectAll()">
        <md-icon>check_box_outline_blank</md-icon>
      </md-button>

      <md-button class="md-fab" ng-click="participantInterview.resetSelect()">
        <md-icon>check_box</md-icon>
      </md-button>

`;

const modName = 'egrid.components.participant-interview';

angular.module(modName, [
  angularfireModule,
  shinsekaiModule,
  commonModules,
  openAddConstructDialogModule,
  rootRefModule
]);

angular.module(modName).factory('ParticipantInterviewController', ($firebaseObject, openConstructFormDialog) => {
  const initialValue = '{"vertices": [], "edges": []}';
  const privates = new WeakMap();

  return class ParticipantInterviewController {
    constructor($scope, participant, constructs) {
      const graph = grid();
      const {vertices, edges} = JSON.parse(constructs.$value || initialValue);
      for (const {u, d} of vertices) {
        graph.graph().addVertex(u, d);
      }
      for (const {u, v, d} of edges) {
        graph.graph().addEdge(u, v, d);
      }

      const x0 = 100,
            y0 = 100;
      privates.set(this, {
        graph: graph,
        constructs: constructs,
        svgX: x0,
        svgY: y0,
        svgX0: x0,
        svgY0: y0
      });
      this.duration = 0.4;
      this.selectedColor = '#dc143c';
      this.svgTranslate = this.translate(x0, y0);
      this.participant = participant;
      this.selectedCount = 0;
      this.selected = {};

      constructs.$loaded(() => {
        this.layout = layout(JSON.parse(constructs.$value || initialValue), this.layout);
      });
      constructs.$watch((event) => {
        this.layout = layout(JSON.parse(constructs.$value || initialValue), this.layout);
      });
    }

    addConstruct($event) {
      openConstructFormDialog($event)
        .then(({text}) => {
          const constructs = privates.get(this).constructs,
                graph = grid();
          load(graph.graph(), JSON.parse(constructs.$value || initialValue));
          graph.addConstruct(text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    ladderDown($event, fromConstruct) {
      openConstructFormDialog($event)
        .then(({text}) => {
          const constructs = privates.get(this).constructs,
                graph = grid();
          load(graph.graph(), JSON.parse(constructs.$value || initialValue));
          graph.ladderDown(fromConstruct, text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    ladderUp($event, fromConstruct) {
      openConstructFormDialog($event)
        .then(({text}) => {
          const constructs = privates.get(this).constructs,
                graph = grid();
          load(graph.graph(), JSON.parse(constructs.$value || initialValue));
          graph.ladderUp(fromConstruct, text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    editText($event, u) {
      const constructs = privates.get(this).constructs,
            graph = grid();
      load(graph.graph(), JSON.parse(constructs.$value || initialValue));
      openConstructFormDialog($event, graph.graph().vertex(u).text)
        .then(({text}) => {
          graph.updateConstruct(u, 'text', text);
          constructs.$value = graph.graph().toString();
          constructs.$save();
        });
    }

    removeSelectedConstruct($event) {
      const constructs = privates.get(this).constructs,
            graph = grid();
      load(graph.graph(), JSON.parse(constructs.$value || initialValue));
      for (const u in this.selected) {
        if (this.selected[u]) {
          this.selectedCount -= 1;
          delete this.selected[u];
          graph.removeConstruct(u);
        }
      }
      constructs.$value = graph.graph().toString();
      constructs.$save();
    }

    onMouseDown($event) {
      const attrs = privates.get(this);
      attrs.svgX0 = $event.clientX;
      attrs.svgY0 = $event.clientY;
    }

    onMouseMove($event) {
      if ($event.buttons > 0) {
        const attrs = privates.get(this);
        attrs.svgX += $event.clientX - attrs.svgX0;
        attrs.svgY += $event.clientY - attrs.svgY0;
        attrs.svgX0 = $event.clientX;
        attrs.svgY0 = $event.clientY;
        this.svgTranslate = this.translate(attrs.svgX, attrs.svgY);
      }
    }

    translate(x, y) {
      return `translate(${x},${y})`;
    }

    toggleSelected(u) {
      if (this.selected[u]) {
        this.selectedCount -= 1;
        this.selected[u] = false;
      } else {
        this.selectedCount += 1;
        this.selected[u] = true;
      }
    }

    selectAll() {
      const vertices = JSON.parse(privates.get(this).constructs.$value).vertices;
      this.selectedCount = vertices.length;
      for (const {u} of vertices) {
        this.selected[u] = true;
      }
    }

    resetSelect() {
      this.selectedCount = 0;
      for (const u in this.selected) {
        this.selected[u] = false;
      }
    }
  };
});

angular.module(modName).config(($stateProvider) => {
  $stateProvider.state('app.projects.detail.participant.interview', {
    url: '/interview',
    views: {
      'base@app': {
        controllerAs: 'participantInterview',
        controllerProvider: (ParticipantInterviewController) => ParticipantInterviewController,
        template: template
      }
    },
    resolve: {
      constructs: ($stateParams, $firebaseObject, rootRef) => {
        const ref = rootRef
          .child('graph')
          .child($stateParams.projectId)
          .child($stateParams.participantId);
        return $firebaseObject(ref).$loaded((data) => data);
      }
    }
  });
});

export default modName;
<|endoftext|>


<commit_before>define([
        'config'
        ],
        function(config){

    var CongregationsView = Backbone.View.extend({
        initialize: function(){
            this.render();
            // Apply tablesorter widget to table containing congregation list
            $("#congregation_list")
            .tablesorter({widgets: ['zebra'], locale: 'us', useUI: true});
            // TODO: For some reason the pager doesn't display, its div wraps around the table and filter widgets,
            //       and it breaks the sorter so when you click on the table header the rows disappear.
            //.tablesorterPager({container: $("#pager")});
            https://blueprints.launchpad.net/reformedchurcheslocator/+spec/get-tablesorter-pager-to-display
        },
        render: function(){
            config.render_to_id(this, "#congregations_template")
        }
    });
    return CongregationsView

});<commit_msg>Making running the tests conditional<commit_after>define([
        'config'
        ],
        function(config){

    var CongregationsView = Backbone.View.extend({
        initialize: function(){
            this.render();
            // Apply tablesorter widget to table containing congregation list
            $("#congregation_list")
            .tablesorter({widgets: ['zebra'], locale: 'us', useUI: true});
            // TODO: For some reason the pager doesn't display, its div wraps around the table and filter widgets,
            //       and it breaks the sorter so when you click on the table header the rows disappear.
            //.tablesorterPager({container: $("#pager")});
            //https://blueprints.launchpad.net/reformedchurcheslocator/+spec/get-tablesorter-pager-to-display
        },
        render: function(){
            config.render_to_id(this, "#congregations_template")
        }
    });
    return CongregationsView

});<|endoftext|>


<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
import { MatrixClient } from 'matrix-js-sdk';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";

export default class RightPanel extends React.Component {
    static get propTypes() {
        return {
            roomId: PropTypes.string, // if showing panels for a given room, this is set
            groupId: PropTypes.string, // if showing panels for a given group, this is set
            user: PropTypes.object,
        };
    }

    static get contextTypes() {
        return {
            matrixClient: PropTypes.instanceOf(MatrixClient),
        };
    }

    constructor(props, context) {
        super(props, context);
        this.state = {
            phase: this._getPhaseFromProps(),
            isUserPrivilegedInGroup: null,
        };
        this.onAction = this.onAction.bind(this);
        this.onRoomStateMember = this.onRoomStateMember.bind(this);
        this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
        this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
        this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);

        this._delayedUpdate = new RateLimitedFunc(() => {
            this.forceUpdate();
        }, 500);
    }

    _getPhaseFromProps() {
        const rps = RightPanelStore.getSharedInstance();
        if (this.props.groupId) {
            if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
                rps.setPhase(RIGHT_PANEL_PHASES.GroupMemberList);
                return RIGHT_PANEL_PHASES.GroupMemberList;
            }
            return rps.groupPanelPhase;
        } else if (this.props.user) {
            return RIGHT_PANEL_PHASES.RoomMemberInfo;
        } else {
            if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
                rps.setPhase(RIGHT_PANEL_PHASES.RoomMemberList);
                return RIGHT_PANEL_PHASES.RoomMemberList;
            }
            return rps.roomPanelPhase;
        }
    }

    componentWillMount() {
        this.dispatcherRef = dis.register(this.onAction);
        const cli = this.context.matrixClient;
        cli.on("RoomState.members", this.onRoomStateMember);
        this._initGroupStore(this.props.groupId);
        if (this.props.user) {
            this.setState({member: this.props.user});
        }
    }

    componentWillUnmount() {
        dis.unregister(this.dispatcherRef);
        if (this.context.matrixClient) {
            this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
        }
        this._unregisterGroupStore(this.props.groupId);
    }

    componentWillReceiveProps(newProps) {
        if (newProps.groupId !== this.props.groupId) {
            this._unregisterGroupStore(this.props.groupId);
            this._initGroupStore(newProps.groupId);
        }
    }

    _initGroupStore(groupId) {
        if (!groupId) return;
        GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
    }

    _unregisterGroupStore() {
        GroupStore.unregisterListener(this.onGroupStoreUpdated);
    }

    onGroupStoreUpdated() {
        this.setState({
            isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
        });
    }

    onInviteToGroupButtonClick() {
        showGroupInviteDialog(this.props.groupId).then(() => {
            this.setState({
                phase: RIGHT_PANEL_PHASES.GroupMemberList,
            });
        });
    }

    onAddRoomToGroupButtonClick() {
        showGroupAddRoomDialog(this.props.groupId).then(() => {
            this.forceUpdate();
        });
    }

    onRoomStateMember(ev, state, member) {
        if (member.roomId !== this.props.roomId) {
            return;
        }
        // redraw the badge on the membership list
        if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) {
            this._delayedUpdate();
        } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId &&
                member.userId === this.state.member.userId) {
            // refresh the member info (e.g. new power level)
            this._delayedUpdate();
        }
    }

    onAction(payload) {
        if (payload.action === "after_right_panel_phase_change") {
            this.setState({
                phase: payload.phase,
                groupRoomId: payload.groupRoomId,
                groupId: payload.groupId,
                member: payload.member,
                event: payload.event,
            });
        }
    }

    render() {
        const MemberList = sdk.getComponent('rooms.MemberList');
        const MemberInfo = sdk.getComponent('rooms.MemberInfo');
        const UserInfo = sdk.getComponent('right_panel.UserInfo');
        const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
        const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
        const FilePanel = sdk.getComponent('structures.FilePanel');

        const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
        const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
        const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
        const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');

        let panel = <div />;

        if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) {
            panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
        } else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) {
            panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
            panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
            if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
                const onClose = () => {
                    dis.dispatch({
                        action: "view_user",
                        member: null,
                    });
                };
                panel = <UserInfo
                    user={this.state.member}
                    roomId={this.props.roomId}
                    key={this.props.roomId || this.state.member.userId}
                    onClose={onClose}
                />;
            } else {
                panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
            }
        } else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) {
            panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
            if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
                const onClose = () => {
                    dis.dispatch({
                        action: "view_user",
                        member: null,
                    });
                };
                panel = <UserInfo
                    user={this.state.member}
                    groupId={this.props.groupId}
                    key={this.state.member.userId}
                    onClose={onClose} />;
            } else {
                panel = (
                    <GroupMemberInfo
                        groupMember={this.state.member}
                        groupId={this.props.groupId}
                        key={this.state.member.user_id}
                    />
                );
            }
        } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) {
            panel = <GroupRoomInfo
                groupRoomId={this.state.groupRoomId}
                groupId={this.props.groupId}
                key={this.state.groupRoomId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
            panel = <NotificationPanel />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
            panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
        }

        const classes = classNames("mx_RightPanel", "mx_fadable", {
            "collapsed": this.props.collapsed,
            "mx_fadable_faded": this.props.disabled,
            "dark-panel": true,
        });

        return (
            <aside className={classes}>
                { panel }
            </aside>
        );
    }
}
<commit_msg>Fix incorrect function call into RightPanelStore<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../index';
import dis from '../../dispatcher';
import { MatrixClient } from 'matrix-js-sdk';
import RateLimitedFunc from '../../ratelimitedfunc';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import SettingsStore from "../../settings/SettingsStore";
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";

export default class RightPanel extends React.Component {
    static get propTypes() {
        return {
            roomId: PropTypes.string, // if showing panels for a given room, this is set
            groupId: PropTypes.string, // if showing panels for a given group, this is set
            user: PropTypes.object,
        };
    }

    static get contextTypes() {
        return {
            matrixClient: PropTypes.instanceOf(MatrixClient),
        };
    }

    constructor(props, context) {
        super(props, context);
        this.state = {
            phase: this._getPhaseFromProps(),
            isUserPrivilegedInGroup: null,
        };
        this.onAction = this.onAction.bind(this);
        this.onRoomStateMember = this.onRoomStateMember.bind(this);
        this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
        this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
        this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);

        this._delayedUpdate = new RateLimitedFunc(() => {
            this.forceUpdate();
        }, 500);
    }

    _getPhaseFromProps() {
        const rps = RightPanelStore.getSharedInstance();
        if (this.props.groupId) {
            if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
                dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
                return RIGHT_PANEL_PHASES.GroupMemberList;
            }
            return rps.groupPanelPhase;
        } else if (this.props.user) {
            return RIGHT_PANEL_PHASES.RoomMemberInfo;
        } else {
            if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
                dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.RoomMemberList});
                return RIGHT_PANEL_PHASES.RoomMemberList;
            }
            return rps.roomPanelPhase;
        }
    }

    componentWillMount() {
        this.dispatcherRef = dis.register(this.onAction);
        const cli = this.context.matrixClient;
        cli.on("RoomState.members", this.onRoomStateMember);
        this._initGroupStore(this.props.groupId);
        if (this.props.user) {
            this.setState({member: this.props.user});
        }
    }

    componentWillUnmount() {
        dis.unregister(this.dispatcherRef);
        if (this.context.matrixClient) {
            this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
        }
        this._unregisterGroupStore(this.props.groupId);
    }

    componentWillReceiveProps(newProps) {
        if (newProps.groupId !== this.props.groupId) {
            this._unregisterGroupStore(this.props.groupId);
            this._initGroupStore(newProps.groupId);
        }
    }

    _initGroupStore(groupId) {
        if (!groupId) return;
        GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
    }

    _unregisterGroupStore() {
        GroupStore.unregisterListener(this.onGroupStoreUpdated);
    }

    onGroupStoreUpdated() {
        this.setState({
            isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
        });
    }

    onInviteToGroupButtonClick() {
        showGroupInviteDialog(this.props.groupId).then(() => {
            this.setState({
                phase: RIGHT_PANEL_PHASES.GroupMemberList,
            });
        });
    }

    onAddRoomToGroupButtonClick() {
        showGroupAddRoomDialog(this.props.groupId).then(() => {
            this.forceUpdate();
        });
    }

    onRoomStateMember(ev, state, member) {
        if (member.roomId !== this.props.roomId) {
            return;
        }
        // redraw the badge on the membership list
        if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList && member.roomId === this.props.roomId) {
            this._delayedUpdate();
        } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo && member.roomId === this.props.roomId &&
                member.userId === this.state.member.userId) {
            // refresh the member info (e.g. new power level)
            this._delayedUpdate();
        }
    }

    onAction(payload) {
        if (payload.action === "after_right_panel_phase_change") {
            this.setState({
                phase: payload.phase,
                groupRoomId: payload.groupRoomId,
                groupId: payload.groupId,
                member: payload.member,
                event: payload.event,
            });
        }
    }

    render() {
        const MemberList = sdk.getComponent('rooms.MemberList');
        const MemberInfo = sdk.getComponent('rooms.MemberInfo');
        const UserInfo = sdk.getComponent('right_panel.UserInfo');
        const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
        const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
        const FilePanel = sdk.getComponent('structures.FilePanel');

        const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
        const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
        const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
        const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');

        let panel = <div />;

        if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) {
            panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
        } else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) {
            panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) {
            panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) {
            if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
                const onClose = () => {
                    dis.dispatch({
                        action: "view_user",
                        member: null,
                    });
                };
                panel = <UserInfo
                    user={this.state.member}
                    roomId={this.props.roomId}
                    key={this.props.roomId || this.state.member.userId}
                    onClose={onClose}
                />;
            } else {
                panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
            }
        } else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) {
            panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) {
            if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
                const onClose = () => {
                    dis.dispatch({
                        action: "view_user",
                        member: null,
                    });
                };
                panel = <UserInfo
                    user={this.state.member}
                    groupId={this.props.groupId}
                    key={this.state.member.userId}
                    onClose={onClose} />;
            } else {
                panel = (
                    <GroupMemberInfo
                        groupMember={this.state.member}
                        groupId={this.props.groupId}
                        key={this.state.member.user_id}
                    />
                );
            }
        } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) {
            panel = <GroupRoomInfo
                groupRoomId={this.state.groupRoomId}
                groupId={this.props.groupId}
                key={this.state.groupRoomId} />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) {
            panel = <NotificationPanel />;
        } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) {
            panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
        }

        const classes = classNames("mx_RightPanel", "mx_fadable", {
            "collapsed": this.props.collapsed,
            "mx_fadable_faded": this.props.disabled,
            "dark-panel": true,
        });

        return (
            <aside className={classes}>
                { panel }
            </aside>
        );
    }
}
<|endoftext|>


<commit_before>
// jsClipper uses X/Y instead of x/y...
function toClipperCoordinates(polygon){
	var clone = [];
	for(var i=0; i<polygon.length; i++){
		clone.push({
			X: polygon[i].x,
			Y: polygon[i].y
		});
	}

	return clone;
};

function toNestCoordinates(polygon, scale){
	var clone = [];
	for(var i=0; i<polygon.length; i++){
		clone.push({
			x: polygon[i].X/scale,
			y: polygon[i].Y/scale
		});
	}

	return clone;
};

function rotatePolygon(polygon, angle){
	var rotated = [];
	angle = angle * Math.PI / 180;
	for(var i=0; i<polygon.length; i++){
		var x = polygon[i].x;
		var y = polygon[i].y;
		var x1 = x*Math.cos(angle)-y*Math.sin(angle);
		var y1 = x*Math.sin(angle)+y*Math.cos(angle);

		rotated.push({x:x1, y:y1});
	}

	return rotated;
};

function PlacementWorker(binPolygon, paths, ids, rotations, config, nfpCache){
	this.binPolygon = binPolygon;
	this.paths = paths;
	this.ids = ids;
	this.rotations = rotations;
	this.config = config;
	this.nfpCache = nfpCache || {};

	// return a placement for the paths/rotations given
	// happens inside a webworker
	this.placePaths = function(paths){

		var self = global.env.self;

		if(!self.binPolygon){
			return null;
		}

		var i, j, k, m, n, path;

		// rotate paths by given rotation
		var rotated = [];
		for(i=0; i<paths.length; i++){
			r = rotatePolygon(paths[i], paths[i].rotation);
			r.rotation = paths[i].rotation;
			r.source = paths[i].source;
			r.id = paths[i].id;
			rotated.push(r);
		}

		paths = rotated;

		var allplacements = [];
		var fitness = 0;
		var binarea = Math.abs(GeometryUtil.polygonArea(self.binPolygon));

		while(paths.length > 0){

			var placed = [];
			var placements = [];
			fitness += 1; // add 1 for each new bin opened (lower fitness is better)

			for(i=0; i<paths.length; i++){
				path = paths[i];
				var key = JSON.stringify({A:-1,B:path.id,inside:true,Arotation:0,Brotation:path.rotation});
				var binNfp = self.nfpCache[key];

				if(!binNfp || binNfp.length == 0){
					continue;
				}

				var position = null;
				if(placed.length == 0){
					// first placement, put it on the left
					for(j=0; j<binNfp.length; j++){
						for(k=0; k<binNfp[j].length; k++){
							if(position === null || binNfp[j][k].x-path[0].x < position.x ){
								position = {
									x: binNfp[j][k].x-path[0].x,
									y: binNfp[j][k].y-path[0].y,
									id: path.id,
									rotation: path.rotation
								}
							}
						}
					}

					placements.push(position);
					placed.push(path);

					continue;
				}

				var clipperBinNfp = [];
				for(j=0; j<binNfp.length; j++){
					clipperBinNfp.push(toClipperCoordinates(binNfp[j]));
				}

				ClipperLib.JS.ScaleUpPaths(clipperBinNfp, self.config.clipperScale);

				var clipper = new ClipperLib.Clipper();
				var combinedNfp = new ClipperLib.Paths();

				for(j=0; j<placed.length; j++){
					var key = JSON.stringify({A:placed[j].id,B:path.id,inside:false,Arotation:placed[j].rotation,Brotation:path.rotation});
					var nfp = self.nfpCache[key];

					if(!nfp){
						continue;
					}

					for(k=0; k<nfp.length; k++){
						var clone = toClipperCoordinates(nfp[k]);
						for(m=0; m<clone.length; m++){
							clone[m].X += placements[j].x;
							clone[m].Y += placements[j].y;
						}

						ClipperLib.JS.ScaleUpPath(clone, self.config.clipperScale);
						clone = ClipperLib.Clipper.CleanPolygon(clone, 0.0001*self.config.clipperScale);
						var area = Math.abs(ClipperLib.Clipper.Area(clone));
						if(clone.length > 2 && area > 0.1*self.config.clipperScale*self.config.clipperScale){
							clipper.AddPath(clone, ClipperLib.PolyType.ptSubject, true);
						}
					}
				}

				if(!clipper.Execute(ClipperLib.ClipType.ctUnion, combinedNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){
					continue;
				}

				// difference with bin polygon
				var finalNfp = new ClipperLib.Paths();
				clipper = new ClipperLib.Clipper();

				clipper.AddPaths(combinedNfp, ClipperLib.PolyType.ptClip, true);
				clipper.AddPaths(clipperBinNfp, ClipperLib.PolyType.ptSubject, true);
				if(!clipper.Execute(ClipperLib.ClipType.ctDifference, finalNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){
					continue;
				}

				finalNfp = ClipperLib.Clipper.CleanPolygons(finalNfp, 0.0001*self.config.clipperScale);

				for(j=0; j<finalNfp.length; j++){
					var area = Math.abs(ClipperLib.Clipper.Area(finalNfp[j]));
					if(finalNfp[j].length < 3 || area < 0.1*self.config.clipperScale*self.config.clipperScale){
						finalNfp.splice(j,1);
						j--;
					}
				}

				if(!finalNfp || finalNfp.length == 0){
					continue;
				}

				var f = [];
				for(j=0; j<finalNfp.length; j++){
					// back to normal scale
					f.push(toNestCoordinates(finalNfp[j], self.config.clipperScale));
				}
				finalNfp = f;

				// choose placement that results in the smallest rectangular bounding box
				// could use convex hull instead, but it can create oddly shaped nests (triangles or long slivers) which are not optimal for real-world use
				// todo: generalize gravity direction
				var minwidth = null;
				var minx = null;
				var nf, area, shiftvector;

				for(j=0; j<finalNfp.length; j++){
					nf = finalNfp[j];
					if(Math.abs(GeometryUtil.polygonArea(nf)) < 2){
						continue;
					}

					for(k=0; k<nf.length; k++){
						var allpoints = [];
						for(m=0; m<placed.length; m++){
							for(n=0; n<placed[m].length; n++){
								allpoints.push({x:placed[m][n].x+placements[m].x, y: placed[m][n].y+placements[m].y});
							}
						}

						shiftvector = {
							x: nf[k].x-path[0].x,
							y: nf[k].y-path[0].y,
							id: path.id,
							rotation: path.rotation,
							nfp: combinedNfp
						};

						for(m=0; m<path.length; m++){
							allpoints.push({x: path[m].x+shiftvector.x, y:path[m].y+shiftvector.y});
						}

						var rectbounds = GeometryUtil.getPolygonBounds(allpoints);

						area = rectbounds.width*rectbounds.height;

						if(minwidth === null || rectbounds.width < minwidth || (GeometryUtil.almostEqual(minwidth, rectbounds.width) && (minx === null || shiftvector.x < minx))){
							minwidth = rectbounds.width;
							position = shiftvector;
							minx = shiftvector.x;
						}
					}
				}
				if(position){
					placed.push(path);
					placements.push(position);
				}
			}

			if(minwidth){
				fitness += minwidth/binarea;
			}

			for(i=0; i<placed.length; i++){
				var index = paths.indexOf(placed[i]);
				if(index >= 0){
					paths.splice(index,1);
				}
			}

			if(placements && placements.length > 0){
				allplacements.push(placements);
			}
			else{
				break; // something went wrong
			}
		}

		// there were parts that couldn't be placed
		fitness += 2*paths.length;

		return {placements: allplacements, fitness: fitness, paths: paths, area: binarea };
	};

}

// clipperjs uses alerts for warnings
function alert(message) {
    console.log('alert: ', message);
}<commit_msg>use bounding box for placement instead of just width<commit_after>
// jsClipper uses X/Y instead of x/y...
function toClipperCoordinates(polygon){
	var clone = [];
	for(var i=0; i<polygon.length; i++){
		clone.push({
			X: polygon[i].x,
			Y: polygon[i].y
		});
	}

	return clone;
};

function toNestCoordinates(polygon, scale){
	var clone = [];
	for(var i=0; i<polygon.length; i++){
		clone.push({
			x: polygon[i].X/scale,
			y: polygon[i].Y/scale
		});
	}

	return clone;
};

function rotatePolygon(polygon, angle){
	var rotated = [];
	angle = angle * Math.PI / 180;
	for(var i=0; i<polygon.length; i++){
		var x = polygon[i].x;
		var y = polygon[i].y;
		var x1 = x*Math.cos(angle)-y*Math.sin(angle);
		var y1 = x*Math.sin(angle)+y*Math.cos(angle);

		rotated.push({x:x1, y:y1});
	}

	return rotated;
};

function PlacementWorker(binPolygon, paths, ids, rotations, config, nfpCache){
	this.binPolygon = binPolygon;
	this.paths = paths;
	this.ids = ids;
	this.rotations = rotations;
	this.config = config;
	this.nfpCache = nfpCache || {};

	// return a placement for the paths/rotations given
	// happens inside a webworker
	this.placePaths = function(paths){

		var self = global.env.self;

		if(!self.binPolygon){
			return null;
		}

		var i, j, k, m, n, path;

		// rotate paths by given rotation
		var rotated = [];
		for(i=0; i<paths.length; i++){
			r = rotatePolygon(paths[i], paths[i].rotation);
			r.rotation = paths[i].rotation;
			r.source = paths[i].source;
			r.id = paths[i].id;
			rotated.push(r);
		}

		paths = rotated;

		var allplacements = [];
		var fitness = 0;
		var binarea = Math.abs(GeometryUtil.polygonArea(self.binPolygon));

		while(paths.length > 0){

			var placed = [];
			var placements = [];
			fitness += 1; // add 1 for each new bin opened (lower fitness is better)

			for(i=0; i<paths.length; i++){
				path = paths[i];
				var key = JSON.stringify({A:-1,B:path.id,inside:true,Arotation:0,Brotation:path.rotation});
				var binNfp = self.nfpCache[key];

				if(!binNfp || binNfp.length == 0){
					continue;
				}

				var position = null;
				if(placed.length == 0){
					// first placement, put it on the left
					for(j=0; j<binNfp.length; j++){
						for(k=0; k<binNfp[j].length; k++){
							if(position === null || binNfp[j][k].x-path[0].x < position.x ){
								position = {
									x: binNfp[j][k].x-path[0].x,
									y: binNfp[j][k].y-path[0].y,
									id: path.id,
									rotation: path.rotation
								}
							}
						}
					}

					placements.push(position);
					placed.push(path);

					continue;
				}

				var clipperBinNfp = [];
				for(j=0; j<binNfp.length; j++){
					clipperBinNfp.push(toClipperCoordinates(binNfp[j]));
				}

				ClipperLib.JS.ScaleUpPaths(clipperBinNfp, self.config.clipperScale);

				var clipper = new ClipperLib.Clipper();
				var combinedNfp = new ClipperLib.Paths();

				for(j=0; j<placed.length; j++){
					var key = JSON.stringify({A:placed[j].id,B:path.id,inside:false,Arotation:placed[j].rotation,Brotation:path.rotation});
					var nfp = self.nfpCache[key];

					if(!nfp){
						continue;
					}

					for(k=0; k<nfp.length; k++){
						var clone = toClipperCoordinates(nfp[k]);
						for(m=0; m<clone.length; m++){
							clone[m].X += placements[j].x;
							clone[m].Y += placements[j].y;
						}

						ClipperLib.JS.ScaleUpPath(clone, self.config.clipperScale);
						clone = ClipperLib.Clipper.CleanPolygon(clone, 0.0001*self.config.clipperScale);
						var area = Math.abs(ClipperLib.Clipper.Area(clone));
						if(clone.length > 2 && area > 0.1*self.config.clipperScale*self.config.clipperScale){
							clipper.AddPath(clone, ClipperLib.PolyType.ptSubject, true);
						}
					}
				}

				if(!clipper.Execute(ClipperLib.ClipType.ctUnion, combinedNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){
					continue;
				}

				// difference with bin polygon
				var finalNfp = new ClipperLib.Paths();
				clipper = new ClipperLib.Clipper();

				clipper.AddPaths(combinedNfp, ClipperLib.PolyType.ptClip, true);
				clipper.AddPaths(clipperBinNfp, ClipperLib.PolyType.ptSubject, true);
				if(!clipper.Execute(ClipperLib.ClipType.ctDifference, finalNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){
					continue;
				}

				finalNfp = ClipperLib.Clipper.CleanPolygons(finalNfp, 0.0001*self.config.clipperScale);

				for(j=0; j<finalNfp.length; j++){
					var area = Math.abs(ClipperLib.Clipper.Area(finalNfp[j]));
					if(finalNfp[j].length < 3 || area < 0.1*self.config.clipperScale*self.config.clipperScale){
						finalNfp.splice(j,1);
						j--;
					}
				}

				if(!finalNfp || finalNfp.length == 0){
					continue;
				}

				var f = [];
				for(j=0; j<finalNfp.length; j++){
					// back to normal scale
					f.push(toNestCoordinates(finalNfp[j], self.config.clipperScale));
				}
				finalNfp = f;

				// choose placement that results in the smallest bounding box
				// could use convex hull instead, but it can create oddly shaped nests (triangles or long slivers) which are not optimal for real-world use
				// todo: generalize gravity direction
				var minwidth = null;
				var minarea = null;
				var minx = null;
				var nf, area, shiftvector;

				for(j=0; j<finalNfp.length; j++){
					nf = finalNfp[j];
					if(Math.abs(GeometryUtil.polygonArea(nf)) < 2){
						continue;
					}

					for(k=0; k<nf.length; k++){
						var allpoints = [];
						for(m=0; m<placed.length; m++){
							for(n=0; n<placed[m].length; n++){
								allpoints.push({x:placed[m][n].x+placements[m].x, y: placed[m][n].y+placements[m].y});
							}
						}

						shiftvector = {
							x: nf[k].x-path[0].x,
							y: nf[k].y-path[0].y,
							id: path.id,
							rotation: path.rotation,
							nfp: combinedNfp
						};

						for(m=0; m<path.length; m++){
							allpoints.push({x: path[m].x+shiftvector.x, y:path[m].y+shiftvector.y});
						}

						var rectbounds = GeometryUtil.getPolygonBounds(allpoints);

						// weigh width more, to help compress in direction of gravity
						area = rectbounds.width*2 + rectbounds.height;

						if(minarea === null || area < minarea || (GeometryUtil.almostEqual(minarea, area) && (minx === null || shiftvector.x < minx))){
							minarea = area;
							minwidth = rectbounds.width;
							position = shiftvector;
							minx = shiftvector.x;
						}
					}
				}
				if(position){
					placed.push(path);
					placements.push(position);
				}
			}

			if(minwidth){
				fitness += minwidth/binarea;
			}

			for(i=0; i<placed.length; i++){
				var index = paths.indexOf(placed[i]);
				if(index >= 0){
					paths.splice(index,1);
				}
			}

			if(placements && placements.length > 0){
				allplacements.push(placements);
			}
			else{
				break; // something went wrong
			}
		}

		// there were parts that couldn't be placed
		fitness += 2*paths.length;

		return {placements: allplacements, fitness: fitness, paths: paths, area: binarea };
	};

}

// clipperjs uses alerts for warnings
function alert(message) {
    console.log('alert: ', message);
}<|endoftext|>


<commit_before>import React from "react";
import { infoPanelStyles } from "../../../globalStyles";
import { numericToCalendar } from "../../../util/dateHelpers";
import { getTipColorAttribute } from "../../../util/colorHelpers";
import { isColorByGenotype, decodeColorByGenotype } from "../../../util/getGenotype";
import { getTraitFromNode, getDivFromNode, getVaccineFromNode, getFullAuthorInfoFromNode } from "../../../util/treeMiscHelpers";
import { isValueValid } from "../../../util/globals";

const InfoLine = ({name, value, padBelow=false}) => {
  const renderValues = () => {
    if (!value) return null;
    if (Array.isArray(value)) {
      return value.map((v) => (

          {v}

      ));
    }
    return ({value});
  };

  return (

        {name + " "}

      {renderValues()}

  );
};

const StrainName = ({name}) => (

    {name}

);

/**
 * A React component to display information about the branch's time & divergence (where applicable)
 * @param  {Object} props
 * @param  {Object} props.node branch node currently highlighted
 */
const BranchLength = ({node}) => {
  const elements = []; // elements to render
  const divergence = getDivFromNode(node);
  const numDate = getTraitFromNode(node, "num_date");

  if (divergence) {
    elements.push(<InfoLine name="Divergence:" value={divergence.toExponential(3)} key="div"/>);
  }

  if (numDate !== undefined) {
    const date = numericToCalendar(numDate);
    const numDateConfidence = getTraitFromNode(node, "num_date", {confidence: true});
    if (numDateConfidence && numDateConfidence[0] !== numDateConfidence[1]) {
      elements.push(<InfoLine name="Inferred Date:" value={date} key="inferredDate"/>);
      const dateRange = [numericToCalendar(numDateConfidence[0]), numericToCalendar(numDateConfidence[1])];
      if (dateRange[0] !== dateRange[1]) {
        elements.push(<InfoLine name="Date Confidence Interval:" value={`(${dateRange[0]}, ${dateRange[1]})`} key="dateConf"/>);
      }
    } else {
      elements.push(<InfoLine name="Date:" value={date} key="date"/>);
    }
  }

  return elements;
};

/**
 * A React component to display information about the colorBy for a tip/branch,
 * potentially including a table with confidences.
 * @param  {Object} props
 * @param  {Object} props.node              branch / tip node currently highlighted
 * @param  {string} props.colorBy
 * @param  {bool}   props.colorByConfidence should these (colorBy conf) be displayed, if applicable?
 * @param  {func}   props.colorScale
 * @param  {object} props.colorings
 */
const ColorBy = ({node, colorBy, colorByConfidence, colorScale, colorings}) => {
  if (colorBy === "num_date") {
    return null; /* date has already been displayed via <BranchLength> */
  }
  /* handle genotype as a special case */
  if (isColorByGenotype(colorBy)) {
    const genotype = decodeColorByGenotype(colorBy);
    const name = genotype.aa ?
      `Amino Acid at ${genotype.gene} site ${genotype.positions.join(", ")}:` :
      `Nucleotide at pos ${genotype.positions.join(", ")}:`;
    return <InfoLine name={name} value={getTipColorAttribute(node, colorScale)}/>;
  }
  /* handle author as a special case */
  if (colorBy === "author") {
    const authorInfo = getFullAuthorInfoFromNode(node);
    if (!authorInfo) return null;
    // <InfoLine name="Author:" value={authorInfo.value}/> This is already displayed by AttributionInfo
    return (
      <>
        {authorInfo.title ? <InfoLine name="Title:" value={authorInfo.title}/> : null}
        {authorInfo.journal ? <InfoLine name="Journal:" value={authorInfo.journal}/> : null}
      </>
    );
  }
  /* general case */
  const name = (colorings && colorings[colorBy] && colorings[colorBy].title) ?
    colorings[colorBy].title :
    colorBy;

  /* helper function to avoid code duplication */
  const showCurrentColorByWithoutConfidence = () => {
    const value = getTraitFromNode(node, colorBy);
    return isValueValid(value) ?
      <InfoLine name={`${name}:`} value={value}/> :
      null;
  };

  /* handle trait confidences with lots of edge cases.
  This can be much improved upon resolution of https://github.com/nextstrain/augur/issues/386 */
  if (colorByConfidence === true) {
    const confidenceData = getTraitFromNode(node, colorBy, {confidence: true});
    if (!confidenceData) {
      console.error("couldn't find confidence vals for ", colorBy);
      return null;
    }
    /* if it's a tip with one confidence value > 0.99 then we interpret this as a known (i.e. not inferred) state */
    if (!node.hasChildren && Object.keys(confidenceData).length === 1 && Object.values(confidenceData)[0] > 0.99) {
      return showCurrentColorByWithoutConfidence();
    }
    const vals = Object.keys(confidenceData)
      .filter((v) => isValueValid(v))
      .sort((a, b) => confidenceData[a] > confidenceData[b] ? -1 : 1)
      .slice(0, 4)
      .map((v) => `${v} (${(100 * confidenceData[v]).toFixed(0)}%)`);
    if (!vals.length) return null; // can happen if values are invalid
    return <InfoLine name={`${name} (confidence):`} value={vals}/>;
  }
  return showCurrentColorByWithoutConfidence();
};

/**
 * A React Component to Display AA / NT mutations, if present.
 * @param  {Object} props
 * @param  {Object} props.node     branch node which is currently highlighted
 */
const Mutations = ({node}) => {
  if (!node.branch_attrs || !node.branch_attrs.mutations) return null;
  const elements = []; // elements to render
  const mutations = node.branch_attrs.mutations;

  /* --------- NUCLEOTIDE MUTATIONS --------------- */
  /* Nt mutations are found at `mutations.nuc` -> Array of strings */
  if (mutations.nuc && mutations.nuc.length) {
    const nDisplay = 9; // max number of mutations to display
    const nGapDisp = 4; // max number of gaps/Ns to display

    // gather muts with N/-
    const ngaps = mutations.nuc.filter((mut) => {
      return mut.slice(-1) === "N" || mut.slice(-1) === "-" ||
        mut.slice(0, 1) === "N" || mut.slice(0, 1) === "-";
    });
    const gapLen = ngaps.length; // number of mutations that exist with N/-

    // gather muts without N/-
    const nucs = mutations.nuc.filter((mut) => {
      return mut.slice(-1) !== "N" && mut.slice(-1) !== "-" &&
        mut.slice(0, 1) !== "N" && mut.slice(0, 1) !== "-";
    });
    const nucLen = nucs.length; // number of mutations that exist without N/-

    let m = nucs.slice(0, Math.min(nDisplay, nucLen)).join(", ");
    m += nucLen > nDisplay ? " + " + (nucLen - nDisplay) + " more" : "";
    let mGap = ngaps.slice(0, Math.min(nGapDisp, gapLen)).join(", ");
    mGap += gapLen > nGapDisp ? " + " + (gapLen - nGapDisp) + " more" : "";

    if (nucLen !== 0) {
      elements.push(<InfoLine name="Nucleotide mutations:" value={m} key="nuc"/>);
    }
    if (gapLen !== 0) {
      elements.push(<InfoLine name="Gap/N mutations:" value={mGap} key="gaps"/>);
    }
  } else {
    elements.push(<InfoLine name="No nucleotide mutations" value="" key="nuc"/>);
  }

  /* --------- AMINO ACID MUTATIONS --------------- */
  /* AA mutations are found at `mutations[prot_name]` -> Array of strings */
  const prots = Object.keys(mutations).filter((v) => v !== "nuc");
  const nMutsPerProt = {};
  let numberOfAaMuts = 0;
  for (const prot of prots) {
    nMutsPerProt[prot] = mutations[prot].length;
    numberOfAaMuts += mutations[prot].length;
  }
  if (numberOfAaMuts > 0) {
    const nDisplay = 3; // number of mutations to display per protein
    const nProtsToDisplay = 7; // max number of proteins to display
    let protsRendered = 0;
    const mutationsToRender = [];
    prots.forEach((prot) => {
      if (nMutsPerProt[prot] && protsRendered < nProtsToDisplay) {
        let x = prot + ":\u00A0\u00A0" + mutations[prot].slice(0, Math.min(nDisplay, nMutsPerProt[prot])).join(", ");
        if (nMutsPerProt[prot] > nDisplay) {
          x += " + " + (nMutsPerProt[prot] - nDisplay) + " more";
        }
        mutationsToRender.push(x);
        protsRendered++;
        if (protsRendered === nProtsToDisplay) {
          mutationsToRender.push(`(protein mutations truncated)`);
        }
      }
    });
    elements.push(<InfoLine name="AA mutations:" value={mutationsToRender} key="aa"/>);
  } else if (mutations.nuc && mutations.nuc.length) {
    /* we only print "No amino acid mutations" if we didn't already print
    "No nucleotide mutations" above */
    elements.push(<InfoLine name="No amino acid mutations" key="aa"/>);
  }

  return elements;
};

/**
 * A React component to render the descendent(s) of the current branch
 * @param  {Object} props
 * @param  {Object} props.node  branch node which is currently highlighted
 */
const BranchDescendents = ({node}) => {
  const [name, value] = node.fullTipCount === 1 ?
    ["Branch leading to", node.name] :
    ["Number of descendants:", node.fullTipCount];
  return <InfoLine name={name} value={value} padBelow/>;
};

/**
 * A React component to show vaccine information, if present
 * @param  {Object} props
 * @param  {Object} props.node  branch node which is currently highlighted
 */
const VaccineInfo = ({node}) => {
  const vaccineInfo = getVaccineFromNode(node);
  if (!vaccineInfo) return null;
  const renderElements = [];
  if (vaccineInfo.selection_date) {
    renderElements.push(<InfoLine name="Vaccine selected:" value={vaccineInfo.selection_date} key="seldate"/>);
  }
  if (vaccineInfo.start_date) {
    renderElements.push(<InfoLine name="Vaccine start date:" value={vaccineInfo.start_date} key="startdate"/>);
  }
  if (vaccineInfo.end_date) {
    renderElements.push(<InfoLine name="Vaccine end date:" value={vaccineInfo.end_date} key="enddate"/>);
  }
  if (vaccineInfo.serum) {
    renderElements.push(<InfoLine name="Serum strain" key="serum"/>);
  }
  return renderElements;
};

/**
 * A React component to show attribution information, if present
 * @param  {Object} props
 * @param  {Object} props.node  branch node which is currently highlighted
 */
const AttributionInfo = ({node}) => {
  const renderElements = [];
  const authorInfo = getFullAuthorInfoFromNode(node);
  if (authorInfo.value) {
    renderElements.push(<InfoLine name="Author:" value={authorInfo.value} key="author"/>);
  }

  /* The `gisaid_epi_isl` is a special value attached to nodes introduced during the 2019 nCoV outbreak.
  If set, we display this extra piece of information on hover */
  const gisaid_epi_isl = getTraitFromNode(node, "gisaid_epi_isl");
  if (isValueValid(gisaid_epi_isl)) {
    const epi_isl = gisaid_epi_isl.split("_")[2];
    renderElements.push(<InfoLine name="GISAID EPI ISL:" value={epi_isl} key="gisaid_epi_isl"/>);
  }

  return renderElements;
};

const Container = ({node, panelDims, children}) => {
  const xOffset = 10;
  const yOffset = 10;
  const width = 200;

  /* this adjusts the x-axis for the right tree in dual tree view */
  const xPos = node.shell.that.params.orientation[0] === -1 ?
    panelDims.width / 2 + panelDims.spaceBetweenTrees + node.shell.xTip :
    node.shell.xTip;
  const yPos = node.shell.yTip;
  const styles = {
    container: {
      position: "absolute",
      width,
      padding: "10px",
      borderRadius: 10,
      zIndex: 1000,
      pointerEvents: "none",
      fontFamily: infoPanelStyles.panel.fontFamily,
      fontSize: 14,
      lineHeight: 1,
      fontWeight: infoPanelStyles.panel.fontWeight,
      color: infoPanelStyles.panel.color,
      backgroundColor: infoPanelStyles.panel.backgroundColor,
      wordWrap: "break-word",
      wordBreak: "break-word"
    }
  };
  if (xPos < panelDims.width * 0.6) {
    styles.container.left = xPos + xOffset;
  } else {
    styles.container.right = panelDims.width - xPos + xOffset;
  }
  if (yPos < panelDims.height * 0.55) {
    styles.container.top = yPos + 4 * yOffset;
  } else {
    styles.container.bottom = panelDims.height - yPos + yOffset;
  }

  return (

        {children}

  );
};

const Comment = ({children}) => (

    {children}

);

const HoverInfoPanel = ({
  hovered,
  colorBy,
  colorByConfidence,
  colorScale,
  panelDims,
  colorings
}) => {
  if (!hovered) return null;
  const node = hovered.d.n;
  return (
    <Container node={node} panelDims={panelDims}>
      {hovered.type === ".tip" ? (
        <>
          <StrainName name={node.name}/>
          <VaccineInfo node={node}/>
          <Mutations node={node}/>
          <BranchLength node={node}/>
          <ColorBy node={node} colorBy={colorBy} colorByConfidence={colorByConfidence} colorScale={colorScale} colorings={colorings}/>
          <AttributionInfo node={node}/>
          <Comment>Click on tip to display more info</Comment>
        </>
      ) : (
        <>
          <BranchDescendents node={node}/>
          <Mutations node={node}/>
          <BranchLength node={node}/>
          <ColorBy node={node} colorBy={colorBy} colorByConfidence={colorByConfidence} colorScale={colorScale} colorings={colorings}/>
          <Comment>Click to zoom into clade</Comment>
        </>
      )}
    </Container>
  );
};

export default HoverInfoPanel;
<commit_msg>better display of divergence values above 0.01<commit_after>import React from "react";
import { infoPanelStyles } from "../../../globalStyles";
import { numericToCalendar } from "../../../util/dateHelpers";
import { getTipColorAttribute } from "../../../util/colorHelpers";
import { isColorByGenotype, decodeColorByGenotype } from "../../../util/getGenotype";
import { getTraitFromNode, getDivFromNode, getVaccineFromNode, getFullAuthorInfoFromNode } from "../../../util/treeMiscHelpers";
import { isValueValid } from "../../../util/globals";

const InfoLine = ({name, value, padBelow=false}) => {
  const renderValues = () => {
    if (!value) return null;
    if (Array.isArray(value)) {
      return value.map((v) => (

          {v}

      ));
    }
    return ({value});
  };

  return (

        {name + " "}

      {renderValues()}

  );
};

const StrainName = ({name}) => (

    {name}

);

/**
 * A React component to display information about the branch's time & divergence (where applicable)
 * @param  {Object} props
 * @param  {Object} props.node branch node currently highlighted
 */
const BranchLength = ({node}) => {
  const elements = []; // elements to render
  const divergence = getDivFromNode(node);
  const numDate = getTraitFromNode(node, "num_date");

  if (divergence) {
    const divergenceToDisplay = divergence > 1 ?
      Math.round((divergence + Number.EPSILON) * 1000) / 1000 :
      divergence > 0.01 ?
        Math.round((divergence + Number.EPSILON) * 10000) / 10000 :
        divergence.toExponential(3);
    elements.push(<InfoLine name="Divergence:" value={divergenceToDisplay} key="div"/>);
  }

  if (numDate !== undefined) {
    const date = numericToCalendar(numDate);
    const numDateConfidence = getTraitFromNode(node, "num_date", {confidence: true});
    if (numDateConfidence && numDateConfidence[0] !== numDateConfidence[1]) {
      elements.push(<InfoLine name="Inferred Date:" value={date} key="inferredDate"/>);
      const dateRange = [numericToCalendar(numDateConfidence[0]), numericToCalendar(numDateConfidence[1])];
      if (dateRange[0] !== dateRange[1]) {
        elements.push(<InfoLine name="Date Confidence Interval:" value={`(${dateRange[0]}, ${dateRange[1]})`} key="dateConf"/>);
      }
    } else {
      elements.push(<InfoLine name="Date:" value={date} key="date"/>);
    }
  }

  return elements;
};

/**
 * A React component to display information about the colorBy for a tip/branch,
 * potentially including a table with confidences.
 * @param  {Object} props
 * @param  {Object} props.node              branch / tip node currently highlighted
 * @param  {string} props.colorBy
 * @param  {bool}   props.colorByConfidence should these (colorBy conf) be displayed, if applicable?
 * @param  {func}   props.colorScale
 * @param  {object} props.colorings
 */
const ColorBy = ({node, colorBy, colorByConfidence, colorScale, colorings}) => {
  if (colorBy === "num_date") {
    return null; /* date has already been displayed via <BranchLength> */
  }
  /* handle genotype as a special case */
  if (isColorByGenotype(colorBy)) {
    const genotype = decodeColorByGenotype(colorBy);
    const name = genotype.aa ?
      `Amino Acid at ${genotype.gene} site ${genotype.positions.join(", ")}:` :
      `Nucleotide at pos ${genotype.positions.join(", ")}:`;
    return <InfoLine name={name} value={getTipColorAttribute(node, colorScale)}/>;
  }
  /* handle author as a special case */
  if (colorBy === "author") {
    const authorInfo = getFullAuthorInfoFromNode(node);
    if (!authorInfo) return null;
    // <InfoLine name="Author:" value={authorInfo.value}/> This is already displayed by AttributionInfo
    return (
      <>
        {authorInfo.title ? <InfoLine name="Title:" value={authorInfo.title}/> : null}
        {authorInfo.journal ? <InfoLine name="Journal:" value={authorInfo.journal}/> : null}
      </>
    );
  }
  /* general case */
  const name = (colorings && colorings[colorBy] && colorings[colorBy].title) ?
    colorings[colorBy].title :
    colorBy;

  /* helper function to avoid code duplication */
  const showCurrentColorByWithoutConfidence = () => {
    const value = getTraitFromNode(node, colorBy);
    return isValueValid(value) ?
      <InfoLine name={`${name}:`} value={value}/> :
      null;
  };

  /* handle trait confidences with lots of edge cases.
  This can be much improved upon resolution of https://github.com/nextstrain/augur/issues/386 */
  if (colorByConfidence === true) {
    const confidenceData = getTraitFromNode(node, colorBy, {confidence: true});
    if (!confidenceData) {
      console.error("couldn't find confidence vals for ", colorBy);
      return null;
    }
    /* if it's a tip with one confidence value > 0.99 then we interpret this as a known (i.e. not inferred) state */
    if (!node.hasChildren && Object.keys(confidenceData).length === 1 && Object.values(confidenceData)[0] > 0.99) {
      return showCurrentColorByWithoutConfidence();
    }
    const vals = Object.keys(confidenceData)
      .filter((v) => isValueValid(v))
      .sort((a, b) => confidenceData[a] > confidenceData[b] ? -1 : 1)
      .slice(0, 4)
      .map((v) => `${v} (${(100 * confidenceData[v]).toFixed(0)}%)`);
    if (!vals.length) return null; // can happen if values are invalid
    return <InfoLine name={`${name} (confidence):`} value={vals}/>;
  }
  return showCurrentColorByWithoutConfidence();
};

/**
 * A React Component to Display AA / NT mutations, if present.
 * @param  {Object} props
 * @param  {Object} props.node     branch node which is currently highlighted
 */
const Mutations = ({node}) => {
  if (!node.branch_attrs || !node.branch_attrs.mutations) return null;
  const elements = []; // elements to render
  const mutations = node.branch_attrs.mutations;

  /* --------- NUCLEOTIDE MUTATIONS --------------- */
  /* Nt mutations are found at `mutations.nuc` -> Array of strings */
  if (mutations.nuc && mutations.nuc.length) {
    const nDisplay = 9; // max number of mutations to display
    const nGapDisp = 4; // max number of gaps/Ns to display

    // gather muts with N/-
    const ngaps = mutations.nuc.filter((mut) => {
      return mut.slice(-1) === "N" || mut.slice(-1) === "-" ||
        mut.slice(0, 1) === "N" || mut.slice(0, 1) === "-";
    });
    const gapLen = ngaps.length; // number of mutations that exist with N/-

    // gather muts without N/-
    const nucs = mutations.nuc.filter((mut) => {
      return mut.slice(-1) !== "N" && mut.slice(-1) !== "-" &&
        mut.slice(0, 1) !== "N" && mut.slice(0, 1) !== "-";
    });
    const nucLen = nucs.length; // number of mutations that exist without N/-

    let m = nucs.slice(0, Math.min(nDisplay, nucLen)).join(", ");
    m += nucLen > nDisplay ? " + " + (nucLen - nDisplay) + " more" : "";
    let mGap = ngaps.slice(0, Math.min(nGapDisp, gapLen)).join(", ");
    mGap += gapLen > nGapDisp ? " + " + (gapLen - nGapDisp) + " more" : "";

    if (nucLen !== 0) {
      elements.push(<InfoLine name="Nucleotide mutations:" value={m} key="nuc"/>);
    }
    if (gapLen !== 0) {
      elements.push(<InfoLine name="Gap/N mutations:" value={mGap} key="gaps"/>);
    }
  } else {
    elements.push(<InfoLine name="No nucleotide mutations" value="" key="nuc"/>);
  }

  /* --------- AMINO ACID MUTATIONS --------------- */
  /* AA mutations are found at `mutations[prot_name]` -> Array of strings */
  const prots = Object.keys(mutations).filter((v) => v !== "nuc");
  const nMutsPerProt = {};
  let numberOfAaMuts = 0;
  for (const prot of prots) {
    nMutsPerProt[prot] = mutations[prot].length;
    numberOfAaMuts += mutations[prot].length;
  }
  if (numberOfAaMuts > 0) {
    const nDisplay = 3; // number of mutations to display per protein
    const nProtsToDisplay = 7; // max number of proteins to display
    let protsRendered = 0;
    const mutationsToRender = [];
    prots.forEach((prot) => {
      if (nMutsPerProt[prot] && protsRendered < nProtsToDisplay) {
        let x = prot + ":\u00A0\u00A0" + mutations[prot].slice(0, Math.min(nDisplay, nMutsPerProt[prot])).join(", ");
        if (nMutsPerProt[prot] > nDisplay) {
          x += " + " + (nMutsPerProt[prot] - nDisplay) + " more";
        }
        mutationsToRender.push(x);
        protsRendered++;
        if (protsRendered === nProtsToDisplay) {
          mutationsToRender.push(`(protein mutations truncated)`);
        }
      }
    });
    elements.push(<InfoLine name="AA mutations:" value={mutationsToRender} key="aa"/>);
  } else if (mutations.nuc && mutations.nuc.length) {
    /* we only print "No amino acid mutations" if we didn't already print
    "No nucleotide mutations" above */
    elements.push(<InfoLine name="No amino acid mutations" key="aa"/>);
  }

  return elements;
};

/**
 * A React component to render the descendent(s) of the current branch
 * @param  {Object} props
 * @param  {Object} props.node  branch node which is currently highlighted
 */
const BranchDescendents = ({node}) => {
  const [name, value] = node.fullTipCount === 1 ?
    ["Branch leading to", node.name] :
    ["Number of descendants:", node.fullTipCount];
  return <InfoLine name={name} value={value} padBelow/>;
};

/**
 * A React component to show vaccine information, if present
 * @param  {Object} props
 * @param  {Object} props.node  branch node which is currently highlighted
 */
const VaccineInfo = ({node}) => {
  const vaccineInfo = getVaccineFromNode(node);
  if (!vaccineInfo) return null;
  const renderElements = [];
  if (vaccineInfo.selection_date) {
    renderElements.push(<InfoLine name="Vaccine selected:" value={vaccineInfo.selection_date} key="seldate"/>);
  }
  if (vaccineInfo.start_date) {
    renderElements.push(<InfoLine name="Vaccine start date:" value={vaccineInfo.start_date} key="startdate"/>);
  }
  if (vaccineInfo.end_date) {
    renderElements.push(<InfoLine name="Vaccine end date:" value={vaccineInfo.end_date} key="enddate"/>);
  }
  if (vaccineInfo.serum) {
    renderElements.push(<InfoLine name="Serum strain" key="serum"/>);
  }
  return renderElements;
};

/**
 * A React component to show attribution information, if present
 * @param  {Object} props
 * @param  {Object} props.node  branch node which is currently highlighted
 */
const AttributionInfo = ({node}) => {
  const renderElements = [];
  const authorInfo = getFullAuthorInfoFromNode(node);
  if (authorInfo.value) {
    renderElements.push(<InfoLine name="Author:" value={authorInfo.value} key="author"/>);
  }

  /* The `gisaid_epi_isl` is a special value attached to nodes introduced during the 2019 nCoV outbreak.
  If set, we display this extra piece of information on hover */
  const gisaid_epi_isl = getTraitFromNode(node, "gisaid_epi_isl");
  if (isValueValid(gisaid_epi_isl)) {
    const epi_isl = gisaid_epi_isl.split("_")[2];
    renderElements.push(<InfoLine name="GISAID EPI ISL:" value={epi_isl} key="gisaid_epi_isl"/>);
  }

  return renderElements;
};

const Container = ({node, panelDims, children}) => {
  const xOffset = 10;
  const yOffset = 10;
  const width = 200;

  /* this adjusts the x-axis for the right tree in dual tree view */
  const xPos = node.shell.that.params.orientation[0] === -1 ?
    panelDims.width / 2 + panelDims.spaceBetweenTrees + node.shell.xTip :
    node.shell.xTip;
  const yPos = node.shell.yTip;
  const styles = {
    container: {
      position: "absolute",
      width,
      padding: "10px",
      borderRadius: 10,
      zIndex: 1000,
      pointerEvents: "none",
      fontFamily: infoPanelStyles.panel.fontFamily,
      fontSize: 14,
      lineHeight: 1,
      fontWeight: infoPanelStyles.panel.fontWeight,
      color: infoPanelStyles.panel.color,
      backgroundColor: infoPanelStyles.panel.backgroundColor,
      wordWrap: "break-word",
      wordBreak: "break-word"
    }
  };
  if (xPos < panelDims.width * 0.6) {
    styles.container.left = xPos + xOffset;
  } else {
    styles.container.right = panelDims.width - xPos + xOffset;
  }
  if (yPos < panelDims.height * 0.55) {
    styles.container.top = yPos + 4 * yOffset;
  } else {
    styles.container.bottom = panelDims.height - yPos + yOffset;
  }

  return (

        {children}

  );
};

const Comment = ({children}) => (

    {children}

);

const HoverInfoPanel = ({
  hovered,
  colorBy,
  colorByConfidence,
  colorScale,
  panelDims,
  colorings
}) => {
  if (!hovered) return null;
  const node = hovered.d.n;
  return (
    <Container node={node} panelDims={panelDims}>
      {hovered.type === ".tip" ? (
        <>
          <StrainName name={node.name}/>
          <VaccineInfo node={node}/>
          <Mutations node={node}/>
          <BranchLength node={node}/>
          <ColorBy node={node} colorBy={colorBy} colorByConfidence={colorByConfidence} colorScale={colorScale} colorings={colorings}/>
          <AttributionInfo node={node}/>
          <Comment>Click on tip to display more info</Comment>
        </>
      ) : (
        <>
          <BranchDescendents node={node}/>
          <Mutations node={node}/>
          <BranchLength node={node}/>
          <ColorBy node={node} colorBy={colorBy} colorByConfidence={colorByConfidence} colorScale={colorScale} colorings={colorings}/>
          <Comment>Click to zoom into clade</Comment>
        </>
      )}
    </Container>
  );
};

export default HoverInfoPanel;
<|endoftext|>