/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.model.grid; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.awt.geom.Area; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.Action; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import com.t3.client.AppPreferences; import com.t3.client.AppState; import com.t3.client.ScreenPoint; import com.t3.client.TabletopTool; import com.t3.client.tool.PointerTool; import com.t3.client.ui.zone.ZoneRenderer; import com.t3.client.walker.WalkerMetric; import com.t3.client.walker.ZoneWalker; import com.t3.client.walker.astar.AStarSquareEuclideanWalker; import com.t3.image.ImageUtil; import com.t3.model.CellPoint; import com.t3.model.MovementKey; import com.t3.model.TokenFootprint; import com.t3.model.ZonePoint; import com.t3.swing.SwingUtil; import com.t3.xstreamversioned.version.SerializationVersion; @SerializationVersion(0) public class SquareGrid extends Grid { private static final String alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; //$NON-NLS-1$ private static final Dimension CELL_OFFSET = new Dimension(0, 0); private static BufferedImage pathHighlight; private static List<TokenFootprint> footprintList; static { try { pathHighlight = ImageUtil.getCompatibleImage("com/t3/client/image/whiteBorder.png"); } catch (IOException ioe) { TabletopTool.showError("SquareGrid.error.pathhighlightingNotLoaded", ioe); } } // @formatter:off private static final GridCapabilities CAPABILITIES = new GridCapabilities() { @Override public boolean isPathingSupported() { return true; } @Override public boolean isSnapToGridSupported() { return true; } @Override public boolean isPathLineSupported() { return true; } @Override public boolean isSecondDimensionAdjustmentSupported() { return false; } @Override public boolean isCoordinatesSupported() { return true; } }; // @formatter:on private static final int[] ALL_ANGLES = new int[] { -135, -90, -45, 0, 45, 90, 135, 180 }; private static int[] FACING_ANGLES; public SquareGrid() { super(); if (FACING_ANGLES == null) { boolean faceEdges = AppPreferences.getFaceEdge(); boolean faceVertices = AppPreferences.getFaceVertex(); setFacings(faceEdges, faceVertices); } } public SquareGrid(boolean faceEdges, boolean faceVertices) { setFacings(faceEdges, faceVertices); } @Override public void installMovementKeys(PointerTool callback, Map<KeyStroke, Action> actionMap) { if (movementKeys == null) { movementKeys = new HashMap<KeyStroke, Action>(18); // This is 13/0.75, rounded up int size = getSize(); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, 0), new MovementKey(callback, -size, -size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, 0), new MovementKey(callback, 0, -size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, 0), new MovementKey(callback, size, -size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, 0), new MovementKey(callback, -size, 0)); // movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, 0), new MovementKey(callback, 0, 0)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, 0), new MovementKey(callback, size, 0)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, 0), new MovementKey(callback, -size, size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, 0), new MovementKey(callback, 0, size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, 0), new MovementKey(callback, size, size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), new MovementKey(callback, -size, 0)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), new MovementKey(callback, size, 0)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), new MovementKey(callback, 0, -size)); movementKeys.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), new MovementKey(callback, 0, size)); } actionMap.putAll(movementKeys); } @Override public void uninstallMovementKeys(Map<KeyStroke, Action> actionMap) { if (movementKeys != null) { for (KeyStroke key : movementKeys.keySet()) { actionMap.remove(key); } } } @Override public void setFacings(boolean faceEdges, boolean faceVertices) { if (faceEdges && faceVertices) { FACING_ANGLES = ALL_ANGLES; } else if (!faceEdges && faceVertices) { FACING_ANGLES = new int[] { -135, -45, 45, 135 }; } else if (faceEdges && !faceVertices) { FACING_ANGLES = new int[] { -90, 0, 90, 180 }; } else { FACING_ANGLES = new int[] { 90 }; } } @Override public void drawCoordinatesOverlay(Graphics2D g, ZoneRenderer renderer) { Object oldAA = SwingUtil.useAntiAliasing(g); Font oldFont = g.getFont(); g.setFont(g.getFont().deriveFont(20f).deriveFont(Font.BOLD)); FontMetrics fm = g.getFontMetrics(); double cellSize = renderer.getScaledGridSize(); CellPoint topLeft = convert(new ScreenPoint(0, 0).convertToZone(renderer)); ScreenPoint sp = ScreenPoint.fromZonePoint(renderer, convert(topLeft)); Dimension size = renderer.getSize(); int startX = SwingUtilities.computeStringWidth(fm, "MMM") + 10; double x = sp.x + cellSize / 2; // Start at middle of the cell that's on screen int nextAvailableSpace = -1; while (x < size.width) { String coord = Integer.toString(topLeft.x); int strWidth = SwingUtilities.computeStringWidth(fm, coord); int strX = (int) x - strWidth / 2; if (x > startX && strX > nextAvailableSpace) { g.setColor(Color.black); g.drawString(coord, strX, fm.getHeight()); g.setColor(Color.orange); g.drawString(coord, strX - 1, fm.getHeight() - 1); nextAvailableSpace = strX + strWidth + 10; } x += cellSize; topLeft.x++; } double y = sp.y + cellSize / 2; // Start at middle of the cell that's on screen nextAvailableSpace = -1; while (y < size.height) { String coord = decimalToAlphaCoord(topLeft.y); int strY = (int) y + fm.getAscent() / 2; if (y > fm.getHeight() && strY > nextAvailableSpace) { g.setColor(Color.black); g.drawString(coord, 10, strY); g.setColor(Color.yellow); g.drawString(coord, 10 - 1, strY - 1); nextAvailableSpace = strY + fm.getAscent() / 2 + 10; } y += cellSize; topLeft.y++; } g.setFont(oldFont); SwingUtil.restoreAntiAliasing(g, oldAA); } @Override public List<TokenFootprint> getFootprints() { if (footprintList == null) { footprintList = loadFootprints("squareGridFootprints.xml"); } return footprintList; } @Override public Rectangle getBounds(CellPoint cp) { return new Rectangle(cp.x * getSize(), cp.y * getSize(), getSize(), getSize()); } @Override public BufferedImage getCellHighlight() { return pathHighlight; } @Override protected Area createCellShape(int size) { return new Area(new Rectangle(0, 0, size, size)); } @Override public Dimension getCellOffset() { return CELL_OFFSET; } @Override public double getCellHeight() { return getSize(); } @Override public double getCellWidth() { return getSize(); } @Override public int[] getFacingAngles() { return FACING_ANGLES; } @Override public CellPoint convert(ZonePoint zp) { double calcX = (zp.x - getOffsetX()) / (float) getSize(); double calcY = (zp.y - getOffsetY()) / (float) getSize(); boolean exactCalcX = (zp.x - getOffsetX()) % getSize() == 0; boolean exactCalcY = (zp.y - getOffsetY()) % getSize() == 0; int newX = (int) (zp.x < 0 && !exactCalcX ? calcX - 1 : calcX); int newY = (int) (zp.y < 0 && !exactCalcY ? calcY - 1 : calcY); //System.out.format("%d / %d => %f, %f => %d, %d\n", zp.x, getSize(), calcX, calcY, newX, newY); return new CellPoint(newX, newY); } @Override public ZoneWalker createZoneWalker() { WalkerMetric metric = TabletopTool.isPersonalServer() ? AppPreferences.getMovementMetric() : TabletopTool.getServerPolicy().getMovementMetric(); return new AStarSquareEuclideanWalker(getZone(), metric); } @Override public ZonePoint convert(CellPoint cp) { return new ZonePoint((cp.x * getSize() + getOffsetX()), (cp.y * getSize() + getOffsetY())); } @Override public GridCapabilities getCapabilities() { return CAPABILITIES; } @Override public void draw(ZoneRenderer renderer, Graphics2D g, Rectangle bounds) { double scale = renderer.getScale(); double gridSize = getSize() * scale; g.setColor(new Color(getZone().getGridColor())); int offX = (int) (renderer.getViewOffsetX() % gridSize + getOffsetX() * scale); int offY = (int) (renderer.getViewOffsetY() % gridSize + getOffsetY() * scale); int startCol = (int) ((int) (bounds.x / gridSize) * gridSize); int startRow = (int) ((int) (bounds.y / gridSize) * gridSize); for (double row = startRow; row < bounds.y + bounds.height + gridSize; row += gridSize) { if (AppState.getGridSize() == 1) { g.drawLine(bounds.x, (int) (row + offY), bounds.x + bounds.width, (int) (row + offY)); } else { g.fillRect(bounds.x, (int) (row + offY - (AppState.getGridSize() / 2)), bounds.width, AppState.getGridSize()); } } for (double col = startCol; col < bounds.x + bounds.width + gridSize; col += gridSize) { if (AppState.getGridSize() == 1) { g.drawLine((int) (col + offX), bounds.y, (int) (col + offX), bounds.y + bounds.height); } else { g.fillRect((int) (col + offX - (AppState.getGridSize() / 2)), bounds.y, AppState.getGridSize(), bounds.height); } } } public ZonePoint getCenterPoint(CellPoint cellPoint) { ZonePoint zp = convert(cellPoint); zp.x += getCellWidth() / 2; zp.y += getCellHeight() / 2; return zp; } public static String decimalToAlphaCoord(int value) { String result = new String(); int temp; boolean isNegative = false; if (value < 0) { value *= -1; value--; // Shift down so -1 is -A instead of -B isNegative = true; } while (value >= 26) { temp = value % 26; value = (value - temp) / 26 - 1; result = alpha.charAt(temp) + result; } result = alpha.charAt(value) + result; if (isNegative) { result = "-" + result; } return result; } }