org.vectomatic.svg.edu.client.push.PushMain.java Source code

Java tutorial

Introduction

Here is the source code for org.vectomatic.svg.edu.client.push.PushMain.java

Source

/**********************************************
 * Copyright (C) 2010 Lukas Laag
 * This file is part of lib-gwt-svg-edu.
 * 
 * libgwtsvg-edu 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.
 * 
 * libgwtsvg-edu 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 libgwtsvg-edu.  If not, see http://www.gnu.org/licenses/
 **********************************************/
package org.vectomatic.svg.edu.client.push;

import java.util.ArrayList;
import java.util.List;

import org.vectomatic.dom.svg.OMNode;
import org.vectomatic.dom.svg.OMSVGClipPathElement;
import org.vectomatic.dom.svg.OMSVGDefsElement;
import org.vectomatic.dom.svg.OMSVGDocument;
import org.vectomatic.dom.svg.OMSVGGElement;
import org.vectomatic.dom.svg.OMSVGMatrix;
import org.vectomatic.dom.svg.OMSVGPoint;
import org.vectomatic.dom.svg.OMSVGRect;
import org.vectomatic.dom.svg.OMSVGRectElement;
import org.vectomatic.dom.svg.OMSVGSVGElement;
import org.vectomatic.dom.svg.OMSVGTransform;
import org.vectomatic.dom.svg.OMSVGTransformList;
import org.vectomatic.dom.svg.OMSVGUseElement;
import org.vectomatic.dom.svg.ui.SVGPushButton;
import org.vectomatic.dom.svg.utils.AsyncXmlLoader;
import org.vectomatic.dom.svg.utils.AsyncXmlLoaderCallback;
import org.vectomatic.dom.svg.utils.OMSVGParser;
import org.vectomatic.dom.svg.utils.SVGConstants;
import org.vectomatic.svg.edu.client.commons.CommonBundle;
import org.vectomatic.svg.edu.client.commons.CommonConstants;
import org.vectomatic.svg.edu.client.commons.LicenseBox;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.StyleInjector;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Random;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

public class PushMain implements MouseDownHandler, EntryPoint {
    private static final String DIR = "push";
    private static final String ID_CLIP_PATH = "cp";
    private static final String ID_CLIP_RECT = "cpr";
    private static final String ID_IMAGE = "puzzle";
    private static final String ID_TILE = "t";
    private static final int MARGIN = 3;

    @UiField(provided = true)
    PushBundle resources = PushBundle.INSTANCE;
    @UiField(provided = true)
    CommonBundle common = CommonBundle.INSTANCE;
    PushCss style = resources.getCss();
    @UiField
    SVGPushButton prevButton;
    @UiField
    SVGPushButton nextButton;
    @UiField
    HTML svgContainer;
    @UiField
    ListBox levelList;
    @UiField
    FlowPanel navigationPanel;
    Widget menuWidget;

    /**
     * To load game levels
     */
    AsyncXmlLoader loader;
    private String[] levels;
    private int currentLevel;
    /**
     * The source image svg element
     */
    private OMSVGSVGElement srcSvg;
    /**
     * The puzzle root svg element
     */
    private OMSVGSVGElement pushSvg;
    /**
     * BBox of the puzzle image
     */
    private OMSVGRect bbox;
    /**
     * A matrix of tile ids reflecting the game state
     */
    private int[][] game;
    /**
     *  The tile id of the tile representing the hole
     */
    private int hole;
    /**
     * Width of the puzzle border
     */
    int xcount;
    /**
     * Number of puzzle tiles in a column
     */
    int ycount;
    /**
     * The game tiles (upper left has index 0,
     * lower right has coordinate length - 1)
     * There are xcount * ycount -1 tiles (-1
     * is for the hole)
     */
    private OMSVGUseElement[] tiles;
    /**
     * True when the game has begun
     */
    private boolean playing;
    /**
     * Use to free the UI at specific times (when displaying help,
     * when loading a new level, ...)
     */
    boolean frozen;
    /**
     * Timer to briefly display the puzzle assembled
     */
    private Timer waitTimer;
    /**
     * Time to time scrambles apart
     */
    private Timer scrambleTimer;
    /**
     * Class to display an animation when the player wins
     */
    private int animCount;
    private Animation animation = new Animation() {
        @Override
        protected void onUpdate(double progress) {
            for (int i = 0; i < xcount; i++) {
                for (int j = 0; j < ycount; j++) {
                    int index = i * ycount + j;
                    if (index != hole) {
                        OMSVGTransformList xformList = tiles[index].getTransform().getBaseVal();
                        xformList.clear();
                        OMSVGTransform r = pushSvg.createSVGTransform();
                        r.setRotate((float) (360 * progress * ((animCount % 2 == 0) ? 1f : -1f)),
                                (i + 0.5f) * bbox.getWidth() / xcount, (j + 0.5f) * bbox.getHeight() / ycount);
                        xformList.appendItem(r);
                    }
                }
            }
        }

        @Override
        protected void onComplete() {
            super.onComplete();
            animCount++;
            if (animCount < 5) {
                DeferredCommand.addCommand(new Command() {
                    @Override
                    public void execute() {
                        animation.run(1000, Duration.currentTimeMillis() + 500);
                    }
                });
            } else {
                playing = true;
                GWT.log(getDescription());
            }
        }
    };

    interface PushMainBinder extends UiBinder<FlowPanel, PushMain> {
    }

    private static PushMainBinder mainBinder = GWT.create(PushMainBinder.class);

    /**
     * Constructor for standalone game
     */
    public PushMain() {
    }

    /**
     * Constructor for integration in a menu
     */
    public PushMain(Widget menuWidget) {
        this.menuWidget = menuWidget;
    }

    /**
     * Entry point when the game is run in standalone mode
     */
    @Override
    public void onModuleLoad() {
        common.css().ensureInjected();
        StyleInjector.inject(style.getText(), true);

        // Load the game levels
        levels = resources.levels().getText().split("\\s");
        loader = GWT.create(AsyncXmlLoader.class);

        // Initialize the UI with UiBinder
        FlowPanel panel = mainBinder.createAndBindUi(this);
        if (menuWidget == null) {
            menuWidget = LicenseBox.createAboutButton();
        }
        navigationPanel.insert(menuWidget, 0);
        levelList.addItem(PushConstants.INSTANCE.easy());
        levelList.addItem(PushConstants.INSTANCE.medium());
        levelList.addItem(PushConstants.INSTANCE.hard());
        levelList.setSelectedIndex(0);
        RootPanel.get(CommonConstants.ID_UIROOT).add(panel);
        readPushDef();
    }

    @UiHandler("prevButton")
    public void prevButton(ClickEvent event) {
        currentLevel--;
        if (currentLevel < 0) {
            currentLevel = levels.length - 1;
        }
        readPushDef();
    }

    @UiHandler("nextButton")
    public void nextButton(ClickEvent event) {
        currentLevel++;
        if (currentLevel >= levels.length) {
            currentLevel = 0;
        }
        readPushDef();
    }

    @UiHandler("levelList")
    public void levelChange(ChangeEvent event) {
        generate();
    }

    private void generate() {
        OMSVGDocument document = OMSVGParser.currentDocument();
        OMSVGSVGElement rootSvg = document.createSVGSVGElement();
        rootSvg.addMouseDownHandler(this);
        OMSVGDefsElement defs = document.createSVGDefsElement();
        rootSvg.appendChild(defs);

        // Copy the source SVG in a dedicated group inside
        // the defs
        OMSVGGElement imgGroup = document.createSVGGElement();
        imgGroup.setId(ID_IMAGE);
        for (OMNode node : srcSvg.getChildNodes()) {
            imgGroup.appendChild(node.cloneNode(true));
        }
        defs.appendChild(imgGroup);

        OMSVGRect viewBox = srcSvg.getViewBox().getBaseVal();
        float width = viewBox.getWidth();
        float height = viewBox.getHeight();
        bbox = rootSvg.createSVGRect();
        viewBox.assignTo(bbox);

        // Compute the number of tiles
        if (width < height) {
            xcount = levelList.getSelectedIndex() + 3;
            ycount = (int) (xcount * height / width);
        } else {
            ycount = levelList.getSelectedIndex() + 3;
            xcount = (int) (ycount * width / height);
        }
        hole = xcount * ycount - 1;

        // Create a thick border with rounded corners around the
        // drawing (15% of the original drawing size, corner radius
        // 2.5% of the original drawing size)
        // Add a 3 pixel margin around the tiles
        float borderWidth = (int) (0.075f * width);
        float borderHeight = (int) (0.075f * height);
        float borderRx = (int) (0.025f * width);
        float borderRy = (int) (0.025f * height);
        OMSVGRectElement borderOut = document.createSVGRectElement(viewBox.getX() - borderWidth - MARGIN,
                viewBox.getY() - borderHeight - MARGIN, viewBox.getWidth() + 2 * (borderWidth + MARGIN),
                viewBox.getHeight() + 2 * (borderHeight + MARGIN), borderRx, borderRy);
        borderOut.setClassNameBaseVal(style.borderOut());
        OMSVGRectElement borderIn = document.createSVGRectElement(viewBox.getX() - MARGIN, viewBox.getY() - MARGIN,
                viewBox.getWidth() + 2 * MARGIN, viewBox.getHeight() + 2 * MARGIN, borderRx, borderRy);
        borderIn.setClassNameBaseVal(style.borderIn());
        rootSvg.appendChild(borderOut);
        rootSvg.appendChild(borderIn);
        //      rootSvg.setWidth(OMSVGLength.SVG_LENGTHTYPE_PERCENTAGE, 65f);
        //      rootSvg.setHeight(OMSVGLength.SVG_LENGTHTYPE_PERCENTAGE, 65f);
        rootSvg.setViewBox(viewBox.getX() - borderWidth - MARGIN, viewBox.getY() - borderHeight - MARGIN,
                viewBox.getWidth() + 2 * (borderWidth + MARGIN), viewBox.getHeight() + 2 * (borderHeight + MARGIN));

        // Create the tile clip-path
        // <clipPath id="cp">
        //  <rect id="cpr" x="0" y="0" width="130" height="130" rx="10" ry="10"/>
        // </clipPath>
        OMSVGClipPathElement clipPath = document.createSVGClipPathElement();
        clipPath.setId(ID_CLIP_PATH);
        OMSVGRectElement clipRect = document.createSVGRectElement(viewBox.getX(), viewBox.getY(), width / xcount,
                height / ycount, borderRx, borderRy);
        clipRect.setId(ID_CLIP_RECT);
        clipPath.appendChild(clipRect);
        defs.appendChild(clipPath);

        // Create the tiles
        tiles = new OMSVGUseElement[xcount * ycount];
        game = new int[xcount][];
        for (int i = 0; i < xcount; i++) {
            game[i] = new int[ycount];
            for (int j = 0; j < ycount; j++) {
                int index = i * ycount + j;
                if (index != hole) {
                    // Create the tile definition
                    // Each tile definition has the following structure
                    // <g id="tileXXX">
                    //  <g style="clip-path:url(#cp)">
                    //   <g transform="translate(-260,0)">
                    //    <use x="0" y="0" xlink:href="#puzzle"/>
                    //   </g>
                    //  </g>
                    //  <use x="0" y="0" xlink:href="#cp1r" style="fill:none;stroke:black;"/>
                    // </g>      
                    OMSVGGElement tileDef = document.createSVGGElement();
                    tileDef.setId(ID_TILE + index);
                    OMSVGGElement tileClipPath = document.createSVGGElement();
                    tileClipPath.getStyle().setSVGProperty(SVGConstants.CSS_CLIP_PATH_PROPERTY,
                            "url(#" + ID_CLIP_PATH + ")");
                    OMSVGGElement tileTransform = document.createSVGGElement();
                    OMSVGTransform xform = rootSvg.createSVGTransform();
                    xform.setTranslate(viewBox.getX() - i * width / xcount, viewBox.getY() - j * height / ycount);
                    tileTransform.getTransform().getBaseVal().appendItem(xform);
                    OMSVGUseElement imgUse = document.createSVGUseElement();
                    imgUse.getX().getBaseVal().setValue(viewBox.getX());
                    imgUse.getY().getBaseVal().setValue(viewBox.getY());
                    imgUse.getHref().setBaseVal("#" + ID_IMAGE);
                    OMSVGUseElement tileBorder = document.createSVGUseElement();
                    tileBorder.getX().getBaseVal().setValue(viewBox.getX());
                    tileBorder.getY().getBaseVal().setValue(viewBox.getY());
                    tileBorder.getHref().setBaseVal("#" + ID_CLIP_RECT);
                    tileBorder.setClassNameBaseVal(style.tileBorder());
                    tileDef.appendChild(tileClipPath);
                    tileClipPath.appendChild(tileTransform);
                    tileTransform.appendChild(imgUse);
                    tileDef.appendChild(tileBorder);
                    defs.appendChild(tileDef);

                    // Create the tile itself
                    // <use x="130" y="260" xlink:href="#tileXXX"/>
                    tiles[index] = document.createSVGUseElement();
                    tiles[index].getHref().setBaseVal("#" + ID_TILE + index);
                    rootSvg.appendChild(tiles[index]);
                }
                setTile(i, j, index);
            }
        }

        // Add the SVG to the HTML page
        Element div = svgContainer.getElement();
        if (pushSvg != null) {
            div.replaceChild(rootSvg.getElement(), pushSvg.getElement());
        } else {
            div.appendChild(rootSvg.getElement());
        }
        pushSvg = rootSvg;
        if (!GWT.isScript()) {
            GWT.log(pushSvg.getMarkup());
        }

        // Display the puzzle in order for 1 sec, then scramble it
        waitTimer = new Timer() {
            public void run() {
                scrambleTimer = new Timer() {
                    int repeatCount;

                    @Override
                    public void run() {
                        int tileCount = xcount * ycount;
                        List<Integer> array = new ArrayList<Integer>();
                        for (int i = 0; i < tileCount; i++) {
                            array.add(i);
                        }
                        // Shuffle the tiles
                        for (int i = 0; i < xcount; i++) {
                            for (int j = 0; j < ycount; j++) {
                                setTile(i, j, array.remove(Random.nextInt(tileCount--)));
                            }
                        }
                        repeatCount++;
                        if (repeatCount >= 5) {
                            playing = true;
                            cancel();
                        }
                    }
                };
                if ("true".equals(Window.Location.getParameter("win"))) {
                    winAnimation();
                } else {
                    scrambleTimer.scheduleRepeating(200);
                }
            }
        };
        waitTimer.schedule(1000);
    }

    public boolean gameOver() {
        for (int i = 0; i < xcount; i++) {
            for (int j = 0; j < ycount; j++) {
                if (!(getTile(i, j) == i * ycount + j)) {
                    return false;
                }
            }
        }
        return true;
    }

    public void setTile(int x, int y, int value) {
        game[x][y] = value;
        if (value != hole) {
            tiles[value].getX().getBaseVal().setValue(x * bbox.getWidth() / xcount);
            tiles[value].getY().getBaseVal().setValue(y * bbox.getHeight() / ycount);
        }
    }

    public int getTile(int x, int y) {
        return game[x][y];
    }

    private String getDescription() {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < xcount; i++) {
            builder.append("| ");
            for (int j = 0; j < ycount; j++) {
                builder.append(" ");
                builder.append(getTile(i, j));
            }
            builder.append(" |\n");
        }
        return builder.toString();
    }

    @Override
    public void onMouseDown(MouseDownEvent event) {
        if (playing) {
            OMSVGPoint coords = getTileCoordinates(event);
            if (coords != null) {
                GWT.log("mouseDown: " + coords.getDescription());
                int x = (int) coords.getX();
                int y = (int) coords.getY();
                boolean shifted = false;
                for (int i = 0; i < xcount; i++) {
                    if (game[i][y] == hole) {
                        if (x < i) {
                            for (int j = i; j > x; j--) {
                                setTile(j, y, getTile(j - 1, y));
                            }
                        } else {
                            for (int j = i; j < x; j++) {
                                setTile(j, y, getTile(j + 1, y));
                            }
                        }
                        setTile(x, y, hole);
                        GWT.log(getDescription());
                        shifted = true;
                    }
                }
                if (!shifted) {
                    for (int i = 0; i < ycount; i++) {
                        if (game[x][i] == hole) {
                            if (y < i) {
                                for (int j = i; j > y; j--) {
                                    setTile(x, j, getTile(x, j - 1));
                                }
                            } else {
                                for (int j = i; j < y; j++) {
                                    setTile(x, j, getTile(x, j + 1));
                                }
                            }
                            setTile(x, y, hole);
                            GWT.log(getDescription());
                        }
                    }
                }
                if (gameOver()) {
                    winAnimation();
                }
                event.stopPropagation();
            }
        }
    }

    public OMSVGPoint getTileCoordinates(MouseEvent<? extends EventHandler> e) {
        OMSVGPoint p = pushSvg.createSVGPoint(e.getClientX(), e.getClientY());
        OMSVGMatrix m = pushSvg.getScreenCTM().inverse();
        p = p.matrixTransform(m).substract(pushSvg.createSVGPoint(bbox.getX(), bbox.getY()))
                .product(pushSvg.createSVGPoint(xcount / bbox.getWidth(), ycount / bbox.getHeight())).floor();
        return pushSvg.createSVGRect(0, 0, xcount - 1, ycount - 1).contains(p) ? p : null;
    }

    public void winAnimation() {
        playing = false;
        animCount = 0;
        animation.run(1000, Duration.currentTimeMillis() + 500);
    }

    private String getLevelUrl() {
        return GWT.getModuleBaseURL() + DIR + "/" + levels[currentLevel];
    }

    public void readPushDef() {
        playing = false;
        if (waitTimer != null) {
            waitTimer.cancel();
        }
        if (scrambleTimer != null) {
            scrambleTimer.cancel();
        }
        if (animation != null) {
            animation.cancel();
        }
        String url = GWT.getModuleBaseURL() + DIR + "/" + levels[currentLevel];
        loader.loadResource(url, new AsyncXmlLoaderCallback() {
            @Override
            public void onError(String resourceName, Throwable error) {
                svgContainer.setHTML("Cannot find resource");
            }

            @Override
            public void onSuccess(String resourceName, com.google.gwt.dom.client.Element root) {
                srcSvg = OMNode.convert(root);
                generate();
            }
        });
    }
}