/*FreeMind - A Program for creating and viewing Mindmaps
 *Copyright (C) 2000-2001  Joerg Mueller <joergmueller@bigfoot.com>
 *See COPYING for Details
 *
 *This program 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 2
 *of the License, or (at your option) any later version.
 *
 *This program 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 this program; if not, write to the Free Software
 *Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
/*$Id: MapView.java,v 1.30.16.14 2005/08/21 14:47:49 christianfoltin Exp $*/
 
package freemind.view.mindmapview;

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;

import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;

import freemind.controller.Controller;
import freemind.controller.NodeKeyListener;
import freemind.controller.NodeMotionListener;
import freemind.controller.NodeMouseMotionListener;
import freemind.extensions.PermanentNodeHook;
import freemind.main.Tools;
import freemind.modes.MindMap;
import freemind.modes.MindMapArrowLink;
import freemind.modes.MindMapLink;
import freemind.modes.MindMapNode;


/**
 * This class represents the view of a whole MindMap
 * (in analogy to class JTree).
 */
public class MapView extends JPanel implements Printable {

	private class Selected {
		private Vector mySelected = new Vector();
		public Selected() {};
		public void clear() {
			if(size() >0 ) {
				removeSelectionForHooks(get(0)); 
			}
			mySelected.clear();
			logger.finest("Cleared selected.");
		}
		public int size() { return mySelected.size();
		}
		public void remove(NodeView node) { 
			if(mySelected.indexOf(node)==0) {
				removeSelectionForHooks(node);
			}
			mySelected.remove(node); 
			logger.finest("Removed selected "+node);
		}
		public void add(NodeView node) {
			if(size() >0 ) {
				removeSelectionForHooks(get(0)); 
			}
			mySelected.add(0, node);
			addSelectionForHooks(node); 
			logger.finest("Added selected "+node + "\nAll="+mySelected);
		}
		private void removeSelectionForHooks(NodeView node) {
		    if(node.getModel() == null) 
		        return;
			// deselect the old node:
			for(Iterator i= node.getModel().getActivatedHooks().iterator(); i.hasNext();){
				PermanentNodeHook hook = (PermanentNodeHook) i.next();
				hook.onLooseFocusHook();
			}
		}
		private void addSelectionForHooks(NodeView node) {
			// select the new node:
			for(Iterator i= node.getModel().getActivatedHooks().iterator(); i.hasNext();){
				PermanentNodeHook hook = (PermanentNodeHook) i.next();
				hook.onReceiveFocusHook();
			}
		}
		public NodeView get(int i) { return (NodeView) mySelected.get(i); 
		}
		public boolean contains(NodeView node) { return mySelected.contains(node);
		}
		/**
		 * @param newSelected
		 */
		public void moveToFirst(NodeView newSelected) {
			if(contains(newSelected)) {
				int pos = mySelected.indexOf(newSelected);
				if( pos > 0 ){ // move
					if(size() >0 ) {
						removeSelectionForHooks(get(0)); 
					}
					mySelected.remove(newSelected);
					mySelected.add(0, newSelected);
				}
			} else {
				add(newSelected);
			}
			addSelectionForHooks(newSelected);
			logger.finest("MovedToFront selected "+newSelected + "\nAll="+mySelected);
		}
	}

	//	Logging: 
	private static java.util.logging.Logger logger;

	private class DelayedScroller extends ComponentAdapter{
		/**
		 * the only one interface method.
		 * @param map
		 * @param node
		 * @param extraWidth
		 */
		public void componentMoved(ComponentEvent e){
			if (m_map != null) { // fc, 22.2.2005: bug fix.
                m_map.scrollNodeToVisible(m_node, m_extraWidth);
            }
			m_map = null;
			m_node = null;		
		}		
	
		public void scrollNodeToVisible(MapView map, NodeView node, int extraWidth){
			deactivate();
			m_map = map;
			m_node = node;
			m_extraWidth = extraWidth;
			node.addComponentListener(this);
		}
		
		public void deactivate(){
			if (m_node != null) 
				m_node.removeComponentListener(this);
		}
		MapView m_map = null;
		NodeView m_node = null;
		int m_extraWidth = 0;
	}

	private DelayedScroller m_delayedScroller = new DelayedScroller();

    private MindMap model;
    private NodeView rootView;
    private Selected selected = new Selected();
    private Controller controller;
    private float zoom=1F;
    private boolean disableMoveCursor = true;
    private int siblingMaxLevel;
    private boolean isPrinting = false; // use for remove selection from print
    private NodeView shiftSelectionOrigin = null;
    private int maxNodeWidth = 0;
    private Color background = null;
	private Rectangle boundingRectangle = null;
	private boolean fitToPage = true;
	private boolean isPreparedForPrinting = false;
        
    /** Used to identify a right click onto a link curve.*/
    private Vector/* of ArrowLinkViews*/ ArrowLinkViews;
    //
    // Constructors
    //

    public MapView( MindMap model, Controller controller ) {
        super();
		
        this.model = model;
        this.controller = controller;
		if(logger == null)
			logger = controller.getFrame().getLogger(this.getClass().getName());

        this.setAutoscrolls(true); //For some reason this doesn't work.

        getModel().addTreeModelListener( new MapModelHandler() );

        this.setLayout( new MindMapLayout( this ) );

        initRoot();

        setBackground(getModel().getBackgroundColor());

        addMouseListener( controller.getMapMouseMotionListener() );
        addMouseMotionListener( controller.getMapMouseMotionListener() );
        addMouseWheelListener( controller.getMapMouseWheelListener() );

		// fc, 20.6.2004: to enable tab for insert.
		setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.EMPTY_SET);
		setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, Collections.EMPTY_SET);
		setFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS, Collections.EMPTY_SET);
		// end change.

        // like in excel - write a letter means edit (PN)
        // on the other hand it doesn't allow key navigation (sdfe)
        disableMoveCursor = Tools.safeEquals(
                                             controller.getProperty("disable_cursor_move_paper"),"true");
    }

    public void initRoot() {
        rootView = NodeView.newNodeView( (MindMapNode)getModel().getRoot(), this );
        rootView.insert();
        getMindMapLayout().updateTreeHeightsAndRelativeYOfWholeMap();
        revalidate();
    }
    
        public int getMaxNodeWidth() {
        if (maxNodeWidth == 0) {
            try {
                maxNodeWidth = Integer.parseInt(controller.getProperty("max_node_width")); }
            catch (NumberFormatException e) {
                maxNodeWidth = Integer.parseInt(controller.getProperty("el__max_default_window_width")); }}
        return maxNodeWidth; }

    //
    // Navigation
    //

    /**
     * Problem: Before  scrollRectToVisible is called, the node has the location (0,0), ie. the location first gets
     * calculated after the scrollpane is actually scrolled. Thus, as a workaround, I simply call scrollRectToVisible
     * twice, the first time the location of the node is calculated, the second time the scrollPane is actually
     * scrolled.
     */
    public void centerNode( NodeView node ) {
        JViewport viewPort = (JViewport)getParent();
        Dimension d = viewPort.getExtentSize();
		if (! isValid()){
			final Rectangle r0 = new Rectangle(0,0,d.width, d.height);
			scrollRectToVisible(r0);
		}
		Rectangle rect = new Rectangle(node.getLocation().x + (node.getPreferredSize().width/2) - (d.width/2),
                                          node.getLocation().y + (node.getPreferredSize().height/2) - (d.height/2),
			d.width, d.height);
			
		// One call of scrollRectToVisible suffices 
		// after patching the FreeMind.java
		// and the FreeMindApplet
		scrollRectToVisible(rect);		
    }

    // scroll with extension (PN 6.2)
    public void scrollNodeToVisible( NodeView node ) {
			scrollNodeToVisible( node, 0);
    }

    // scroll with extension (PN)
    // e.g. the input field is bigger than the node view => scroll in order to
    //      fit the input field into the screen
    public void scrollNodeToVisible( NodeView node, int extraWidth) {
        //see centerNode()
		if (! node.isValid()){
			m_delayedScroller.scrollNodeToVisible(this, node, extraWidth);
			return;
		}
		m_delayedScroller.deactivate();
        final int HORIZ_SPACE  = 10;
        final int HORIZ_SPACE2 = 20;
        final int VERT_SPACE   = 5; 
        final int VERT_SPACE2  = 10;

        // get left/right dimension
        int x  =  node.getX() ;
        int width = node.getWidth();
        if (extraWidth < 0) { // extra left width
            x += extraWidth;
			width -= extraWidth; 
        }
        else {                // extra right width
            width += extraWidth; 
        }
		// get top/bottom dimension
		int y  =  node.getY();
		int height = node.getHeight();
        // adjusting container size is needed no more
        //this is buggy! 
        // %%% and therefore is the scrollRectToVisible called twice ? (PN)
        //     I don't think so and therefore I'll comment one call out... (PN)
        if (node != null) {
    		JViewport mapViewport = (JViewport)getParent();
          int viewPortScrollMode = mapViewport.getScrollMode();
           mapViewport.setScrollMode(JViewport.SIMPLE_SCROLL_MODE );
            scrollRectToVisible( new Rectangle(x - HORIZ_SPACE, node.getLocation().y - VERT_SPACE,
                                               width + HORIZ_SPACE2, node.getSize().height + VERT_SPACE2) );
            mapViewport.setScrollMode(viewPortScrollMode);
            // (PN)   scrollRectToVisible( new Rectangle(xLeft, node.getLocation().y - VERT_SPACE,
            //                                           xRight, node.getSize().height + VERT_SPACE2) );
        }
    }
	/**
	 * Returns the size of the visible part of the view in view coordinates. 
	 */
	public Dimension getViewportSize(){
		JViewport mapViewport = (JViewport)getParent();
		return mapViewport == null ? null : mapViewport.getExtentSize();
	}

    /**
     * Scroll the viewport of the map to the south-west, i.e. scroll the map itself to the north-east.
     */
    public void scrollBy(int x, int y, boolean repaint) {
        JViewport mapViewport = (JViewport)getParent();
		if(mapViewport != null){
        Point currentPoint = mapViewport.getViewPosition();                 // Get View Position
        currentPoint.translate(x, y);                                       // Add the difference to it
        // Watch for the boundaries
        //    Low boundaries
        if (currentPoint.getX()<0) {
            currentPoint.setLocation(0, currentPoint.getY()); }
        if (currentPoint.getY()<0) {
            currentPoint.setLocation(currentPoint.getX(), 0); }
        //    High boundaries
        double maxX = getSize().getWidth() - mapViewport.getExtentSize().getWidth();     // getView() gets viewed area - JPanel
        double maxY = getSize().getHeight() - mapViewport.getExtentSize().getHeight();
        if (currentPoint.getX()>maxX) {
            currentPoint.setLocation(maxX, currentPoint.getY()); }
        if (currentPoint.getY()>maxY) {
            currentPoint.setLocation(currentPoint.getX(), maxY); }
		int oldScrollMode = mapViewport.getScrollMode();
		if (repaint) {
			mapViewport.setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
		}
		mapViewport.setViewPosition(currentPoint);
		if (repaint) {
			mapViewport.setScrollMode(oldScrollMode);
		}
    }
    }

    //
    // Node Navigation
    //

    private NodeView getNeighbour(int directionCode) {
        NodeView oldSelected = getSelected();
        NodeView newSelected = null;

        switch (directionCode) {
        case KeyEvent.VK_LEFT:
            setSiblingMaxLevel(oldSelected.getModel().getNodeLevel()); // for case of return
            if(oldSelected.isRoot()){
                LinkedList left = ((RootNodeView)oldSelected).getLeft();
                if (left.size() == 0) return null;
                newSelected = oldSelected.getModel().getPreferredChild().getViewer();
                if (!left.contains(newSelected)) {
                    newSelected = (NodeView)left.getFirst();
                }
            } else if(!oldSelected.isLeft()) {
                newSelected = oldSelected.getParentView();
            } else {
                if (oldSelected.getModel().isFolded()) { // If folded in the direction, unfold
                    getController().getModeController().setFolded(oldSelected.getModel(), false);
                    return null;
                }

                if (oldSelected.getChildrenViews().size() == 0) return null;
                newSelected = oldSelected.getModel().getPreferredChild().getViewer();
            }
            setSiblingMaxLevel(newSelected.getModel().getNodeLevel());
            break;

        case KeyEvent.VK_RIGHT:
            setSiblingMaxLevel(oldSelected.getModel().getNodeLevel()); // for case of return
            if(oldSelected.isRoot()) {
                LinkedList right = ((RootNodeView)oldSelected).getRight();
                if (right.size() == 0) return null;
                newSelected = oldSelected.getModel().getPreferredChild().getViewer();
                if (!right.contains(newSelected)) {
                    newSelected = (NodeView)right.getFirst();
                }
            } else if(oldSelected.isLeft()) {
                newSelected = oldSelected.getParentView();
            } else {
                if (oldSelected.getModel().isFolded()) { // If folded in the direction, unfold
//                  URGENT: Change to controller setFolded.
//                    getModel().setFolded(oldSelected.getModel(), false); 
                    getController().getModeController().setFolded(oldSelected.getModel(), false);
                    return null;
                }

                if (oldSelected.getChildrenViews().size() == 0) return null;
                newSelected = oldSelected.getModel().getPreferredChild().getViewer();
            }
            setSiblingMaxLevel(newSelected.getModel().getNodeLevel());
            break;

        case KeyEvent.VK_UP:
            newSelected = oldSelected.getPreviousSibling();
            break;

        case KeyEvent.VK_DOWN:
            newSelected = oldSelected.getNextSibling();
            break;

        case KeyEvent.VK_PAGE_UP:
            newSelected = oldSelected.getPreviousPage();
            break;

        case KeyEvent.VK_PAGE_DOWN:
            newSelected = oldSelected.getNextPage();
            break;
        }
        return newSelected;
    }
    
    public void move(KeyEvent e) {
        NodeView newSelected = getNeighbour(e.getKeyCode());
        if (newSelected != null) {
            extendSelectionWithKeyMove(newSelected, e);
            scrollNodeToVisible( newSelected );
            e.consume();
        }
    }

    private void extendSelectionWithKeyMove(NodeView newlySelectedNodeView, KeyEvent e) {
        if (e.isShiftDown()) {
            // left or right
            if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_RIGHT) {
                shiftSelectionOrigin = null;
                NodeView toBeNewSelected = newlySelectedNodeView.isParentOf(getSelected()) ?
                    newlySelectedNodeView : getSelected();

                selectBranch(toBeNewSelected, false);
                makeTheSelected(toBeNewSelected);
                return; }

            if (shiftSelectionOrigin == null) {
                shiftSelectionOrigin = getSelected(); }

            int deltaY = newlySelectedNodeView.getY() - shiftSelectionOrigin.getY();

            // page up and page down
            NodeView currentSelected = getSelected();
            if (e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
                for (;;) {
                    if (currentSelected.getY() > shiftSelectionOrigin.getY())
                        deselect(currentSelected); 
                    else
                        makeTheSelected(currentSelected);                
                    if (currentSelected.getY() <= newlySelectedNodeView.getY())
                        break;
                    currentSelected = currentSelected.getPreviousSibling(); }
                return; }

            if (e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
                for (;;) {
                    if (currentSelected.getY() < shiftSelectionOrigin.getY())
                        deselect(currentSelected); 
                    else
                        makeTheSelected(currentSelected);
                    if (currentSelected.getY() >= newlySelectedNodeView.getY())
                        break;
                    currentSelected = currentSelected.getNextSibling(); }
                return; }

            boolean enlargingMove = (deltaY > 0) && (e.getKeyCode() == KeyEvent.VK_DOWN) ||
                (deltaY < 0) && (e.getKeyCode() == KeyEvent.VK_UP);

            if (enlargingMove) {
                toggleSelected(newlySelectedNodeView); }
            else {
                toggleSelected(getSelected());
                makeTheSelected(newlySelectedNodeView); }}
        else {
            shiftSelectionOrigin = null;
            selectAsTheOnlyOneSelected(newlySelectedNodeView); }}

    public void moveToRoot() {
        selectAsTheOnlyOneSelected( getRoot() );
        centerNode( getRoot() );
    }


    /**
     * Select the node, resulting in only that one being selected.
     */
    public void selectAsTheOnlyOneSelected(NodeView newSelected) {
        LinkedList oldSelecteds = getSelecteds();
        //select new node
        this.selected.clear();
        this.selected.add(newSelected);
        newSelected.requestFocus();

        // set last focused as preferred (PN) 
        if (newSelected.getModel().getParentNode() != null) {
            newSelected.getModel().getParentNode().setPreferredChild(newSelected.getModel());
        }

        scrollNodeToVisible(newSelected);
        newSelected.repaint();

        for(ListIterator e = oldSelecteds.listIterator();e.hasNext();) {
            NodeView oldSelected = (NodeView)e.next();
            if (oldSelected != null) {
                oldSelected.repaint();
            }
        }
    }

    /**
     * Add the node to the selection if it is not yet there, remove it otherwise.
     */
    public void toggleSelected(NodeView newSelected) {
        NodeView oldSelected = getSelected();
        if (isSelected(newSelected)) {
            if (selected.size() > 1) {
                selected.remove(newSelected);
                oldSelected=newSelected;
            }
        }
        else {
            selected.add(newSelected);
        }
        getSelected().requestFocus();
        getSelected().repaint();
        if(oldSelected != null)
            oldSelected.repaint();
    }

    /**
     * Add the node to the selection if it is not yet there, making it the focused selected node.
     */

    public void makeTheSelected(NodeView newSelected) {
        if (isSelected(newSelected)) {
            selected.moveToFirst(newSelected); 
        } else {
			selected.add(newSelected);
        }           
        getSelected().requestFocus();
        getSelected().repaint(); }

    public void deselect(NodeView newSelected) {
        if (isSelected(newSelected)) {
            selected.remove(newSelected);
            newSelected.repaint();
        }
    }


    /**
     * Select the node and his descendants. On extend = false clear up the previous selection.
     * if extend is false, the past selection will be empty.
     * if yes, the selection will extended with this node and its children
     */
    public void selectBranch(NodeView newlySelectedNodeView, boolean extend) {
        //if (!extend || !isSelected(newlySelectedNodeView))
        //    toggleSelected(newlySelectedNodeView);
        if (!extend) {
            selectAsTheOnlyOneSelected(newlySelectedNodeView); }
        else if (!isSelected(newlySelectedNodeView)) {
            toggleSelected(newlySelectedNodeView); }
        //select(newSelected,extend);
        for (ListIterator e = newlySelectedNodeView.getChildrenViews().listIterator(); e.hasNext(); ) {
            NodeView target = (NodeView)e.next();
            selectBranch(target,true);
        }
    }

    public boolean selectContinuous(NodeView newSelected) {
        /* fc, 25.1.2004: corrected due to completely inconsistent behaviour.*/
        NodeView oldSelected = null;
        // search for the last already selected item among the siblings:
        LinkedList selList = getSelecteds();
        ListIterator j = selList.listIterator(/*selList.size()*/);
        while(j.hasNext()) {
            NodeView selectedNode = (NodeView)j.next();
            if(selectedNode != newSelected && newSelected.isSiblingOf(selectedNode)) {
                oldSelected = selectedNode;
                break;
            }
        }
        // no such sibling found. select the new one, and good bye.
        if(oldSelected == null) {
            if(!isSelected(newSelected)) {
                toggleSelected(newSelected);
                return true;
            }
            return false;
        }
        // fc, bug fix: only select the nodes on the same side:
        boolean oldPositionLeft = oldSelected.isLeft();
        boolean newPositionLeft = newSelected.isLeft();
        /* find old starting point. */
        ListIterator i = newSelected.getSiblingViews().listIterator();
        while (i.hasNext()) {
            NodeView nodeView = (NodeView)i.next();
            if ( nodeView == oldSelected ) {
                break; 
            }
        }
        /* Remove all selections for the siblings in the connected component between old and new.*/
        ListIterator i_backup = i;
        while (i.hasNext()) {
            NodeView nodeView = (NodeView)i.next();
            if((nodeView.isLeft() == oldPositionLeft || nodeView.isLeft() == newPositionLeft)) {
                if ( isSelected(nodeView) ) 
                    deselect(nodeView);
                else
                    break;
            }
        }
        /* other direction. */
        i = i_backup;
        if(i.hasPrevious()) {
            i.previous(); /* this is old selected! */
            while (i.hasPrevious()) {
                NodeView nodeView = (NodeView)i.previous();
                if(nodeView.isLeft() == oldPositionLeft || nodeView.isLeft() == newPositionLeft){
                    if ( isSelected(nodeView) ) 
                        deselect(nodeView);
                    else
                        break;
                }
            }
        }
        /* reset iterator */
        i = newSelected.getSiblingViews().listIterator();
        /* find starting point. */
        i = newSelected.getSiblingViews().listIterator();
        while (i.hasNext()) {
            NodeView nodeView = (NodeView)i.next();
            if ( nodeView == newSelected || nodeView == oldSelected ) {
                if( ! isSelected(nodeView) )
                    toggleSelected(nodeView);
                break; 
            }
        }
        /* select all up to the end point.*/
        while (i.hasNext()) {
            NodeView nodeView = (NodeView)i.next();
            if( (nodeView.isLeft() == oldPositionLeft || nodeView.isLeft() == newPositionLeft) && ! isSelected(nodeView) )
                toggleSelected(nodeView);
            if ( nodeView == newSelected || nodeView == oldSelected ) {
                break; 
            }
        }
        // now, make oldSelected the last of the list in order to make this repeatable:
        toggleSelected(oldSelected);
        toggleSelected(oldSelected);
        return true;
    }

    //
    // get/set methods
    //

    public MindMap getModel() {
        return model;
    }

    // e.g. for dragging cursor (PN)
    public void setMoveCursor(boolean isHand) {
        int requiredCursor = (isHand && !disableMoveCursor) 
            ? Cursor.MOVE_CURSOR : Cursor.DEFAULT_CURSOR;
        if (getCursor().getType() != requiredCursor) {
            setCursor(new Cursor(requiredCursor));
        }
    }

    NodeMouseMotionListener getNodeMouseMotionListener() {
        return getController().getNodeMouseMotionListener();
    }

    NodeMotionListener getNodeMotionListener() {
        return getController().getNodeMotionListener();
    }

    NodeKeyListener getNodeKeyListener() {
        return getController().getNodeKeyListener();
    }

    DragGestureListener getNodeDragListener() {
        return getController().getNodeDragListener();
    }

    DropTargetListener getNodeDropListener() {
        return getController().getNodeDropListener();
    }

    public NodeView getSelected() {
        if(selected.size() > 0)
            return (NodeView)selected.get(0);
        else
            return null;
    }

    private NodeView getSelected(int i) {
        return (NodeView)selected.get(i);
    }

    public LinkedList getSelecteds() {
        // return an ArrayList of NodeViews.
        LinkedList result = new LinkedList();
        for (int i=0; i<selected.size();i++) {
            result.add( getSelected(i) );
        }
        return result;
    }


    public ArrayList /*of NodeViews*/ getSelectedsSortedByY() {
        TreeMap sortedNodes = new TreeMap();
        for (int i=0; i<selected.size();i++) {
            sortedNodes.put(new Integer(getSelected(i).getY()), getSelected(i)); }

        ArrayList selectedNodes = new ArrayList();
        for(Iterator it = sortedNodes.entrySet().iterator();it.hasNext();) {
            selectedNodes.add( ((Map.Entry)it.next()).getValue() ); }

        return selectedNodes;
    }

    
    /**
     * @return an ArrayList of MindMapNode objects.
     */
    public ArrayList /*of MindMapNodes*/ getSelectedNodesSortedByY() {
        TreeMap sortedNodes = new TreeMap();
        for (int i=0; i<selected.size();i++) {
            sortedNodes.put(new Integer(getSelected(i).getY()), getSelected(i).getModel()); }

        ArrayList selectedNodes = new ArrayList();
        for(Iterator it = sortedNodes.entrySet().iterator();it.hasNext();) {
            selectedNodes.add( ((Map.Entry)it.next()).getValue() ); }

        return selectedNodes;
    }

    public boolean isSelected(NodeView n) {
        if (isPrinting) return false;
        return selected.contains(n);
    }


    public float getZoom() {
        return zoom; }

    public int getZoomed(int number) {
        return (int)(number*zoom); }

    public void setZoom(float zoom) {
        this.zoom = zoom;
        getRoot().updateAll();
        getMindMapLayout().updateTreeHeightsAndRelativeYOfWholeMap();
        getMindMapLayout().layout(true);
        repaint();
    }

    /*****************************************************************
     **             P A I N T I N G                                 **
     *****************************************************************/


    public void paintChildren(Graphics graphics) {
        paintClouds(rootView, graphics);
        HashMap labels = new HashMap();
        ArrowLinkViews = new Vector();
        collectLabels(rootView, labels);
        paintLinks(rootView, (Graphics2D)graphics, labels, null);
        super.paintChildren(graphics);
        paintEdges(rootView, (Graphics2D)graphics);
        
    }

    /** @param iterativeLevel describes the n-th nested cloud that is to be painted.*/
    protected void paintClouds(NodeView source, Graphics graphics){
        for(ListIterator e = source.getChildrenViews().listIterator(); e.hasNext(); ) {
            NodeView target = (NodeView)e.next();
            if(target.getModel().getCloud() != null) {
                CloudView cloud = new CloudView(target.getModel().getCloud(), target);
                cloud.paint(graphics);
            }                
			paintClouds(target,graphics);
        }
    }

    /** collect all existing labels in the current map.*/
    protected void collectLabels(NodeView source, HashMap labels) {
        // check for existing registry:
        if(getModel().getLinkRegistry()==null)
            return;
        // apply own label:
        String label = getModel().getLinkRegistry().getLabel(source.getModel());
        if(label != null) 
            labels.put(label, source);
        for(ListIterator e = source.getChildrenViews().listIterator(); e.hasNext(); ) {
            NodeView target = (NodeView)e.next();
            collectLabels(target, labels);
        }
    }

    protected void paintLinks(NodeView source, Graphics2D graphics, HashMap labels, HashSet /* MindMapLink s*/ LinkAlreadyVisited) {
        // check for existing registry:
        if(getModel().getLinkRegistry()==null)
            return;
        if(LinkAlreadyVisited == null)
            LinkAlreadyVisited = new HashSet();
        // references first
        // paint own labels:
        Vector vec = getModel().getLinkRegistry().getAllLinks(source.getModel());
        for(int i = 0; i< vec.size(); ++i) {
            MindMapLink ref = (MindMapLink) vec.get(i);
            if(LinkAlreadyVisited.add(ref)) {
                // determine type of link
                if(ref  instanceof MindMapArrowLink) {
                    ArrowLinkView arrowLink = new ArrowLinkView((MindMapArrowLink) ref, ref.getSource().getViewer(), ref.getTarget().getViewer());
                    arrowLink.paint(graphics);
                    ArrowLinkViews.add(arrowLink);
                    // resize map?
                    // adjust container size
                    Rectangle rec = arrowLink.getBounds();
                    // the following does not work correctly. fc, 23.10.2003:
//                     if (rec.x < 0) {
//                         getMindMapLayout().resizeMap(rec.x);
//                     } else if (rec.x+rec.width > getSize().width) {
//                         getMindMapLayout().resizeMap(rec.x+rec.width);
//                     }

                }
            }
        }
        for(ListIterator e = source.getChildrenViews().listIterator(); e.hasNext(); ) {
            NodeView target = (NodeView)e.next();
            paintLinks(target, graphics, labels, LinkAlreadyVisited);
        }
    }

    public MindMapArrowLink detectCollision(Point p) {
        if(ArrowLinkViews == null)
            return null;
        for(int i = 0; i < ArrowLinkViews.size(); ++i) {
            ArrowLinkView arrowView = (ArrowLinkView) ArrowLinkViews.get(i);
            if(arrowView.detectCollision(p))
                return arrowView.getModel();
        }
        return null;
    }
    
	/**
	  * Call preparePrinting() before printing 
	  * and  endPrinting() after printing
	  * to minimize calculation efforts
	  */
	public void preparePrinting() {
		_preparePrinting();
		isPreparedForPrinting = true;
	}
	 
	private void _preparePrinting() {
		if (isPreparedForPrinting == false){
			isPrinting = true;
			/* repaint for printing:*/
			setZoom(getZoom());        
			background = getBackground();
			boundingRectangle = getInnerBounds(rootView);
		}
	}

	 /**
	  * Call preparePrinting() before printing 
	  * and  endPrinting() after printing
	  * to minimize calculation efforts
	  */
	public void endPrinting() {
		isPreparedForPrinting = false;
		_endPrinting();
	}
	
	 private void _endPrinting() {
		if (isPreparedForPrinting == false){
			setBackground(background);
			isPrinting = false;
			/* repaint for end printing:*/
			setZoom(getZoom());
			fitToPage = Tools.safeEquals(controller.getProperty("fit_to_page"),"true");
		}
	 }

    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) {
        // TODO:
        // ask user for :
        // - center in page (in page format ?)
        // - print zoom or maximize (in page format ?)
        // - print selection only
        // remember those parameters from one session to another
        //    (as orientation & margin from pf)

        // User parameters

        double userZoomFactor = 1;
        try {
            userZoomFactor = Double.parseDouble(controller.getProperty("user_zoom")); }
        catch (Exception e) {}
        userZoomFactor = Math.max(0,userZoomFactor);
        userZoomFactor = Math.min(2,userZoomFactor);

        // TODO: read user parameters from properties, make sure the multiple page
        // printing really works, have look at Book class.

        if (fitToPage && pageIndex > 0) {
            return Printable.NO_SUCH_PAGE; }

        Graphics2D graphics2D = (Graphics2D)graphics;

		_preparePrinting();
        double zoomFactor = 1;
        if (fitToPage) {
            double zoomFactorX = pageFormat.getImageableWidth()/boundingRectangle.getWidth();
            double zoomFactorY = pageFormat.getImageableHeight()/boundingRectangle.getHeight();
            zoomFactor = Math.min (zoomFactorX, zoomFactorY); }
        else {
            zoomFactor = userZoomFactor;

            int nrPagesInWidth = (int)Math.ceil(zoomFactor * boundingRectangle.getWidth() /
                                                pageFormat.getImageableWidth());
            int nrPagesInHeight = (int)Math.ceil(zoomFactor *boundingRectangle.getHeight() / 
                                                 pageFormat.getImageableHeight());
            if (pageIndex >= nrPagesInWidth * nrPagesInHeight) {
                return Printable.NO_SUCH_PAGE; }
            int yPageCoord = (int)Math.floor(pageIndex / nrPagesInWidth);
            int xPageCoord = pageIndex - yPageCoord * nrPagesInWidth;

            graphics2D.translate(- pageFormat.getImageableWidth() * xPageCoord,
                                 - pageFormat.getImageableHeight() * yPageCoord); }

        graphics2D.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
        graphics2D.scale(zoomFactor, zoomFactor);
        graphics2D.translate(-boundingRectangle.getX(), -boundingRectangle.getY());
        

        print(graphics2D);
		_endPrinting();
        return Printable.PAGE_EXISTS;
    }

    /** For nodes, they can ask, whether or not the width must be bigger to prevent the "..." at the output. (Bug of java).*/
    public boolean isCurrentlyPrinting() { return isPrinting;};

    public Dimension getPreferredSize() {
        return getLayout().preferredLayoutSize(this);
    }


    ///////////
    // private methods. Internal implementation
    /////////
	
    /**
     * Return the bounding box of all the descendants of the source view, that without BORDER.
     * Should that be implemented in LayoutManager as minimum size?
     */
    public Rectangle getInnerBounds(NodeView source) {
        Rectangle innerBounds = source.getBounds();
        for(ListIterator e = source.getChildrenViews().listIterator(); e.hasNext(); ) {
            NodeView target = (NodeView)e.next();
            innerBounds.add(getInnerBounds(target));//recursive
        }
        
        // add heigth of the cloud
        int additionalCloudHeigth =  (source.getAdditionalCloudHeigth() + 1) / 2;
        if (additionalCloudHeigth != 0){
        	innerBounds.grow(additionalCloudHeigth, additionalCloudHeigth);
        }
        
 		if (source.getModel().isRoot())
 		{
 			for(int i = 0; i < ArrowLinkViews.size(); ++i) {
 				ArrowLinkView arrowView = (ArrowLinkView) ArrowLinkViews.get(i);
 				Rectangle arrowViewBigBounds = arrowView.arrowLinkCurve.getBounds();
 				if (! innerBounds.contains(arrowViewBigBounds)){
					Rectangle arrowViewBounds =PathBBox.getBBox(arrowView.arrowLinkCurve).getBounds(); 
					innerBounds.add(arrowViewBounds);
 				}
 			}        	
 		}
      return innerBounds;
    }

    private void paintEdges(NodeView source, Graphics2D g) {
        for(ListIterator e = source.getChildrenViews().listIterator(); e.hasNext(); ) {
            NodeView target = (NodeView)e.next();
            target.getEdge().paint(g);
            paintEdges( target, g );//recursive
        }
    }
    
    protected NodeView getRoot() {
        return rootView; }

    private MindMapLayout getMindMapLayout() {
        return (MindMapLayout)getLayout();  }

    /**
     * This method is a workaround to allow the inner class access to "this".
     * Change it as soon the correct syntax is known.
     */
    private MapView getMap() {
        return this; }

    public Controller getController() {
        return controller;  }


    ///////////
    // Inner Class MapModelHandler
    //////////

    /**
     * This inner class updates the Tree when the model is changed.
     */
    private class MapModelHandler implements TreeModelListener {
        public void treeNodesChanged( TreeModelEvent e ) {
            // must be in structureChanged instead ?
            // or in is own Listerner
            setBackground(getModel().getBackgroundColor());
            // Daniel: Why would I want to change background because some
            // nodes have changed? That's really strange.
            NodeView nodeView;
            try {
                nodeView = ( (MindMapNode)e.getChildren()[0] ).getViewer(); }
            catch (Exception ex) {    //thrown if changed is root
                nodeView = ( (MindMapNode)e.getTreePath().getLastPathComponent() ).getViewer(); }
            // ^ This is not a good solution, but it works
            // here, the nodeView is checked for existence.
            if (nodeView != null) {
                logger.finest("The update node is " + nodeView
                        + " with treemodelevent=" + e);
                nodeView.update();
                getMindMapLayout()
                        .updateTreeHeightsAndRelativeYOfDescendantsAndAncestors(
                                nodeView);
                /* Layout only, if node is visible. fc, 03.07.2005 */
                getMindMapLayout().layout(true);
                repaint();
                /* end fc, 03.07.2005 */
            }
        }
	
        public void treeNodesInserted( TreeModelEvent e ) {
            // Daniel: This is some kind of hackery implementation, which
            // does not keep the semantics of the method defined in
            // TreeModelListerer. This implementation does only that which
            // FreeMind actually needs, which I find dubious.

            MindMapNode parent = (MindMapNode)e.getTreePath().getLastPathComponent();
            NodeView parentView = parent.getViewer();
            // Works only with one child
            MindMapNode child = (MindMapNode)e.getChildren()[0];

            if (child != null) {  // if a node is folded, child is null.
                // Here, the view will be created if it does no exist already
//              for (int i = 0; i < e.getPath().length; i++) {
//              MindMapNode node = (MindMapNode) e.getPath()[i];
//              if(node.isFolded()) {
//                  logger.info("Found folded node in path of a new children to be inserted.");
//                  return;
//              }
//          }
          // Here, the view will be created if it does no exist already
                parentView.insert(child);
                getMindMapLayout().updateTreeGeometry(child.getViewer()); }
            getMindMapLayout().updateTreeHeightsAndRelativeYOfAncestors(parentView);
            // Here, the view of child gets its size and position
            getMindMapLayout().layout(false);
            //fc, 29.3.2004: here, I change parentView.requestFocus() to:
            child.getViewer().requestFocus();
            repaint();
        }

        public void treeNodesRemoved (TreeModelEvent e) {
            //This is called once for each removed node, with just one child (the removed)
            NodeView node = ( (MindMapNode)e.getChildren()[0] ).getViewer();
            //Take care not to remove root in Controller
            NodeView parent = ( (MindMapNode)e.getTreePath().getLastPathComponent() ).getViewer();
            if(node == null) {
                System.err.println("MapView.treeNodesRemoved tried to delete null as node.");
                return;
            }
            node.remove();   //Just that one
            MindMapNode preferred = parent.getModel().getPreferredChild();
            if (preferred != null) { // after delete focus on a brother (PN)
                selectAsTheOnlyOneSelected(preferred.getViewer());
            }
            else {
                selectAsTheOnlyOneSelected(parent);
            }
            getMindMapLayout().updateTreeHeightsAndRelativeYOfAncestors(parent);
            getMindMapLayout().layout(false);
            getSelected().requestFocus();
			// scrollNodeToVisible(getSelected());
            //fc, 5.4.2004. is already done by selectAsTheOnlyOneSelected:
            //parent.requestFocus();
            repaint();
        }

        /**
         * Assures that every and at least one selected node is visible. If this is not the case,
         * the node is deselected. 
         * If every selected node is hidden, the first visible part in the path of the selected
         * node to the root is selected.
         */
        public void treeStructureChanged (TreeModelEvent e) {

            // Keep selected nodes

            ArrayList selectedNodes = new ArrayList();
            for (ListIterator it = getSelecteds().listIterator();it.hasNext();) {
                NodeView nodeView = (NodeView)it.next();
                if (nodeView != null) {
                    selectedNodes.add((nodeView).getModel()); 
                } 
            }
//          if the focussed is deleted:
            MindMapNode focussedNode = null;
            if (getSelected()!=null) {
                focussedNode = getSelected().getModel();
            }

            // Update everything

            MindMapNode subtreeRoot = (MindMapNode)e.getTreePath().getLastPathComponent();

            if(subtreeRoot == null || subtreeRoot.getViewer() == null) {
	            // fc,14.1.2005: no viewer -> no update.
            } else {
	            
	            
				boolean nodeIsLeft = subtreeRoot.getViewer().isLeft();
				NodeView oldNodeView = subtreeRoot.getViewer();
				int x = oldNodeView.getX();
				int y = oldNodeView.getY();
				subtreeRoot.getViewer().remove();
				NodeView nodeView = null;
	            if (subtreeRoot.isRoot()) {
					nodeView = NodeView.newNodeView(getRoot().getModel(), getMap());
	                rootView = nodeView; 
	            }
	            else {
	                nodeView = NodeView.newNodeView(subtreeRoot, getMap());
	            }
				nodeView.setLocation(x, y);
	            nodeView.setLeft(nodeIsLeft);
	            nodeView.insert(); 
	            getMindMapLayout().updateTreeHeightsAndRelativeYOfDescendantsAndAncestors(subtreeRoot.getViewer());
	            // the layout function will be called later by AWT framework itself
	            // comment it out
	            // getMindMapLayout().layout();
            }
            // Restore selected nodes

            // Warning, the old views still exist, because JVM has not deleted them. But don't use them!
            selected.clear();
            for (ListIterator it = selectedNodes.listIterator();it.hasNext();) {
                MindMapNode mindMapNode = ((MindMapNode)it.next());
//              test, whether or not the node is still visible:
                if (mindMapNode.getViewer() != null) {
                    selected.add(mindMapNode.getViewer()); 
                } 
            }
            // determine focussed node:
            if(focussedNode == null) {
//              if the focussed is deleted:
                focussedNode = getRoot().getModel();
            } 
//          test for visibility of the focussed:
            while(focussedNode.getParentNode()!= null && focussedNode.getViewer()==null) {
                focussedNode = focussedNode.getParentNode();
            }
            if(focussedNode.getParentNode()==null) {
//              test, whether or not the node still belongs to the map:
                if(!focussedNode.equals(getRoot().getModel())) {
	                focussedNode = getRoot().getModel();
                }
            }
//          now select focussed:
            focussedNode.getViewer().requestFocus();
            repaint();
        }
    }
    
    // this property is used when the user navigates up/down using cursor keys (PN)
    // it will keep the level of nodes that are understand as "siblings"

    public int getSiblingMaxLevel() {
        return this.siblingMaxLevel;
    }
    public void setSiblingMaxLevel(int level) {
        this.siblingMaxLevel = level;
    }
    
	private int FOLDING_SYMBOL_WIDTH = -1;
	public int getZoomedFoldingSymbolHalfWidth() {
		if (FOLDING_SYMBOL_WIDTH == -1)
		{
			FOLDING_SYMBOL_WIDTH = getController().getIntProperty("foldingsymbolwidth", 8);
		}
		return (int) ((FOLDING_SYMBOL_WIDTH * getZoom()) / 2);
	}


 }
