/* * $Id$ * * Copyright (c) 2000-2003 by Rodney Kinney * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.build.module.map.boardPicker.board; import static java.lang.Math.abs; import static java.lang.Math.floor; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.round; import java.awt.Color; import java.awt.Container; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.util.HashMap; import java.util.Map; import javax.swing.JButton; import VASSAL.build.AbstractConfigurable; import VASSAL.build.AutoConfigurable; import VASSAL.build.Buildable; import VASSAL.build.module.documentation.HelpFile; import VASSAL.build.module.map.boardPicker.board.mapgrid.GridContainer; import VASSAL.build.module.map.boardPicker.board.mapgrid.GridNumbering; import VASSAL.build.module.map.boardPicker.board.mapgrid.SquareGridNumbering; import VASSAL.configure.AutoConfigurer; import VASSAL.configure.ColorConfigurer; import VASSAL.configure.Configurer; import VASSAL.configure.StringEnum; import VASSAL.configure.VisibilityCondition; import VASSAL.i18n.Resources; public class SquareGrid extends AbstractConfigurable implements GeometricGrid, GridEditor.EditableGrid { protected double dx = 48.0; protected double dy = 48.0; protected int snapScale = 0; protected Point origin = new Point(24, 24); protected boolean visible = false; protected boolean edgesLegal = false; protected boolean cornersLegal = false; protected boolean dotsVisible = false; protected Color color = Color.black; protected GridContainer container; protected Map<Integer,Area> shapeCache = new HashMap<Integer,Area>(); protected SquareGridEditor gridEditor; protected String rangeOption = RANGE_METRIC; protected boolean snapTo = true; private GridNumbering gridNumbering; public GridNumbering getGridNumbering() { return gridNumbering; } public void setGridNumbering(GridNumbering gridNumbering) { this.gridNumbering = gridNumbering; } public double getDx() { return dx; } public void setDx(double d) { dx = d; } public double getDy() { return dy; } public void setDy(double d) { dy = d; } public Point getOrigin() { return new Point(origin); } public void setOrigin(Point p) { origin.x = p.x; origin.y = p.y; } public boolean isSideways() { return false; } public void setSideways(boolean b) { return; } public GridContainer getContainer() { return container; } public static final String DX = "dx"; //$NON-NLS-1$ public static final String DY = "dy"; //$NON-NLS-1$ public static final String X0 = "x0"; //$NON-NLS-1$ public static final String Y0 = "y0"; //$NON-NLS-1$ public static final String VISIBLE = "visible"; //$NON-NLS-1$ public static final String CORNERS = "cornersLegal"; //$NON-NLS-1$ public static final String EDGES = "edgesLegal"; //$NON-NLS-1$ public static final String COLOR = "color"; //$NON-NLS-1$ public static final String DOTS_VISIBLE = "dotsVisible"; //$NON-NLS-1$ public static final String RANGE = "range"; //$NON-NLS-1$ public static final String RANGE_MANHATTAN = "Manhattan"; //$NON-NLS-1$ public static final String RANGE_METRIC = "Metric"; //$NON-NLS-1$ public static final String SNAP_TO = "snapTo"; //$NON-NLS-1$ public static class RangeOptions extends StringEnum { public String[] getValidValues(AutoConfigurable target) { return new String[]{RANGE_METRIC, RANGE_MANHATTAN}; } } public String[] getAttributeNames() { return new String[] { X0, Y0, DX, DY, RANGE, SNAP_TO, EDGES, CORNERS, VISIBLE, DOTS_VISIBLE, COLOR }; } public String[] getAttributeDescriptions() { return new String[]{ Resources.getString("Editor.Grid.x_offset"), //$NON-NLS-1$ Resources.getString("Editor.Grid.y_offset"), //$NON-NLS-1$ Resources.getString("Editor.RectangleGrid.width"), //$NON-NLS-1$ Resources.getString("Editor.RectangleGrid.height"), //$NON-NLS-1$ Resources.getString("Editor.RectangleGrid.range_method"), //$NON-NLS-1$ Resources.getString("Editor.Grid.snap"), //$NON-NLS-1$ Resources.getString("Editor.Grid.edges"), //$NON-NLS-1$ Resources.getString("Editor.RectangleGrid.corners"), //$NON-NLS-1$ Resources.getString("Editor.Grid.show_grid"), //$NON-NLS-1$ Resources.getString("Editor.Grid.center_dots"), //$NON-NLS-1$ Resources.getString(Resources.COLOR_LABEL), }; } public Class<?>[] getAttributeTypes() { return new Class<?>[]{ Integer.class, Integer.class, Double.class, Double.class, RangeOptions.class, Boolean.class, Boolean.class, Boolean.class, Boolean.class, Boolean.class, Color.class }; } public VisibilityCondition getAttributeVisibility(String name) { if (COLOR.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return visible; } }; } else if (EDGES.equals(name) || CORNERS.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return snapTo; } }; } else { return super.getAttributeVisibility(name); } } public void addTo(Buildable b) { container = (GridContainer) b; container.setGrid(this); } public void removeFrom(Buildable b) { ((GridContainer) b).removeGrid(this); } public static String getConfigureTypeName() { return Resources.getString("Editor.RectangleGrid.component_type"); //$NON-NLS-1$ } public String getGridName() { return getConfigureTypeName(); } public String getConfigureName() { return null; } public VASSAL.build.module.documentation.HelpFile getHelpFile() { return HelpFile.getReferenceManualPage("RectangularGrid.htm"); //$NON-NLS-1$ } public String getAttributeValueString(String key) { if (X0.equals(key)) { return String.valueOf(origin.x); } else if (Y0.equals(key)) { return String.valueOf(origin.y); } else if (DY.equals(key)) { return String.valueOf(dy); } else if (DX.equals(key)) { return String.valueOf(dx); } else if (RANGE.equals(key)) { return rangeOption; } else if (SNAP_TO.equals(key)) { return String.valueOf(snapTo); } else if (CORNERS.equals(key)) { return String.valueOf(cornersLegal); } else if (EDGES.equals(key)) { return String.valueOf(edgesLegal); } else if (VISIBLE.equals(key)) { return String.valueOf(visible); } else if (DOTS_VISIBLE.equals(key)) { return String.valueOf(dotsVisible); } else if (COLOR.equals(key)) { return ColorConfigurer.colorToString(color); } return null; } public void setAttribute(String key, Object val) { if (X0.equals(key)) { if (val instanceof String) { val = Integer.valueOf((String) val); } origin.x = ((Integer) val).intValue(); } else if (Y0.equals(key)) { if (val instanceof String) { val = Integer.valueOf((String) val); } origin.y = ((Integer) val).intValue(); } else if (DY.equals(key)) { if (val instanceof String) { val = Double.valueOf((String) val); } dy = ((Double) val).doubleValue(); } else if (DX.equals(key)) { if (val instanceof String) { val = Double.valueOf((String) val); } dx = ((Double) val).doubleValue(); } else if (RANGE.equals(key)) { rangeOption = (String) val; } else if (SNAP_TO.equals(key)) { if (val instanceof String) { val = Boolean.valueOf((String) val); } snapTo = ((Boolean) val).booleanValue(); } else if (CORNERS.equals(key)) { if (val instanceof String) { val = Boolean.valueOf((String) val); } cornersLegal = ((Boolean) val).booleanValue(); } else if (EDGES.equals(key)) { if (val instanceof String) { val = Boolean.valueOf((String) val); } edgesLegal = ((Boolean) val).booleanValue(); } else if (VISIBLE.equals(key)) { if (val instanceof String) { val = Boolean.valueOf((String) val); } visible = ((Boolean) val).booleanValue(); } else if (DOTS_VISIBLE.equals(key)) { if (val instanceof String) { val = Boolean.valueOf((String) val); } dotsVisible = ((Boolean) val).booleanValue(); } else if (COLOR.equals(key)) { if (val instanceof String) { val = ColorConfigurer.stringToColor((String) val); } color = (Color) val; } shapeCache.clear(); } public Class<?>[] getAllowableConfigureComponents() { return new Class[]{SquareGridNumbering.class}; } public Point getLocation(String location) throws BadCoords { if (gridNumbering == null) throw new BadCoords(); else return gridNumbering.getLocation(location); } public int range(Point p1, Point p2) { if (rangeOption.equals(RANGE_METRIC)) { return max(abs((int) floor((p2.x - p1.x) / dx + 0.5)) , abs((int) floor((p2.y - p1.y) / dy + 0.5))); } else { return abs((int) floor((p2.x - p1.x) / dx + 0.5)) + abs((int) floor((p2.y - p1.y) / dy + 0.5)); } } public Area getGridShape(Point center, int range) { Area shape = shapeCache.get(range); if (shape == null) { shape = getSingleSquareShape(0, 0); double dx = getDx(); double dy = getDy(); for (int x = -range; x < range + 1; x++) { int x1 = (int) (x * dx); // int yRange = range - abs(x); /* This creates a diamond-shaped range. Configuration option? */ int yRange = range; for (int y = -yRange; y < yRange + 1; y++) { int y1 = (int) (y * dy); shape.add(getSingleSquareShape(x1, y1)); } } shapeCache.put(range, shape); } shape = new Area(AffineTransform.getTranslateInstance(center.x, center.y).createTransformedShape(shape)); return shape; } /** * Return the Shape of a single grid square */ public Area getSingleSquareShape(int centerX, int centerY) { double dx = getDx(); double dy = getDy(); Rectangle rect = new Rectangle((int) (centerX - dx / 2), (int) (centerY - dy / 2), (int) dx, (int) dy); return new Area(rect); } public Point snapTo(Point p) { if (! snapTo) { return p; } // nx,ny are the closest points to the half-grid // (0,0) is the center of the origin cell // (1,0) is the east edge of the origin cell // (1,1) is the lower-right corner of the origin cell int offsetX = p.x - origin.x; int nx = (int) round(offsetX / (0.5 * dx)); int offsetY = p.y - origin.y; int ny = (int) round(offsetY / (0.5 * dy)); Point snap = null; if (cornersLegal && edgesLegal) { ; } else if (cornersLegal) { if (ny % 2 == 0) { // on a cell center nx = 2 * (int) round(offsetX/dx); } else { // on a corner nx = 1 + 2 * (int) round(offsetX/dx - 0.5); } } else if (edgesLegal) { if (ny % 2 == 0) { if (nx % 2 == 0) { // Cell center nx = 2 * (int) round(offsetX/dx); } else { // Vertical edge ; } } else { // Horizontal edge nx = 2 * (int) round(offsetX/dx); } } else { nx = 2*(int)round(offsetX/dx); ny = 2*(int)round(offsetY/dy); if (snapScale > 0) { int deltaX = offsetX - (int)round(nx*dx/2); deltaX = (int)round(deltaX/(0.5*dx/snapScale)); deltaX = max(deltaX,1-snapScale); deltaX = min(deltaX,snapScale-1); deltaX = (int)round(deltaX*0.5*dx/snapScale); int deltaY = offsetY - (int)round(ny*dy/2); deltaY = (int)round(deltaY/(0.5*dy/snapScale)); deltaY = max(deltaY,1-snapScale); deltaY = min(deltaY,snapScale-1); deltaY = (int)round(deltaY*0.5*dy/snapScale); snap = new Point((int)round(nx*dx/2 + deltaX),(int)round(ny*dy/2+deltaY)); snap.translate(origin.x, origin.y); } } if (snap == null) { snap = new Point(origin.x + (int)round(nx * dx / 2), origin.y + (int) round(ny * dy / 2)); } return snap; } public boolean isLocationRestricted(Point p) { return snapTo; } public String locationName(Point p) { return gridNumbering == null ? null : gridNumbering.locationName(p); } public String localizedLocationName(Point p) { return gridNumbering == null ? null : gridNumbering.localizedLocationName(p); } public boolean isVisible() { return visible == true || (gridNumbering != null && gridNumbering.isVisible()); } public void setVisible(boolean b) { visible = true; } protected void reverse(Point p, Rectangle bounds) { p.x = bounds.x + bounds.width - (p.x - bounds.x); p.y = bounds.y + bounds.height - (p.y - bounds.y); } /** Draw the grid, if visible, and accompanying numbering, if set */ public void draw(Graphics g, Rectangle bounds, Rectangle visibleRect, double scale, boolean reversed) { if (visible) { forceDraw(g, bounds, visibleRect, scale, reversed); } if (gridNumbering != null) { gridNumbering.draw(g, bounds, visibleRect, scale, reversed); } } /** Draw the grid even if not marked visible */ public void forceDraw(Graphics g, Rectangle bounds, Rectangle visibleRect, double scale, boolean reversed) { if (!bounds.intersects(visibleRect) || color == null) { return; } Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Rectangle region = bounds.intersection(visibleRect); Shape oldClip = g2d.getClip(); if (oldClip != null) { Area clipArea = new Area(oldClip); clipArea.intersect(new Area(region)); g2d.setClip(clipArea); } double deltaX = scale * dx; double deltaY = scale * dy; double xmin = reversed ? bounds.x + scale * origin.x + bounds.width - deltaX * round((bounds.x + scale * origin.x + bounds.width - region.x) / deltaX) + deltaX / 2 : bounds.x + scale * origin.x + deltaX * round((region.x - bounds.x - scale * origin.x) / deltaX) + deltaX / 2; double xmax = region.x + region.width; double ymin = reversed ? bounds.y + scale * origin.y + bounds.height - deltaY * round((bounds.y + scale * origin.y + bounds.height - region.y) / deltaY) + deltaY / 2 : bounds.y + scale * origin.y + deltaY * round((region.y - bounds.y - scale * origin.y) / deltaY) + deltaY / 2; double ymax = region.y + region.height; Point p1 = new Point(); Point p2 = new Point(); g2d.setColor(color); // x is the location of a vertical line for (double x = xmin; x < xmax; x += deltaX) { p1.move((int) round(x), region.y); p2.move((int) round(x), region.y + region.height); g2d.drawLine(p1.x, p1.y, p2.x, p2.y); } for (double y = ymin; y < ymax; y += deltaY) { g2d.drawLine(region.x, (int) round(y), region.x + region.width, (int) round(y)); } if (dotsVisible) { xmin = reversed ? bounds.x + scale * origin.x + bounds.width - deltaX * round((bounds.x + scale * origin.x + bounds.width - region.x) / deltaX) : bounds.x + scale * origin.x + deltaX * round((region.x - bounds.x - scale * origin.x) / deltaX); ymin = reversed ? bounds.y + scale * origin.y + bounds.height - deltaY * round((bounds.y + scale * origin.y + bounds.height - region.y) / deltaY) : bounds.y + scale * origin.y + deltaY * round((region.y - bounds.y - scale * origin.y) / deltaY); for (double x = xmin; x < xmax; x += deltaX) { for (double y = ymin; y < ymax; y += deltaY) { p1.move((int) round(x - 0.5), (int) round(y - 0.5)); g2d.fillRect(p1.x, p1.y, 2, 2); } } } g2d.setClip(oldClip); } public Configurer getConfigurer() { boolean buttonExists = config != null; Configurer c = super.getConfigurer(); if (!buttonExists) { JButton b = new JButton(Resources.getString("Editor.Grid.edit_grid")); //$NON-NLS-1$ b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editGrid(); } }); ((Container) c.getControls()).add(b); } return c; } public void editGrid() { gridEditor = new SquareGridEditor((GridEditor.EditableGrid) this); gridEditor.setVisible(true); // Local variables may have been updated by GridEditor so refresh // configurers. AutoConfigurer cfg = (AutoConfigurer) getConfigurer(); cfg.getConfigurer(DX).setValue(String.valueOf(dx)); cfg.getConfigurer(DY).setValue(String.valueOf(dy)); cfg.getConfigurer(X0).setValue(String.valueOf(origin.x)); cfg.getConfigurer(Y0).setValue(String.valueOf(origin.y)); } public static class SquareGridEditor extends GridEditor { private static final long serialVersionUID = 1L; public SquareGridEditor(EditableGrid grid) { super(grid); } /* * Calculate Grid metrics based on three selected points */ public void calculate() { if ((isPerpendicular(hp1, hp2) && isPerpendicular(hp1, hp3) && !isPerpendicular(hp2, hp3)) || (isPerpendicular(hp2, hp1) && isPerpendicular(hp2, hp3) && !isPerpendicular(hp1, hp3)) || (isPerpendicular(hp3, hp1) && isPerpendicular(hp3, hp2) && !isPerpendicular(hp1, hp2))) { int height = max(abs(hp1.y-hp2.y), abs(hp1.y-hp3.y)); int width = max(abs(hp1.x-hp2.x), abs(hp1.x-hp3.x)); int top = min(hp1.y, min(hp2.y, hp3.y)); int left = min(hp1.x, min(hp2.x, hp3.x)); grid.setDx(width); grid.setDy(height); setNewOrigin(new Point(left+width/2, top+height/2)); } else { reportShapeError(); } } } public int getSnapScale() { return snapScale; } public void setSnapScale(int snapScale) { this.snapScale = snapScale; } }