/* * $Id$ * * Copyright (c) 2000-2012 by Rodney Kinney, Brent Easton * * 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.mapgrid; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import VASSAL.build.AbstractConfigurable; import VASSAL.build.AutoConfigurable; import VASSAL.build.Buildable; import VASSAL.build.GameModule; import VASSAL.build.module.GameComponent; import VASSAL.build.module.Map; import VASSAL.build.module.map.boardPicker.Board; import VASSAL.build.module.map.boardPicker.board.HexGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid; import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords; import VASSAL.build.module.map.boardPicker.board.RegionGrid; import VASSAL.build.module.map.boardPicker.board.SquareGrid; import VASSAL.build.module.map.boardPicker.board.ZonedGrid; import VASSAL.build.module.properties.ChangePropertyCommandEncoder; import VASSAL.build.module.properties.MutablePropertiesContainer; import VASSAL.build.module.properties.MutableProperty; import VASSAL.build.module.properties.PropertySource; import VASSAL.build.module.properties.ZoneProperty; import VASSAL.command.Command; import VASSAL.configure.Configurer; import VASSAL.configure.ConfigurerFactory; import VASSAL.configure.FormattedStringConfigurer; import VASSAL.configure.VisibilityCondition; import VASSAL.i18n.TranslatableConfigurerFactory; import VASSAL.tools.AdjustableSpeedScrollPane; import VASSAL.tools.FormattedString; import VASSAL.tools.SequenceEncoder; public class Zone extends AbstractConfigurable implements GridContainer, MutablePropertiesContainer, PropertySource, GameComponent { public static final String NAME = "name"; public static final String PATH = "path"; public static final String USE_PARENT_GRID = "useParentGrid"; public static final String LOCATION_FORMAT = "locationFormat"; public static final String GRID_LOCATION = "gridLocation"; public static final String USE_HIGHLIGHT = "useHighlight"; public static final String HIGHLIGHT_PROPERTY = "highlightProperty"; protected static final Dimension DEFAULT_SIZE = new Dimension(600, 600); protected String locationFormat = "$" + NAME + "$"; protected FormattedString format = new FormattedString(); protected Polygon myPolygon; protected MapGrid grid = null; protected ZonedGrid parentGrid; protected boolean useParentGrid; protected PropertyChangeListener globalPropertyListener; protected MutablePropertiesContainer propsContainer = new Impl(); protected PropertyChangeListener repaintOnPropertyChange = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { repaint(); } }; /* * Cache as much as possible to minimise the number of Affine Transformations that need to be performed. */ protected int lastBoundsX = -1; protected int lastBoundsY = -1; protected double lastScale = -1; protected Shape lastScaledShape = null; protected Shape lastTransformedShape = null; protected Polygon lastPolygon = null; /* * Record details of the current highlighter and the property that is controlling highlighting. */ protected ZoneHighlight highlighter = null; protected boolean useHighlight = false; protected String highlightPropertyName = ""; protected MutableProperty highlightProperty = null; protected PropertyChangeListener highlightPropertyChangeListener = null; public Zone() { myPolygon = new Polygon(); setConfigureName(""); } public String getName() { return getConfigureName(); } public String getLocalizedName() { return getLocalizedConfigureName(); } public String[] getAttributeNames() { return new String[]{ NAME, LOCATION_FORMAT, PATH, USE_PARENT_GRID, USE_HIGHLIGHT, HIGHLIGHT_PROPERTY }; } public String[] getAttributeDescriptions() { return new String[]{ "Name: ", "Location Format: ", "Shape", "Use board's grid?", "Use Highlighting?", "Highlight Property: " }; } public Class<?>[] getAttributeTypes() { return new Class<?>[]{ String.class, LocationFormatConfig.class, ShapeEditor.class, Boolean.class, Boolean.class, String.class }; } public static class LocationFormatConfig implements TranslatableConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new FormattedStringConfigurer(key, name, new String[]{NAME, GRID_LOCATION}); } } public static class ShapeEditor implements ConfigurerFactory { public Configurer getConfigurer(AutoConfigurable c, String key, String name) { return new Editor((Zone) c); } } public void addTo(Buildable b) { parentGrid = (ZonedGrid) b; parentGrid.addZone(this); GameModule.getGameModule().getGameState().addGameComponent(this); GameModule.getGameModule().addCommandEncoder(new ChangePropertyCommandEncoder(this)); setAttributeTranslatable(HIGHLIGHT_PROPERTY, false); } public void repaint() { if (getMap() != null) { getMap().repaint(); } } public void removeFrom(Buildable b) { ((ZonedGrid) b).removeZone(this); GameModule.getGameModule().getGameState().removeGameComponent(this); } public static String getConfigureTypeName() { return "Zone"; } public VASSAL.build.module.documentation.HelpFile getHelpFile() { return null; } public String getAttributeValueString(String key) { if (NAME.equals(key)) { return getConfigureName(); } else if (PATH.equals(key)) { return PolygonEditor.polygonToString(myPolygon); } else if (LOCATION_FORMAT.equals(key)) { return locationFormat; } else if (USE_PARENT_GRID.equals(key)) { return String.valueOf(useParentGrid); } else if (USE_HIGHLIGHT.equals(key)) { return String.valueOf(useHighlight); } else if (HIGHLIGHT_PROPERTY.equals(key)) { return highlightPropertyName; } return null; } public void setAttribute(String key, Object val) { if (val == null) return; if (NAME.equals(key)) { setConfigureName((String) val); } else if (PATH.equals(key)) { PolygonEditor.reset(myPolygon, (String) val); } else if (LOCATION_FORMAT.equals(key)) { locationFormat = (String) val; } else if (USE_PARENT_GRID.equals(key)) { useParentGrid = "true".equals(val) || Boolean.TRUE.equals(val); } else if (USE_HIGHLIGHT.equals(key)) { useHighlight = "true".equals(val) || Boolean.TRUE.equals(val); } else if (HIGHLIGHT_PROPERTY.equals(key)) { highlightPropertyName = (String) val; } } public VisibilityCondition getAttributeVisibility(String name) { if (HIGHLIGHT_PROPERTY.equals(name)) { return new VisibilityCondition() { public boolean shouldBeVisible() { return useHighlight; } }; } else { return super.getAttributeVisibility(name); } } public Class<?>[] getAllowableConfigureComponents() { return useParentGrid ? new Class<?>[]{ZoneProperty.class} : new Class<?>[]{ HexGrid.class, SquareGrid.class, RegionGrid.class, ZoneProperty.class }; } public void addMutableProperty(String key, MutableProperty p) { p.addMutablePropertyChangeListener(repaintOnPropertyChange); propsContainer.addMutableProperty(key, p); } public MutableProperty removeMutableProperty(String key) { final MutableProperty existing = propsContainer.removeMutableProperty(key); if (existing != null) { existing.removeMutablePropertyChangeListener(repaintOnPropertyChange); } return existing; } public Point getLocation(String location) throws BadCoords { final SequenceEncoder.Decoder se = new SequenceEncoder.Decoder(locationFormat, '$'); boolean isProperty = true; final StringBuilder regex = new StringBuilder(); int groupCount = 0; while (se.hasMoreTokens()) { String token = se.nextToken(); isProperty = !isProperty; if (token.length() > 0) { if (!isProperty || !se.hasMoreTokens()) { regex.append(Pattern.quote(token)); } else if (token.equals(NAME)) { regex.append(Pattern.quote(getConfigureName())); } else if (token.equals(GRID_LOCATION) && getGrid() != null) { regex.append("(.*)"); ++groupCount; } } } if (regex.length() == 0) { throw new BadCoords(); // nothing to match! } final Pattern pattern = Pattern.compile(regex.toString()); final Matcher matcher = pattern.matcher(location); if (!matcher.matches()) { throw new BadCoords(); } assert(matcher.groupCount() == groupCount); Point p = null; if (groupCount > 0) { String locationName = location.substring(matcher.start(groupCount), matcher.end(groupCount)); p = getGrid().getLocation(locationName); if (p == null || !contains(p)) { throw new BadCoords(); } else { return p; } } else { // no grid to match against // try the geographic mean p = new Point(0, 0); for (int i = 0; i < myPolygon.npoints; ++i) { p.translate(myPolygon.xpoints[i], myPolygon.ypoints[i]); } p.x /= myPolygon.npoints; p.y /= myPolygon.npoints; if (contains(p)) { return p; } else { // concave polygon // default to the first point p.x = myPolygon.xpoints[0]; p.y = myPolygon.ypoints[0]; return p; } } } public String locationName(Point p) { format.setFormat(locationFormat); format.setProperty(NAME, getConfigureName()); String gridLocation = null; if (getGrid() != null) { gridLocation = getGrid().locationName(p); } format.setProperty(GRID_LOCATION, gridLocation); return format.getText(); } public String localizedLocationName(Point p) { format.setFormat(locationFormat); format.setProperty(NAME, getLocalizedConfigureName()); String gridLocation = null; if (getGrid() != null) { gridLocation = getGrid().localizedLocationName(p); } format.setProperty(GRID_LOCATION, gridLocation); return format.getLocalizedText(); } public boolean contains(Point p) { return myPolygon.contains(p); } /** * Snap to the grid in this zone, */ public Point snapTo(Point p) { Point snap = p; if (getGrid() != null) { snap = getGrid().snapTo(p); } return snap; } public Dimension getSize() { return myPolygon.getBounds().getSize(); } public void removeGrid(MapGrid grid) { if (this.grid == grid) { grid = null; } } public Board getBoard() { return parentGrid == null ? null : parentGrid.getBoard(); } public Map getMap() { return parentGrid == null ? null : parentGrid.getBoard().getMap(); } public ZonedGrid getParentGrid() { return parentGrid; } public void setGrid(MapGrid m) { grid = m; } public MapGrid getGrid() { if (useParentGrid) { return parentGrid != null ? parentGrid.getBackgroundGrid() : null; } return grid; } public boolean isUseParentGrid() { return useParentGrid; } public Shape getShape() { return myPolygon; } public Rectangle getBounds() { return myPolygon.getBounds(); } public void setHighlight(ZoneHighlight h) { highlighter = h; } /* * Draw the grid if visible and the highlighter if set. */ public void draw(Graphics g, Rectangle bounds, Rectangle visibleRect, double scale, boolean reversed) { if ((getGrid() != null && getGrid().isVisible()) || highlighter != null) { final Graphics2D g2d = (Graphics2D) g; final Shape oldClip = g2d.getClip(); final Area newClip = new Area(visibleRect); final Shape s = getCachedShape(myPolygon, bounds.x, bounds.y, scale); newClip.intersect(new Area(s)); g2d.setClip(newClip); if (getGrid() != null && getGrid().isVisible()) { getGrid().draw(g, bounds, visibleRect, scale, reversed); } if (highlighter != null) { highlighter.draw(g2d, s, scale); } g2d.setClip(oldClip); } } /* * Calculate and cache the scaled zone shape */ protected Shape getScaledShape(Polygon myPolygon, double scale) { if (scale == lastScale && lastPolygon == myPolygon && lastScaledShape != null) { return lastScaledShape; } final AffineTransform transform = AffineTransform.getScaleInstance(scale, scale); lastScaledShape = transform.createTransformedShape(myPolygon); lastScale = scale; lastPolygon = myPolygon; return lastScaledShape; } /* * Calculate and cache the scaled, translated zone shape */ protected Shape getCachedShape(Polygon poly, int x, int y, double scale) { if (poly.equals(lastPolygon) && x == lastBoundsX && y == lastBoundsY && scale == lastScale) { return lastTransformedShape; } final Shape scaled = getScaledShape(myPolygon, scale); final AffineTransform transform = AffineTransform.getTranslateInstance(x, y); lastTransformedShape = transform.createTransformedShape(scaled); lastPolygon = myPolygon; lastBoundsX = x; lastBoundsY = y; lastScale = scale; return lastTransformedShape; } /* * Get the value of a property. Pass the call up to the enclosing map if the zone doesn't know about it. * * @see VASSAL.build.module.properties.PropertySource#getProperty(java.lang.Object) */ public Object getProperty(Object key) { Object value = null; final MutableProperty p = propsContainer.getMutableProperty(String.valueOf(key)); if (p != null) { value = p.getPropertyValue(); } else { value = getMap().getProperty(key); } return value; } public Object getLocalizedProperty(Object key) { Object value = null; final MutableProperty p = propsContainer.getMutableProperty(String.valueOf(key)); if (p != null) { value = p.getPropertyValue(); } if (value == null) { value = getMap().getLocalizedProperty(key); } return value; } /** * Implement PropertNameSource - expose names of my ZoneProperties */ public List<String> getPropertyNames() { List<String> l = new ArrayList<String>(); for (ZoneProperty zp : getComponentsOf(ZoneProperty.class)) { l.add(zp.getConfigureName()); } return l; } /* * Return a named Global Property * * @see VASSAL.build.module.properties.GlobalPropertiesContainer#getGlobalProperty(java.lang.String) */ public MutableProperty getMutableProperty(String name) { return propsContainer.getMutableProperty(name); } public String getMutablePropertiesContainerId() { return (getMap() == null ? "" : getMap().getMapName())+":"+getConfigureName(); } /* * If using a highlighter, then locate the property and set a propertyListener when the game starts. */ public void setup(boolean gameStarting) { if (gameStarting) { if (useHighlight && highlightPropertyName.length() > 0) { highlightProperty = MutableProperty.Util .findMutableProperty(highlightPropertyName, Arrays.asList(new MutablePropertiesContainer[]{this, getMap(), GameModule.getGameModule()})); if (highlightProperty != null) { if (highlightPropertyChangeListener == null) { highlightPropertyChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { setHighlighter((String) e.getNewValue()); } }; } highlightProperty.addMutablePropertyChangeListener(highlightPropertyChangeListener); setHighlighter(highlightProperty.getPropertyValue()); } } } else { if (highlightProperty != null && highlightPropertyChangeListener != null) { highlightProperty.removeMutablePropertyChangeListener(highlightPropertyChangeListener); highlightProperty = null; } } } /* * The Global Property controlling our highlighter has changed value, so find the new highlighter. Highlighters are * stored in the ZonedGrid containing this zone */ public void setHighlighter(String highlightName) { highlighter = parentGrid.getZoneHighlight(highlightName); repaint(); } public Command getRestoreCommand() { return null; } public static class Editor extends Configurer { private JButton button; private PolygonEditor editor; private Board board; private JDialog frame; protected AdjustableSpeedScrollPane scroll; protected Polygon savePoly; final protected JLabel warning = new JLabel("Zone has not been defined"); public Editor(final Zone zone) { super(PATH, null); button = new JButton("Define Shape"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { init(zone); } }); editor = new PolygonEditor(new Polygon(zone.myPolygon.xpoints, zone.myPolygon.ypoints, zone.myPolygon.npoints)) { private static final long serialVersionUID = 1L; protected void paintBackground(Graphics g) { if (board != null) { Rectangle b = getVisibleRect(); g.clearRect(b.x, b.y, b.width, b.height); board.draw(g, 0, 0, 1.0, editor); } else { super.paintBackground(g); } warning.setVisible(editor != null && (editor.getPolygon() == null || editor.getPolygon().npoints == 0)); } }; frame = new JDialog((Frame) null, zone.getConfigureName(), true); frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS)); final JPanel labels = new JPanel(); labels.setLayout(new GridLayout(3, 2)); labels.add(new JLabel("Drag to create initial shape")); labels.add(new JLabel("Right-click to add point")); labels.add(new JLabel("Left-click to move points")); labels.add(new JLabel("DEL to remove point")); warning.setForeground(Color.red); warning.setVisible(false); labels.add(warning); labels.setAlignmentX(0.0f); frame.add(labels); final JButton direct = new JButton("Set Coordinates directly"); direct.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String newShape = JOptionPane.showInputDialog(frame, "Enter x,y coordinates of polygon vertices,\nseparated by spaces", PolygonEditor .polygonToString(editor.getPolygon()).replace(';', ' ')); if (newShape != null) { final StringBuilder buffer = new StringBuilder(); final StringTokenizer st = new StringTokenizer(newShape); while (st.hasMoreTokens()) { buffer.append(st.nextToken()); if (st.hasMoreTokens()) { buffer.append(';'); } } newShape = buffer.toString(); PolygonEditor.reset(editor.getPolygon(), newShape); editor.repaint(); } } }); direct.setAlignmentX(0.0f); frame.add(direct); scroll = new AdjustableSpeedScrollPane(editor); editor.setScroll(scroll); frame.add(scroll); final JPanel buttonPanel = new JPanel(); final JButton closeButton = new JButton("Ok"); closeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setValue((Object) getValueString()); frame.setVisible(false); } }); final JButton canButton = new JButton("Cancel"); canButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editor.setPolygon(savePoly); setValue((Object) getValueString()); frame.setVisible(false); } }); buttonPanel.add(closeButton); buttonPanel.add(canButton); frame.add(buttonPanel); } private void init(Zone zone) { board = zone.getBoard(); editor.setPreferredSize(board != null ? board.getSize() : DEFAULT_SIZE); editor.reset(); savePoly = editor.clonePolygon(); final Rectangle polyBounds = editor.getPolygon().getBounds(); final Point polyCenter = new Point(polyBounds.x + polyBounds.width/2, polyBounds.y + polyBounds.height/2); if (!editor.getVisibleRect().contains(polyCenter)) { editor.center(polyCenter); } frame.pack(); final Dimension d = Toolkit.getDefaultToolkit().getScreenSize(); frame.setSize(Math.min(frame.getWidth(), d.width * 2 / 3), Math.min(frame.getHeight(), d.height * 2 / 3)); frame.setVisible(true); } public Component getControls() { return button; } public String getValueString() { return PolygonEditor.polygonToString(editor.getPolygon()); } public void setValue(String s) { PolygonEditor.reset(editor.getPolygon(), s); } } }