/****************************************************************************
**
**  (C) 1992-2008 Trolltech ASA. All rights reserved.
**
** This file is part of Qt Jambi.
**
** ** This file may be used under the terms of the GNU General Public
** License version 2.0 as published by the Free Software Foundation
** and appearing in the file LICENSE.GPL included in the packaging of
** this file.  Please review the following information to ensure GNU
** General Public Licensing requirements will be met:
** http://www.trolltech.com/products/qt/opensource.html
**
** If you are unsure which license is appropriate for your use, please
** review the following information:
** http://www.trolltech.com/products/qt/licensing.html or contact the
** sales department at sales@trolltech.com.

**
** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
**
****************************************************************************/


package com.trolltech.examples;

import java.util.Vector;

import com.trolltech.qt.core.*;
import com.trolltech.qt.gui.*;

@QtJambiExample(name = "Elastic Nodes")
public class ElasticNodes extends QGraphicsView {

    public static void main(String args[]) {
        QApplication.initialize(args);
        ElasticNodes elasticNodes = new ElasticNodes();
        elasticNodes.show();
        QApplication.exec();
    }

    private int timerId;
    private Node centerNode;
    private Vector<Node> nodes = new Vector<Node>();

    private static final QBrush BRUSH_DARK_GRAY = new QBrush(QColor.darkGray);
    private static final QPen QPEN_EDGE = new QPen(QColor.black, 1, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin);
    private static final QPen QPEN_BLACK = new QPen(QColor.black, 0);

    private static QRadialGradient GRADIENT_SUNKEN;
    private static QRadialGradient GRADIENT_NORMAL;

    private static QPainterPath NODE_SHAPE;
    static{
        NODE_SHAPE = new QPainterPath();
        NODE_SHAPE.addEllipse(-10, -10, 20, 20);

        GRADIENT_SUNKEN = new QRadialGradient(-3, -3, 10);
        GRADIENT_SUNKEN.setCenter(3, 3);
        GRADIENT_SUNKEN.setFocalPoint(3, 3);
        GRADIENT_SUNKEN.setColorAt(1, new QColor(QColor.yellow).lighter(120));
        GRADIENT_SUNKEN.setColorAt(0, new QColor(QColor.darkYellow).lighter(120));

        GRADIENT_NORMAL = new QRadialGradient(-3, -3, 10);
        GRADIENT_NORMAL.setColorAt(0, QColor.yellow);
        GRADIENT_NORMAL.setColorAt(1, QColor.darkYellow);
    }


    public ElasticNodes() {
        QGraphicsScene scene = new QGraphicsScene(this);
        scene.setItemIndexMethod(QGraphicsScene.ItemIndexMethod.NoIndex);
        scene.setSceneRect(-200, -200, 400, 400);
        setScene(scene);

        setCacheMode(new QGraphicsView.CacheMode(QGraphicsView.CacheModeFlag.CacheBackground));

        setRenderHint(QPainter.RenderHint.Antialiasing);
        setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse);
        setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter);

        Node node1 = new Node(this);
        Node node2 = new Node(this);
        Node node3 = new Node(this);
        Node node4 = new Node(this);
        centerNode = new Node(this);
        Node node6 = new Node(this);
        Node node7 = new Node(this);
        Node node8 = new Node(this);
        Node node9 = new Node(this);
        scene.addItem(node1);
        scene.addItem(node2);
        scene.addItem(node3);
        scene.addItem(node4);
        scene.addItem(centerNode);
        scene.addItem(node6);
        scene.addItem(node7);
        scene.addItem(node8);
        scene.addItem(node9);
        scene.addItem(new Edge(node1, node2));
        scene.addItem(new Edge(node2, node3));
        scene.addItem(new Edge(node2, centerNode));
        scene.addItem(new Edge(node3, node6));
        scene.addItem(new Edge(node4, node1));
        scene.addItem(new Edge(node4, centerNode));
        scene.addItem(new Edge(centerNode, node6));
        scene.addItem(new Edge(centerNode, node8));
        scene.addItem(new Edge(node6, node9));
        scene.addItem(new Edge(node7, node4));
        scene.addItem(new Edge(node8, node7));
        scene.addItem(new Edge(node9, node8));

        node1.setPos(-50, -50);
        node2.setPos(0, -50);
        node3.setPos(50, -50);
        node4.setPos(-50, 0);
        centerNode.setPos(0, 0);
        node6.setPos(50, 0);
        node7.setPos(-50, 50);
        node8.setPos(0, 50);
        node9.setPos(50, 50);

        scale(0.8, 0.8);



        for (QGraphicsItemInterface item : scene().items()) {
            if (item instanceof Node)
                nodes.add((Node) item);
        }


        setMinimumSize(400, 400);
        setWindowTitle(tr("Elastic Nodes"));
        setWindowIcon(new QIcon("classpath:com/trolltech/images/qt-logo.png"));
    }

    private void itemMoved() {
        if (timerId == 0)
            timerId = startTimer(1000 / 25);
    }

    @Override
    protected void keyPressEvent(QKeyEvent event) {
        Qt.Key key = Qt.Key.resolve(event.key());
        switch (key) {
        case Key_Up:
            centerNode.moveBy(0, -20);
            break;
        case Key_Down:
            centerNode.moveBy(0, 20);
            break;
        case Key_Left:
            centerNode.moveBy(-20, 0);
            break;
        case Key_Right:
            centerNode.moveBy(20, 0);
            break;
        case Key_Plus:
            scaleView(1.2);
            break;
        case Key_Minus:
            scaleView(1 / 1.2);
            break;
        case Key_Space:
        case Key_Enter:
            for (QGraphicsItemInterface item : scene().items()) {
                if (item instanceof Node)
                    item.setPos(-150 + Math.random() * 300, -150 + Math.random() * 300);
            }
            break;
        default:
            super.keyPressEvent(event);
        }
    }

    @Override
    protected void timerEvent(QTimerEvent event) {
       for (Node node : nodes)
            node.calculateForces();

        boolean itemsMoved = false;
        for (Node node : nodes) {
            if (node.advance())
                itemsMoved = true;
        }

        if (!itemsMoved) {
            killTimer(timerId);
            timerId = 0;
        }
    }

    @Override
    protected void wheelEvent(QWheelEvent event) {
        scaleView(Math.pow(2, -event.delta() / 240.0));
    }

    @Override
    protected void drawBackground(QPainter painter, QRectF rect) {
        // Shadow
        QRectF sceneRect = this.sceneRect();
        QRectF rightShadow = new QRectF(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height());
        QRectF bottomShadow = new QRectF(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5);
        if (rightShadow.intersects(rect) || rightShadow.contains(rect))
            painter.fillRect(rightShadow, BRUSH_DARK_GRAY);
        if (bottomShadow.intersects(rect) || bottomShadow.contains(rect))
            painter.fillRect(bottomShadow, BRUSH_DARK_GRAY);

        // Fill
        QLinearGradient gradient = new QLinearGradient(sceneRect.topLeft(), sceneRect.bottomRight());
        gradient.setColorAt(0, QColor.white);
        gradient.setColorAt(1, QColor.lightGray);
        painter.fillRect(rect.intersected(sceneRect), new QBrush(gradient));
        painter.setBrush(QBrush.NoBrush);
        painter.drawRect(sceneRect);

        // Text
        QRectF textRect = new QRectF(sceneRect.left() + 4, sceneRect.top() + 4, sceneRect.width() - 4, sceneRect.height() - 4);
        String message = tr("Click and drag the nodes around, and zoom with the mouse wheel or the '+' and '-' keys");

        QFont font = painter.font();
        font.setBold(true);
        font.setPointSize(14);
        painter.setFont(font);
        painter.setPen(QColor.lightGray);
        painter.drawText(textRect.translated(2, 2), message);
        painter.setPen(QColor.black);
        painter.drawText(textRect, message);
    }

    private void scaleView(double scaleFactor) {
        QMatrix m = matrix();
        m.scale(scaleFactor, scaleFactor);
        double factor = m.mapRect(new QRectF(0, 0, 1, 1)).width();
        if (factor < 0.07 || factor > 100)
            return;

        scale(scaleFactor, scaleFactor);
    }

    public class Node extends QGraphicsItem {

        private Vector<Edge> edgeList = new Vector<Edge>();
        private QPointF newPos;
        private ElasticNodes graph;
        private double adjust = 2;
        private QRectF boundingRect = new QRectF(-10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust);

        Node(ElasticNodes graphWidget) {
            graph = graphWidget;
            setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable);
            setZValue(1);
            newPos = pos();
        }

        private void addEdge(Edge edge) {
            edgeList.add(edge);
            edge.adjust();
        }

        private void calculateForces() {
            if (scene() == null || scene().mouseGrabberItem() == this) {
                newPos = pos();
                return;
            }
            // Sum up all forces pushing this item away
            double xvel = 0;
            double yvel = 0;
            for (Node node : nodes) {
                QPointF deltaPoint = mapFromItem(node, 0, 0);
                double dx = -deltaPoint.x();
                double dy = -deltaPoint.y();
                double l = 2.0 * (dx * dx + dy * dy);
                if (l > 0) {
                    xvel += (dx * 150.0) / l;
                    yvel += (dy * 150.0) / l;
                }
            }

            // Now subtract all forces pulling items together
            double weight = (edgeList.size() + 1) * 10;
            for (Edge edge : edgeList) {
                QPointF pos;
                if (edge.sourceNode() == this)
                    pos = mapFromItem(edge.destNode(), 0, 0);
                else
                    pos = mapFromItem(edge.sourceNode(), 0, 0);
                xvel += pos.x() / weight;
                yvel += pos.y() / weight;
            }

            if (Math.abs(xvel) < 0.1 && Math.abs(yvel) < 0.1)
                xvel = yvel = 0;

            QRectF sceneRect = scene().sceneRect();
            newPos.setX(Math.min(Math.max(newPos.x() + xvel, sceneRect.left() + 10), sceneRect.right() - 10));
            newPos.setY(Math.min(Math.max(newPos.y() + yvel, sceneRect.top() + 10), sceneRect.bottom() - 10));
        }

        private boolean advance() {
            if (newPos == pos())
                return false;

            setPos(newPos);
            return true;
        }

        @Override
        public QRectF boundingRect() {
            return boundingRect;
        }

        @Override
        public QPainterPath shape() {
            return NODE_SHAPE;
        }

        @Override
        public void paint(QPainter painter, QStyleOptionGraphicsItem option, QWidget widget) {
            painter.setPen(Qt.PenStyle.NoPen);
            painter.setBrush(QColor.fromRgba(QColor.black.rgb() & 0x7fffffff));
            painter.drawEllipse(-7, -7, 20, 20);

            if ((option.state().isSet(QStyle.StateFlag.State_Sunken))) {
                painter.setBrush(GRADIENT_SUNKEN);
            } else {
                painter.setBrush(GRADIENT_NORMAL);
            }

            painter.setPen(QPEN_BLACK);
            painter.drawEllipse(-10, -10, 20, 20);
        }

        @Override
        public Object itemChange(GraphicsItemChange change, Object value) {
            switch (change) {
            case ItemPositionChange:
                for (Edge edge : edgeList)
                    edge.adjust();
                graph.itemMoved();
                break;
            default:
                break;
            }

            return super.itemChange(change, value);
        }

        @Override
        public void mousePressEvent(QGraphicsSceneMouseEvent event) {
            update();
            super.mousePressEvent(event);
        }

        @Override
        public void mouseReleaseEvent(QGraphicsSceneMouseEvent event) {
            update();
            super.mouseReleaseEvent(event);
        }
    }

    public class Edge extends QGraphicsItem {
        private Node source;
        private Node dest;

        private QPointF sourcePoint = new QPointF();
        private QPointF destPoint = new QPointF();
        private double arrowSize = 10;
        private double penWidth = 1;
        private double extra = (penWidth + arrowSize) / 2.0;

        private QRectF boundingRect = new QRectF();


        QPointF sourceArrowP1 = new QPointF();
        QPointF sourceArrowP2 = new QPointF();
        QPointF destArrowP1 = new QPointF();
        QPointF destArrowP2 = new QPointF();

        QPolygonF pol1 = new QPolygonF();
        QPolygonF pol2 = new QPolygonF();

        public Edge(Node sourceNode, Node destNode) {
            // setAcceptedMouseButtons(LeftButton);
            source = sourceNode;
            dest = destNode;
            source.addEdge(this);
            dest.addEdge(this);
            adjust();
        }

        private Node sourceNode() {
            return source;
        }

        private Node destNode() {
            return dest;
        }

        private void adjust() {
            double dx = source.pos().x()-dest.pos().x();
            double dy = source.pos().y()-dest.pos().y();

            double length = Math.sqrt(dx*dx+dy*dy);
            if (length == 0.0) return;

            double paddingX = dx/length*10;
            double paddingY = dy/length*10;

            removeFromIndex();
            sourcePoint.setX(source.pos().x() - paddingX);
            sourcePoint.setY(source.pos().y() - paddingY);

            destPoint.setX(dest.pos().x() + paddingX);
            destPoint.setY(dest.pos().y() + paddingY);

            boundingRect.setBottomLeft(source.pos());
            boundingRect.setTopRight(dest.pos());

            boundingRect = boundingRect.normalized();

            boundingRect.adjust(-extra, -extra, extra, extra);
            addToIndex();

        }

        @Override
        public QRectF boundingRect() {
            return boundingRect;
        }

        @Override
        public void paint(QPainter painter, QStyleOptionGraphicsItem option, QWidget widget) {

            if (source == null || dest == null)
                return;

            // Draw the line itself
            QLineF line = new QLineF(sourcePoint, destPoint);

            painter.setPen(QPEN_EDGE);
            painter.drawLine(line);

            // Draw the arrows if there's enough room
            double angle;
            if (line.length() > 0)
                angle = Math.acos(line.dx() / line.length());
            else
                angle = 0;

            if (line.dy() >= 0)
                angle = (Math.PI * 2) - angle;

            sourceArrowP1.setX(sourcePoint.x() + Math.sin(angle + Math.PI / 3) * arrowSize);
            sourceArrowP1.setY(sourcePoint.y() + Math.cos(angle + Math.PI / 3) * arrowSize);

            sourceArrowP2.setX(sourcePoint.x() + Math.sin(angle + Math.PI - Math.PI / 3) * arrowSize);
            sourceArrowP2.setY(sourcePoint.y() + Math.cos(angle + Math.PI - Math.PI / 3) * arrowSize);

            destArrowP1.setX(destPoint.x() + Math.sin(angle - Math.PI / 3) * arrowSize);
            destArrowP1.setY(destPoint.y() + Math.cos(angle - Math.PI / 3) * arrowSize);

            destArrowP2.setX(destPoint.x() + Math.sin(angle - Math.PI + Math.PI / 3) * arrowSize);
            destArrowP2.setY(destPoint.y() + Math.cos(angle - Math.PI + Math.PI / 3) * arrowSize);

            pol1.clear();
            pol2.clear();

            pol1.append(line.p1());
            pol1.append(sourceArrowP1);
            pol1.append(sourceArrowP2);

            pol2.append(line.p2());
            pol2.append(destArrowP1);
            pol2.append(destArrowP2);

            painter.setBrush(QColor.black);
            painter.drawPolygon(pol1);
            painter.drawPolygon(pol2);
        }
    }
}
