// // @(#)GUIMove.java 12/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.gui.order; import dip.gui.map.MapMetadata; import dip.gui.map.DefaultMapRenderer2; import dip.gui.order.GUIOrder.MapInfo; import dip.order.Orderable; import dip.order.Move; import dip.order.ValidationOptions; import dip.order.OrderFormat; import dip.misc.Utils; import dip.world.Position; import dip.world.Location; import dip.world.Province; import dip.world.Coast; import dip.world.Path; import dip.world.Unit; import dip.world.Power; import dip.world.RuleOptions; import dip.process.Adjustment.AdjustmentInfoMap; import java.util.List; import java.util.ArrayList; import java.util.LinkedList; import java.util.Iterator; import java.awt.geom.Point2D; import org.apache.batik.util.SVGConstants; import org.apache.batik.util.CSSConstants; import org.apache.batik.dom.svg.SVGDOMImplementation; import org.apache.batik.dom.util.XLinkSupport; import org.w3c.dom.svg.*; import org.w3c.dom.*; /** * * GUIOrder subclass of Move order. * <p> * This differs from GUIMove in that Explicit convoy routes are <b>always</b> * created. Implicit convoy routes <b>cannot</b> be created via GUI entry. * <p> * This should be used instead of GUIMove for games with RuleOptions that * enforce explicit convoy routes (such as Judge-based games). * */ public class GUIMoveExplicit extends Move implements GUIOrder { // i18n keys private final static String CLICK_TO_SET_DEST = "GUIMove.set.dest"; private final static String CANNOT_MOVE_TO_ORIGIN = "GUIMove.cannot_to_origin"; private final static String NO_CONVOY_ROUTE = "GUIMove.no_convoy_route"; private final static String CANNOT_MOVE_HERE = "GUIMove.cannot_move_here"; // i18n keys for convoys private final static String CANNOT_BACKTRACK = "GUIMoveExplicit.convoy.backtrack"; private final static String FINAL_DESTINATION = "GUIMoveExplicit.convoy.location.destination"; private final static String OK_CONVOY_LOCATION = "GUIMoveExplicit.convoy.location.ok"; private final static String BAD_CONVOY_LOCATION = "GUIMoveExplicit.convoy.location.bad"; private final static String NONADJACENT_CONVOY_LOCATION = "GUIMoveExplicit.convoy.location.nonadjacent"; private final static String ADDED_CONVOY_LOCATION = "GUIMoveExplicit.convoy.location.added"; // instance variables private transient static final int REQ_LOC = 2; private transient boolean isConvoyableArmy = false; private transient boolean isComplete = false; private transient LinkedList tmpConvoyPath = null; private transient int currentLocNum = 0; private transient int numSupports = -9999; private transient Point2D.Float failPt = null; private transient SVGGElement group = null; /** Creates a GUIMoveExplicit */ protected GUIMoveExplicit() { super(); }// GUIMoveExplicit() /** Creates a GUIMoveExplicit */ protected GUIMoveExplicit(Power power, Location source, Unit.Type srcUnitType, Location dest, boolean isConvoying) { super(power, source, srcUnitType, dest, isConvoying); }// GUIMoveExplicit() /** Creates a GUIMoveExplicit */ protected GUIMoveExplicit(Power power, Location src, Unit.Type srcUnitType, Location dest, Province[] convoyRoute) { super(power, src, srcUnitType, dest, convoyRoute); }// GUIMoveExplicit() /** Creates a GUIMoveExplicit */ protected GUIMoveExplicit(Power power, Location src, Unit.Type srcUnitType, Location dest, List routes) { super(power, src, srcUnitType, dest, routes); }// GUIMoveExplicit() /** This only accepts Move orders. All others will throw an IllegalArgumentException. */ public void deriveFrom(Orderable order) { if( !(order instanceof Move) ) { throw new IllegalArgumentException(); } Move move = (Move) order; power = move.getPower(); src = move.getSource(); srcUnitType = move.getSourceUnitType(); dest = move.getDest(); _isViaConvoy = move.isViaConvoy(); _isAdjWithPossibleConvoy = move.isAdjWithPossibleConvoy(); _isConvoyIntent = isConvoyIntent(); // set completed isComplete = true; }// GUIMove() public boolean testLocation(StateInfo stateInfo, Location location, StringBuffer sb) { final LocationTestResult result = testLocationLTR(stateInfo, location, sb); return result.isValid; }// testLocation() /** * More complex version of testLocation(), that returns extended * results that can be used by setLocation(). * */ private LocationTestResult testLocationLTR(StateInfo stateInfo, Location location, StringBuffer sb) { sb.setLength(0); final LocationTestResult result = new LocationTestResult(); if(isComplete()) { sb.append( Utils.getLocalString(GUIOrder.COMPLETE, getFullName()) ); return result; } final Position position = stateInfo.getPosition(); final Province province = location.getProvince(); if(currentLocNum == 0) { // set Move source // we require a unit present. We will check unit ownership too, if appropriate Unit unit = position.getUnit(province); if(unit != null) { if( !stateInfo.canIssueOrder(unit.getPower()) ) { sb.append( Utils.getLocalString(GUIOrder.NOT_OWNER, unit.getPower()) ); result.isValid = false; return result; } if( !GUIOrderUtils.checkBorder(this, new Location(province, unit.getCoast()), unit.getType(), stateInfo.getPhase(), sb) ) { result.isValid = false; return result; } sb.append( Utils.getLocalString(GUIOrder.CLICK_TO_ISSUE, getFullName()) ); result.isValid = true; return result; } // no unit in province sb.append( Utils.getLocalString(GUIOrder.NO_UNIT, getFullName()) ); result.isValid = false; return result; } else if(currentLocNum >= 1) { // If the convoy route is explicit only, we MUST create // a defined path, if we are actually convoying/convoyable. if(currentLocNum == 1) { StringBuffer sbTmp = new StringBuffer(); if(testNonConvoyDest(stateInfo, location, sbTmp)) { // not a convoyed move. successful. sb.append(sbTmp); result.isValid = true; result.isFinalDest = true; return result; } else { if(!isConvoyableArmy) { // invalid destination, and not a convoyable unit sb.append( Utils.getLocalString(CANNOT_MOVE_HERE) ); result.isValid = false; return result; } } } // we now have to check for convoy-path acceptability. assert (isConvoyableArmy); assert (tmpConvoyPath != null); result.isConvoy = true; // the last location must be adjacent to this location. We use // 'correct' adjacency; e.g., Coast.SEA or Coast.(direction) for // first check // final Province lastProv = (Province) tmpConvoyPath.getLast(); Coast coast = Coast.SEA; if(lastProv.equals(getSource()) && lastProv.isMultiCoastal()) { coast = getSource().getCoast(); } // current province cannot already be in our tmpConvoyPath. if( tmpConvoyPath.contains(province) ) { if(location.isProvinceEqual(getSource())) { // kinder, gentler message for src-location sb.append( Utils.getLocalString(CANNOT_MOVE_TO_ORIGIN) ); } else { // cannot backtrack message sb.append( Utils.getLocalString(CANNOT_BACKTRACK) ); } result.isValid = false; return result; } if(lastProv.isAdjacent(coast, province)) { // this location must be a sea, convoyable coast, and contain a fleet // unless it is the final destination, which should be coastal land if(province.isCoastal()) { result.isValid = true; result.isFinalDest = true; sb.append( Utils.getLocalString(FINAL_DESTINATION) ); // final destination message return result; } else { if(province.isConvoyable() && position.hasUnit(province, Unit.Type.FLEET)) { sb.append( Utils.getLocalString(OK_CONVOY_LOCATION) ); // OK location for convoy path result.isValid = true; return result; } else { sb.append( Utils.getLocalString(BAD_CONVOY_LOCATION) ); // invalid location for convoy path result.isValid = false; return result; } } } else { sb.append( Utils.getLocalString(NONADJACENT_CONVOY_LOCATION) ); // non-adjacent location result.isValid = false; return result; } } else { throw new IllegalStateException(); } // NO return here: thus we must appropriately exit within an if/else block above. }// testLocationLTR() /** * Tests a destination location for acceptability. Does not * check for convoy-acceptability; thus will return false * in that case. */ private boolean testNonConvoyDest(StateInfo stateInfo, Location location, StringBuffer sb) { assert (currentLocNum == 1); final Province province = location.getProvince(); // set move destination // - If we are not validating, any destination is acceptable (even source) // - If we are validating, we check that the move is adjacent // if(stateInfo.getValidationOptions().getOption(ValidationOptions.KEY_GLOBAL_PARSING).equals(ValidationOptions.VALUE_GLOBAL_PARSING_LOOSE)) { // lenient parsing enabled; we'll take anything! sb.append( Utils.getLocalString(CLICK_TO_SET_DEST) ); return true; } // strict parsing is enabled. We are more selective. if(province.equals(src.getProvince())) { sb.append( Utils.getLocalString(CANNOT_MOVE_TO_ORIGIN) ); return false; } else if(src.isAdjacent(province)) { sb.append( Utils.getLocalString(CLICK_TO_SET_DEST) ); return true; } else if( !GUIOrderUtils.checkBorder(this, location, srcUnitType, stateInfo.getPhase(), sb) ) { // text already set by checkBorder() method return false; } sb.append( Utils.getLocalString(CANNOT_MOVE_HERE) ); return false; }// testNonConvoyDest() public boolean clearLocations() { if(isComplete()) { return false; } currentLocNum = 0; power = null; src = null; srcUnitType = null; dest = null; _isViaConvoy = false; _isAdjWithPossibleConvoy = false; _isConvoyIntent = false; isConvoyableArmy = false; isComplete = false; return true; }// clearLocations() private class LocationTestResult { public boolean isValid = false; // true if this location is valid public boolean isConvoy = false; // true if this is part of a convoy path--or could be public boolean isFinalDest = false; // true if Location is a possible destination (convoyed or not) }// inner class LocationTestResult public boolean setLocation(StateInfo stateInfo, Location location, StringBuffer sb) { // WE need to manage isComplete here, as well as // setting the tmpConvoyPath // use testLocationLTR // and currentLocNum.... // final LocationTestResult ltr = testLocationLTR(stateInfo, location, sb); if(ltr.isValid) { if(currentLocNum == 0) { Unit unit = stateInfo.getPosition().getUnit(location.getProvince()); src = new Location(location.getProvince(), unit.getCoast()); power = unit.getPower(); srcUnitType = unit.getType(); currentLocNum++; // we're good to go. If this unit is a coastal army, it is // considered "possibly convoyable". We may use this later. isConvoyableArmy = (location.getProvince().isCoastal() && Unit.Type.ARMY.equals(srcUnitType)); if(isConvoyableArmy) { assert (tmpConvoyPath == null); tmpConvoyPath = new LinkedList(); tmpConvoyPath.add( getSource().getProvince() ); } return true; } else if(currentLocNum > 0) { if(ltr.isFinalDest && currentLocNum == 1) { // nonconvoyed; we are done. // dest = new Location(location.getProvince(), location.getCoast()); sb.setLength(0); sb.append( Utils.getLocalString(GUIOrder.COMPLETE, getFullName()) ); currentLocNum++; isComplete = true; return true; } // we are convoyed.... // // add to tmp path assert (isConvoyableArmy); tmpConvoyPath.add( location.getProvince() ); currentLocNum++; updateConvoyPath(); if(ltr.isFinalDest) { dest = new Location(location.getProvince(), location.getCoast()); sb.setLength(0); sb.append( Utils.getLocalString(GUIOrder.COMPLETE, getFullName()) ); isComplete = true; } else { sb.setLength(0); sb.append( Utils.getLocalString(ADDED_CONVOY_LOCATION, location.getProvince()) ); } return true; } } return false; }// setLocation() /** Updates convoyPath() from tmpConvoyPath() */ private void updateConvoyPath() { if(tmpConvoyPath == null) { convoyRoutes = null; } else { final Province[] provinceRoute = (Province[]) tmpConvoyPath.toArray( new Province[tmpConvoyPath.size()]); convoyRoutes = new ArrayList(1); convoyRoutes.add(provinceRoute); } }// updateConvoyPath() public boolean isComplete() { return isComplete; }// isComplete() public int getNumRequiredLocations() { return REQ_LOC; } public int getCurrentLocationNum() { return currentLocNum; } /** Always throws an IllegalArgumentException */ public void setParam(Parameter param, Object value) { throw new IllegalArgumentException(); } /** Always throws an IllegalArgumentException */ public Object getParam(Parameter param) { throw new IllegalArgumentException(); } public void removeFromDOM(MapInfo mapInfo) { if(group != null) { SVGGElement powerGroup = mapInfo.getPowerSVGGElement(power, LAYER_TYPICAL); GUIOrderUtils.removeChild(powerGroup, group); group = null; numSupports = -9999; } }// removeFromDOM() /** Draws a line with an arrow. */ public void updateDOM(MapInfo mapInfo) { // if we are not displayable, we exit, after remove the order (if // it was created) if( !GUIOrderUtils.isDisplayable(power, mapInfo) ) { removeFromDOM(mapInfo); return; } // determine if any change has occured. If no change has occured, // we will not change the DOM. // // check supports int support = GUIOrderUtils.getMatchingSupportCount(mapInfo, src.getProvince(), dest.getProvince()); // modify move support with BaseMoveModifier (if any) support += getDest().getProvince().getBaseMoveModifier(getSource()); if(numSupports == support && group != null) { return; // no change } // we are only at this point if a change has occured. // numSupports = support; // if we've not yet been created, we will create; if we've // already been created, we must remove the existing elements // in our group if(group == null) { // create group group = (SVGGElement) mapInfo.getDocument().createElementNS( SVGDOMImplementation.SVG_NAMESPACE_URI, SVGConstants.SVG_G_TAG); group.setId("order_" + this.src.toString()); mapInfo.getPowerSVGGElement(power, LAYER_TYPICAL).appendChild(group); } else { // remove group children GUIOrderUtils.deleteChildren(group); } // now, render the order // SVGElement element = null; // create hilight line String cssStyle = mapInfo.getMapMetadata().getOrderParamString(MapMetadata.EL_MOVE, MapMetadata.ATT_HILIGHT_CLASS); if(!cssStyle.equalsIgnoreCase("none")) { float offset = mapInfo.getMapMetadata().getOrderParamFloat(MapMetadata.EL_MOVE, MapMetadata.ATT_HILIGHT_OFFSET); float width = GUIOrderUtils.getLineWidth(mapInfo, MapMetadata.EL_MOVE, MapMetadata.ATT_SHADOW_WIDTHS, numSupports); element = drawOrder(mapInfo, offset, false); element.setAttributeNS(null, SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, GUIOrderUtils.floatToString(width)); GUIOrderUtils.makeHilight(element, mapInfo.getMapMetadata(), MapMetadata.EL_MOVE); group.appendChild(element); } // create real line float width = GUIOrderUtils.getLineWidth(mapInfo, MapMetadata.EL_MOVE, MapMetadata.ATT_WIDTHS, numSupports); element = drawOrder(mapInfo, 0, true); element.setAttributeNS(null, SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, GUIOrderUtils.floatToString(width)); GUIOrderUtils.makeStyled(element, mapInfo.getMapMetadata(), MapMetadata.EL_MOVE, power); group.appendChild(element); // draw 'failed' marker, if appropriate. if(!mapInfo.getTurnState().isOrderSuccessful(this)) { SVGUseElement useElement = GUIOrderUtils.createFailedOrderSymbol(mapInfo, failPt.x, failPt.y); group.appendChild(useElement); } }// updateDOM() /** draws convoyed or non-convoyed order, depending upon flag */ private SVGElement drawOrder(MapInfo mapInfo, float offset, boolean addMarker) { /* if(isByConvoy()) { return drawConvoyedOrder(mapInfo, offset, addMarker); } else { return drawNCOrder(mapInfo, offset, addMarker); } */ return drawNCOrder(mapInfo, offset, addMarker); }// drawOrder() /** if addMarker == true, ALWAYS add marker; otherwise, only added if offset is non-zero */ private SVGElement drawNCOrder(MapInfo mapInfo, float offset, boolean addMarker) { MapMetadata mmd = mapInfo.getMapMetadata(); Point2D.Float ptFrom = mmd.getUnitPt(src.getProvince(), src.getCoast()); Point2D.Float ptTo = mmd.getUnitPt(dest.getProvince(), dest.getCoast()); // respect radius, if there is a unit present in destination. Point2D.Float newPtTo = ptTo; Position position = mapInfo.getTurnState().getPosition(); if(position.hasUnit(dest.getProvince())) { Unit.Type destUnitType = position.getUnit(dest.getProvince()).getType(); float r = mmd.getOrderRadius(MapMetadata.EL_MOVE, mapInfo.getSymbolName(destUnitType)); newPtTo = GUIOrderUtils.getLineCircleIntersection(ptFrom.x+offset, ptFrom.y+offset, ptTo.x+offset, ptTo.y+offset, ptTo.x+offset, ptTo.y+offset, r); } // calculate (but don't yet use) failPt failPt = GUIOrderUtils.getLineMidpoint(ptFrom.x, ptFrom.y, newPtTo.x, newPtTo.y); // create SVG element(s) SVGLineElement line = (SVGLineElement) mapInfo.getDocument().createElementNS( SVGDOMImplementation.SVG_NAMESPACE_URI, SVGConstants.SVG_LINE_TAG); line.setAttributeNS(null, SVGConstants.SVG_X1_ATTRIBUTE, GUIOrderUtils.floatToString(ptFrom.x+offset)); line.setAttributeNS(null, SVGConstants.SVG_Y1_ATTRIBUTE, GUIOrderUtils.floatToString(ptFrom.y+offset)); line.setAttributeNS(null, SVGConstants.SVG_X2_ATTRIBUTE, GUIOrderUtils.floatToString(newPtTo.x+offset)); line.setAttributeNS(null, SVGConstants.SVG_Y2_ATTRIBUTE, GUIOrderUtils.floatToString(newPtTo.y+offset)); // style line.setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE, mmd.getOrderParamString(MapMetadata.EL_MOVE, MapMetadata.ATT_STROKESTYLE)); // marker if(addMarker || offset != 0.0f) { GUIOrderUtils.addMarker(line, mmd, MapMetadata.EL_MOVE); } // end return line; }// drawNCOrder() /* Draw a convoyed order. Note that this doesn't really work yet (problems with getConvoyRoute()) private SVGElement drawConvoyedOrder(MapInfo mapInfo, float offset, boolean addMarker) { // if we are convoyed, and no theoretical path, just use a regular // move draw Position position = mapInfo.getTurnState().getPosition(); Path path = new Path(position); List route = path.getConvoyRoute(src, dest); if(!route.isEmpty()) { drawNCOrder(mapInfo, offset, addMarker); } // a valid convoy route exists. // get that route so we can draw the order. // the unit position of each route is required. // create the path data string // MapMetadata mmd = mapInfo.getMapMetadata(); StringBuffer sb = new StringBuffer(256); Iterator iter = route.iterator(); int count = 0; final int last = route.size() - 1; Point2D.Float lastPoint = null; final float convoyRadius = mmd.getOrderParamFloat(MapMetadata.EL_CONVOY, MapMetadata.ATT_RADIUS); while(iter.hasNext()) { Location loc = (Location) iter.next(); // append path type if(count == 0) { sb.append(" M "); // MoveTo } else { sb.append(" L "); // LineTo } // append coordinate Point2D.Float currentPoint; if(count == 0) { // use source point directly currentPoint = mmd.getUnitPt(src.getProvince(), src.getCoast()); } else if(count == last) { // respect radius for final path segment, if unit present Point2D.Float ptTo = mmd.getUnitPt(loc.getProvince(), loc.getCoast()); if(position.hasUnit(dest.getProvince())) { float r = mmd.getOrderParamFloat(MapMetadata.EL_MOVE, MapMetadata.ATT_RADIUS); currentPoint = GUIOrderUtils.getLineCircleIntersection( lastPoint.x+offset, lastPoint.y+offset, ptTo.x+offset, ptTo.y+offset, ptTo.x+offset, ptTo.y+offset, r); } else { currentPoint = ptTo; } } else { // use 12-o'clock triangle point of convoy order for convoying unit currentPoint = mmd.getUnitPt(loc.getProvince(), loc.getCoast()); currentPoint.y -= convoyRadius; } // append currentPoint GUIOrderUtils.appendFloat(currentPoint.x + offset); sb.append(','); GUIOrderUtils.appendFloat(currentPoint.y + offset); // set last point lastPoint = currentPoint; count++; } // create SVG element(s) SVGPathElement pathElement = (SVGPathElement) mapInfo.getDocument().createElementNS( SVGDOMImplementation.SVG_NAMESPACE_URI, SVGConstants.SVG_PATH_TAG); pathElement.setAttributeNS(null, SVGConstants.SVG_D_ATTRIBUTE, sb.toString()); // style pathElement.setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE, mmd.getOrderParamString(MapMetadata.EL_MOVE, MapMetadata.ATT_STROKESTYLE)); // marker if(addMarker || offset != 0.0f) { GUIOrderUtils.addMarker(pathElement, mmd, MapMetadata.EL_MOVE); } // end return pathElement; }// drawConvoyedOrder() */ /** We are dependent on the presence of Support orders for certain drawing parameters. */ public boolean isDependent() { return true; } /** * Typesafe Enumerated Parameter class for setting * optional Move parameters. * */ protected static class MoveParameter extends Parameter { /** Creates a MoveParameter */ public MoveParameter(String name) { super(name); }// MoveParameter() }// nested class MoveParameter public SVGElement orderSVG(MapInfo mapInfo){ updateDOM(mapInfo); return group; } }// class GUIMoveExplicit