Package edu.mit.blocks.workspace

Source Code of edu.mit.blocks.workspace.MiniMap$MiniMapEnlargerTimer

package edu.mit.blocks.workspace;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Collection;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import edu.mit.blocks.codeblocks.BlockConnector;
import edu.mit.blocks.renderable.Comment;
import edu.mit.blocks.renderable.RenderableBlock;

/**
* A MiniMap is a miniturized representation of the
* current Workspace's block canvas. Each MiniMap may
* only be associated with only one block canvas.
*
* A MiniMap should display its associated set of
* RenderableBlocks and Comments at their current position,
* or render their motion in real time.
*
* The Minimap must tranform the block canvas's width:height ratio
* such that it would fit within the MiniMap's area.
* This may warp the blocks to appear thinner/wider
* than their real size.
*
* The MiniMap should expand and shrink with some delay
* as the user mouses over or moves the mouse out of focus.
*
* blockCanvas : BlockCanvas     //the block canvas that this MiniMap renders
* blocks : Set<RenderableBlocks>   //the set of blocks that this renders
* comments : Set<Comment>       //the set of comments that this renders
* mapwidth : Integer        //this MiniMap's maximum width
* mapheight: Integer        //this MiniMap's maximum height
* ratio : Double          //the aspect ratio that this should maintain
*                   //    when rendering this.blocks and this.comments
*/
public class MiniMap extends JPanel implements WorkspaceWidget, MouseListener, MouseMotionListener, SearchableContainer, PageChangeListener {
    /*
     * Implementation Notes:
     * -should be initialized BEFORE blockCanvas (that is, should initialize miniMap
     *  before invoking Workspace.initBlockCanvas)
     * -Pointer to the BlockCanvas is "updated" upon every repaint in case
     *  a different BlockCanvas is used half way in the game.
     *
     * Assumptions Made:
     * Workspace.instance not null upon first call to paint
     * Workspace.instance.blockCanvas not null upon first call to point
     *
     * Bug Tracking:
     * Problem: Throws exeption when dropping a stack into MiniMap
     * Solution: invoke w.blockDropped rather than block.stopDragging(w)
     *        in this.blockDropped(RenderableBLock).  Basically, calling
     *        stopDragging throws an error since the block was no longer
     *        draggin after being dropped on the MiniMap
     * Problem: block dots on corner
     * Solution: set miniMap to opaque(false) BUT call super.paint() in this.paint()
     * Problem: page divider; renders an empty slit
     * Solution: set miniMap to opaque(false) BUT call super.paint() in this.paint()
     */

    private static final long serialVersionUID = 328149080271L;
    /**the border width of the this mini map*/
    private static final int BORDER_WIDTH = 5;
    /**the default width of a mini map*/
    private static final int DEFAULT_WIDTH = 150;
    /**the default height of a mini map*/
    private static final int DEFAULT_HEIGHT = 75;
    /**this.width*/
    private int MAPWIDTH = 150;
    /**this.height*/
    private int MAPHEIGHT = 75;
    /**this.blockCanvas*/
    private BlockCanvas blockCanvas;
    private boolean expand = false;
    private final MiniMapEnlargerTimer enlarger;
    /**this.ratio*/
    private double transformX = 1;
    private double transformY = 1;
    private final Workspace workspace;

    /**
     * @effects  constructs a MiniMap, M, such that
     *       M.blockCanvas = The current blockcanvas in Workspace &&
     *       M.blocks = M.blockCanvas.getBlocks &&
     *       M.comments = Set of all M.blockCanvas.getBlocks.getComment &&
     *       M.mapwidth = width parameter &&
     *       M.ratio = M.blockCanvas : M.wdith &&
     *       M.location = ALWAYS 16 pixels away from the upper-right edge corner
     */
    public MiniMap(Workspace workspace) {
        super();
        this.workspace = workspace;
        //this.blockCanvas=workspace.getBlockCanvas();
        //this.setBounds(500,0, MAPHEIGHT, MAPHEIGHT);
        this.setPreferredSize(new Dimension(MAPHEIGHT, MAPHEIGHT));
        this.setLayout(null);
        this.setOpaque(false);
        this.setFont(new Font("Ariel", Font.PLAIN, 9));
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
        this.enlarger = new MiniMapEnlargerTimer();
        PageChangeEventManager.addPageChangeListener(this);
    }

    /**
     * @modifies this.preferredSize && this.size && this.isVisible
     * @effects mutates this MiniMap by making it invisible
     */
    public void hideMiniMap() {
        this.setSize(0, 0);
        this.setPreferredSize(new Dimension(0, 0));
        this.setVisible(false);
    }

    public void update() {
        this.repaint();
    }

    /**
     * Specified by codeblocks.workspace.SearchableContainer interface
     */
    public Iterable<RenderableBlock> getSearchableElements() {
        return new ArrayList<RenderableBlock>();
    }

    /**
     * Specified by codeblocks.workspace.SearchableContainer interface
     */
    public void updateContainsSearchResults(boolean containsSearchResults) {
        this.repaint();
    }

    public void repositionMiniMap() {
        if (this.getParent() != null) {
            this.setBounds(this.getParent().getWidth() - MAPWIDTH - 26, 0,
                    MAPWIDTH + 2 * BORDER_WIDTH, MAPHEIGHT + 2 * BORDER_WIDTH);
        }
    }

    /**
     * @modifies this.bounds && this.blockCanvas && this.blocks && this.comments
     * @effects 1] Point this.blockCanvas to whatever current block
     *          canvas is in Workspace
     *       2] Reset this.bounds to maintain aspect ratio and be
     *          16 pixels away from upper-right edge corner &&
     *       3] Rerender this.blocks and this.comment toreflect
     *          real-time relative positions and dimension
     */
    public void paint(Graphics g) {
        //should paint super first then reset canvas.
        //using new canvas, find new height and ratio.
        super.paint(g);

        // draw shadow border
        for (int i = 0; i < BORDER_WIDTH; i++) {
            g.setColor(new Color(200, 200, 150, 50 * (i + 1)));
            g.drawRect(i, i, this.getWidth() - 1 - 2 * i, this.getHeight() - 1 - 2 * i);
        }

        //Aspect-Ratio Logic
        this.blockCanvas = workspace.getBlockCanvas();
        this.transformX = (double) (MAPWIDTH) / this.getCanvas().getWidth();//MUST CAST MAPHEIGHT TO DOUBLE!!
        this.transformY = (double) (MAPHEIGHT) / this.getCanvas().getHeight();

        g.translate(5, 5);
        for (Page page : this.blockCanvas.getPages()) {
            Color pageColor = page.getPageColor();
            g.setColor(new Color(pageColor.getRed(), pageColor.getGreen(), pageColor.getBlue(), 200));
            Rectangle pageRect = rescaleRect(page.getJComponent().getBounds());
            g.fillRect(pageRect.x, pageRect.y, pageRect.width, pageRect.height);
            g.setColor(Color.white);
            g.clipRect(pageRect.x, pageRect.y, pageRect.width, pageRect.height);
            g.drawString(page.getPageName(), pageRect.x + 1, pageRect.height - 3);
            if (page.getIcon() != null && expand) {
                g.drawImage(page.getIcon(), pageRect.x + 1, pageRect.height - 28, 15, 15, null);
            }
            g.setClip(null);
            for (Component component : page.getJComponent().getComponents()) {
                //re-render this.blocks and this.comments
                if (component instanceof RenderableBlock && component != null && component.isVisible()) {
                    if (((RenderableBlock) component).isSearchResult()) {
                        g.setColor(Color.yellow);
                    } else {
                        g.setColor(((RenderableBlock) component).getBLockColor());
                    }
                    drawBoundingBox(g, component);
                } else if (component instanceof Comment && component.isVisible()) {
                    g.setColor(Color.yellow);
                    drawBoundingBox(g, component);
                }
            }

        }
        for (Component component : this.getCanvas().getComponents()) {
            if (component instanceof PageDivider) {
                g.setColor(Color.GRAY);
                Rectangle dividerRect = rescaleRect(component.getBounds());
                g.fillRect(dividerRect.x, dividerRect.y, dividerRect.width + 1, dividerRect.height);
            }
        }
        for (Component component : workspace.getComponentsInLayer(Workspace.DRAGGED_BLOCK_LAYER)) {
            if (component instanceof RenderableBlock && component != null && component.isVisible()) {
                g.setColor(((RenderableBlock) component).getBLockColor());
                drawBoundingBox(g, component);
            } else if (component instanceof Comment && component.isVisible()) {
                g.setColor(Color.yellow);
                drawBoundingBox(g, component);
            }
        }
        g.setColor(Color.red);
        g.drawRect(
                rescaleX(blockCanvas.getHorizontalModel().getValue()),
                rescaleY(blockCanvas.getVerticalModel().getValue()),
                rescaleX(blockCanvas.getWidth()),
                rescaleY(blockCanvas.getHeight()));
    }

    /**
     * @effects Renders a JComponent by drawing a rectangle around
     *        its bounding box (rescaled to fit MiniMap) using
     *        the given graphics context
     */
    private void drawBoundingBox(Graphics g, Component block) {
        Rectangle blockRect = block.getBounds();
        blockRect.setLocation(SwingUtilities.convertPoint(
                block.getParent(),
                blockRect.getLocation(),
                getCanvas()));
        blockRect = rescaleRect(blockRect);
        g.fillRect((blockRect.x),
                (blockRect.y),
                (blockRect.width),
                (blockRect.height));
        g.setColor(Color.white);
        g.drawRect((blockRect.x),
                (blockRect.y),
                (blockRect.width),
                (blockRect.height));
    }

    /**
     * @return viewPort of this.blockCanvas
     */
    private JComponent getCanvas() {
        return blockCanvas.getCanvas();
    }

    /**
     * Scales i to current aspect ratio.
     * @param x
     *
     * @requires x != null
     * @return new Point that is a copy of "p" tranformed
     *        by (this.transformX, this.transformY)
     */
    private int rescaleX(int x) {
        return (int) (x * this.transformX);
    }

    private int rescaleY(int y) {
        return (int) (y * this.transformY);
    }
    /*  private Point rescale(Point p){
    Point point = new Point((int)(p.x*this.transformX), (int)(p.y*this.transformY));
    return point;
    }*/

    private Rectangle rescaleRect(Rectangle rec) {
        return new Rectangle(
                (int) (rec.x * this.transformX),
                (int) (rec.y * this.transformY),
                (int) (rec.width * this.transformX),
                (int) (rec.height * this.transformY));
    }

    /**
     * Scales i to World using curent aspect ratio
     * @param p
     *
     * @requires p != null && this.transformX != 0 && this.transformY != 0
     * @return new Point that is a copy of "p" tranformed
     *        by (1/this.transformX, 1/this.transformY)
     */
    private Point rescaleToWorld(Point p) {
        Point point = new Point((int) (p.x / this.transformX), (int) (p.y / this.transformY));
        return point;
    }

    /**
     * Set this.blockCanvas.viewport to be centered around p if posible
     * @param p
     */
    private void scrollToPoint(Point p) {
        Point transform = rescaleToWorld(p);
        blockCanvas.getHorizontalModel().setValue((int) (transform.x - 0.5 * blockCanvas.getWidth()));
        blockCanvas.getVerticalModel().setValue((int) (transform.y - 0.5 * blockCanvas.getHeight()));
        this.repaint();
    }

    /**
     * When dragging along miniMap, zoom to new point
     */
    public void mouseDragged(MouseEvent e) {
        scrollToPoint(e.getPoint());
    }

    /**
     * When releasing a mouse in a MiniMap, scroll to point
     */
    public void mouseReleased(MouseEvent e) {
        scrollToPoint(e.getPoint());
    }

    /**MouseEvent methods not interested by this WorkspceWidget**/
    public void mouseMoved(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
        expand = true;
        enlarger.expand();
    }

    public void mouseExited(MouseEvent e) {
        expand = false;
        enlarger.shrink();
    }

    public void mouseClicked(MouseEvent e) {
    }

    /**
     * @param block
     * @param WorkspacePoint
     * @modifies block
     * @effects  change block.location to canvas point
     *        represented by WorkspacePoint.  In other words,
     *        find what point WorkspacePoint represents on the
     *        MiniMap adn find its corresponding point on the
     *        blockCanvas.  Then set block.location to that point
     *        on the blockCanvas.
     */
    public void blockDragged(RenderableBlock block, Point WorkspacePoint) {
        Point mapPoint = SwingUtilities.convertPoint(
                block,
                WorkspacePoint,
                this);
        mapPoint.translate(-6, -6);
        Point worldPoint = SwingUtilities.convertPoint(
                this.getCanvas(),
                rescaleToWorld(mapPoint),
                workspace);

        //checking bounds
        int width = block.getStackBounds().width + 3;
        int height = block.getStackBounds().height + 3;
        int canvasWidth = this.getCanvas().getWidth() - workspace.getCanvasOffset().width;
        int canvasHeight = this.getCanvas().getHeight() - workspace.getCanvasOffset().height;
        if (worldPoint.y + height > canvasHeight) {
            worldPoint.setLocation(worldPoint.x, canvasHeight - height);
        }
        if (worldPoint.x + width > canvasWidth) {
            worldPoint.setLocation(canvasWidth - width, worldPoint.y);
        }

        block.setLocation(worldPoint);
        block.getDragHandler().myLoc.setLocation(worldPoint);
        block.moveConnectedBlocks();
        this.repaint();
    }

    /**
     * @param block
     * @modifies block
     * @effects  Set block to whatever page it is on is one exist.
     *        Otherwise, set it to whatever Widget is currently underneath
     *        it.
     */
    public void blockDropped(RenderableBlock block) {
        // TODO - djwendel - this needs to choose the widget of the top block of the stack, not the widget under the block necessarily
        Point location = block.getLocation();
        if (location.getY() <= 0) {
            location.setLocation(location.getX(), 1);
        }
        //find page that it should drop on. w could be null.  so watch out.
        WorkspaceWidget w = workspace.getWidgetAt(location);
        for (Page page : this.blockCanvas.getPages()) {
            if (page.contains(SwingUtilities.convertPoint(block.getParent(), location, page.getJComponent()))) {
                w = page;
            }
        }

        /* before dropping, check if any connected blocks were dropped elsewhere.
        if they were, drop there instead. (this relies on the fact that stacks
        of blocks are dropped bottom-first, so the place to check is at the
        socket blocks.  If stacks were dropped top-first, this would check the
        blocks up the stack rather than down the stack. TODO - this doesn't
        always work, because there can be multiple "branches" that might get
        dropped on different places.  Need to find a way to drop according the
        widget of the top block of the stack...*/
        RenderableBlock socketBlock;
        for (BlockConnector con : (workspace.getEnv().getBlock(block.getBlockID()).getSockets())) {
            socketBlock = block.getWorkspace().getEnv().getRenderableBlock(con.getBlockID());
            if (socketBlock != null) {
                w = socketBlock.getParentWidget();
            }
        }
        w.blockDropped(block);
        this.repaint();
    }

    /**Block action methods not interested by MiniMap.  Does nothing*/
    public void blockEntered(RenderableBlock block) {
    }

    public void blockExited(RenderableBlock block) {
    }

    public void blockDragged(RenderableBlock block) {
    }

    public void addBlock(RenderableBlock block) {
    }

    public void addBlocks(Collection<RenderableBlock> blocks) {
    }

    public void removeBlock(RenderableBlock block) {
    }

    /**JComponent representation of this*/
    public JComponent getJComponent() {
        return this;
    }

    /**defined by JComponent.contains()*/
    public boolean contains(int x, int y) {
        return new Rectangle(7, 7, this.getWidth() - 15, this.getHeight() - 15).contains(x, y);
    }

    public Collection<RenderableBlock> getBlocks() {
        return new ArrayList<RenderableBlock>();
    }

    /**
     * Animate A fly to block's location on the MiniMap and BlockCanvas
     * @param block
     */
    public void animateAutoCenter(RenderableBlock block) {
        //Zoom to new position slowly.  Let MiniMapAutoCenter manage the zooming
        MiniMapAutoCenterTimer timer = new MiniMapAutoCenterTimer(block);
        timer.start();
    }

    /**
     * Used to zoom to the center of a particular "block".
     * MiniMapAutoCenterTimer moves closer to the center of the block
     * by (dx, dy) for every 5 milliseconds and continues to do so
     * for "count" number of times.
     *
     * It wraps a Timer that stops only when count is less than 0.
     */
    private class MiniMapAutoCenterTimer implements ActionListener {

        /**Internal Timer*/
        private javax.swing.Timer timer;
        /**Number of repititions*/
        private int count;
        /**Change in x per repitition*/
        private int dx;
        /**Change in y per repitition*/
        private int dy;

        /**Constructs this*/
        public MiniMapAutoCenterTimer(RenderableBlock block) {
            timer = new javax.swing.Timer(5, this);
            count = 25;
            Point blockPosition = SwingUtilities.convertPoint(
                    block,
                    new Point((int) (block.getStackBounds().getWidth() / 2), (int) (block.getStackBounds().getHeight() / 2)),
                    getCanvas());
            dx = (blockPosition.x - (blockCanvas.getHorizontalModel().getValue() + blockCanvas.getWidth() / 2)) / count;
            dy = (blockPosition.y - (blockCanvas.getVerticalModel().getValue() + blockCanvas.getHeight() / 2)) / count;
        }

        /**starts internal Timer*/
        public void start() {
            timer.start();
        }

        /**stops internal Timer*/
        public void stop() {
            timer.stop();
        }

        /**Repositions view in blockCanvas iff count>=0; Otherwise stop internal Timer*/
        public void actionPerformed(ActionEvent e) {
            if (count < 0) {
                timer.stop();
            } else {
                blockCanvas.getHorizontalModel().setValue(blockCanvas.getHorizontalModel().getValue() + dx);
                blockCanvas.getVerticalModel().setValue(blockCanvas.getVerticalModel().getValue() + dy);
                count--;
            }
        }
    }

    /**
     * This animator is responsible for enlarging or shrinking the
     * size of the MiniMap when expand() or shrink() is called,
     * respectively.
     */
    private class MiniMapEnlargerTimer implements ActionListener {

        /**Growth count*/
        private int count;
        /**Internal Timer*/
        private javax.swing.Timer timer;
        /**absolute value of width growth*/
        private int dx = DEFAULT_WIDTH / 10;

        ;
        /**absolute value of height Growth*/
        private int dy = DEFAULT_HEIGHT / 10;

        ;
        /**Indicates whether the MiniMap is/was expanding (true)
         * or skrinking (false)*/
        private boolean expand;
        private javax.swing.Timer delayTimer;

        /**
         * Constuctors an animator that can enlarge or skrink the miniMap
         */
        public MiniMapEnlargerTimer() {
            count = 0;
            this.expand = true;
            timer = new Timer(10, this);
            delayTimer = new Timer(1000, new ActionListener() {

                public void actionPerformed(ActionEvent e) {
                    delayTimer.stop();
                    timer.start();
                }
            });
        }

        /**
         * expands/shrinks the miniMap untill count is 0 or 15.
         * At 0, the map is smallest as possible and at 15, the
         * map is largest as possible
         */
        public void actionPerformed(ActionEvent e) {
            if (count <= 0 || count > 15) {
                timer.stop();
            } else {
                if (expand) {
                    count = count + 1;
                } else {
                    count = count - 1;
                }
                MAPWIDTH = DEFAULT_WIDTH + count * dx;
                MAPHEIGHT = DEFAULT_HEIGHT + count * dy;
                repositionMiniMap();
                repaint();
            }
        }

        /**
         * enlargest this minimap
         */
        public void expand() {
            this.expand = true;
            count++;
            //this.delayTimer.start();
            this.timer.start();
        }

        /**
         * shrinks this minimap
         */
        public void shrink() {
            count--;
            this.expand = false;
            this.delayTimer.start();
            //this.timer.start();
        }
    }
}
TOP

Related Classes of edu.mit.blocks.workspace.MiniMap$MiniMapEnlargerTimer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.