/**
 * Copyright 2010-2014 Jingjing Li.
 *
 * This file is part of jplot2d.
 *
 * jplot2d 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 any later version.
 *
 * jplot2d 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 General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with jplot2d. If not, see .
 */
package org.jplot2d.element.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.jplot2d.data.GraphData;
import org.jplot2d.transform.NormalTransform;
import org.jplot2d.util.Range;

/**
 * @author Jingjing Li
 * 
 */
public class AxisRangeUtils {

	/**
	 * The depth of zoom-in is controlled by this constant. The precision limit of double is 0x1.0p-52. 65536x to ensure
	 * the ticks look like even-distributed.
	 */
	protected static final double PRECISIONLIMIT = 0x1.0p-36;

	/**
	 * The default precision limit when range == 0. This make the range is large enough to produce tick labels within 4
	 * digits.
	 */
	protected static final double DIGI4LIMIT = 0.004;

	private AxisRangeUtils() {

	}

	/**
	 * find the virtual normalized intersected range of valid world range
	 * 
	 * @param axes
	 *            locked axes
	 * @return
	 */
	private static Range getBounds(Map vtMap) {
		Range pbnds = new Range.Double(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
		for (Map.Entry me : vtMap.entrySet()) {
			AxisTransformEx ax = me.getKey();
			Range aprange = me.getValue().convToNR(ax.getType().getBoundary(ax.getTransform()));
			pbnds = pbnds.intersect(aprange);
		}
		return pbnds;
	}

	/**
	 * Create a set of virtual NormalTransform correspond to the NormalTransform of the given axes. The virtual
	 * NormalTransform will try to remove the offset to minimize the double calculation round error.
	 * 
	 * @param axes
	 * @return
	 */
	public static Map createVirtualTransformMap(Collection axes) {

		if (axes.size() == 0) {
			return Collections.emptyMap();
		}

		Map result = new HashMap();

		boolean match = false;
		double offset = Double.NaN;
		for (AxisTransformEx axis : axes) {
			if (!match) {
				offset = axis.getNormalTransform().getOffset();
				match = true;
			} else if (axis.getNormalTransform().getOffset() != offset) {
				match = false;
				break;
			}
		}
		if (match) {
			for (AxisTransformEx axis : axes) {
				NormalTransform nx = axis.getNormalTransform().deriveNoOffset();
				result.put(axis, nx);
			}
		} else {
			for (AxisTransformEx axis : axes) {
				NormalTransform nx = axis.getNormalTransform();
				result.put(axis, nx);
			}
		}

		return result;
	}

	public static Map createNormalTransformMap(Collection axes) {
		Map vtMap = new LinkedHashMap();
		for (AxisTransformEx axis : axes) {
			vtMap.put(axis, axis.getNormalTransform());
		}
		return vtMap;
	}

	/**
	 * Find a nice normalized range to make sure all corresponding world range only contain valid values.
	 * 
    *
  • If any corresponding world range of layer span invalid value, the end of the given normalized range is * adjusted inward to match the data that most close to boundary.
  • *
  • If there is no valid data inside the given range, null will be returned.
  • *
  • No margin added to the result.
  • *
* * @return a RangeStatus contain the nice normalized range. The Boolean status to indicate if there are some data * points outside the value bounds */ public static RangeStatus calcNiceVirtualRange(Map vtMap) { /* find the normalized intersected range of valid world range among layers */ Range pbnds = new Range.Double(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); for (AxisTransformEx ax : vtMap.keySet()) { Range aprange = vtMap.get(ax).convToNR(ax.getType().getBoundary(ax.getTransform())); pbnds = pbnds.intersect(aprange); } if (pbnds == null) { /* The given range on all layers are outside the boundary. */ return null; } Range padRange = null; boolean dataOutsideBounds = false; for (AxisTransformEx at : vtMap.keySet()) { for (LayerEx layer : at.getLayers()) { Range urange = vtMap.get(at).convFromNR(pbnds); Range wDRange = new Range.Double(); if (layer.getXAxisTransform() == at) { AxisTransformEx yat = layer.getYAxisTransform(); Range yRange; if (yat.getLockGroup().isAutoRange()) { yRange = yat.getType().getBoundary(yat.getTransform()); } else { yRange = yat.getRange(); } for (GraphEx dp : layer.getGraphs()) { if (dp.getData() != null) { GraphData dataInBounds = dp.getData().applyBoundary(urange, yRange); wDRange = dataInBounds.getXRange().union(wDRange); if (dataInBounds.hasPointOutsideXBounds()) { dataOutsideBounds = true; } } } } else if (layer.getYAxisTransform() == at) { AxisTransformEx xat = layer.getXAxisTransform(); Range xRange; if (xat.getLockGroup().isAutoRange()) { xRange = xat.getType().getBoundary(xat.getTransform()); } else { xRange = xat.getRange(); } for (GraphEx dp : layer.getGraphs()) { if (dp.getData() != null) { GraphData dataInBounds = dp.getData().applyBoundary(xRange, urange); wDRange = dataInBounds.getYRange().union(wDRange); if (dataInBounds.hasPointOutsideYBounds()) { dataOutsideBounds = true; } } } } if (!wDRange.isEmpty()) { Range pDRange = vtMap.get(at).convToNR(wDRange); /* expand range by margin factor */ double pLo = pDRange.getMin(); double pHi = pDRange.getMax(); double span = pDRange.getSpan(); pLo -= span * at.getMarginFactor(); pHi += span * at.getMarginFactor(); Range pXdRange = new Range.Double(pLo, pHi).intersect(pbnds); if (padRange == null) { padRange = pXdRange; } else { padRange = padRange.union(pXdRange); } } } } if (padRange == null) { return null; } return new RangeStatus(padRange.getMin(), padRange.getMax(), dataOutsideBounds); } /** * Adjust the given range and to make sure all corresponding user range only contain valid values. *
    *
  • If there is no intersection among the given range and valid boundaries, null is returned.
  • *
  • If all corresponding world range of layers are valid, the given norm range is returned without any * adjustment.
  • *
  • If any corresponding world range of layers spans over invalid value, the end of given range is adjusted * inward to the valid boundary.
  • *
  • If the given range start/end at INFINITY, then "nearest data" well be found instead of the valid boundary. *
      *
    • If there is no valid data inside the valid range, null will be returned.
    • *
    • A margin is appended on the "nearest data" end.
    • *
    *
  • *
* * @param range * The caller should guarantee pLo < pHi * @return a RangeStatus contain the adjusted normalized range. The RangeAdjustTag status indicate if the given * range is adjusted. */ public static Range validateNormalRange(Range range, Collection axes, boolean findNearsetData) { Map axisMap = createNormalTransformMap(axes); return validateNormalRange(range, axisMap, findNearsetData); } /** * Find a nice norm range inside the given range and to make sure all corresponding user range only contain valid * values. *
    *
  • If there is no intersection among the given range and valid boundaries, null is returned.
  • *
  • If all corresponding world range of layers are valid, the given norm range is returned without any * adjustment.
  • *
  • If any corresponding world range of layers spans over invalid value, the end of given range is adjusted * inward to the data that most close to boundary. *
      *
    • If there is no valid data inside the valid range, null will be returned.
    • *
    • A margin is appended on the "nearest data" end.
    • *
    *
  • *
* * @param range * The caller should guarantee pLo < pHi * @return a RangeStatus contain the nice normalized range. The RangeAdjustTag status indicate if the given range is * adjusted. */ public static Range validateNormalRange(Range range, Map axisMap, boolean findNearsetData) { if (range.isInverted()) { throw new IllegalArgumentException(); } /* find the normalized intersected range of valid world range among layers */ Range pbnds = new Range.Double(range); for (Map.Entry me : axisMap.entrySet()) { AxisTransformEx ax = me.getKey(); // virtual normalized range of the axis type boundary Range aprange = me.getValue().convToNR(me.getKey().getType().getBoundary(ax.getTransform())); pbnds = pbnds.intersect(aprange); } /* There is no intersect among boundaries */ if (pbnds == null) { return null; } /* * The pbnds may be not null if a round error cause an intersection. In this case, the pbnds.span() is 0. */ for (Map.Entry me : axisMap.entrySet()) { Range urange = me.getValue().convFromNR(pbnds); AxisTransformEx ax = me.getKey(); urange = urange.intersect(ax.getType().getBoundary(ax.getTransform())); if (urange == null) { return null; } } boolean findLoData = false; boolean findHiData = false; if (findNearsetData) { for (Map.Entry me : axisMap.entrySet()) { Range urange = me.getValue().convFromNR(range); AxisTransformEx ax = me.getKey(); if (urange.getStart() < ax.getType().getBoundary(ax.getTransform()).getStart()) { findLoData = true; } if (urange.getEnd() > ax.getType().getBoundary(ax.getTransform()).getEnd()) { findHiData = true; } } } else { findLoData = range.getStart() == Double.NEGATIVE_INFINITY; findHiData = range.getStart() == Double.POSITIVE_INFINITY; } /* should find the value that most close to pLo/pHi */ if (findLoData || findHiData) { pbnds = findDataRange(pbnds, axisMap, findLoData, findHiData); } if (pbnds == null) { return null; } return pbnds; } /** * If there is no data point in the given range, null is returned. The range on the end of found data point will * expand by a margin factor. * * @param pbnds * The caller guarantee this is valid on all axes. * @param axes * @param findLoData * indicate the low data should be found * @param findHiData * indicate the high data should be found * @return */ private static Range findDataRange(Range pbnds, Map axisMap, boolean findLoData, boolean findHiData) { // the normalized adjusted range that find the data edge Range padRange = null; for (Map.Entry me : axisMap.entrySet()) { double pLo = Double.POSITIVE_INFINITY; double pHi = Double.NEGATIVE_INFINITY; AxisTransformEx arm = (AxisTransformEx) me.getKey(); NormalTransform vnt = me.getValue(); Range urange = vnt.convFromNR(pbnds); // in case of the round error exceed valid bounds urange = urange.intersect(arm.getType().getBoundary(arm.getTransform())); for (LayerEx layer : arm.getLayers()) { Range wDRange = new Range.Double(); if (layer.getXAxisTransform() == arm) { for (GraphEx dp : layer.getGraphs()) { if (dp.getData() != null) { wDRange = dp.getData().applyBoundary(urange, null).getXRange().union(wDRange); } } } else if (layer.getYAxisTransform() == arm) { for (GraphEx dp : layer.getGraphs()) { if (dp.getData() != null) { wDRange = dp.getData().applyBoundary(null, urange).getYRange().union(wDRange); } } } if (wDRange != null) { Range pDRange = vnt.convToNR(wDRange); if (pLo > pDRange.getMin()) { pLo = pDRange.getMin(); } if (pHi < pDRange.getMax()) { pHi = pDRange.getMax(); } } } if (!findLoData) { pLo = pbnds.getStart(); } if (!findHiData) { pHi = pbnds.getEnd(); } /* expand range by margin factor */ // normalized extended range Range pXdRange = null; if (pLo == pHi) { pXdRange = new Range.Double(pLo, pHi); } else if (pLo < pHi) { if (arm.getTransform() == vnt.getType()) { double span = pHi - pLo; pLo -= span * arm.getMarginFactor(); pHi += span * arm.getMarginFactor(); pXdRange = new Range.Double(pLo, pHi); } else { Range wr = vnt.convFromNR(new Range.Double(pLo, pHi)); NormalTransform npt = arm.getTransform().createNormalTransform(wr); Range exnpr = new Range.Double(-arm.getMarginFactor(), 1 + arm.getMarginFactor()); Range exwr = npt.convFromNR(exnpr); pXdRange = vnt.convToNR(exwr); } } if (pXdRange != null) { // to prevent expand over bounds pXdRange = pXdRange.intersect(pbnds); if (padRange == null) { padRange = pXdRange; } else { padRange = padRange.union(pXdRange); } } } if (padRange == null) { return null; } return new Range.Double((findLoData) ? padRange.getMin() : pbnds.getStart(), (findHiData) ? padRange.getMax() : pbnds.getEnd()); } /** * The actually range maybe different from the given range to satisfy the precision limit. * * @param pLo * the normalized range low value * @param pHi * the normalized range high value * @param lockedAxes * @return the new range that satisfy the precision limit. The status object is PrecisionException if the range is * adjusted. */ public static RangeStatus ensurePrecision(Range prange, Collection axes) { Map axisMap = createNormalTransformMap(axes); return ensurePrecision(prange, axisMap); } public static RangeStatus ensurePrecision(Range prange, Map vtMap) { double pLo = prange.getStart(); double pHi = prange.getEnd(); if (pLo > pHi) { throw new IllegalArgumentException("pLo > pHi is not allowed."); } double precisionLimit = (pHi == pLo) ? DIGI4LIMIT : PRECISIONLIMIT; PrecisionState ex = null; /* the minimal virtual range to satisfy the precision limit */ double minRange = 0; /* the axis id that require the maximal normalized range expansion */ String minRangeId = null; // find new range to avoid PrecisionException for (Map.Entry me : vtMap.entrySet()) { AxisTransformEx axis = me.getKey(); NormalTransform vt = me.getValue(); /* * the required normalized range to satisfy the precision limit for the axis */ double r = vt.getMinPSpan4PrecisionLimit(pLo, pHi, precisionLimit); if (minRange < r) { minRange = r; minRangeId = axis.getFullId(); } } /* adjust range */ if (pHi == pLo && minRange == 0) { /* when auto range and range == 0 */ /* only when all axes are linear and contain constant 0 values */ double minScale = Double.POSITIVE_INFINITY; for (Map.Entry me : vtMap.entrySet()) { NormalTransform vt = me.getValue(); // np to world scale double ascale = Math.abs(vt.getScale()); if (minScale > ascale) { minScale = ascale; } } minRange = DIGI4LIMIT / minScale; } double span = Double.NaN; if (minRange > pHi - pLo) { span = minRange; double avg = (pLo + pHi) / 2; pLo = avg - minRange / 2; pHi = avg + minRange / 2; ex = new PrecisionState("The range has been adjusted to satisfy the precision limit for axis " + minRangeId); } if (ex != null) { Range bnds = getBounds(vtMap); if (bnds.getSpan() < span) { pLo = bnds.getStart(); pHi = bnds.getEnd(); } else if (!bnds.contains(pLo)) { pLo = bnds.getStart(); pHi = pLo + span; } else if (!bnds.contains(pHi)) { pHi = bnds.getEnd(); pLo = pHi - span; } } RangeStatus result = new RangeStatus(pLo, pHi, ex); return result; } public static RangeStatus ensureCircleSpan(Range prange, Collection axes) { Map vtMap = createNormalTransformMap(axes); return ensureCircleSpan(prange, vtMap); } public static RangeStatus ensureCircleSpan(Range prange, Map vtMap) { double pLo = prange.getStart(); double pHi = prange.getEnd(); if (pLo > pHi) { throw new IllegalArgumentException("pLo > pHi is not allowed."); } /* the maximum normalized range to satisfy the circle */ double maxRange = Double.POSITIVE_INFINITY; String maxRangeId = null; for (Map.Entry me : vtMap.entrySet()) { AxisTransformEx axis = me.getKey(); NormalTransform vt = me.getValue(); if (axis.getType().getCircularRange() != null) { double ascale = Math.abs(vt.getScale()); double pcircle = axis.getType().getCircularRange().getSpan() / ascale; if (maxRange > pcircle) { maxRange = pcircle; maxRangeId = axis.getFullId(); } } } PrecisionState ex = null; if (maxRange < pHi - pLo) { double avg = (pLo + pHi) / 2; pLo = avg - maxRange / 2; pHi = avg + maxRange / 2; ex = new PrecisionState("The range has been adjusted to satisfy the circle limit for axis " + maxRangeId); } return new RangeStatus(pLo, pHi, ex); } }

Ads help maintain this website.