content: // ===============================================================================
  // Authors: AFRL/RQQD
  // Organization: Air Force Research Laboratory, Aerospace Systems Directorate, Power and Control Division
  // 
  // Copyright (c) 2017 Government of the United State of America, as represented by
  // the Secretary of the Air Force.  No copyright is claimed in the United States under
  // Title 17, U.S. Code.  All Other Rights Reserved.
  // ===============================================================================
  
  /*
   * GRAL: GRAphing Library for Java(R)
   *
   * (C) Copyright 2009-2013 Erich Seifert <dev[at]erichseifert.de>,
   * Michael Seifert <mseifert[at]error-reports.org>
   *
   * This file is part of GRAL.
   *
   * GRAL is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * GRAL is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with GRAL.  If not, see <http://www.gnu.org/licenses/>.
   */
  package de.erichseifert.gral.plots;
  
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  
  import de.erichseifert.gral.navigation.AbstractNavigator;
  import de.erichseifert.gral.navigation.NavigationEvent;
  import de.erichseifert.gral.plots.axes.Axis;
  import de.erichseifert.gral.plots.axes.AxisRenderer;
  import de.erichseifert.gral.util.MathUtils;
  import de.erichseifert.gral.util.PointND;
  
  /**
   * <p>Abstract base class that can be used to control the zoom and panning of a
   * plot. The navigator translates the interaction to operations on a defined
   * set of axes: Zooming is translated as scaling, panning is done by uniformly
   * changing the minimum and maximum values of the axes.</p>
   *
   * <p>Additionally, the actions can also be bound to a certain direction by
   * defining a more restricted set of axes. The methods {@link #getDirection()}
   * and {@link #setDirection(de.erichseifert.gral.navigation.NavigationDirection)}
   * provide a convenient way for setting predefined sets of axes:</p>
   *
   * <pre>
   *
   */
  public abstract class PlotNavigator extends AbstractNavigator {
  	/** AbstractPlot that will be navigated. */
  	private final Plot plot;
  	/** Mapping of axis name to informations on center and zoom. */
  	private final Map<String, NavigationInfo> infos;
  	/** Axes affected by navigation. */
  	private final List<String> axes;
  
  	/**
  	 * Data class for storing navigational information for an axis.
  	 */
  	protected static final class NavigationInfo {
  		/** Minimum value of the original axis. */
  		private final Number minOriginal;
  		/** Maximum value of the original axis. */
  		private final Number maxOriginal;
  		/** Center value of the original axis. */
  		private final double centerOriginal;
  		/** Current center value. */
  		private double center;
  		/** Current zoom level. */
  		private double zoom;
  
  		/**
  		 * Initializes a new {@code NavigationInfo} instance.
  		 * @param min Minimum value in axis units.
  		 * @param max Maximum value in axis units.
  		 * @param center Center in axis units.
  		 */
  		public NavigationInfo(Number min, Number max, double center) {
  			this.minOriginal = min;
  			this.maxOriginal = max;
  			this.centerOriginal = center;
  			this.center = centerOriginal;
  			this.zoom = 1.0;
  		}
  
  		/**
  		 * Returns the original minimum value.
  		 * @return Original minimum value.
  		 */
  		public Number getMinOriginal() {
  			return minOriginal;
  		}
  		/**
  		 * Returns the original maximum value.
  		 * @return Original maximum value.
  		 */
  		public Number getMaxOriginal() {
  			return maxOriginal;
  		}
  		/**
  		 * Returns the original center value.
  		 * @return Original center value.
  		 */
  		public double getCenterOriginal() {
  			return centerOriginal;
  		}
  
  		/**
  		 * Returns the current center value.
  		 * @return Current center value.
  		 */
  		public double getCenter() {
  			return center;
  		}
  		/**
  		 * Sets the current center value.
  		 * @param center New center value.
  		 */
  		public void setCenter(double center) {
  			this.center = center;
  		}
  
  		/**
  		 * Returns the current zoom factor.
  		 * @return Current zoom factor.
  		 */
  		public double getZoom() {
  			return zoom;
  		}
  		/**
  		 * Sets the current zoom factor.
  		 * @param zoom New zoom factor.
  		 */
  		public void setZoom(double zoom) {
  			this.zoom = zoom;
  		}
  	}
  
  	/**
  	 * Initializes a new instance that is responsible for zooming and panning
  	 * the axes with the specified names of the specified plot.
  	 * @param plot AbstractPlot to be zoomed and panned.
  	 * @param axesNames Names of the axes that should be controlled by this
  	 *        navigator.
  	 */
  	public PlotNavigator(Plot plot, List<String> axesNames) {
  		axes = new LinkedList<String>();
  		infos = new HashMap<String, NavigationInfo>();
  
  		this.plot = plot;
  
  		setAxes(axesNames);
  	}
  
  	/**
  	 * Initializes a new instance that is responsible for zooming and panning
  	 * the axes with the specified names of the specified plot.
  	 * @param plot AbstractPlot to be zoomed and panned.
  	 * @param axesNames Names of the axes that should be controlled by this
  	 *        navigator.
  	 */
  	public PlotNavigator(Plot plot, String... axesNames) {
  		this(plot, Arrays.asList(axesNames));
  	}
  
  	/**
  	 * Refreshes the values of all axis to reflect navigation actions.
  	 */
  	private void refresh() {
  		for (String axisName : getAxes()) {
  			NavigationInfo info = getInfo(axisName);
  			if (info == null) {
  				continue;
  			}
  
  			AxisRenderer renderer = getPlot().getAxisRenderer(axisName);
  			if (renderer == null) {
  				continue;
  			}
  
  			Axis axis = getPlot().getAxis(axisName);
  
  			// Original range in screen units
  			// Most up-to-date view coordinates (axis's layout) must be used
  			double minOrig = renderer.worldToView(
  				axis, info.getMinOriginal(), true);
  			double maxOrig = renderer.worldToView(
  				axis, info.getMaxOriginal(), true);
  			double rangeOrig = maxOrig - minOrig;
  
  			// New axis scale
  			double zoom = info.getZoom();
  			double range = rangeOrig*zoom;
  			double center = renderer.worldToView(axis, info.getCenter(), true);
  			Number min = renderer.viewToWorld(axis, center - 0.5*range, true);
  			Number max = renderer.viewToWorld(axis, center + 0.5*range, true);
  
  			// Change axis
  			axis.setRange(min, max);
  		}
  	}
  
  	/**
  	 * Returns the plot stored in this instance.
  	 * @return Stored plot object.
  	 */
  	protected Plot getPlot() {
  		return plot;
  	}
  
  	/**
  	 * Returns the current zoom level of the associated object.
  	 * @return Current zoom level.
  	 */
  	public double getZoom() {
  		double zoom = 0.0;
  		int count = 0;
  		for (String axisName : getAxes()) {
  			NavigationInfo info = getInfo(axisName);
  			if (info == null) {
  				continue;
  			}
  			if (!MathUtils.isCalculatable(info.getZoom())) {
  				continue;
  			}
  			zoom += info.getZoom();
  			count++;
  		}
  		return zoom / count;
  	}
  
  	/**
  	 * Sets the zoom level of the associated object to the specified value.
  	 * @param zoomNew New zoom level.
  	 */
  	public void setZoom(double zoomNew) {
  		if (!isZoomable() || (zoomNew <= 0.0) ||
  				!MathUtils.isCalculatable(zoomNew)) {
  			return;
  		}
  		double zoomOld = getZoom();
  		zoomNew = MathUtils.limit(zoomNew, getZoomMin(), getZoomMax());
  		if (zoomOld == zoomNew) {
  			return;
  		}
  		for (String axisName : getAxes()) {
  			NavigationInfo info = getInfo(axisName);
  			if (info == null) {
  				continue;
  			}
  			info.setZoom(zoomNew);
  		}
  		NavigationEvent<Double> event =
  			new NavigationEvent<Double>(this, zoomOld, zoomNew);
  		fireZoomChanged(event);
  		refresh();
  	}
  
  	/**
  	 * Returns the current center point. The returned point contains value in
  	 * world units.
  	 * @return Center point in world units.
  	 */
  	public PointND<? extends Number> getCenter() {
  		List<String> axesNames = getAxes();
  		Double[] centerCoords = new Double[axesNames.size()];
  		int axisIndex = 0;
  		for (String axisName : axesNames) {
  			NavigationInfo info = getInfo(axisName);
  			if (info != null) {
  				double axisCenter = info.getCenter();
  				centerCoords[axisIndex] = axisCenter;
  			}
  			axisIndex++;
  		}
  		return new PointND<Double>(centerCoords);
  	}
  
  	/**
  	 * Sets a new center point. The values of the point are in world units.
  	 * @param center New center point in world units.
  	 */
  	public void setCenter(PointND<? extends Number> center) {
  		if (!isPannable()) {
  			return;
  		}
  		PointND<? extends Number> centerOld = getCenter();
  		if (centerOld.equals(center)) {
  			return;
  		}
  		List<String> axesNames = getAxes();
  		int axisIndex = 0;
  		for (String axisName : axesNames) {
  			NavigationInfo info = getInfo(axisName);
  			if (info != null) {
  				Number centerCoordNew = center.get(axisIndex);
  				info.setCenter(centerCoordNew.doubleValue());
  			}
  			axisIndex++;
  		}
  
  		NavigationEvent<PointND<? extends Number>> event =
  			new NavigationEvent<PointND<? extends Number>>(this, centerOld, center);
  		fireCenterChanged(event);
  		refresh();
  	}
  
  	/**
  	 * Moves the center by the relative values of the specified point.
  	 * The values of the point are in screen units.
  	 * @param deltas Relative values to use for panning.
  	 */
  	public void pan(PointND<? extends Number> deltas) {
  		if (!isPannable()) {
  			return;
  		}
  		PointND<? extends Number> centerOld = getCenter();
  		Double[] centerCoords = new Double[centerOld.getDimensions()];
  		int axisIndex = 0;
  		for (String axisName : getAxes()) {
  			NavigationInfo info = getInfo(axisName);
  			if (info != null) {
  				double delta = getDimensionValue(axisName, deltas).doubleValue();
  				AxisRenderer renderer =
  					getPlot().getAxisRenderer(axisName);
  				if (renderer != null) {
  					boolean swapped = renderer.isShapeDirectionSwapped();
  					if (swapped) {
  						delta = -delta;
  					}
  					Axis axis = getPlot().getAxis(axisName);
  					// Fetch current center on screen
  					double center = renderer.worldToView(
  						axis, info.getCenter(), true);
  					// Move center and convert it to axis coordinates
  					Number centerNew = renderer.viewToWorld(
  						axis, center - delta, true);
  					// Change axis (world units)
  					info.setCenter(centerNew.doubleValue());
  					centerCoords[axisIndex] = centerNew.doubleValue();
  				}
  			}
  			axisIndex++;
  		}
  		PointND<? extends Number> centerNew = new PointND<Double>(centerCoords);
  		NavigationEvent<PointND<? extends Number>> event =
  			new NavigationEvent<PointND<? extends Number>>(this, centerOld, centerNew);
  		fireCenterChanged(event);
  		refresh();
  	}
  
  	/**
  	 * Sets the current state as the default state of the object.
  	 * Resetting the navigator will then return to the default state.
  	 */
  	public void setDefaultState() {
  		infos.clear();
  		for (String axisName : getAxes()) {
  			Axis axis = getPlot().getAxis(axisName);
  			if (axis == null) {
  				continue;
  			}
  			double min = 0.0;
  			double max = 0.0;
  			Number center = 0.0;
  			AxisRenderer renderer = getPlot().getAxisRenderer(axisName);
  			if (renderer != null && axis.isValid()) {
  				min = renderer.worldToView(axis, axis.getMin(), false);
  				max = renderer.worldToView(axis, axis.getMax(), false);
  				if (MathUtils.isCalculatable(min) && MathUtils.isCalculatable(max)) {
  					center = renderer.viewToWorld(axis, (min + max)/2.0, false);
  				}
  			}
  			NavigationInfo info = new NavigationInfo(
  				axis.getMin(), axis.getMax(), center.doubleValue());
  			infos.put(axisName, info);
  		}
  	}
  
  	/**
  	 * Sets the object's position and zoom level to the default state.
  	 */
  	public void reset() {
  		double zoomOld = getZoom();
  		double zoomNew = 1.0;
  		PointND<? extends Number> centerOld = getCenter();
  
  		List<String> axesNames = getAxes();
  		Double[] centerCoordsOriginal = new Double[centerOld.getDimensions()];
  		int axisIndex = 0;
  		for (String axisName : axesNames) {
  			NavigationInfo info = getInfo(axisName);
  			if (info != null) {
  				double centerCoordOriginal = info.getCenterOriginal();
  				centerCoordsOriginal[axisIndex] = centerCoordOriginal;
  
  				info.setCenter(centerCoordOriginal);
  				info.setZoom(zoomNew);
  			}
  			axisIndex++;
  		}
  		PointND<Double> centerNew = new PointND<Double>(centerCoordsOriginal);
  
  		NavigationEvent<PointND<? extends Number>> panEvent =
  			new NavigationEvent<PointND<? extends Number>>(this, centerOld, centerNew);
  		fireCenterChanged(panEvent);
  
  		NavigationEvent<Double> zoomEvent =
  			new NavigationEvent<Double>(this, zoomOld, 1.0);
  		fireZoomChanged(zoomEvent);
  
  		refresh();
  	}
  
  	/**
  	 * Returns navigational information for the axis with specified name.
  	 * @param axisName Axis name.
  	 * @return Navigational information.
  	 */
  	protected NavigationInfo getInfo(String axisName) {
  		return infos.get(axisName);
  	}
  
  	/**
  	 * Returns the names of all axes handled by this object.
  	 * @return Names of all axes handled by this object.
  	 */
  	protected List<String> getAxes() {
  		return Collections.unmodifiableList(axes);
  	}
  
  	/**
  	 * Sets the names of the axes that should be handled by this object.
  	 * @param axesNames Names of the axes that should be handled.
  	 */
  	protected void setAxes(List<String> axesNames) {
  		axes.clear();
  		axes.addAll(axesNames);
  		setDefaultState();
  	}
  
  	/**
  	 * Sets the names of the axes that should be handled by this object.
  	 * @param axesNames Names of the axes that should be handled.
  	 */
  	protected void setAxes(String... axesNames) {
  		setAxes(Arrays.asList(axesNames));
  	}
  
  	/**
  	 * Returns the number dimensions the associated plot can handle. For a
  	 * one-dimensional plot like {@link PiePlot} this is 1, for a
  	 * two-dimensional plot like {@link XYPlot} this is 2, and so on.
  	 * @return Number of dimensions the associated plot can handle.
  	 */
  	protected abstract int getDimensions();
  
  	/**
  	 * Return the index that can be used to access data for the axis with the
  	 * specified name. The returned index must be larger than or equal to 0 and
  	 * smaller than the result of {@link #getDimensions()}.
  	 * @param axisName Name of the axis.
  	 * @param values Data values.
  	 * @return Dimension index.
  	 */
  	protected abstract Number getDimensionValue(
  		String axisName, PointND<? extends Number> values);
  }
  