TuneDiagramChart.java

// @formatter:off
 /*******************************************************************************
 *
 * This file is part of JMad.
 * 
 * Copyright (c) 2008-2011, CERN. All rights reserved.
 *
 * 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.
 * 
 ******************************************************************************/
// @formatter:on

package cern.accsoft.steering.util.gui.dv.chart;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

import cern.jdve.Chart;
import cern.jdve.ChartRenderer;
import cern.jdve.Scale;
import cern.jdve.Style;
import cern.jdve.data.DefaultDataSet;
import cern.jdve.data.DefaultDataSource;
import cern.jdve.event.ChartInteractionListener;
import cern.jdve.graphic.DefaultRenderingHint;
import cern.jdve.graphic.Marker;
import cern.jdve.graphic.MarkerFactory;
import cern.jdve.interactor.ZoomInteractor;
import cern.jdve.renderer.PolylineChartRenderer;
import cern.jdve.renderer.ScatterChartRenderer;
import cern.jdve.utils.DataWindow;

/**
 * Chart for a tune diagram. It just takes two tune values, the H and V tune
 * 
 * @author rstein
 */
public class TuneDiagramChart extends Chart {
    private static final long serialVersionUID = 1L;
    private final int MAX_NUMBER_OF_MARKER = 10;
    private ZoomInteractor fzoomInteractor;
    private ScatterChartRenderer fScatterChartRenderer;
    private PolylineChartRenderer fpolyrenderer;
    private DefaultDataSource fresonances = new DefaultDataSource();
    private DefaultDataSet fdataset = new DefaultDataSet("TuneTrace");
    private Collection<ChartInteractionListener> flisteners = new ArrayList<ChartInteractionListener>();
    private Style[] fstyles = new Style[MAX_NUMBER_OF_MARKER];
    private Style[] flinestyles = new Style[24];
    private int fresonanceOrder = 10;

    public TuneDiagramChart(int resonanceOrder) {
        super();
        this.fresonanceOrder = resonanceOrder;

        this.setName("TuneDiagram");
        this.setPreferredSize(new Dimension(500, 300));
        this.setAntiAliasing(true);
        this.setAntiAliasingText(true);
        this.setRenderingType(ChartRenderer.POLYLINE_WITH_MARKERS);
        this.getYGrid().setMinorLineVisible(true);
        this.getYGrid().setMinorColor(new Color(230, 230, 230));
        this.getXScale().setTitleAlignment(Scale.TITLE_RIGHT);
        this.getYScale().setTitleAlignment(Scale.TITLE_RIGHT);
        this.setAntiAliasing(false);
        this.setXScaleTitle("Q1 [2 pi]");
        this.setYScaleTitle("Q2 [2 pi]");
        this.setDoubleBuffered(true);

        this.setDataSet(fdataset);

        fScatterChartRenderer = new ScatterChartRenderer();
        fScatterChartRenderer.setDataSet(fdataset);
        this.addRenderer(fScatterChartRenderer);

        updateTuneMarkerStyles(MAX_NUMBER_OF_MARKER);

        fzoomInteractor = new ZoomInteractor();
        fzoomInteractor.setAnimationStep(0);
        fzoomInteractor.setOutOfRangeZoomAllowed(false);
        this.addInteractor(fzoomInteractor);

        drawResonanceLines(true, true, fresonanceOrder);
    }

    private void updateTuneMarkerStyles(int maxMarker) {
        int tmaxMarker = Math.min(maxMarker, MAX_NUMBER_OF_MARKER);
        if (tmaxMarker <= 0) {
            tmaxMarker = 1;
        }

        fstyles = new Style[tmaxMarker];
        final int scale = (int) (255.0 / (double) (tmaxMarker * 1.1));
        for (int i = 0; i < fstyles.length; i++) {
            Color c = new Color(i * scale, i * scale, 255);
            fstyles[i] = new Style(new BasicStroke(1.5f), c, c);
        }

        fScatterChartRenderer.setMarker(MarkerFactory.getMarker(Marker.CIRCLE));
        fScatterChartRenderer.setMarkerSize(10);
        for (int i = 0; i < fstyles.length; i++) {
            fScatterChartRenderer.setRenderingHint(fdataset, i, new DefaultRenderingHint(
                    fstyles[fstyles.length - 1 - i]));
        }
    }

    /**
     * Transforms value to above/below 0.5
     * 
     * @param val - the tune value
     * @param below05 - whether value is supposed to be 0.5
     * @return
     */
    private double Qtrafo(double val, boolean below) {
        if (below)
            return val;
        else
            return (1.0 - val);
    }

    private void drawResonanceLines(boolean Qxbelow, boolean Qybelow, int resonanceOrder) {

        Color c;
        boolean update = false;
        if (fpolyrenderer != null) {
            update = true;
        } else {
            update = false;
            // first and second order resonances
            c = new Color(255, 0, 0);
            flinestyles[0] = new Style(new BasicStroke(1.5f), c, c);

            // third order resonances
            c = new Color(0, 0, 255);
            flinestyles[1] = new Style(new BasicStroke(1.5f), c, c);
            flinestyles[2] = new Style(new BasicStroke(1.5f), c, c);
            flinestyles[3] = new Style(new BasicStroke(1.5f), c, c);

            // fourth order resonances
            c = new Color(100, 100, 100);
            flinestyles[4] = new Style(new BasicStroke(1.0f), c, c);
            flinestyles[5] = new Style(new BasicStroke(1.0f), c, c);

            // fifth order resonances
            c = new Color(100, 100, 100);
            flinestyles[6] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[7] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[8] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[9] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[10] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[11] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[12] = new Style(new BasicStroke(0.5f), c, c);

            // sixth order resonances
            c = new Color(150, 150, 150);
            flinestyles[13] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[14] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[15] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[16] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[17] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[18] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[19] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[20] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[21] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[22] = new Style(new BasicStroke(0.5f), c, c);
            flinestyles[23] = new Style(new BasicStroke(0.5f), c, c);

            fpolyrenderer = new PolylineChartRenderer();
            fpolyrenderer.setStyles(flinestyles);
        }

        /* overwrites eventual older resonance lines */
        fresonances = new DefaultDataSource();

        DefaultDataSet dataframe = new DefaultDataSet("frame");
        dataframe.setEditable(true);
        dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.0, Qybelow));
        dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.0, Qybelow));
        dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.5, Qybelow));
        dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.5, Qybelow));
        dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.0, Qybelow));
        dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.5, Qybelow));
        fresonances.addDataSet(dataframe);

        /* add and plot all third order resonances */
        if (resonanceOrder >= 3) {
            dataframe = new DefaultDataSet("third_1");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("third_2");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(1.0 / 3.0, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(1.0 / 3.0, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("third_3");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(1.0 / 3.0, Qxbelow), Qtrafo(0.5, Qybelow));
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);
        }

        /* add and plot all fourth order resonances */
        if (resonanceOrder >= 4) {
            dataframe = new DefaultDataSet("fourth_1");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.25, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(0.25, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fourth_2");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.25, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.25, Qybelow));
            fresonances.addDataSet(dataframe);
        }

        /* add and plot all fifth order resonances */
        if (resonanceOrder >= 5) {
            dataframe = new DefaultDataSet("fifth_1");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.2, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(0.2, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fifth_2");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.4, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(0.4, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fifth_3");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.2, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.2, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fifth_4");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.4, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.4, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fifth_5");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.5, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fifth_6");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(1.0 / 6.0, Qxbelow), Qtrafo(0.5, Qybelow));
            dataframe.add(Qtrafo(1.0 / 3.0, Qxbelow), Qtrafo(0.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("fifth_7");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 6.0, Qybelow));
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            fresonances.addDataSet(dataframe);
        }

        /* add and plot all sixth order resonances */
        if (resonanceOrder >= 6) {
            dataframe = new DefaultDataSet("sixth_1");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(1.0 / 6.0, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 6.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_2");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(1.0 / 6.0, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(1.0 / 6.0, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_3");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.25, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(1.0 / 8.0, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_4");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(1.0 / 3.0, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_5");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_6");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.0, Qybelow));
            dataframe.add(Qtrafo(3.0 / 8.0, Qxbelow), Qtrafo(0.5, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_7");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.25, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(0.12569, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_8");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 6.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_9");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(0.0, Qxbelow), Qtrafo(0.5, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(3.0 / 8.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_10");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(1.0 / 4.0, Qxbelow), Qtrafo(0.5, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 3.0, Qybelow));
            fresonances.addDataSet(dataframe);

            dataframe = new DefaultDataSet("sixth_11");
            dataframe.setEditable(true);
            dataframe.add(Qtrafo(1.0 / 3.0, Qxbelow), Qtrafo(0.5, Qybelow));
            dataframe.add(Qtrafo(0.5, Qxbelow), Qtrafo(1.0 / 4.0, Qybelow));
            fresonances.addDataSet(dataframe);
        }

        /* add and plot all seventh order resonances */
        if (resonanceOrder >= 7) {
            // TODO: to populate
        }

        /* add and plot all eighth order resonances */
        if (resonanceOrder >= 8) {
            // TODO: to populate
        }

        fpolyrenderer.setDataSource(fresonances);
        if (!update) {
            this.addRenderer(fpolyrenderer);
        }
    }

    /**
     * Sets the <code>xZoomAllowed</code> flag.
     */
    public void setXZoomAllowed(boolean zoom_mode) {
        fzoomInteractor.setXZoomAllowed(true);
    }

    /**
     * Sets the <code>yZoomAllowed</code> flag.
     */
    public void setYZoomAllowed(boolean zoom_mode) {
        fzoomInteractor.setYZoomAllowed(true);
    }

    /**
     * Sets the <code>xZoomAllowed=true</code> and <code>yZoomAllowed=true</code> flag.
     */
    public void setBothZoomAllowed() {
        fzoomInteractor.setXZoomAllowed(true);
        fzoomInteractor.setYZoomAllowed(true);
    }

    /**
     * Sets the <code>xZoomAllowed=true</code> and <code>yZoomAllowed=false</code> flag.
     */
    public void setXZoomAllowedOnly() {
        fzoomInteractor.setXZoomAllowed(true);
        fzoomInteractor.setYZoomAllowed(false);
    }

    /**
     * Sets the <code>xZoomAllowed=false</code> and <code>yZoomAllowed=true</code> flag.
     */
    public void setYZoomAllowedOnly() {
        fzoomInteractor.setXZoomAllowed(false);
        fzoomInteractor.setYZoomAllowed(true);
    }

    public void zoomToOrigin() {
        zoom(fdataset.getXRange().getMin(), fdataset.getXRange().getMax(), fdataset.getYRange().getMin(), fdataset
                .getYRange().getMax());
    }

    public void zoomLimit(double minX, double maxX, double minY, double maxY) {
        double min_x = fdataset.getXRange().getMin();
        double max_x = fdataset.getXRange().getMax();
        double min_y = fdataset.getYRange().getMin();
        double max_y = fdataset.getYRange().getMax();

        if (!((Double) minX).isNaN())
            min_x = minX;
        if (!((Double) maxX).isNaN())
            max_x = maxX;
        if (!((Double) minY).isNaN())
            min_y = minY;
        if (!((Double) maxY).isNaN())
            max_y = maxY;

        zoom(min_x, max_x, min_y, max_y);
    }

    /**
     * local zoom implementation with some safe guards
     * 
     * @param minX
     * @param maxX
     * @param minY
     * @param maxY
     */
    private void zoom(double minX, double maxX, double minY, double maxY) {
        boolean isAdjusting = isAdjusting();
        if (!isAdjusting)
            setAdjusting(true);

        fzoomInteractor.clearZoomList();
        // some sanity checks
        if (minX > maxX) {
            double temp = minX;
            minX = maxX;
            maxX = temp;
        }
        if (minY > maxY) {
            double temp = minY;
            minY = maxY;
            maxY = temp;
        }

        if (minX == maxX) {
            maxX += 1;
        }

        if (minY == maxY) {
            maxY += 1;
        }

        zoom(0, new DataWindow(minX, maxX, minY, maxY));
        if (!isAdjusting)
            this.setAdjusting(false);
    }

    private double adjustToHalfInteger(double value, boolean aboveHalfInteger) {
        if ((aboveHalfInteger && value < 0.5) || (!aboveHalfInteger && value > 0.5))
            return (1.0 - value);
        else
            return value;
    }

    /**
     * Sets the tune trace
     * 
     * @param tunes
     */
    private void set(double[] qXValues, double[] qYValues) {
        int length = qXValues.length;

        if (qYValues.length != length) {
            throw new IllegalArgumentException("Arrays for Qx and Qy must be of same length!");
        }

        if (length > MAX_NUMBER_OF_MARKER)
            length = MAX_NUMBER_OF_MARKER;

        boolean xAboveHalfInteger = isAboveHalfInteger(qXValues);
        boolean yAboveHalfInteger = isAboveHalfInteger(qYValues);

        this.setAdjusting(true);

        double xmin = 1e99, xmax = -1e99, ymin = 1e99, ymax = -1e99;
        double[] xtrace = new double[length];
        double[] ytrace = new double[length];

        for (int i = 0; i < length; i++) {

            double x = adjustToHalfInteger(qXValues[i], xAboveHalfInteger);
            double y = adjustToHalfInteger(qYValues[i], yAboveHalfInteger);

            if (i < xtrace.length) {
                xtrace[i] = x;
                ytrace[i] = y;
            }

            if (x > xmax)
                xmax = x;
            if (x < xmin)
                xmin = x;
            if (y > ymax)
                ymax = y;
            if (y < ymin)
                ymin = y;
        }

        fdataset.set(xtrace, ytrace);
        updateTuneMarkerStyles(xtrace.length);

        this.getXAxis().setRange(0.9 * xmin, 1.1 * xmax);
        this.getYAxis().setRange(0.9 * ymin, 1.1 * ymax);
        this.setAdjusting(false);
    }

    public void addChartInteractionEventListener(ChartInteractionListener listener) {
        flisteners.add(listener);
    }

    public void removeChartInteractionListener(ChartInteractionListener listener) {
        if (flisteners != null) {
            flisteners.remove(listener);
        }
    }

    private boolean isAboveHalfInteger(double[] values) {
        // TODO: define exact behaviour
        if (values.length < 1) {
            return false;
        }
        return values[0] > 0.5;
    }

    public void setValues(double qX, double qY) {
        setValues(new double[] { qX }, new double[] { qY });
    }

    public void setValues(double[] qXValues, double[] qYValues) {
        if (!isAdjusting() && this.isShowing()) {
            double[] qXCopy = getFractionalPart(qXValues);
            double[] qYCOpy = getFractionalPart(qYValues);

            drawResonanceLines(!isAboveHalfInteger(qXCopy), !isAboveHalfInteger(qYCOpy), fresonanceOrder);
            set(qXCopy, qYCOpy);
        }
    }

    private static final double[] getFractionalPart(double[] values) {
        double[] valuesCopy = Arrays.copyOf(values, values.length);
        for (int i = 0; i < valuesCopy.length; i++) {
            double value = valuesCopy[i];
            valuesCopy[i] = value - Math.floor(value);
        }
        return valuesCopy;
    }

}