//
// @(#)GUIConvoy.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.order.Orderable;
import dip.order.Convoy;
import dip.order.ValidationOptions;
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.gui.order.GUIOrder.MapInfo;
import dip.process.Adjustment.AdjustmentInfoMap;
import dip.process.RetreatChecker;
import dip.gui.map.MapMetadata;
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.w3c.dom.svg.*;
import org.w3c.dom.*;
/**
*
* GUIOrder subclass of Convoy order.
*
*/
public class GUIConvoy extends Convoy implements GUIOrder
{
/**
*
*/
private static final long serialVersionUID = -6697266601127024865L;
// i18n keys
private final static String ONLY_SEA_OR_CC_FLEETS_CAN_CONVOY = "GUIConvoy.only_fleets_can_convoy";
private final static String CLICK_TO_CONVOY = "GUIConvoy.click_to_convoy";
private final static String NO_UNIT = "GUIConvoy.no_unit";
private final static String CLICK_TO_CONVOY_ARMY = "GUIConvoy.click_to_convoy_army";
private final static String CANNOT_CONVOY_LANDLOCKED = "GUIConvoy.no_convoy_landlocked";
private final static String MUST_CONVOY_FROM_COAST = "GUIConvoy.must_convoy_from_coast";
private final static String CLICK_TO_CONVOY_FROM = "GUIConvoy.click_to_convoy_from";
private final static String NO_POSSIBLE_CONVOY_PATH = "GUIConvoy.no_path";
private final static String MUST_CONVOY_TO_COAST = "GUIConvoy.must_convoy_to_coast";
// instance variables
private transient static final int REQ_LOC = 3;
private transient int currentLocNum = 0;
private transient Point2D.Float failPt = null;
private transient SVGGElement group = null;
/** Creates a GUIConvoy */
protected GUIConvoy()
{
super();
}// GUIConvoy()
/** Creates a GUIConvoy */
protected GUIConvoy(Power power, Location src, Unit.Type srcUnitType,
Location convoySrc, Power convoyPower, Unit.Type convoySrcUnitType,
Location convoyDest)
{
super(power, src, srcUnitType, convoySrc, convoyPower,
convoySrcUnitType, convoyDest);
}// GUIConvoy()
/** This only accepts Convoy orders. All others will throw an IllegalArgumentException. */
public void deriveFrom(Orderable order)
{
if( !(order instanceof Convoy) )
{
throw new IllegalArgumentException();
}
Convoy convoy = (Convoy) order;
power = convoy.getPower();
src = convoy.getSource();
srcUnitType = convoy.getSourceUnitType();
convoySrc = convoy.getConvoySrc();
convoyDest = convoy.getConvoyDest();
convoyPower = convoy.getConvoyedPower();
convoyUnitType = convoy.getConvoyUnitType();
// set completed
currentLocNum = REQ_LOC;
}// deriveFrom()
public boolean testLocation(StateInfo stateInfo, Location location, StringBuffer sb)
{
sb.setLength(0);
if(isComplete())
{
sb.append( Utils.getLocalString(GUIOrder.COMPLETE, getFullName()) );
return false;
}
Position position = stateInfo.getPosition();
Province province = location.getProvince();
if(currentLocNum == 0)
{
// set Convoy origin (supporting unit)
// 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()) );
return false;
}
// we require a Fleet in a sea space or convoyable coast to be present.
if(unit.getType() == Unit.Type.FLEET)
{
if(province.isSea() || province.isConvoyableCoast())
{
// check borders
if( !GUIOrderUtils.checkBorder(this, new Location(province, unit.getCoast()), unit.getType(), stateInfo.getPhase(), sb) )
{
return false;
}
// order is acceptable
sb.append( Utils.getLocalString(GUIOrder.CLICK_TO_ISSUE, getFullName()) );
return true;
}
else
{
sb.append( Utils.getLocalString(ONLY_SEA_OR_CC_FLEETS_CAN_CONVOY) );
return false;
}
}
else
{
sb.append( Utils.getLocalString(ONLY_SEA_OR_CC_FLEETS_CAN_CONVOY) );
return false;
}
}
// no unit in province
sb.append( Utils.getLocalString(GUIOrder.NO_UNIT, getFullName()) );
return false;
}
else if(currentLocNum == 1)
{
// set Convoy source (unit being convoyed)
// - If we are not validating, any location with a unit is acceptable (even source)
// - If we are validating,
//
if(stateInfo.getValidationOptions().getOption(ValidationOptions.KEY_GLOBAL_PARSING).equals(ValidationOptions.VALUE_GLOBAL_PARSING_LOOSE))
{
// lenient parsing enabled; we'll take anything with a unit!
if(position.hasUnit(province))
{
sb.append( Utils.getLocalString(CLICK_TO_CONVOY));
return true;
}
// no unit in province
sb.append( Utils.getLocalString(NO_UNIT));
return false;
}
// strict parsing is enabled. We are more selective.
// The location must contain a coastal Army unit
//
Unit unit = position.getUnit(province);
if(unit != null)
{
if(unit.getType() == Unit.Type.ARMY)
{
if(province.isCoastal())
{
// check borders
if( !GUIOrderUtils.checkBorder(this, new Location(province, unit.getCoast()), unit.getType(), stateInfo.getPhase(), sb) )
{
return false;
}
sb.append( Utils.getLocalString(CLICK_TO_CONVOY_ARMY) );
return true;
}
else
{
sb.append( Utils.getLocalString(CANNOT_CONVOY_LANDLOCKED) );
return false;
}
}
else
{
sb.append( Utils.getLocalString(MUST_CONVOY_FROM_COAST) );
return false;
}
}
// no unit in province
sb.append( Utils.getLocalString(NO_UNIT) );
return false;
}
else if(currentLocNum == 2)
{
// set Convoy destination
// - If we are not validating, any destination is acceptable (even source)
// - If we are validating, we check that a theoretical (possible) convoy route to
// the destination exists (could exist)
//
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_CONVOY_FROM, province.getFullName()) );
return true;
}
// strict parsing is enabled. We are more selective. Check for a possible convoy route.
if(province.isCoastal())
{
Path path = new Path(position);
if(path.isPossibleConvoyRoute(convoySrc, new Location(province, Coast.NONE)))
{
// check borders
if( !GUIOrderUtils.checkBorder(this, location, convoyUnitType, stateInfo.getPhase(), sb) )
{
return false;
}
sb.append( Utils.getLocalString(CLICK_TO_CONVOY_FROM, province.getFullName()) );
return true;
}
else
{
sb.append( Utils.getLocalString(NO_POSSIBLE_CONVOY_PATH, convoySrc.getProvince().getFullName()) );
return false;
}
}
else
{
sb.append( Utils.getLocalString(MUST_CONVOY_TO_COAST) );
return false;
}
}
else
{
// should not occur.
throw new IllegalStateException();
}
// NO return here: thus we must appropriately exit within an if/else block above.
}// testLocation()
public boolean clearLocations()
{
if(isComplete())
{
return false;
}
currentLocNum = 0;
power = null;
src = null;
srcUnitType = null;
convoySrc = null;
convoyDest = null;
convoyPower = null;
convoyUnitType = null;
return true;
}// clearLocations()
public boolean setLocation(StateInfo stateInfo, Location location, StringBuffer sb)
{
if(isComplete())
{
return false;
}
if(testLocation(stateInfo, location, sb))
{
if(currentLocNum == 0)
{
Unit unit = stateInfo.getPosition().getUnit(location.getProvince());
src = new Location(location.getProvince(), unit.getCoast());
power = unit.getPower();
srcUnitType = unit.getType();
currentLocNum++;
return true;
}
else if(currentLocNum == 1)
{
Unit unit = stateInfo.getPosition().getUnit(location.getProvince());
convoySrc = new Location(location.getProvince(), unit.getCoast());
convoyUnitType = unit.getType();
sb.setLength(0);
sb.append("Convoying this unit.");
currentLocNum++;
return true;
}
else if(currentLocNum == 2)
{
convoyDest = new Location(location.getProvince(), location.getCoast());
sb.setLength(0);
sb.append( Utils.getLocalString(GUIOrder.COMPLETE, getFullName()) );
currentLocNum++;
return true;
}
}
return false;
}// setLocation()
public boolean isComplete()
{
assert (currentLocNum <= getNumRequiredLocations());
return (currentLocNum == getNumRequiredLocations());
}// 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_LOWEST);
GUIOrderUtils.removeChild(powerGroup, group);
group = null;
}
}// removeFromDOM()
/**
* Draws a dashed line to a triangle surrounding convoyed unit, and then a
* dashed line from convoyed unit to destination.
*/
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.
//
// we have nothing (yet) to check for change; isDependent() == false.
// so just return if we have not been drawn.
if(group != null)
{
return;
}
// there has been a change, if we are at this point.
//
// 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_LOWEST).appendChild(group);
}
else
{
// remove group children
GUIOrderUtils.deleteChildren(group);
}
// now, render the order
//
SVGElement[] elements = null;
// create hilight line
String cssStyle = mapInfo.getMapMetadata().getOrderParamString(MapMetadata.EL_CONVOY, MapMetadata.ATT_HILIGHT_CLASS);
if(!cssStyle.equalsIgnoreCase("none"))
{
float offset = mapInfo.getMapMetadata().getOrderParamFloat(MapMetadata.EL_CONVOY, MapMetadata.ATT_HILIGHT_OFFSET);
elements = drawOrder(mapInfo, offset, false);
GUIOrderUtils.makeHilight(elements, mapInfo.getMapMetadata(), MapMetadata.EL_CONVOY);
for(int i=0; i<elements.length; i++)
{
group.appendChild(elements[i]);
}
}
// create real line
elements = drawOrder(mapInfo, 0, true);
GUIOrderUtils.makeStyled(elements, mapInfo.getMapMetadata(), MapMetadata.EL_CONVOY, power);
for(int i=0; i<elements.length; i++)
{
group.appendChild(elements[i]);
}
// draw 'failed' marker, if appropriate.
if(!mapInfo.getTurnState().isOrderSuccessful(this))
{
SVGElement useElement = GUIOrderUtils.createFailedOrderSymbol(mapInfo, failPt.x, failPt.y);
group.appendChild(useElement);
}
}// updateDOM()
public SVGElement orderSVG(MapInfo mapInfo){
updateDOM(mapInfo);
return group;
}
// private SVGElement[] drawOrder(MapInfo mapInfo)
// {
// return drawOrder(mapInfo, 0, true);
// }
private SVGElement[] drawOrder(MapInfo mapInfo, float offset, boolean addMarker)
{
// setup
SVGElement[] elements = new SVGElement[3];
Position position = mapInfo.getTurnState().getPosition();
MapMetadata mmd = mapInfo.getMapMetadata();
Point2D.Float ptSrc = mmd.getUnitPt(src.getProvince(), src.getCoast());
Point2D.Float ptConvoySrc = mmd.getUnitPt(convoySrc.getProvince(), convoySrc.getCoast());
Point2D.Float ptConvoyDest = mmd.getUnitPt(convoyDest.getProvince(), convoyDest.getCoast());
ptSrc.x += offset;
ptSrc.y += offset;
ptConvoySrc.x += offset;
ptConvoySrc.y += offset;
ptConvoyDest.x += offset;
ptConvoyDest.y += offset;
// radius
float radius = mmd.getOrderRadius(MapMetadata.EL_CONVOY, mapInfo.getSymbolName(getConvoyUnitType()));
// draw line to convoyed unit
Point2D.Float newPtTo = GUIOrderUtils.getLineCircleIntersection(ptSrc.x, ptSrc.y,
ptConvoySrc.x, ptConvoySrc.y, ptConvoySrc.x, ptConvoySrc.y, radius);
elements[0] = (SVGLineElement)
mapInfo.getDocument().createElementNS(
SVGDOMImplementation.SVG_NAMESPACE_URI,
SVGConstants.SVG_LINE_TAG);
elements[0].setAttributeNS(null, SVGConstants.SVG_X1_ATTRIBUTE, GUIOrderUtils.floatToString(ptSrc.x));
elements[0].setAttributeNS(null, SVGConstants.SVG_Y1_ATTRIBUTE, GUIOrderUtils.floatToString(ptSrc.y));
elements[0].setAttributeNS(null, SVGConstants.SVG_X2_ATTRIBUTE, GUIOrderUtils.floatToString(newPtTo.x));
elements[0].setAttributeNS(null, SVGConstants.SVG_Y2_ATTRIBUTE, GUIOrderUtils.floatToString(newPtTo.y));
elements[0].setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE,
mmd.getOrderParamString(MapMetadata.EL_CONVOY, MapMetadata.ATT_STROKESTYLE));
// draw triangle around supported unit
Point2D.Float[] triPts = GUIOrderUtils.makeTriangle(ptConvoySrc, radius);
StringBuffer sb = new StringBuffer(160);
for(int i=0; i<triPts.length; i++)
{
GUIOrderUtils.appendFloat(sb, triPts[i].x);
sb.append(',');
GUIOrderUtils.appendFloat(sb, triPts[i].y);
sb.append(' ');
}
elements[1] = (SVGPolygonElement)
mapInfo.getDocument().createElementNS(
SVGDOMImplementation.SVG_NAMESPACE_URI,
SVGConstants.SVG_POLYGON_TAG);
elements[1].setAttributeNS(null, SVGConstants.SVG_POINTS_ATTRIBUTE, sb.toString());
elements[1].setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE, mmd.getOrderParamString(MapMetadata.EL_CONVOY, MapMetadata.ATT_STROKESTYLE));
// failPt will be a triangle vertext (12 o'clock vertex)
failPt = new Point2D.Float(triPts[0].x, triPts[0].y);
// draw line from triangle to convoyDest.
// line will come from the closest triangular vertext, by distance.
//
Point2D.Float newPtFrom = null;
float maxDistSquared = 0.0f;
for(int i=0; i<triPts.length; i++)
{
float distSquared = (float) (Math.pow((ptConvoyDest.x - triPts[i].x), 2.0) + Math.pow((ptConvoyDest.y - triPts[i].y), 2.0));
if(distSquared > maxDistSquared)
{
maxDistSquared = distSquared;
newPtFrom = triPts[i];
}
}
// only respect convoyDest iff there is a unit present.
if(position.hasUnit(convoyDest.getProvince()))
{
// use 'move' (EL_MOVE) order radius; 'hold' could also be appropriate.
// we do this because the destination unit may have an order, and this
// results in a better display.
//
Unit.Type destUnitType = position.getUnit(convoyDest.getProvince()).getType();
float moveRadius = mmd.getOrderRadius(MapMetadata.EL_MOVE, mapInfo.getSymbolName(destUnitType));
newPtTo = GUIOrderUtils.getLineCircleIntersection(newPtFrom.x, newPtFrom.y, ptConvoyDest.x, ptConvoyDest.y, ptConvoyDest.x, ptConvoyDest.y, moveRadius);
}
else
{
newPtTo = ptConvoyDest;
}
elements[2] = (SVGLineElement)
mapInfo.getDocument().createElementNS(
SVGDOMImplementation.SVG_NAMESPACE_URI,
SVGConstants.SVG_LINE_TAG);
elements[2].setAttributeNS(null, SVGConstants.SVG_X1_ATTRIBUTE, GUIOrderUtils.floatToString(newPtFrom.x));
elements[2].setAttributeNS(null, SVGConstants.SVG_Y1_ATTRIBUTE, GUIOrderUtils.floatToString(newPtFrom.y));
elements[2].setAttributeNS(null, SVGConstants.SVG_X2_ATTRIBUTE, GUIOrderUtils.floatToString(newPtTo.x));
elements[2].setAttributeNS(null, SVGConstants.SVG_Y2_ATTRIBUTE, GUIOrderUtils.floatToString(newPtTo.y));
elements[2].setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE,
mmd.getOrderParamString(MapMetadata.EL_CONVOY, MapMetadata.ATT_STROKESTYLE));
// marker
if(addMarker || offset != 0.0f)
{
GUIOrderUtils.addMarker(elements[2], mmd, MapMetadata.EL_CONVOY);
}
// add to parent
return elements;
}// drawOrder()
public boolean isDependent() { return false; }
}// class GUIConvoy