API Docs for:
Show:

File: app/views/topology/panzoom.js

'use strict';

/**
 * Provide the PanZoomModule class.
 *
 * @module topology
 * @submodule topology.panzoom
 */

YUI.add('juju-topology-panzoom', function(Y) {
  var views = Y.namespace('juju.views'),
      models = Y.namespace('juju.models'),
      d3ns = Y.namespace('d3');

  /**
   * Handle PanZoom within a Topology.
   *
   * ## Emitted events:
   *
   * - *rescaled:* post-zoom event, after the scene has been rescaled,
   *   queried object positions should be accurate.
   *
   * @class PanZoomModule
   */
  var PanZoomModule = Y.Base.create('PanZoomModule', d3ns.Module, [], {

    events: {
      scene: {
        '#zoom-out-btn': {click: 'zoom_out'},
        '#zoom-in-btn': {click: 'zoom_in'}
      },
      yui: {
        /**
          Fired when the canvas is zoomed.

          @event zoom
        */
        zoom: 'zoomHandler',
        /**
          Build pan and zoom functionality when the canvas is rendered.

          @event rendered
        */
        rendered: 'renderedHandler'
      }
    },

    componentBound: function() {
      var topo = this.get('component'),
          options = topo.options;

      this.toScale = d3.scale.linear()
                            .domain([options.minZoom, options.maxZoom])
                            .range([0.25, 2])
                            .clamp(true);
      this.toSlider = d3.scale.linear()
                            .domain([0.25, 2])
                            .range([options.minZoom, options.maxZoom])
                            .clamp(true);
    },

    renderSlider: function() {
      var self = this,
          topo = this.get('component'),
          options = topo.options,
          currentScale = topo.get('scale'),
          slider;

      if (self.slider) {
        return;
      }

      slider = new Y.Slider({
        min: options.minZoom,
        max: options.maxZoom,
        value: this.toSlider(currentScale)
      });
      // XXX: selection to module option
      slider.render('#slider-parent');
      topo.recordSubscription(this,
                              slider.after('valueChange', function(evt) {
                                if (d3.event && d3.event.scale &&
                                    d3.event.translate) {
                                  return;
                                }
                                self._fire_zoom(self.toScale(evt.newVal));
                              }));
      this.slider = slider;
    },

    /**
     * Handler for 'zoom' event.
     *
     * @method zoomHandler
     */
    zoomHandler: function(evt) {
      var slider = this.slider,
          topo = this.get('component'),
          height = topo.get('height'),
          width = topo.get('width'),
          options = topo.options;

      if (!this.slider) {
        return;
      }

      // If this is a scroll wheel event translate
      // delta and apply to scale.
      if (evt.sourceEvent && evt.sourceEvent.type === 'mousewheel') {
        evt = evt.sourceEvent;
      }
      if (evt.type === 'mousewheel') {
        var delta = (evt.wheelDelta > 0) ? 0.1 : -0.1;
        evt.scale = (topo.get('scale') + delta);
        evt.translate = topo.get('translate');
      }
      slider._set('value', this.toSlider(evt.scale));
      this.rescale(evt);
    },

    /**
     * Zoom out event handler.
     *
     * @method zoom_out
     */
    zoom_out: function(data, context) {
      var slider = context.slider,
          val = slider.get('value');
      slider.set('value', val - 25);
    },

    /**
     * Zoom in event handler.
     *
     * @method zoom_in
     */
    zoom_in: function(data, context) {
      var slider = context.slider,
          val = slider.get('value');
      slider.set('value', val + 25);
    },

    /**
     * Wrapper around the actual rescale method for zoom buttons.
     *
     * @method _fire_zoom
     */
    _fire_zoom: function(scale) {
      var topo = this.get('component'),
          vis = topo.vis,
          zoom = topo.zoom,
          rect = topo.zoomPlane,
          delta,
          evt = {};

      delta = scale - topo.get('scale');

      // Build a temporary event that rescale can use of a similar
      // construction to d3.event.
      evt.scale = scale;
      // Update the scale in our zoom behavior manager to maintain state.
      zoom.scale(Math.floor(scale));
      // Update the translate so that we scale from the center
      // instead of the origin.
      evt.translate = zoom.translate();
      evt.translate[0] -= (parseInt(rect.attr('width'), 10) / 2) * delta;
      evt.translate[1] -= (parseInt(rect.attr('height'), 10) / 2) * delta;
      zoom.translate(evt.translate);

      this.rescale(evt);
    },

    /**
     * Rescale the visualization on a zoom/pan event.
     *
     * @method rescale
     */
    rescale: function(evt) {
      // Make sure we don't scale outside of our bounds.
      // This check is needed because we're messing with d3's zoom
      // behavior outside of mouse events (e.g.: with the slider),
      // and can't trust that zoomExtent will play well.
      var topo = this.get('component'),
          options = topo.options,
          vis = topo.vis;

      if (!vis) {
        return;
      }

      evt.scale = this.toSlider(evt.scale) / 100.0;

      // Store the current value of scale so that it can be restored later.
      topo.set('scale', evt.scale);
      // Store the current value of translate as well, by copying the event
      // array in order to avoid reference sharing.
      topo.set('translate', Y.mix(evt.translate));
      vis.attr('transform', 'translate(' + topo.get('translate') + ')' +
              ' scale(' + topo.get('scale') + ')');
      topo.fire('rescaled');
    },

    renderedHandler: function(evt) {
      // Preserve zoom when the scene is updated.
      var topo = this.get('component'),
          changed = false,
          currentScale = topo.get('scale'),
          currentTranslate = topo.get('translate');

      this.renderSlider();
      if (currentTranslate && currentTranslate !== topo.get('translate')) {
        topo.zoom.translate(currentTranslate);
        changed = true;
      }
      if (currentScale && currentScale !== topo.zoom.scale()) {
        topo.zoom.scale(currentScale);
        changed = true;
      }
      if (changed) {
        this.rescale({scale: currentScale, translate: currentTranslate});
      }
    }
  }, {
    ATTRS: {}

  });
  views.PanZoomModule = PanZoomModule;
}, '0.1.0', {
  requires: [
    'node',
    'event',
    'slider',
    'd3',
    'd3-components',
    'juju-models',
    'juju-env'
  ]
});