// // @(#)DefaultMapRenderer2.java 5/2003 // // Copyright 2003 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.map; //import dip.gui.ClientFrame; //import dip.gui.ClientMenu; import dip.gui.order.GUIOrder; import dip.gui.order.GUIOrder.MapInfo; import java.awt.geom.Point2D; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.batik.dom.svg.SVGDOMImplementation; import org.apache.batik.util.CSSConstants; import org.apache.batik.util.SVGConstants; import org.w3c.dom.Node; import org.w3c.dom.events.EventTarget; import org.w3c.dom.svg.SVGDocument; import org.w3c.dom.svg.SVGElement; import org.w3c.dom.svg.SVGGElement; import dip.gui.map.RenderCommandFactory.RenderCommand; import dip.misc.Log; import dip.order.Orderable; import dip.world.Coast; import dip.world.Location; import dip.world.Phase; import dip.world.Position; import dip.world.Power; import dip.world.Province; import dip.world.TurnState; import dip.world.Unit; import dip.world.World; import dip.world.variant.data.SymbolPack; //import org.apache.batik.swing.JSVGCanvas; import org.apache.batik.dom.svg.*; import org.apache.batik.css.engine.*; //import org.apache.batik.bridge.CSSUtilities; // //import org.apache.batik.bridge.BridgeContext; //import org.apache.batik.gvt.*; /** * * Default Rendering logic. * * <p> * <b>Debugging Hint:</b> To see if the DOM has been altered, it can be 'dumped' by * using File | Export Map As... | SVG and looking at the SVG output. * * */ public class DefaultMapRenderer2 extends MapRenderer2 implements Serializable { /** * */ private static final long serialVersionUID = 5935826195444933214L; // Symbol Names // /** Army symbol ID */ public static final String SYMBOL_ARMY = "Army"; /** Fleet symbol ID */ public static final String SYMBOL_FLEET = "Fleet"; /** Dislodged Army symbol ID */ public static final String SYMBOL_DISLODGED_ARMY = "DislodgedArmy"; /** Dislodged Fleet symbol ID */ public static final String SYMBOL_DISLODGED_FLEET = "DislodgedFleet"; /** Supply Center symbol ID */ public static final String SYMBOL_SC = "SupplyCenter"; /** Wing symbol ID */ public static final String SYMBOL_WING = "Wing"; /** Dislodged Wing symbol ID */ public static final String SYMBOL_DISLODGED_WING = "DislodgedWing"; /** Failed Order symbol ID */ public static final String SYMBOL_FAILEDORDER = "FailedOrder"; /** Build marker symbol ID */ public static final String SYMBOL_BUILDUNIT = "BuildUnit"; /** Remove (and disband) symbol ID */ public static final String SYMBOL_REMOVEUNIT = "RemoveUnit"; /** Waived Build symbol ID */ public static final String SYMBOL_WAIVEDBUILD = "WaivedBuild"; /** Symbol List */ public static final String[] SYMBOLS = { SYMBOL_ARMY, SYMBOL_FLEET, SYMBOL_DISLODGED_ARMY, SYMBOL_DISLODGED_FLEET, SYMBOL_WING, SYMBOL_DISLODGED_WING, SYMBOL_SC, SYMBOL_FAILEDORDER, SYMBOL_BUILDUNIT, SYMBOL_REMOVEUNIT, SYMBOL_WAIVEDBUILD }; /** Layer: Map */ public static final String LAYER_MAP = "MapLayer"; /** Layer: Supply Center */ protected static final String LAYER_SC = "SupplyCenterLayer"; /** Layer: Orders */ protected static final String LAYER_ORDERS = "OrderLayer"; /** Layer: Highest (z=0) Orders */ protected static final String HIGHEST_ORDER_LAYER = "HighestOrderLayer"; /** Layer: Units */ protected static final String LAYER_UNITS = "UnitLayer"; /** Layer: Dislodged Units */ protected static final String LAYER_DISLODGED_UNITS = "DislodgedUnitLayer"; /** Layer: region definitions for mouse */ protected static final String LAYER_MOUSE = "MouseLayer"; /** Label Layer: Abbreviated */ public static final String LABEL_LAYER_BRIEF = "BriefLabelLayer"; /** Label Layer: Full */ public static final String LABEL_LAYER_FULL = "FullLabelLayer"; /** All Label layers */ protected static final String[] LABEL_LAYERS = {LABEL_LAYER_BRIEF, LABEL_LAYER_FULL}; /** All Layers */ protected static final String[] LAYERS = { LAYER_MAP, LAYER_SC, LAYER_ORDERS, HIGHEST_ORDER_LAYER, LAYER_UNITS, LAYER_DISLODGED_UNITS, LABEL_LAYER_BRIEF, LABEL_LAYER_FULL, LAYER_MOUSE }; // layers for Z-ordering private static final String LAYER_1 = "Layer1"; private static final String LAYER_2 = "Layer2"; private static final String[] Z_LAYER_NAMES = {HIGHEST_ORDER_LAYER, LAYER_1, LAYER_2}; protected static final String NOPOWER = "nopower"; protected static final String SC_NOPOWER = "scnopower"; protected static final String UNORDERED = "unordered"; // instance variables protected Map trackerMap; // for rendering units & dislodged units; keyed by Province protected HashMap layerMap; // layers to which we render; keyed by LAYER; includes label layers private HashMap renderSettings; // control rendering options. private HashMap locMap; // maps multicoastal province ids -> Location objects for multicoastal provinces private HashMap[] powerOrderMap; private HashMap oldRenderSettings; // old render settings private dip.world.Map worldMap; // World Map reference private TurnState turnState = null; // current TurnState private Province[] provinces; private Power[] powers; private Position position = null; // current Position private MapMetadata mapMeta = null; //private DOMUIEventListener domEventListener = null; private boolean isDislodgedPhase = false; // true if we are in Phase.RETREAT private static final DMR2RenderCommandFactory rcf; // default render command factory instance. private SymbolPack symbolPack; static { rcf = new DMR2RenderCommandFactory(); } /** Creates a DefaultMapRenderer object * @throws InterruptedException */ public DefaultMapRenderer2(){} public DefaultMapRenderer2(SVGDocument doc, World w, SymbolPack sp) throws MapException, InterruptedException { super(doc); this.symbolPack = sp; //Log.printTimed(mapPanel.startTime, "DMR2 constructor start"); // init variables worldMap = w.getMap(); provinces = worldMap.getProvinces(); powers = w.getMap().getPowers(); // setup object maps trackerMap = new HashMap(113); renderSettings = new HashMap(11); layerMap = new HashMap(11); locMap = new HashMap(17); // power order hashmap (now with z-axis) setup powerOrderMap = new HashMap[Z_LAYER_NAMES.length]; for(int i=0; i<powerOrderMap.length; i++) { powerOrderMap[i] = new HashMap(11); } // set default render settings renderSettings.put(KEY_SHOW_MAP, Boolean.TRUE); renderSettings.put(KEY_SHOW_SUPPLY_CENTERS, Boolean.TRUE); renderSettings.put(KEY_SHOW_UNITS, Boolean.TRUE); renderSettings.put(KEY_SHOW_DISLODGED_UNITS, Boolean.TRUE); renderSettings.put(KEY_SHOW_ORDERS_FOR_POWERS, powers); renderSettings.put(KEY_SHOW_UNORDERED, Boolean.FALSE); renderSettings.put(KEY_INFLUENCE_MODE, Boolean.FALSE); renderSettings.put(KEY_LABELS, VALUE_LABELS_NONE); // get map metadata mapMeta = new MapMetadata(doc,w, sp, false); // tell others that mmd has been parsed and is ready //mapPanel.getClientFrame().fireMMDReady(mapMeta); // get and check symbols & rendering layers checkSymbols(); mapLayers(); // add mouse listeners to the MouseLayer // add key listeners //domEventListener = mapPanel.getDOMUIEventListener(); //domEventListener.setMapRenderer(this); validateAndSetupMouseRegions(); // Root SVG element listeners // one is for general key events, and the other, for 'null' locations // (some maps may have 'null' space). // doc.getRootElement().addEventListener(SVGConstants.SVG_KEYPRESS_EVENT_TYPE, domEventListener, false); // note: false // doc.getRootElement().addEventListener(SVGConstants.SVG_EVENT_CLICK, domEventListener, true); // doc.getRootElement().addEventListener(SVGConstants.SVG_EVENT_MOUSEOUT, domEventListener, true); // doc.getRootElement().addEventListener(SVGConstants.SVG_EVENT_MOUSEOVER, domEventListener, true); // Dragging stuff //doc.getRootElement().addEventListener(SVGConstants.SVG_MOUSEDOWN_EVENT_TYPE, domEventListener, true); //doc.getRootElement().addEventListener(SVGConstants.SVG_MOUSEUP_EVENT_TYPE, domEventListener, true); // create a complete set of Tracker objects for all provinces for(int i=0; i<provinces.length; i++) { trackerMap.put(provinces[i], new Tracker()); } // add province hilites to Tracker object addProvinceHilitesToTracker(); // create the per-power order layers createDOMOrderTree(); // add all SC to the map. The SC are never removed or added, however, // their attributes (attribute class) may change. // after adding to DOM, the element is added to the Tracker object. // RunnableQueue rq = getRunnableQueue(); // if(rq != null) // { // rq.invokeAndWait(new Runnable() // { // public void run() // { synchronized(trackerMap) { for(int i=0; i<provinces.length; i++) { if(provinces[i].hasSupplyCenter()) { // create element SVGElement element = makeSCUse(provinces[i], null); // add element to tracker Tracker tracker = (Tracker) trackerMap.get(provinces[i]); tracker.setSCElement(element); // add to DOM SVGElement parent = (SVGElement) layerMap.get(LAYER_SC); parent.appendChild(element); } } } // }// run() // }); // } //Log.printTimed(mapPanel.startTime, "DMR2 constructor end"); }// DefaultMapRenderer() /** Returns a DMR2RenderCommandFactory */ public RenderCommandFactory getRenderCommandFactory() { return rcf; }// getRenderCommandFactory() /** * Close the MapRenderer, releasing all resources. * <p> * WARNING: render events must not be processed after or * during a call to this method. */ public void close() { // super cleanup super.close(); // remove Root SVG element listeners //doc.getRootElement().removeEventListener(SVGConstants.SVG_KEYPRESS_EVENT_TYPE, domEventListener, false); // Remove other mouse/key listeners SVGElement[] mouseElements = SVGUtils.idFinderSVG((SVGElement) layerMap.get(LAYER_MOUSE)); for(int i=0; i<mouseElements.length; i++) { if(mouseElements[i] instanceof EventTarget) { // add mouse listeners //EventTarget et = (EventTarget) mouseElements[i]; //et.removeEventListener(SVGConstants.SVG_EVENT_CLICK, domEventListener, false); //et.removeEventListener(SVGConstants.SVG_EVENT_MOUSEOUT, domEventListener, false); //et.removeEventListener(SVGConstants.SVG_EVENT_MOUSEOVER, domEventListener, false); } } // clear maps synchronized(trackerMap) { trackerMap.clear(); } layerMap.clear(); renderSettings.clear(); locMap.clear(); // clear metadata mapMeta.close(); }// close() /** Gets the MapMetadata object */ public MapMetadata getMapMetadata() { return mapMeta; }// getMapMetadata() /** Get a mapped layer */ public SVGGElement getLayer(String key) { return (SVGGElement) layerMap.get(key); }// getLayer() /** Called when an order has been added to the order list * @throws InterruptedException */ public void orderCreated(final GUIOrder order) { execRenderCommand(new RenderCommand(this) { public void execute() { Log.println("DMR2: orderCreated(): ", order); MapInfo mapInfo = new DMRMapInfo(turnState); order.updateDOM(mapInfo); unsyncUpdateDependentOrders(new GUIOrder[] {order}); unsyncUpdateProvince(order.getSource().getProvince()); }// execute() }); }// orderAdded() /** Called when an order has been deleted from the order list */ protected void orderDeleted(final GUIOrder order) { execRenderCommand(new RenderCommand(this) { public void execute() { Log.println("DMR2: orderDeleted(): ", order); MapInfo mapInfo = new DMRMapInfo(turnState); order.removeFromDOM(mapInfo); unsyncUpdateDependentOrders(null); unsyncUpdateProvince(order.getSource().getProvince()); }// execute() }); }// orderDeleted() /** Called when multiple orders have been added from the order list */ public void multipleOrdersCreated(final GUIOrder[] orders) { execRenderCommand(new RenderCommand(this) { public void execute() { Log.println("DMR2: multipleOrdersCreated(): ", orders); MapInfo mapInfo = new DMRMapInfo(turnState); // render orders and update provinces for(int i=0; i<orders.length; i++) { orders[i].updateDOM(mapInfo); unsyncUpdateProvince(orders[i].getSource().getProvince()); } // update dependent orders unsyncUpdateDependentOrders(orders); }// execute() }); }// multipleOrdersCreated() /** Called when multiple orders have been deleted from the order list */ protected void multipleOrdersDeleted(final GUIOrder[] orders) { execRenderCommand(new RenderCommand(this) { public void execute() { Log.println("DMR2: multipleOrdersDeleted(): ", orders); MapInfo mapInfo = new DMRMapInfo(turnState); // render orders and update provinces for(int i=0; i<orders.length; i++) { orders[i].removeFromDOM(mapInfo); unsyncUpdateProvince(orders[i].getSource().getProvince()); } // update dependent orders unsyncUpdateDependentOrders(null); }// execute() }); }// multipleOrdersDeleted() /** Called when the displayable powers have changed * @throws InterruptedException */ public void displayablePowersChanged(final Power[] diplayPowers) throws InterruptedException { execRenderCommand(new RenderCommand(this) { public void execute() { Log.println("DMR2: displayablePowersChanged()"); // update all orders //unsyncUpdateAllOrders(); }// execute() }); }// displayablePowersChanged() /** * Sets the current TurnState object for the renderer. This should * only operate within a run() method... if the turnState is changed * while another run() method is activated, bad things can happen. * */ protected void setTurnState(TurnState ts) { if(ts == null || ts.getPosition() == null) { throw new IllegalArgumentException("null turnstate or position"); } // destroy all orders in old turnstate // before changing if(turnState != null) { unsyncDestroyAllOrders(); } // change turnstate. turnState = ts; position = ts.getPosition(); isDislodgedPhase = (ts.getPhase().getPhaseType() == Phase.PhaseType.RETREAT); }// setTurnState() /** Get a map rendering setting */ public Object getRenderSetting(Object key) { synchronized(renderSettings) { return renderSettings.get(key); } }// getRenderSetting() /** Internally set a Render Setting */ protected void setRenderSetting(Object key, Object value) { synchronized(renderSettings) { renderSettings.put(key, value); } }// setRenderSetting() /** Get the Symbol Name for the given unit type */ public String getSymbolName(Unit.Type unitType) { if(unitType == Unit.Type.ARMY) { return DefaultMapRenderer2.SYMBOL_ARMY; } else if(unitType == Unit.Type.FLEET) { return DefaultMapRenderer2.SYMBOL_FLEET; } else if(unitType == Unit.Type.WING) { return DefaultMapRenderer2.SYMBOL_WING; } else { throw new IllegalStateException("DMR2: Unit Type: "+unitType+" SVG symbol ID unknown"); } }// getSymbolName() /** Gets the location that corresponds to a given string id<br>Assumes ID is lowercase! */ public Location getLocation(String id) { Province province = worldMap.getProvince(id); if(province != null) { return new Location(province, Coast.UNDEFINED); } return (Location) locMap.get(id); }// getLocation() /** * Creates SVG G elements (one for each power) under the OrderLayer * SVG G element layer. Maps each Power to the SVGGElement that * corresponds, in powerOrderMap. * @throws InterruptedException * */ private void createDOMOrderTree() { //RunnableQueue rq = getRunnableQueue(); //if(rq != null) //{ //rq.invokeAndWait(new Runnable() //{ //public void run() //{ SVGGElement orderLayer = (SVGGElement) layerMap.get(LAYER_ORDERS); for(int z=(powerOrderMap.length - 1); z >= 0; z--) { // determine which order layer we should use. if(z == 0) { // special case: this has its own explicit group in the SVG file orderLayer = (SVGGElement) layerMap.get(HIGHEST_ORDER_LAYER); } else { // typical case // these occur under the "OrderLayer" group // Note that we must create the elements in reverse order, because // lower z-orders (closer to viewer) must be rendered after (later) // higher z orders // SVGGElement parentLayer = (SVGGElement) layerMap.get(LAYER_ORDERS); // create order layer under ORDER_LAYERS layer (e.g., id="Layer1", or id="Layer2") // orderLayer = // (SVGGElement) doc.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, // SVGConstants.SVG_G_TAG); orderLayer.setAttributeNS(null, SVGConstants.SVG_ID_ATTRIBUTE, Z_LAYER_NAMES[z]); //parentLayer.appendChild(orderLayer); // now put this into the layer map, so it can be retrieved later //layerMap.put(Z_LAYER_NAMES[z], orderLayer); } // create an order layer for each power. append the z order ID for(int i=0; i<powers.length; i++) { SVGGElement gElement = (SVGGElement) doc.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, SVGConstants.SVG_G_TAG); // make layer name (needs to be unique) StringBuffer sb = new StringBuffer(32); sb.append(getPowerName(powers[i])); sb.append('_'); sb.append(String.valueOf(z)); gElement.setAttributeNS(null, SVGConstants.SVG_ID_ATTRIBUTE, sb.toString()); orderLayer.appendChild(gElement); powerOrderMap[z].put(powers[i], gElement); } //} //} //}); } }// createDOMOrderTree() /** * Makes sure that all orders have been removed from the order * tree and have no associated SVGElement. */ protected void unsyncDestroyAllOrders() { Log.println("DMR2::unsyncDestroyAllOrders()"); MapInfo mapInfo = new DMRMapInfo(turnState); Iterator iter = turnState.getAllOrders().iterator(); while(iter.hasNext()) { GUIOrder order = (GUIOrder) iter.next(); order.removeFromDOM(mapInfo); } }// unsyncDestroyAllOrders() /** * Get the Power order SVGGElement (group) for * then given z-order. z : [0,2] * */ private SVGGElement getPowerSVGGElement(Power p, int z) { return (SVGGElement) powerOrderMap[z].get(p); }// getPowerSVGGElement() /** * Sets the Visibility (CSS visibility) for a Power's orders. * This manipulates all layers for a given power. */ private void setPowerOrderVisibility(Power p, boolean isVisible) { for(int i=0; i<powerOrderMap.length; i++) { setElementVisibility( getPowerSVGGElement(p, i), isVisible ); } }// setPowerOrderVisibility() /** * Uses the KEY_SHOW_ORDERS_FOR_POWERS value to * determine which orders should be displayed. */ protected void unsyncSetVisiblePowers() { Power[] displayedPowers = (Power[]) getRenderSetting(KEY_SHOW_ORDERS_FOR_POWERS); // displayedPowers contains the powers that are visible. // go thru all powers, setting the visibility for(int i=0; i<powers.length; i++) { boolean isVisible = false; for(int j=0; j<displayedPowers.length; j++) { if(powers[i] == displayedPowers[j]) { isVisible = true; break; } } // set visibility //setPowerOrderVisibility(powers[i], isVisible); } }// unsyncSetVisiblePowers() /** * Removes ALL orders, for all powers, in all layers. * Power layer G parent elements remain unaltered. */ protected void unsyncRemoveAllOrdersFromDOM() { for(int z=0; z<powerOrderMap.length; z++) { for(int i=0; i<powers.length; i++) { SVGGElement powerNode = getPowerSVGGElement(powers[i], z); Node child = powerNode.getFirstChild(); while(child != null) { powerNode.removeChild( child ); child = powerNode.getFirstChild(); } } } }// unsyncRemoveAllOrdersFromDOM() /** * Implements Influence mode. Saves and restores old mapRenderer settings. * * */ protected void unsyncSetInfluenceMode(boolean value) { //Log.println("unsyncSetInfluenceMode(): ", String.valueOf(value)); // disable or enable certain View menu items //ClientMenu cm = mapPanel.getClientFrame().getClientMenu(); // save and set, or restore, previous MapRenderer2 if(value) { // ENTERING influence mode // if(oldRenderSettings != null) { throw new IllegalStateException("already in influence mode!"); } // disable menu items early // cm.setEnabled(ClientMenu.VIEW_ORDERS, !value); // cm.setEnabled(ClientMenu.VIEW_UNITS, !value); // cm.setEnabled(ClientMenu.VIEW_DISLODGED_UNITS, !value); // cm.setEnabled(ClientMenu.VIEW_SUPPLY_CENTERS, !value); // cm.setEnabled(ClientMenu.VIEW_UNORDERED, !value); // cm.setEnabled(ClientMenu.VIEW_SHOW_MAP, !value); // now clear the render settings synchronized(renderSettings) { // copy old render settings oldRenderSettings = (HashMap) renderSettings.clone(); // if 'show unordered' was enabled, we must first disable it. if(oldRenderSettings.get(MapRenderer2.KEY_SHOW_UNORDERED) == Boolean.TRUE) { renderSettings.put(MapRenderer2.KEY_SHOW_UNORDERED, Boolean.FALSE); } renderSettings.clear(); renderSettings.put(MapRenderer2.KEY_SHOW_ORDERS_FOR_POWERS, new Power[0]); } // hide layers we don't want (units, orders, sc) SVGElement elLayer = (SVGElement) layerMap.get(LAYER_SC); setElementVisibility(elLayer, false); elLayer = (SVGElement) layerMap.get(LAYER_UNITS); setElementVisibility(elLayer, false); elLayer = (SVGElement) layerMap.get(LAYER_DISLODGED_UNITS); setElementVisibility(elLayer, false); elLayer = (SVGElement) layerMap.get(LAYER_MAP); // always show map in influence mode setElementVisibility(elLayer, true); Power[] visiblePowers = (Power[]) oldRenderSettings.get(MapRenderer2.KEY_SHOW_ORDERS_FOR_POWERS); for(int i=0; i<visiblePowers.length; i++) { //setPowerOrderVisibility(visiblePowers[i], false); } // reset renderSetting influence state, since we just cleared it // this must be set for unsyncUpdateProvince to work correctly synchronized(renderSettings) { renderSettings.put(MapRenderer2.KEY_INFLUENCE_MODE, Boolean.TRUE); } // update province CSS values for(int i=0; i<provinces.length; i++) { unsyncUpdateProvince(provinces[i]); } } else { // EXITING influence mode // if(oldRenderSettings == null) { throw new IllegalStateException("not in influence mode!"); } // reset renderSetting influence state, since we just cleared it // this must be set for unsyncUpdateProvince to work correctly // we also want to reset the KEY_INFLUENCE_MODE synchronized(renderSettings) { Iterator iter = oldRenderSettings.entrySet().iterator(); while(iter.hasNext()) { Map.Entry me = (Map.Entry) iter.next(); renderSettings.put(me.getKey(), me.getValue()); } renderSettings.put(MapRenderer2.KEY_INFLUENCE_MODE, Boolean.FALSE); } // update province CSS values // this also takes care of: // KEY_SHOW_UNORDERED // which require the updating of all province (via unsyncUpdateAllProvinces()) unsyncUpdateAllProvinces(); // activate render settings // we must cover all other keys, that we cleared when we entered influence mode: // KEY_SHOW_UNITS // KEY_SHOW_DISLODGED_UNITS // KEY_SHOW_ORDERS_FOR_POWERS // LAYER_SC // setElementVisibility( (SVGElement) layerMap.get(LAYER_UNITS), ((Boolean) getRenderSetting(KEY_SHOW_UNITS)).booleanValue() ); setElementVisibility( (SVGElement) layerMap.get(LAYER_DISLODGED_UNITS), ((Boolean) getRenderSetting(KEY_SHOW_DISLODGED_UNITS)).booleanValue() ); setElementVisibility( (SVGElement) layerMap.get(LAYER_SC), ((Boolean) getRenderSetting(KEY_SHOW_SUPPLY_CENTERS)).booleanValue() ); unsyncSetVisiblePowers(); // takes care of KEY_SHOW_ORDERS_FOR_POWERS setElementVisibility( (SVGElement) layerMap.get(LAYER_MAP), ((Boolean) getRenderSetting(KEY_SHOW_MAP)).booleanValue() ); // destroy old render settings oldRenderSettings = null; // enable menu items [late] // cm.setEnabled(ClientMenu.VIEW_ORDERS, !value); // cm.setEnabled(ClientMenu.VIEW_UNITS, !value); // cm.setEnabled(ClientMenu.VIEW_DISLODGED_UNITS, !value); // cm.setEnabled(ClientMenu.VIEW_SUPPLY_CENTERS, !value); // cm.setEnabled(ClientMenu.VIEW_UNORDERED, !value); // cm.setEnabled(ClientMenu.VIEW_SHOW_MAP, !value); } }// unsyncSetInfluenceMode() /** * Find orders that are dependent, and call their updateDOM() * method again for possible re-rendering. This is called whenever * an order is added, deleted, or changed. If an order has just * been added or changed, it's update() method is not called. * <p> */ protected void unsyncUpdateDependentOrders(final GUIOrder[] addedOrders) { //Log.println("unsyncUpdateDependentOrders() : ", addedOrder); // get ALL orders MapInfo mapInfo = new DMRMapInfo(turnState); Iterator iter = turnState.getAllOrders().iterator(); while(iter.hasNext()) { GUIOrder order = (GUIOrder) iter.next(); if(order.isDependent()) { if(addedOrders != null) { // do not update if we are in the addedOrders branch for(int i=0; i<addedOrders.length; i++) { if(order == addedOrders[i]) { break; } } } // update! order.updateDOM(mapInfo); } } }// unsyncUpdateDependentOrders() /** * Refresh and/or re-render all orders */ protected void unsyncRecreateAllOrders() { // get ALL orders MapInfo mapInfo = new DMRMapInfo(turnState); Iterator iter = turnState.getAllOrders().iterator(); while(iter.hasNext()) { GUIOrder order = (GUIOrder) iter.next(); order.updateDOM(mapInfo); } }// unsyncRecreateAllOrders() /** Sends an update message to ALL orders, regardless of their dependency status. */ public void unsyncUpdateAllOrders() { // get ALL orders MapInfo mapInfo = new DMRMapInfo(turnState); Iterator iter = turnState.getAllOrders().iterator(); while(iter.hasNext()) { GUIOrder order = (GUIOrder) iter.next(); order.updateDOM(mapInfo); } }// unsyncUpdateAllOrders() /** * Unsynchronized updater: Renders ALL provinces but NOT orders */ protected void unsyncUpdateAllProvinces() { for(int i=0; i<provinces.length; i++) { Province province = provinces[i]; Tracker tracker = (Tracker) trackerMap.get(province); unsyncUpdateProvince(tracker, province, false); } }// unsyncUpdateAllProvincesAndOrders() /** * Unsynchronized province updater, used in both update methods. * We must synchronize around this because this method * will alter the DOM. */ protected void unsyncUpdateProvince(Province province, boolean forceUpdate) { Tracker tracker = (Tracker) trackerMap.get(province); unsyncUpdateProvince(tracker, province, forceUpdate); }// unsyncUpdateProvince() /** Convenience method: non-forced. */ protected void unsyncUpdateProvince(Province province) { unsyncUpdateProvince(province, false); }// unsyncUpdateProvince() /** * Unsynchronized province updater, used in both update methods. * We must synchronize around this because this method * will alter the DOM. If the 'force' flag is true, we will update * the unit information EVEN IF it hasn't changed. */ protected void unsyncUpdateProvince(Tracker tracker, Province province, boolean force) { if(tracker == null) { // avoid NPE when in mid-render and batik exits return; } Unit posUnit = position.getUnit(province); if(tracker.getUnit() != posUnit || force) { changeUnitInDOM(posUnit, tracker, province, false); tracker.setUnit(posUnit); } posUnit = position.getDislodgedUnit(province); if(tracker.getDislodgedUnit() != posUnit || force) { changeUnitInDOM(posUnit, tracker, province, true); tracker.setDislodgedUnit(posUnit); } // set province hiliting based upon current render settings SVGElement provinceGroupElement = tracker.getProvinceHiliteElement(); if(provinceGroupElement != null) { // if we are in 'influence mode', we hilite provinces differently if(renderSettings.get(MapRenderer2.KEY_INFLUENCE_MODE) == Boolean.TRUE) { // we are in influence mode // // we only hilite provinces that have a lastOccupier set and are NOT sea provinces if(position.getLastOccupier(province) != null && province.isLand()) { setCSSIfChanged(provinceGroupElement, tracker.getPowerCSSClass(position.getLastOccupier(province))); } else { // use default province CSS styling, if not already setCSSIfChanged(provinceGroupElement, tracker.getOriginalProvinceCSS()); } } else { // we are NOT in influence mode // if( renderSettings.get(MapRenderer2.KEY_SHOW_UNORDERED) == Boolean.TRUE && (getPhaseApropriateUnit(province) != null) && !isOrdered(province) ) { // we are unordered! // unordered CSS style takes precedence over any existing style. setCSSIfChanged(provinceGroupElement, UNORDERED); } else { if(province.hasSupplyCenter()) { // get supply center owner Power power = position.getSupplyCenterOwner(province); // note: // if we are not showing province SC (supply center) hilites, then // we will just use the original CSS. if(renderSettings.get(MapRenderer2.KEY_SHOW_SUPPLY_CENTERS) == Boolean.TRUE) { setCSSIfChanged(provinceGroupElement, tracker.getPowerCSSClass(power)); } else { setCSSIfChanged(provinceGroupElement, tracker.getOriginalProvinceCSS()); } // supply center hilites (always available, but may always be same color) // if power is null, default is 'scnopower'. // these may be 'hidden' or 'visible' depending upon the Render settings for the above key. setCSSIfChanged(tracker.getSCElement(), getSCCSSClass(power)); } else { // set to original CSS style; no special hiliting here. setCSSIfChanged(provinceGroupElement, tracker.getOriginalProvinceCSS()); } } } }// if(provinceGroupElement != null) }// unsyncUpdateProvince() /** * Updates an SC element with new position information. * No change made to CSS. * */ protected void unsyncUpdateSC(Province province) { Tracker tracker = (Tracker) trackerMap.get(province); SVGElement scEl = tracker.getSCElement(); if(scEl != null) { Point2D.Float pos = mapMeta.getSCPt(province); scEl.setAttributeNS(null, SVGConstants.SVG_X_ATTRIBUTE, String.valueOf(pos.x)); scEl.setAttributeNS(null, SVGConstants.SVG_Y_ATTRIBUTE, String.valueOf(pos.y)); } }// unsyncUpdateSC() /** Changes a Unit in the DOM */ private void changeUnitInDOM(final Unit posUnit, final Tracker tracker, final Province province, boolean isDislodged) { // make tracker unit mirror posUnit // SVGElement newElement = null; // get old element (dislodged or normal) SVGElement oldElement = (isDislodged) ? tracker.getDislodgedUnitElement() : tracker.getUnitElement(); // remove, add, or replace as appropriate if(posUnit == null && oldElement == null) { // case 0: do nothing return; } else if(posUnit == null && oldElement != null) { // case 1: new unit null, old unit not null: delete old unit element oldElement.getParentNode().removeChild(oldElement); } else if(oldElement == null && posUnit != null) { // case 2: new unit not null, old unit null: create new element, then add newElement = makeUnitUse(posUnit, province, isDislodged); SVGElement layer = (SVGElement) ((isDislodged) ? layerMap.get(LAYER_DISLODGED_UNITS) : layerMap.get(LAYER_UNITS)); layer.appendChild(newElement); } else { // case 3: neither unit null, but different; create new element, then replace newElement = makeUnitUse(posUnit, province, isDislodged); oldElement.getParentNode().replaceChild(newElement, oldElement); } // set the tracker unit if(isDislodged) { tracker.setDislodgedUnit(newElement, posUnit); } else { tracker.setUnit(newElement, posUnit); } }// changeUnitInDOM /** Creates a Unit of the given type / owner color, via a <use> symbol, in the right place */ private SVGElement makeUnitUse(Unit u, Province province, boolean isDislodged) { // determine symbol ID String symbolID = null; if(u.getType().equals(Unit.Type.FLEET)) { symbolID = (isDislodged) ? SYMBOL_DISLODGED_FLEET : SYMBOL_FLEET; } else if(u.getType().equals(Unit.Type.ARMY)) { symbolID = (isDislodged) ? SYMBOL_DISLODGED_ARMY : SYMBOL_ARMY; } else if(u.getType().equals(Unit.Type.WING)) { symbolID = (isDislodged) ? SYMBOL_DISLODGED_WING : SYMBOL_WING; } else { throw new IllegalArgumentException("undefined or unknown unit type"); } // get symbol size data MapMetadata.SymbolSize symbolSize = mapMeta.getSymbolSize(symbolID); assert(symbolSize != null); // get the rectangle coordinates Coast coast = u.getCoast(); Point2D.Float pos = (isDislodged) ? mapMeta.getDislodgedUnitPt(province,coast) : mapMeta.getUnitPt(province,coast); float x = pos.x; float y = pos.y; return SVGUtils.createUseElement(doc, symbolID, null, getUnitCSSClass(u.getPower()), x, y, symbolSize); }// makeUnitUse() /** * Returns the CSS class for power fills. * If the power starts with a number, the a capital X is prepended. */ private String getUnitCSSClass(Power power) { StringBuffer sb = new StringBuffer(power.getName().length() + 4); sb.append("unit"); sb.append(getPowerName(power)); return sb.toString(); }// getUnitCSSClass() /** * Returns the CSS class for Supply Center fills. Returns SC_NOPOWER if power is null. * If the power starts with a number, the a capital X is prepended. */ private String getSCCSSClass(Power power) { if(power == null) { return SC_NOPOWER; } StringBuffer sb = new StringBuffer(power.getName().length() + 2); sb.append("sc"); sb.append(getPowerName(power)); return sb.toString(); }// getSCCSSClass() /** * Creates the power name, and prepends an "X" if power name starts with a digit. * Does not accept null arguments. */ private String getPowerName(Power power) { String name = power.getName().toLowerCase(); if( Character.isDigit(name.charAt(0)) ) { StringBuffer sb = new StringBuffer(name.length() + 1); sb.append('X'); sb.append(name); return sb.toString(); } return name; }// getPowerName() /** Creates a Supply Center via a <use> symbol, in the right place. * Power may be null. */ private SVGElement makeSCUse(Province province, Power power) { Point2D.Float pos = mapMeta.getSCPt(province); MapMetadata.SymbolSize symbolSize = mapMeta.getSymbolSize(SYMBOL_SC); return SVGUtils.createUseElement(doc, SYMBOL_SC, null, getSCCSSClass(power), pos.x, pos.y, symbolSize); }// makeSCUse() /** Set the visibility of an element */ protected void setElementVisibility(SVGElement element, boolean value) { // optimization: if no change, make no change to the DOM. String oldValue = element.getAttributeNS(null, CSSConstants.CSS_VISIBILITY_PROPERTY); if(value) { if(!oldValue.equals(CSSConstants.CSS_VISIBLE_VALUE)) { element.setAttributeNS(null, CSSConstants.CSS_VISIBILITY_PROPERTY, CSSConstants.CSS_VISIBLE_VALUE); } } else { if(!oldValue.equals(CSSConstants.CSS_HIDDEN_VALUE)) { element.setAttributeNS(null, CSSConstants.CSS_VISIBILITY_PROPERTY, CSSConstants.CSS_HIDDEN_VALUE); } } }// setElementVisibility() /** * Ensures that all required Symbol elements are present in the SVG file, * and that MapMetadata has appropriate symbol sizing information for all * symbols. */ private void checkSymbols() throws MapException { // check symbol presence Map map = SVGUtils.tagFinderSVG(Arrays.asList(SYMBOLS), doc.getRootElement()); for(int i=0; i<SYMBOLS.length; i++) { if(map.get(SYMBOLS[i]) == null) { throw new MapException("Missing required <symbol> or <g> element with id=\""+SYMBOLS[i]+"\"."); } } // check MMD for(int i=0; i<SYMBOLS.length; i++) { if(mapMeta.getSymbolSize(SYMBOLS[i]) == null) { throw new MapException("Missing required <jdipNS:SYMBOLSIZE> element for symbol name \""+SYMBOLS[i]+"\""); } } }// checkSymbols() /** ensures that we extract all the layer info into the layerMap */ private void mapLayers() throws MapException { SVGUtils.tagFinderSVG(layerMap, Arrays.asList(LAYERS), doc.getRootElement()); for(int i=0; i<LAYERS.length; i++) { if(layerMap.get(LAYERS[i]) == null) { throw new MapException("Missing required layer (<g> element) with id=\""+LAYERS[i]+"\"."); } } }// mapLayers() /** * Ensure that a region exists for each province. Regions are elements that can be * assigned a mouse listeners [G, RECT, closed PATH, etc.]. * <p> * This is critical! If this fails, GUI order input / region detection cannot work. * <p> * This also sets up a special lookup table for multicoastal provinces. * */ private void validateAndSetupMouseRegions() throws MapException { SVGElement[] mouseElements = SVGUtils.idFinderSVG((SVGElement) layerMap.get(LAYER_MOUSE)); for(int i=0; i<mouseElements.length; i++) { // get id, which must be a province with or without a coast String id = mouseElements[i].getAttribute(SVGConstants.SVG_ID_ATTRIBUTE); // parse ID; determine if there is a coast. String provinceID = Coast.getProvinceName(id); Coast coast = Coast.parse(id); Province province = worldMap.getProvince(provinceID); if(province == null) { throw new MapException("Province \""+provinceID+"\" in "+LAYER_MOUSE+" is invalid."); } // can we even target this element?? if(mouseElements[i] instanceof EventTarget) { // map the location, but only if the coast is defined. if(coast != Coast.UNDEFINED) { locMap.put(id.toLowerCase(), new Location(province, coast)); } } else { throw new MapException(LAYER_MOUSE+"element: "+mouseElements[i]+" cannot be targetted by mouse events."); } } }// validateAndSetupMouseRegions() /** * Looks for groups with an ID prefaced by an underscore * If that which follows the underscore is a province ID, * it is added to the tracker objects. * <p> * All SVGElements with underscore-prefaced province IDs will * be rendered using the region coloring CSS styles. If no province * element is found, it will not be so colored. * */ private void addProvinceHilitesToTracker() { // Make a list of all possible provinces with underscores ArrayList uscoreProvList = new ArrayList(125); // stores underscore-preceded names ArrayList lookupProvList = new ArrayList(125); // stores corresponding Province for(int i=0; i<provinces.length; i++) { String[] shortNames = provinces[i].getShortNames(); for(int j=0; j<shortNames.length; j++) { uscoreProvList.add('_'+shortNames[j] ); lookupProvList.add(provinces[i]); } } // try to find as many of the above names as possible Map map = SVGUtils.tagFinderSVG(uscoreProvList, doc.getRootElement(), true); // safety check assert (uscoreProvList.size() == lookupProvList.size()); // go through the map and add non-null objects to the tracker. for(int i=0; i<lookupProvList.size(); i++) { SVGElement element = (SVGElement) map.get( uscoreProvList.get(i) ); if(element != null) { Tracker tracker = (Tracker) trackerMap.get( (Province) lookupProvList.get(i) ); tracker.setProvinceHiliteElement(element); } } }// addProvinceHilitesToTracker() /** * Gets the appropriate Unit for a phase. This gets the non-dislodged unit * during Movement and Adjustment phases, and the dislodged unit during the * Retreat phase. */ private Unit getPhaseApropriateUnit(Province p) { return (isDislodgedPhase) ? position.getDislodgedUnit(p) : position.getUnit(p); }// getPhaseAppropriateUnit() /** * Sets a CSS style on an element, but only if it is different * from the existing CSS style. Returns true if a change was made. * A null css value is not allowed. */ private boolean setCSSIfChanged(SVGElement el, String css) { String oldCSS = el.getAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE); if(!css.equals(oldCSS)) { el.setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE, css); return true; } return false; }// setCSSIfChanged() /** * Searches the TurnState to see if the given province has an order. * <p> * This is made faster by first determining if a unit is present; * we then look at the orders for the power of that unit (phase * appropriate). This cuts the searching time. */ private boolean isOrdered(Province province) { Unit unit = getPhaseApropriateUnit(province); if(unit != null) { List list = turnState.getOrders(unit.getPower()); Iterator iter = list.iterator(); while(iter.hasNext()) { Orderable order = (Orderable) iter.next(); if(order.getSource().isProvinceEqual(province)) { return true; } } } return false; }// isOrdered() /** * Keeps track of the DOM Elements and other info to monitor changes; ONE (and only one) * Tracker object will exist for each Province with any items to be rendered (SC, units, etc.) */ protected class Tracker { // elements (to avoid traversing SVG DOM) private SVGElement elUnit = null; private SVGElement elDislodgedUnit = null; // things to monitor change private Unit unit = null; private Unit dislodgedUnit = null; // associated SVGElement of province, to determine if a hilite should be rendered // really, only provinces with supply centers need be rendered in this manner private SVGElement provHilite = null; // Supply Center SVGElement. Provinces without supply centers // will not have one. private SVGElement scElement = null; // original Province CSS style(s) if any. // if multiple styles, they will be separated by spaces private String provOriginalCSS = null; /** Create a Tracker object */ public Tracker() {} public SVGElement getUnitElement() { return elUnit; } public SVGElement getDislodgedUnitElement() { return elDislodgedUnit; } public Unit getUnit() { return unit; } public Unit getDislodgedUnit() { return dislodgedUnit; } public void setUnit(Unit u) { unit = u; } public void setDislodgedUnit(Unit u) { dislodgedUnit = u; } public void setProvinceHiliteElement(SVGElement el) { provHilite = el; if(provHilite != null) { provOriginalCSS = provHilite.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE); } }// setProvinceHiliteElement() public SVGElement getProvinceHiliteElement() { return provHilite; } /** Returns original province CSS style(s), or null, derived when setProvinceHiliteElement() was called. */ public String getOriginalProvinceCSS() { return provOriginalCSS; } /** * Returns the CSS class for the power, or, the original CSS defined in the * SVG file (if available) if power is null. If no original CSS exists, then, * no CSS value is returned. */ public String getPowerCSSClass(Power power) { if(power == null) { return getOriginalProvinceCSS(); } return getPowerName(power); }// getPowerCSSClass() public void setSCElement(SVGElement el) { scElement = el; } public SVGElement getSCElement() { return scElement; } public void setUnit(SVGElement el, Unit unit) { elUnit = el; this.unit = unit; }// setUnit() public void setDislodgedUnit(SVGElement el, Unit unit) { elDislodgedUnit = el; dislodgedUnit = unit; }// setDislodgedUnit() /** For debugging only */ public String toString() { StringBuffer sb = new StringBuffer(128); sb.append("elUnit="); sb.append(elUnit); sb.append(",unit="); sb.append(unit); sb.append(']'); return sb.toString(); }// toString() }// inner class Tracker /** Implicit class for MapInfo interface */ public class DMRMapInfo extends GUIOrder.MapInfo { public DMRMapInfo(TurnState ts) { super(ts); }// DMRMapInfo() public MapMetadata getMapMetadata() { return mapMeta; } public String getPowerCSS(Power power) { return DefaultMapRenderer2.this.getPowerName(power); } public String getUnitCSS(Power power) { return DefaultMapRenderer2.this.getUnitCSSClass(power); } public String getSymbolName(Unit.Type unitType) { return DefaultMapRenderer2.this.getSymbolName(unitType); } public SVGDocument getDocument() { return doc; } public Power[] getDisplayablePowers() { Power[] powers = super.getDisplayablePowers(); return powers; }// getDisplayablePowers() public SVGGElement getPowerSVGGElement(Power p, int z) { return DefaultMapRenderer2.this.getPowerSVGGElement(p, z); } }// nested class DMRMapInfo }// class DefaultMapRenderer