// // @(#)Coast.java 4/2002 // // Copyright 2002 Zachary DelProposto. All rights reserved. // Use is subject to license terms. // // // This program 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 2 of the License, or // (at your option) any later version. // // This program 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 this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. // Or from http://www.gnu.org/ // package dip.world; import dip.order.OrderException; import java.io.InvalidObjectException; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Coasts are essential to determining connectivity between Provinces. * <p> * Coast constants should be used. * * * * * * * * */ public final class Coast implements java.io.Serializable { // transient data transient static private Pattern[] patterns = null; // internal constants // TODO: these need to be properly internationalized. // To do that, we need internationlization support for coast // normalization and parsing also // private static final String NORTH_FULL = "North Coast"; private static final String NORTH_ABBREV = "nc"; private static final String SOUTH_FULL = "South Coast"; private static final String SOUTH_ABBREV = "sc"; private static final String WEST_FULL = "West Coast"; private static final String WEST_ABBREV = "wc"; private static final String EAST_FULL = "East Coast"; private static final String EAST_ABBREV = "ec"; private static final String NONE_FULL = "None"; private static final String NONE_ABBREV = "mv"; private static final String SINGLE_FULL = "Single"; private static final String SINGLE_ABBREV = "xc"; private static final String WING_FULL = "Wing"; private static final String WING_ABBREV = "wx"; private static final String UNDEFINED_FULL = "Undefined"; private static final String UNDEFINED_ABBREV = "?"; // perhaps make it "?c" ?? /* To be used in the future .... parsing to accomodate private static final String NW_FULL = "Northwest Coast"; private static final String NE_FULL = "Northeast Coast"; private static final String SW_FULL = "Southwest Coast"; private static final String SE_FULL = "Southeast Coast"; private static final String NW_ABBREV = "nw"; private static final String NE_ABBREV = "ne"; private static final String SW_ABBREV = "sw"; private static final String SE_ABBREV = "se"; */ // constants /** Constant indicated an Undefined coast */ public static final Coast UNDEFINED = new Coast(UNDEFINED_FULL, UNDEFINED_ABBREV, 0); /** Constant indicating Wing coast (for Wing movement) */ public static final Coast WING = new Coast(WING_FULL, WING_ABBREV, 1); /** Constant indicating no coast (Army movement) */ public static final Coast NONE = new Coast(NONE_FULL, NONE_ABBREV, 2); /** Constant indicating a single Coast (for fleets in coastal land areas, or sea-only provinces) */ public static final Coast SINGLE = new Coast(SINGLE_FULL, SINGLE_ABBREV, 3); /** Constant indicating North Coast */ public static final Coast NORTH = new Coast(NORTH_FULL, NORTH_ABBREV, 4); /** Constant indicating South Coast */ public static final Coast SOUTH = new Coast(SOUTH_FULL, SOUTH_ABBREV, 5); /** Constant indicating West Coast */ public static final Coast WEST = new Coast(WEST_FULL, WEST_ABBREV, 6); /** Constant indicating East Coast */ public static final Coast EAST = new Coast(EAST_FULL, EAST_ABBREV, 7); /** Alias for Coast.WING */ public static final Coast TOUCHING = WING; /** Alias for Coast.NONE */ public static final Coast LAND = NONE; /** Alias for Coast.SINGLE */ public static final Coast SEA = SINGLE; // index-to-coast array private static final Coast[] IDX_ARRAY = { UNDEFINED, WING, NONE, SINGLE, NORTH, SOUTH, WEST, EAST }; /** * Array of Coasts that are not typically displayed * <b>Warning: this should not be mutated.</b> */ public static final Coast[] NOT_DISPLAYED = { NONE, SINGLE, UNDEFINED, WING }; /** * Array of the 6 main coast types (NONE, SINGLE, NORTH, SOUTH, WEST, EAST) * <b>Warning: this should not be mutated.</b> */ public static final Coast[] ALL_COASTS = { NONE, SINGLE, NORTH, SOUTH, WEST, EAST }; /** * Array of sea coasts (SINGLE, NORTH, SOUTH, WEST, EAST) * <b>Warning: this should not be mutated.</b> */ public static final Coast[] ANY_SEA = { SINGLE, NORTH, SOUTH, WEST, EAST }; /** * Array of directional coasts (NORTH, SOUTH, WEST, EAST) * <b>Warning: this should not be mutated.</b> */ public static final Coast[] ANY_DIRECTIONAL = { NORTH, SOUTH, WEST, EAST }; // class variables private final String name; private final String abbreviation; private final int index; // TODO: ?? hashCode should be == index (since index is unique) private transient int hashCode = 0; // cache the hashCode /** * Constructs a Coast */ private Coast(String name, String abbreviation, int index) { if(index < 0) { throw new IllegalArgumentException(); } this.name = name; this.abbreviation = abbreviation; this.index = index; }// Coast() /** * Returns the full name (long name) of a coast; e.g., "North Coast" */ public String getName() { return name; }// getName() /** * Returns the abbreviated coast name (e.g., "nc") */ public String getAbbreviation() { return abbreviation; }// getAbbreviation() /** Gets the index of a Coast. Indices are >= 0. */ public int getIndex() { return index; }// getIndex() /** Gets the Coast corresponding to an index; null if index is out of range. */ public static Coast getCoast(int idx) { if(idx >= 0 && idx < IDX_ARRAY.length) { return IDX_ARRAY[idx]; } return null; }// getCoast() /** * Returns the full name of the coast */ public String toString() { return name; }// toString() /** * Returns if this Coast is typically displayed */ public static boolean isDisplayable(Coast coast) { for(int i=0; i<NOT_DISPLAYED.length; i++) { if(coast == NOT_DISPLAYED[i]) { return false; } } return true; }// isDisplayable() /** * * Parses the coast from a text token. * <p> * Given a text token such as "spa-sc" or "spa/nc", * Returns the Coast constant. Coasts must begin with * a '/', '-', or '\'; parenthetical notation e.g., "(nc)" * is not supported. * <p> * This method never returns null; for nonexistent or * unparsable coasts, Coast.UNDEFINED is returned. * <p> */ public static Coast parse(String text) { String input = text.toLowerCase().trim(); // check if it is just a coast (2-letter) or // part of a province name. If we don't check // for -/\, then we could be processing part // of a province name if(input.length() >= 3) { char c = input.charAt(input.length() - 3); if(c != '-' && c != '/' && c != '\\') { return UNDEFINED; } } if(input.endsWith("nc")) { return NORTH; } else if(input.endsWith("sc")) { return SOUTH; } else if(input.endsWith("wc")) { return WEST; } else if(input.endsWith("ec")) { return EAST; } else if(input.endsWith("xc")) { return SINGLE; } else if(input.endsWith("mv")) { return LAND; } return Coast.UNDEFINED; }// parse() /** * Returns the Province name upto the first Coast seperator * character ('-', '/', or '\'); Parentheses are not supported. */ public static String getProvinceName(String input) { if(input.length() > 3) { final int idx = (input.length() - 3); final char c = input.charAt(idx); if(c == '-' || c == '/' || c == '\\') { return input.substring(0, idx); } } return input; }// getProvinceName() /** * Normalizes coasts to standard format "/xx". * <p> * The following applies: * <pre> a) input must be lower-case b) normalizes: axy where a = "/" "\" or "-" x where x = any alphanumeric [but is later checked]; a "." may follow y where y = "c" or (if x="m", "v"); a "." my follow c) parenthetical coasts coalesces preceding spaces (before parenthesis), so "stp(sc)", "stp( sc)", "stp(.s.c.)", "stp (sc)", and "stp (sc)" all would become "stp/sc" coast depends upon FIRST character stp(qoieru) ==> invalid! * </pre> * <p> * An OrderException is thrown if the coast is not recognized. The OrderException will contain * the invalid coast text only. * <p> * Bug note: the following "xxx-n.c." will be converted to "xxx-nc ." Note the extra period. * */ public static String normalize(String input) throws OrderException { // create patterns, if we have none. // these are threadsafe if(patterns == null) { patterns = new Pattern[2]; // match /xx, -xx, \xx coasts; also takes care of periods. // also matches /x; will not match /xxx (or -xxx) patterns[0] = Pattern.compile("\\s*[\\-\\\\/](\\p{Alnum}\\.?)(\\p{Alnum}\\.?)\\b"); // // match parenthetical coasts. //patterns[1] = Pattern.compile("\\s*\\([^\\p{Alnum}]*(\\p{Alnum})[^\\p{Alnum}]*(\\p{Alnum})[^)]*\\)"); patterns[1] = Pattern.compile("\\s*\\(([.[^)]]*)(\\))\\s*"); } // start matching. String matchInput = input; for(int i=0; i<patterns.length; i++) { Matcher m = patterns[i].matcher(matchInput); StringBuffer sb = new StringBuffer(matchInput.length()); boolean result = m.find(); while(result) { if(m.groupCount() == 2) { final char c1 = m.group(1).charAt(0); final char c2 = m.group(2).charAt(0); //System.out.println("1: "+m.group(1)+"; 2: "+m.group(2)); if(c2 == ')') { String group1 = superTrim(m.group(1)); // test 'full name' and abbreviated coasts inside parentheses if(group1.startsWith("north") || "nc".equals(group1)) { m.appendReplacement(sb, "/nc "); } else if(group1.startsWith("south") || "sc".equals(group1)) { m.appendReplacement(sb, "/sc "); } else if(group1.startsWith("west") || "wc".equals(group1)) { m.appendReplacement(sb, "/wc "); } else if(group1.startsWith("east") || "ec".equals(group1)) { m.appendReplacement(sb, "/ec "); } else if("mv".equals(group1)) { m.appendReplacement(sb, "/mv "); } else if("xc".equals(group1)) { m.appendReplacement(sb, "/xc "); } } else if( (c2 == 'c' && (c1 == 'n' || c1 == 's' || c1 == 'w' || c1 == 'e' || c1 == 'x')) || (c1 == 'm' && c2 == 'v') ) { StringBuffer rep = new StringBuffer(4); rep.append('/'); rep.append(c1); rep.append(c2); rep.append(' '); // space added afterwards--essential! m.appendReplacement(sb, rep.toString()); } else { throw new OrderException(m.group(0)); } } else { throw new OrderException(m.group(0)); } result = m.find(); } m.appendTail(sb); matchInput = sb.toString(); } return matchInput.trim(); }// normalize() /** * Trims the following characters before, within, and after a given string. * <br> * space, tab, '.' * */ private static String superTrim(String in) { return in.replaceAll("\\.*\\s*\\t*", ""); }// superTrim() /** * Returns <code>true</code> if coast is one of * Coast.NORTH, Coast.SOUTH, Coast.WEST, or Coast.EAST */ public boolean isDirectional() { for(int i=0; i<ANY_DIRECTIONAL.length; i++) { if(this == ANY_DIRECTIONAL[i]) { return true; } } return false; }// isDirectionalCoast() /** Implementation of Object.hashCode() */ public int hashCode() { if(hashCode == 0) { hashCode = name.hashCode(); } return hashCode; }// hashCode() @Override public boolean equals(Object obj) { return this.hashCode() == obj.hashCode(); } /* * * Doesn't work for web version: equals(): We use Object.equals(), which just does a test of referential equality. */ /** Assigns serialized objects to a single constant reference */ protected Object readResolve() throws java.io.ObjectStreamException { Coast coast = null; if(name.equals(NORTH_FULL)) { coast = NORTH; } else if(name.equals(SOUTH_FULL)) { coast = SOUTH; } else if(name.equals(WEST_FULL)) { coast = WEST; } else if(name.equals(EAST_FULL)) { coast = EAST; } else if(name.equals(NONE_FULL)) { coast = NONE; } else if(name.equals(SINGLE_FULL)) { coast = SINGLE; } else if(name.equals(WING_FULL)) { coast = WING; } else if(name.equals(UNDEFINED_FULL)) { coast = UNDEFINED; } else { throw new InvalidObjectException("Unknown coast type: "+name); } return coast; }// readResolve() }// class Coast()