com.golemgame.properties.fengGUI.KnottedFunctionPane.java Source code

Java tutorial

Introduction

Here is the source code for com.golemgame.properties.fengGUI.KnottedFunctionPane.java

Source

/*******************************************************************************
 * Copyright 2008, 2009, 2010 Sam Bayless.
 * 
 *     This file is part of Golems.
 * 
 *     Golems is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     Golems 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 Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with Golems. If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package com.golemgame.properties.fengGUI;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;

import org.apache.commons.math.analysis.UnivariateRealFunction;
import org.fenggui.binding.render.Graphics;
import org.fenggui.binding.render.IOpenGL;
import org.fenggui.event.key.Key;
import org.fenggui.event.key.KeyPressedEvent;
import org.fenggui.event.mouse.MouseButton;
import org.fenggui.event.mouse.MouseDraggedEvent;
import org.fenggui.event.mouse.MousePressedEvent;
import org.fenggui.event.mouse.MouseReleasedEvent;
import org.fenggui.util.Color;

import com.golemgame.functional.FunctionSettings;
import com.golemgame.util.CombinedTransformationFunction;
import com.golemgame.util.LinearInterpolator;
import com.golemgame.util.TransformationFunction;
import com.golemgame.util.LinearInterpolator.LinearInterpolationFunction;

/**
 * This extends the function pane to make it interactive; users can click to place 'knots,' and adjust the knots to create linear interpolated sections of the function
 * 
 * @author Sam
 *
 */
public class KnottedFunctionPane extends GLFunctionPane {

    private ArrayList<Knot> knots;
    private Knot selected = null;
    private Knot hovered = null;
    private Knot current = null;

    private UnivariateRealFunction baseFunction;
    private CombinedTransformationFunction transformFunction;
    private LinearInterpolationFunction interpolatedFunction;
    private ArrayList<CurrentKnotListener> currentListeners = new ArrayList<CurrentKnotListener>();

    private boolean snap = true;
    private float snapDistance = 0.03f;

    public float getSnapDistance() {
        return snapDistance;
    }

    public void registerCurrentKnotListener(CurrentKnotListener l) {
        this.currentListeners.add(l);
    }

    public void setSnapDistance(float snapDistance) {
        this.snapDistance = snapDistance;
    }

    public boolean isSnap() {
        return snap;
    }

    public void setSnap(boolean snap) {
        this.snap = snap;
    }

    public TransformationFunction getTransformFunction() {
        return transformFunction;
    }

    public void paintContent(Graphics g, IOpenGL gl) {

        super.paintContent(g, gl);

        //paint knots

        for (Knot knot : knots) {
            knot.paint(g);

        }

    }

    public void setFunction(UnivariateRealFunction function, FunctionSettings f) {
        if (knots == null)
            knots = new ArrayList<Knot>();
        clearKnots();
        if (function instanceof CombinedTransformationFunction) {
            this.transformFunction = (CombinedTransformationFunction) function;
            this.baseFunction = transformFunction.getBaseFunction();
            this.interpolatedFunction = transformFunction.getKnottedFunction();
            buildKnots(interpolatedFunction);
        } else {
            this.baseFunction = function;
            this.interpolatedFunction = LinearInterpolator.getInstance().interpolate(null, null);
            this.transformFunction = new CombinedTransformationFunction(interpolatedFunction, baseFunction,
                    f.getMinX(), f.getMaxX(), f.getMaxY(), f.getMinY());

        }
        super.setFunction(transformFunction, f);
    }

    private void buildKnots(LinearInterpolationFunction function) {
        clearKnots();
        if (function == null)
            return;
        if (function.getXKnots() == null)
            return;

        for (double x : function.getXKnots()) {
            float y = (float) function.value(x);

            Knot newKnot = new Knot((float) x, y);
            knots.add(newKnot);
        }
        Collections.sort(knots);

    }

    public void clearKnots() {

        for (Knot knot : knots) {
            knot.delete();
        }
        knots.clear();
        hovered = null;
        selected = null;
        setCurrent(null);

    }

    public Knot getCurrent() {
        return current;
    }

    public void setCurrent(Knot current) {

        this.current = current;
        if (currentListeners != null) {
            for (CurrentKnotListener l : currentListeners)
                l.currentKnotChanged(current);
        }
    }

    public KnottedFunctionPane() {
        super();
        if (knots == null)//Might get set in setFunction called from super class
            knots = new ArrayList<Knot>();
        super.setTraversable(true);
    }

    private void dehover() {
        if (hovered != null)
            hovered.setHovered(false);
    }

    private void hover(Knot knot) {
        dehover();
        knot.setHovered(true);
        hovered = knot;
    }

    private void deselect() {
        if (selected != null) {
            selected.setSelected(false);
            selected = null;
        }
    }

    private void select(Knot knot) {
        deselect();
        dehover();
        setCurrent(knot);
        selected = knot;
        selected.setSelected(true);
    }

    private Knot comparator = new Knot();

    public void mouseDragged(MouseDraggedEvent mouseDraggedEvent) {
        super.mouseDragged(mouseDraggedEvent);
        if (!super.isEnabled())
            return;
        if (selected != null && mouseDraggedEvent.getButton() == MouseButton.LEFT) {

            float x = mouseDraggedEvent.getLocalX(KnottedFunctionPane.this) - getAppearance().getLeftMargins();
            float y = mouseDraggedEvent.getLocalY(KnottedFunctionPane.this) - getAppearance().getBottomMargins();

            float fX = deScaleX(x);
            float fY = deScaleY(y);

            if (isSnap()) {
                //snap to midles
                if (Math.abs(fY) < getSnapDistance())
                    fY = 0;

                if (Math.abs(fX - ((super.getMaxX() + super.getMinX()) / 2f)) < getSnapDistance())
                    fX = ((super.getMaxX() + super.getMinX()) / 2f);

                //snap to outer edges
                if (Math.abs(fX - super.getMaxX()) < getSnapDistance())
                    fX = super.getMaxX();
                else if (Math.abs(fX - super.getMinX()) < getSnapDistance())
                    fX = super.getMinX();

                if (Math.abs(fY - super.getMaxY()) < getSnapDistance())
                    fY = super.getMaxY();
                else if (Math.abs(fY - super.getMinY()) < getSnapDistance())
                    fY = super.getMinY();
            }

            fX = (float) transformFunction.invertX(fX);

            fY = (float) transformFunction.invertY(fY);

            selected.getTranslation().setLocation(fX, fY);
            updateKnots();
            setCurrent(selected);
        }
    }

    public void updateCurrentKnot() {
        for (CurrentKnotListener l : currentListeners)
            l.currentKnotChanged(current);
    }

    public void updateKnots() {
        Collections.sort(knots);
        interpolateFunction();
    }

    public void mousePressed(MousePressedEvent mousePressedEvent) {
        //first, try to select a nearby knot

        //intead, vertex locations have to be in an internal unit system, that translates correctly when the graph is scaled.

        super.mousePressed(mousePressedEvent);
        if (!super.isEnabled())
            return;
        if (mousePressedEvent.getButton() == MouseButton.LEFT) {
            float x = mousePressedEvent.getLocalX(KnottedFunctionPane.this) - getAppearance().getLeftMargins();
            float y = mousePressedEvent.getLocalY(KnottedFunctionPane.this) - getAppearance().getBottomMargins();

            float fX = deScaleX(x);
            float fY = deScaleY(y);

            fX = (float) transformFunction.invertX(fX);
            fY = (float) transformFunction.invertY(fY);

            deselect();

            {

                Knot pressed = findKnot(x, y, fX, fY);
                if (pressed != null) {
                    select(pressed);
                }
            }

            if (selected == null) {

                //Create a new knot, select it
                Knot newKnot = new Knot();

                newKnot.getTranslation().setLocation(fX, fY);
                /*
                         float left = (float) transformFunction.invertX(getMinX());
                         float right = (float) transformFunction.invertX(getMaxX());
                             
                         //if this knot is the left most knot, add a new know, farther to the left (if posible)
                         if (knots.isEmpty()|| knots.get(0).getTranslation().x > fX)
                         {
                            Knot leftMost = new Knot();
                            float leftPos = (fX - 0.2f);
                            if (leftPos<left)
                               leftPos = left;
                            leftMost.getTranslation().setLocation(leftPos,fY);
                                
                            if (newKnot.getTranslation().x <= leftPos)
                               newKnot.getTranslation().x =leftPos+ 0.01f;
                                
                            addKnot(leftMost);
                         }
                             
                         //if this knot is the right most knot, add a new know, farther to the left (if posible)
                         if (knots.isEmpty()|| knots.get(knots.size()-1).getTranslation().x < fX)
                         {
                            Knot rightMost = new Knot();
                            float rightPos = (fX + 0.2f);
                            if (rightPos>right)
                               rightPos = right;
                            rightMost.getTranslation().setLocation(rightPos,fY);
                                
                            if (newKnot.getTranslation().x >=rightPos)
                               newKnot.getTranslation().x =rightPos- 0.01f;
                                
                            addKnot(rightMost);
                         }
                      */

                addKnot(newKnot);

                if (newKnot.testContact(x, y)) {//if a prexisting knot was clicked.
                    select(newKnot);
                }
            }

        }
    }

    public void addKnot(Knot newKnot) {
        knots.add(newKnot);
        Collections.sort(knots);
        interpolateFunction();
    }

    public void mouseReleased(MouseReleasedEvent mouseReleasedEvent) {
        super.mouseReleased(mouseReleasedEvent);
        if (!super.isEnabled())
            return;
        if (mouseReleasedEvent.getButton() == MouseButton.LEFT)
            deselect();

    }

    private Knot findKnot(float x, float y, float fX, float fY) {
        if (!knots.isEmpty()) {
            comparator.getTranslation().setLocation(fX, fY);
            int pos = Collections.binarySearch(knots, comparator);
            //find the closest knot, and then check if it contacts this position;
            if (pos < 0) {
                pos = -pos;
            }
            if (pos >= knots.size())
                pos = knots.size() - 1;

            Knot pressed = knots.get(pos);

            if (pressed.testContact(x, y)) {//if a prexisting knot was clicked.
                return pressed;

            }

            if (selected == null && pos > 0) {
                pressed = knots.get(pos - 1);
                if (pressed.testContact(x, y)) {
                    return pressed;

                }
            }
            if (selected == null && pos > 1) {
                pressed = knots.get(pos - 2);
                if (pressed.testContact(x, y)) {
                    return pressed;

                }

            }
            if (selected == null && pos < knots.size() - 2) {
                pressed = knots.get(pos + 1);
                if (pressed.testContact(x, y)) {
                    return pressed;

                }
            }
        }
        return null;
    }

    public void keyPressed(KeyPressedEvent keyPressedEvent) {
        super.keyPressed(keyPressedEvent);
        if (!super.isEnabled())
            return;
        if (selected != null && (keyPressedEvent.getKeyClass() == Key.DELETE
                || keyPressedEvent.getKeyClass() == Key.BACKSPACE)) {
            //delete this knot
            deleteKnot(selected);
        }

    }

    private void deleteKnot(Knot knot) {
        knot.delete();
        knots.remove(knot);
        Collections.sort(knots);

        if (hovered == knot)
            dehover();
        if (selected == knot)
            deselect();
        if (current == knot)
            setCurrent(null);
        interpolateFunction();

    }

    private void interpolateFunction() {
        if (knots.size() > 1) {
            //build set of interpolation points

            double[] xValues = new double[knots.size()];
            double[] yValues = new double[knots.size()];

            //Force the first and last vertices onto the function lines
            /*      Knot left = knots.get(0);
                  Knot right = knots.get(knots.size()-1);
                  try{
                     left.getTranslation().y =(float) baseFunction.value(left.getTranslation().x);
                     right.getTranslation().y =(float) baseFunction.value(right.getTranslation().x);
                         
                  }catch(FunctionEvaluationException e)
                  {
                
                  }*/

            for (int i = 0, size = knots.size(); i < size; i++) {
                Point2D.Float translation = knots.get(i).getTranslation();
                xValues[i] = translation.x;
                yValues[i] = translation.y;
            }

            this.interpolatedFunction = LinearInterpolator.getInstance().interpolate(xValues, yValues);
            this.transformFunction.setKnottedFunction(interpolatedFunction);
        } else {
            /*   if (knots.size() ==1)
               {
                  Knot left = knots.get(0);
                
                  try{
                     left.getTranslation().y =(float) baseFunction.value(left.getTranslation().x);
                
                  }catch(FunctionEvaluationException e)
                  {
                
                  }
               }*/
        }

    }

    /**
     * Knots represent vertices added to a function. Their translations are in function coordinates (use get/set local coordinates to deal with local widget coordinates).
     * @author Sam
     *
     */
    public class Knot implements Comparable<Knot> {
        private boolean selected = false;
        private boolean hovered = false;

        private Color light = Color.YELLOW;
        private Color dark = Color.DARK_YELLOW;
        private Color selectedLight = Color.GREEN;
        private Color selectedDark = Color.DARK_GREEN;

        private Color hoveredLight = Color.RED;
        private Color hoveredDark = Color.DARK_RED;

        private Point2D.Float translation;
        public static final int radius = 5;
        public static final int error = 1;

        public int compareTo(Knot o) {
            return (o.translation.x > this.translation.x) ? -1 : 1;
        }

        private Knot() {
            this(new Point2D.Float());
        }

        private Knot(Point2D.Float translation) {
            this.translation = translation;

        }

        private Knot(float x, float y) {
            this(new Point2D.Float(x, y));
        }

        public void delete() {

        }

        /**
         * Test whether the point is on this knot (in GL coordinates, not function coordinates).
         * @param x
         * @param y
         * @return
         */
        public boolean testContact(float x, float y) {
            boolean result = true;
            result &= (Math.abs(x - getLocalTranslationX())) <= radius + error;

            result &= (Math.abs(y - getLocalTranslationY())) <= (radius + error);

            return result;
        }

        public void paint(Graphics g) {
            if (selected) {
                g.drawBlendedFilledRect(getLocalTranslationX() - radius, getLocalTranslationY() - radius,
                        radius * 2, radius * 2, selectedLight, selectedDark, selectedLight, selectedDark);

            } else if (hovered) {
                g.drawBlendedFilledRect(getLocalTranslationX() - radius, getLocalTranslationY() - radius,
                        radius * 2, radius * 2, hoveredLight, hoveredDark, hoveredLight, hoveredDark);

            } else {
                g.drawBlendedFilledRect(getLocalTranslationX() - radius, getLocalTranslationY() - radius,
                        radius * 2, radius * 2, light, dark, light, dark);

            }
        }

        public Point2D.Float getTranslation() {
            return translation;
        }

        public boolean isSelected() {
            return selected;
        }

        public void setSelected(boolean selected) {
            this.selected = selected;
        }

        public Point2D.Float getLocalTranslation(Point2D.Float store) {
            if (store == null)
                store = new Point2D.Float();

            store.x = getScaleX(translation.x);
            store.y = getScaleY(translation.y);

            return store;
        }

        public int getLocalTranslationX() {
            return (int) getScaleX((float) transformFunction.transformX(translation.x));
        }

        public int getLocalTranslationY() {
            return (int) getScaleY((float) transformFunction.transformY(translation.y));
        }

        public boolean isHovered() {
            return hovered;
        }

        public void setHovered(boolean hovered) {
            this.hovered = hovered;
        }

    }

    public void mouseMoved(int displayX, int displayY) {
        if (!super.isEnabled())
            return;
        super.mouseMoved(displayX, displayY);

        if (selected == null) {

            float x = displayX - getDisplayX() - getAppearance().getLeftMargins();
            float y = displayY - getDisplayY() - getAppearance().getBottomMargins();

            float fX = deScaleX(x);
            float fY = deScaleY(y);

            fX = (float) transformFunction.invertX(fX);
            fY = (float) transformFunction.invertY(fY);

            Knot hovered = findKnot(x, y, fX, fY);
            if (hovered != null) {
                hover(hovered);
            } else
                dehover();
        }

    }

    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);

    }

    public static interface CurrentKnotListener {
        public void currentKnotChanged(Knot current);

    }
}