/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import tufts.Util; import tufts.vue.LWComponent.ColorProperty; import tufts.vue.LWComponent.Key; import tufts.vue.LWComponent.Property; import tufts.vue.filter.*; import tufts.vue.ds.Schema; import java.net.URI; import java.util.*; import java.awt.Dimension; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.AffineTransform; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.print.Printable; import java.awt.print.PageFormat; import java.io.File; /** * This is the top-level VUE model class. * * LWMap is a specialized LWContainer that acts as the top-level node * for a VUE application map. It has special handling code for XML * saves/restores, keeping a reference to an UndoManager & tracking if * map has been user modified, maintaing user values for global zoom & * pan offset, keeping a list of LWPathways and telling them to draw * when needed, generating uniqe ID's for LWComponents, maintaining map-wide * meta-data definitions, etc. * * As LWCEvent's issued by changes to LWComponents are bubbled up * through their parents, listenting to the map for LWCEvents will * tell you about every change that happens anywhere in the map. * For instance: the UndoManager is just another listener on the map. * (Note however, that the application depends on LWKey.UserActionCompleted * events delivered from the UndoManager to indicate the right time * to redisplay; e.g., in the panner tool and in other map viewers * of the same map). * * As it extends LWComponent/LWContainer, in the future it is * ready to be embedded / rendered as a child of another LWContainer/LWMap. * * @author Scott Fraize * @author Anoop Kumar (meta-data) * @version $Revision: 1.262 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ */ public class LWMap extends LWContainer { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWMap.class); /** file we were opened from of saved to, if any */ private File mFile; private String mSaveLocation; private URI mSaveLocationURI; private String mSaveFile; private File mLastSaveLocation; /** the list of LWPathways, if any */ private LWPathwayList mPathways; // todo: rename mPathwayList unless we get rid of mPathways in LWComponent (at least both are private) /** the author of the map **/ private String mAuthor; /** the date created **/ // todo: change to arbirary meta-data, and add modification date & user-name of modifier private String mDateCreated; // /** the current Map Filter **/ // LWCFilter mLWCFilter; /** Metadata for Publishing **/ PropertyMap metadata = new PropertyMap(); /* @deprecated - Map Metadata- this is for adding specific metadata and filtering **/ // MapFilterModel mapFilterModel = new MapFilterModel(); /* user map types -- is this still used? **/ //private UserMapType[] mUserTypes; /** the number of "changes" since we last saved. E.g., on deserialization, and after saves, this is 0. */ private long mChanges = 0; /** just like mChages, but this is never reset to zero. E.g., caches that depend on map state can use * this to see if the map has changed -- e.g., RDFIndex. */ private long mChangeState = 0; private Rectangle2D.Float mCachedBounds = null; // these for persistance use only private float userOriginX; private float userOriginY; private double userZoom = 1; //private transient boolean isLayered; private transient Layer mActiveLayer; /** for use during restores only */ private transient java.util.List<Layer> mLayers = new ArrayList(); //private transient Layer mInternalLayer; private transient int mSaveFileModelVersion = -1; private transient int mModelVersion = getCurrentModelVersion(); /** only used during restore */ private final Collection<Schema> mRestoredSchemas = new ArrayList(); private static final String InitLabel = "<map-during-XML-restoration>"; // only to be used during a restore from persisted public LWMap() { initMap(); setLabel(InitLabel); //mLWCFilter = new LWCFilter(this); // on restore, set to 0 initially: if has a model version in the save file, // it will be overwritten setModelVersion(0); } public LWMap(String label) { initMap(); setID("0"); setFillColor(java.awt.Color.white); setTextColor(COLOR_TEXT); setStrokeColor(COLOR_STROKE); setFont(FONT_DEFAULT); setLabel(label); mPathways = new LWPathwayList(this); //mLWCFilter = new LWCFilter(this); // Always do markDate, then markAsSaved as the last items in the constructor: // (otherwise this map will look like it's user-modified when it first displays) markDate(); installDefaultLayers(); ensureID(this); // make sure the new default layers get ID's markAsSaved(); } public static LWMap create(String filename) { final LWMap map = new LWMap("Empty Map"); map.setFile(new File(filename)); return map; } /** create a temporary, uneditable map that contains just the given component */ LWMap(LWComponent c) { this(c.getDisplayLabel()); if (c instanceof LWGroup && c.getFillColor() != null) setFillColor(c.getFillColor()); mChildren.add(c); // todo: listen to child for events & pass up } protected void initMap() { disablePropertyTypes(KeyType.STYLE); enableProperty(LWKey.FillColor); disableProperty(LWKey.Label); mFillColor.setAllowAlpha(false); // // // TODO: need to handle persistance -- could match via a special name, for maybe persistIsStyle // // mInternalLayer = new Layer("*Internal*"); // // mInternalLayer.setVisible(false); // // mInternalLayer.setFlag(Flag.INTERNAL); // // addChild(mInternalLayer); } // if/when we support maps embedded in maps, we'll want to have these return something real / make not final @Override public final float getX() { return 0; } @Override public final float getY() { return 0; } @Override public final float getMapX() { return 0; } @Override public final float getMapY() { return 0; } @Override protected final double getMapXPrecise() { return 0; } @Override protected final double getMapYPrecise() { return 0; } @Override public final boolean isCollapsed() { return false; } @Override public final boolean isAncestorCollapsed() { return false; } /** @return 1 -- currently for performance: remove this impl if we embed maps-in-maps and allow them to have their own scale */ @Override public final double getMapScale() { return 1; } /** Override LWContainer draw to always call drawInParent (even tho we have absolute children, we * don't want to just call draw, as LWContainer would). */ // todo: something cleaner than draw/drawInParent in LWComponent would be nice so // we wouldn't have to deal with this kind of issue. This is all because of the // slide icon hack -- if we can get rid of that, these issues would go away. @Override protected void drawChild(LWComponent child, DrawContext dc) { child.drawLocal(dc); } /** for persistance */ public int getModelVersion() { return mModelVersion; } /** for persistance */ public void setModelVersion(int version) { if (DEBUG.Enabled) { if (this.label != InitLabel) // don't bother with this message on construction Log.debug("setModelVersion " + version + "; current=" + mModelVersion); } mModelVersion = version; mSaveFileModelVersion = version; } public int getSaveFileModelVersion() { return mSaveFileModelVersion; } @Override public String getDiagnosticLabel() { return "Map: " + getLabel(); } private void markDate() { final long time = System.currentTimeMillis(); super.setCreated(time); java.util.Date date = new java.util.Date( time); java.text.SimpleDateFormat df = new java.text.SimpleDateFormat("yyyy-MM-dd"); String dateStr = df.format( date); setDate(dateStr); } private UndoManager mUndoManager; @Override public UndoManager getUndoManager() { return mUndoManager; } public void setUndoManager(UndoManager um) { if (DEBUG.EVENTS) out("setUndoManager " + um); if (mUndoManager != null) throw new IllegalStateException(this + " already has undo manager " + mUndoManager); mUndoManager = um; markAsSaved(); } public File getFile() { return mFile; } public void setFile(File file) { Log.debug("setFile " + file); mFile = file; if (mFile != null) { setLabel(mFile.getName()); // todo: don't let this be undoable! final File parentDir = mFile.getParentFile(); mSaveLocation = parentDir.toString(); Log.debug("saveLocation " + mSaveLocation); mSaveLocationURI = parentDir.toURI(); Log.debug("saveLocationURI " + mSaveLocationURI); takeResource(Resource.instance(mFile)); } else { takeResource(null); } // Don't handle relativzation here. On save, handle vie makeReadyForSaving, and on restore, // handle in XML_completed. // if (false && mFile != null && !mXMLRestoreUnderway) { // not turned on yet // // only do this on save: will be handled in completeXMLRestore // // for restores // relativizeResources(getAllDescendents(ChildKind.ANY), // mSaveLocationURI); // } } /** persistance only */ public String getSaveLocation() { return mSaveLocation == null ? null : mSaveLocation.toString(); } /** persistance only */ public String getSaveFile() { return mFile == null ? null : mFile.toString(); } /** persistance only */ public void setSaveLocation(String path) { mSaveLocation = path; mSaveLocationURI = null; // may not be valid if save file was from another platform } /** persistance only */ public void setSaveFile(String path) { mSaveFile = path; } public void markAsModified() { mChangeState++; if (DEBUG.Enabled) Log.debug("ensuring marked as modified: " + this + "; newChangeState = " + mChangeState); if (mChanges == 0) { mChanges = 1; if (DEBUG.Enabled) Log.debug("explicit marked as modified: " + this); } // notify with an event mark as not for repaint (and set same bit on "repaint" event) } public void markAsSaved() { if (DEBUG.Enabled && mChanges != 0) Log.debug("marking " + mChanges + " modifications as current: " + this); mChanges = 0; } public boolean isModified() { return mChanges > 0; } long getModCount() { return mChanges; } /** just like getModCount / mChanges, but this is never reset to zero. E.g., caches that depend on map state can use * this to see if the map has changed -- e.g., RDFIndex. */ public long getChangeState() { return mChangeState; } /** @deprecated this api / LWCFilter is no longer used -- always return false */ public final boolean isCurrentlyFiltered() { return false; } /** @deprecated this api / LWCFilter is no longer used -- always return null */ public final LWCFilter getLWCFilter() { return null; } /** @deprecated this api / LWCFilter is no longer used -- always return null */ public final UserMapType[] getUserMapTypes() { return null; } /** @deprecated this api / LWCFilter is no longer used -- always dump exception */ public final void setUserMapTypes(UserMapType[] x) { Log.warn("UserMapType deprecated: " + Util.tags(x), new Throwable("HERE")); } // SMF Summer 2012 commented out below en-masse: LWCFilter no longer used // /** // * getLWCFilter() // * This gets the current LWC filter // **/ // public LWCFilter getLWCFilter() { // return mLWCFilter; // } // /** @return true if this map currently conditionally displaying // * it's components based on a filter */ // public boolean isCurrentlyFiltered() { // return mLWCFilter != null && mLWCFilter.isFilterOn() && mLWCFilter.hasPersistentAction(); // } // /** // * This tells us there's a new LWCFilter or filter state in effect // * for the filtering of node's & links. // * This should be called anytime the filtering is to change, even if we // * already have our filter set to the given filter. We will // * apply / clear as appropriate to the state of the filter. // * @param LWCFilter the filter to install and/or update against // **/ // private boolean filterWasOn = false; // workaround for filter bug // public void setLWCFilter(LWCFilter filter) { // out("setLWCFilter: " + filter); // mLWCFilter = filter; // applyFilter(); // } // public void clearFilter() { // out("clearFilter: cur=" + mLWCFilter); // if (mLWCFilter.getFilterAction() == LWCFilter.ACTION_SELECT) // VUE.getSelection().clear(); // for (LWComponent c : getAllDescendents()) // c.setFiltered(false); // mLWCFilter.setFilterOn(false); // notify(LWKey.MapFilter); // } // public void applyFilter() // { // if (mLWCFilter.getFilterAction() == LWCFilter.ACTION_SELECT) // VUE.getSelection().clear(); // for (LWComponent c : getAllDescendents()) { // if (!(c instanceof LWNode) && !(c instanceof LWLink)) // why are we only doing nodes & links? // continue; // boolean state = mLWCFilter.isMatch(c); // if (mLWCFilter.isLogicalNot()) // state = !state; // if (mLWCFilter.getFilterAction() == LWCFilter.ACTION_HIDE) // c.setFiltered(state); // else if (mLWCFilter.getFilterAction() == LWCFilter.ACTION_SHOW) // c.setFiltered(!state); // else if (mLWCFilter.getFilterAction() == LWCFilter.ACTION_SELECT) { // if (state) // VUE.getSelection().add(c); // } // } // filterWasOn = true; // mLWCFilter.setFilterOn(true); // notify(LWKey.MapFilter); // only MapTabbedPane wants to know this, to display a filtered icon... // } // /** // * getUserMapTypes [was related to LWCfilter] // * This returns an array of available map types for this // * map. // * @return UserMapType [] the array of map types // **/ // public UserMapType [] getUserMapTypes() { // throw new UnsupportedOperationException("de-implemented"); // //return mUserTypes; // } // /** // * setUserMapTypes -- was related to LWCFilter // * \Types if(filterTable.isEditing()) { // * filterTable.getCellEditor(filterTable.getEditingRow(),filterTable.getEditingColumn()).stopCellEditing(); // * System.out.println("Focus Lost: Row="+filterTable.getEditingRow()+ "col ="+ filterTable.getEditingColumn()); // * } // * filterTable.removeEditor(); // * This sets the array of UserMapTypes for teh map // * @param pTypes - uthe array of UserMapTypes // **/ // public void setUserMapTypes( UserMapType [] pTypes) { // throw new UnsupportedOperationException("de-implemented"); // //mUserTypes = pTypes; // //validateUserMapTypes(); // } //--------------------------------------------------------------------------------------------------- // This stuff was previously commented out already: //--------------------------------------------------------------------------------------------------- // /* // * validateUserMapTypes // * Searches the list of LW Compone // private void validateUserMapTypes() { // java.util.List list = getAllDescendents(); // Iterator it = list.iterator(); // while (it.hasNext()) { // LWComponent c = (LWComponent) it.next(); // if ( c.getUserMapType() != null) { // // Check that type still exists... // UserMapType type = c.getUserMapType(); // if( !hasUserMapType( type) ) { // c.setUserMapType( null); // } // } // } // } **/ // /* // * hasUserMapType // * This method verifies that the UserMapType exists for this Map. // * @return boolean true if exists; false if not // private boolean hasUserMapType( UserMapType pType) { // boolean found = false; // if( mUserTypes != null) { // for( int i=0; i< mUserTypes.length; i++) { // if( pType.getID().equals( mUserTypes[i].getID() ) ) { // return true; // } // } // } // return found; // } **/ // todo: change this to arbitrary meta-data public String getAuthor() { return mAuthor; } // todo: change this to arbitrary meta-data public void setAuthor( String pName) { mAuthor = pName; } /** @deprecated - safe file back compat only - use getNotes - this always returns null */ public String getDescription() { //return mDescription; return null; } /** @deprecated - use setNotes -- this does nothing if notes already are set */ public void setDescription(String pDescription) { //mDescription = pDescription; // backward compat: we're getting rid of redunant // "description" field that was supposed to have // used notes in the first place. This should // only be happening from a map-restore, as // nobody should be calling setDescription anymore. if (!hasNotes()) setNotes(pDescription); } // creation date. todo: change this to arbitrary meta-data public String getDate() { return mDateCreated; } // creation date. todo: change this to arbitrary meta-data public void setDate(String pDate) { mDateCreated = pDate; } protected int mNodeCount, mLinkCount, mGroupCount, mConnectionCount, mMinConnections, mMaxConnections; // TODO: would be nice if this info were cached instead of computing // it every time we change the active map! Or better yet, ONLY // if the MapInspectorPanel is actually visibile. protected void countChildren(LWComponent c) { for (LWComponent child : c.getChildren()) { if (child instanceof LWGroup) { mGroupCount++; } else if (child instanceof LWNode) { mNodeCount++; int connections = 0; for (LWLink link: child.getLinks()) { LWComponent head = link.getPersistHead(), tail = link.getPersistTail(); if (head != null && tail != null && head instanceof LWNode && tail instanceof LWNode) { connections++; } } if (mMinConnections == -1 || connections < mMinConnections) { mMinConnections = connections; } if (connections > mMaxConnections) { mMaxConnections = connections; } mConnectionCount += connections; } else if (child instanceof LWLink) { mLinkCount++; } countChildren(child); } } /** the formatting should be in MapInspectorPanel, not LWMap */ public String getObjectStatistics() { mNodeCount = 0; mLinkCount = 0; mGroupCount = 0; mConnectionCount = 0; mMinConnections = -1; mMaxConnections = 0; countChildren(this); if (mMinConnections == -1) { mMinConnections = 0; } return String.format(Locale.getDefault(), VueResources.getString("mapinspectorpanel.objectStats.format"), VueResources.getString("mapinspectorpanel.objectStats.nodes"), mNodeCount, VueResources.getString("mapinspectorpanel.objectStats.links"), mLinkCount, VueResources.getString("mapinspectorpanel.objectStats.groups"), mGroupCount); } public String getConnectivityStatistics() { // Expects that getObjectStatistics() will be called first to make counts -- no need to do it again. double avg = (mNodeCount == 0 ? 0.0 : ((double)mConnectionCount) / (double)mNodeCount); return String.format(Locale.getDefault(), VueResources.getString("mapinspectorpanel.connectivityStats.format"), VueResources.getString("mapinspectorpanel.connectivityStats.min"), mMinConnections, VueResources.getString("mapinspectorpanel.connectivityStats.max"), mMaxConnections, VueResources.getString("mapinspectorpanel.connectivityStats.avg"), avg); } public PropertyMap getMetadata(){ return metadata; } public void setMetadata(PropertyMap metadata) { this.metadata = metadata; } /** @deprecated -- no longer used -- always returns null for old persistance compatability */ public MapFilterModel getMapFilterModel() { return null; //return mapFilterModel; } // /** @deprecated -- MapFilterModel no longer in use */ // public void setMapFilterModel(MapFilterModel mapFilterModel) { // //out("setMapFilterModel " + mapFilterModel); // this.mapFilterModel = mapFilterModel; // } public LWPathwayList getPathwayList() { return mPathways; } /** for persistance restore only */ public void setPathwayList(LWPathwayList l){ //System.out.println(this + " pathways set to " + l); mPathways = l; mPathways.setMap(this); } /** @return true */ @Override public final boolean isTopLevel() { return true; } /** @return true if we have any non-layer children */ @Override public boolean hasContent() { for (LWComponent c : getChildren()) { if (c instanceof Layer) { if (c.hasContent()) return true; } else return true; } return false; } /** @return all LWComponent's at the top level of all unlocked of the given kind */ public Collection<LWComponent> getTopLevelItems(ChildKind kind) { return getAllLayerDescendents(kind, new ArrayList(), Order.DEPTH, true); } private Collection<LWComponent> getAllLayerDescendents(ChildKind kind, Collection bag, Order order, boolean onlyTopLevel) { for (LWComponent layer : getChildren()) { // exclude the layer objects themseleves, but include their children if ((kind == ChildKind.VISIBLE || kind == ChildKind.EDITABLE) && layer.isHidden()) { ; // exclude invisible } else if (kind == ChildKind.EDITABLE && layer.isLocked()) { ; // exclude locked out } else if (layer instanceof Layer) { // should always be the case (is a Layer) if (onlyTopLevel) { // note: this will add hidden/filtered items as well -- could argue either way given // the usage (currently only used for getting nodes to project) bag.addAll(layer.getChildren()); } else { layer.getAllDescendents(kind, bag, order); } } else { // failsafe in case anything has leaked up to the map level Log.warn("child of map, not layer: " + layer); bag.add(layer); } } return bag; } // TODO PERFORMANCE: cache results for each kind in immutable lists; only flush if modification count goes up. // (to verify: modification count goes up when layers are locked, anything is hidden/shown, which will effect EDITABLE lists) @Override public Collection<LWComponent> getAllDescendents(final ChildKind kind, final Collection bag, Order order) { if (kind == ChildKind.ANY) { // include the layers and all descendents super.getAllDescendents(kind, bag, order); } else { getAllLayerDescendents(kind, bag, order, false); } if (kind == ChildKind.ANY) { if (mPathways != null) { for (LWPathway pathway : mPathways) { bag.add(pathway); pathway.getAllDescendents(kind, bag, order); } } } return bag; } /** * @return all instances of Resource objects found in the map, even if some of them point to the same destination. */ public Collection<Resource> getAllResources() { final Map<Resource,Boolean> resources = new IdentityHashMap(); // really, want IdentityHashSet for (LWComponent c : getAllDescendents(ChildKind.ANY)) if (c.hasResource()) resources.put(c.getResource(), Boolean.TRUE); return resources.keySet(); } /** * @return all destination unique (Resource.equals, compairing spec) Resources * found in the map. So even if there are different Resource instances in * the map that both have the same spec, only the first one found will be * in the returned collection. */ public Collection<Resource> getAllUniqueResources() { final Set resources = new HashSet(); for (Resource r : getAllResources()) { if (resources.add(r)) { if (DEBUG.RESOURCE) Log.debug("getAllUniqueResources: Found resource " + r); } else { if (DEBUG.RESOURCE) Log.debug("getAllUniqueResources: duplicate " + r); } } return resources; } private Collection<PropertyEntry> mArchiveManifest; public void setArchiveManifest(Collection<PropertyEntry> manifest) { mArchiveManifest = manifest; } public Collection getArchiveManifest() { if (mXMLRestoreUnderway) { if (mArchiveManifest == null) mArchiveManifest = new ArrayList(); } return mArchiveManifest; } private boolean isArchive; public void setArchiveMap(boolean asArchive) { isArchive = asArchive; } public boolean isArchiveMap() { return isArchive; //return hasLabel() && getLabel().endsWith("$map.vue"); //return hasLabel() && getLabel().endsWith(".vpk"); } private final java.util.concurrent.atomic.AtomicInteger mNextID = new java.util.concurrent.atomic.AtomicInteger(); protected String getNextUniqueID() { return Integer.toString(mNextID.getAndIncrement(), 10); } // private static void changeAbsoluteToRelativeCoords(LWContainer container, HashSet<LWComponent> processed) { // // LWContainer parent = container.getParent(); // // //if ((parent instanceof LWGroup || parent instanceof LWSlide) && !processed.contains(parent)) { // // if (parent instanceof LWSlide && !processed.contains(parent)) { // // // we should never see this (slide in a slide?) -- this was in case // // // we had a group inside a slide, which we saw before the slide itself, // // // and it never would have worked for something more than one level below // // // the slide! // // changeAbsoluteToRelativeCoords(parent, processed); // // } // for (LWComponent c : container.getChildList()) { // c.takeLocation(c.getX() - container.getX(), // c.getY() - container.getY()); // } // processed.add(container); // } /** recursively upgrade all children */ private void upgradeAbsoluteToRelativeCoords(Collection<LWComponent> children) { // We must process the components top-down: adjust parent, then adjust children // Start by calling this on the children of the map itself. The top level // children of the don't need adjusting -- only their children. for (LWComponent c : children) { if (c instanceof LWContainer && c.hasChildren()) { if (c instanceof LWGroup) { // old groups didn't support fill color, but may have had one persisted anyway: if (java.awt.Color.white.equals(c.getFillColor())) c.setFillColor(null); if (isGroupRelative(getModelVersion())) { if (DEBUG.Enabled) System.out.println(" DM#1 ALREADY RELATIVE EXCEPT LINKS: " + c); //((LWGroup)c).normalize(); // we normalize all groups later continue; } } if (!c.isManagingChildLocations()) upgradeChildrenFromAbsoluteToRelative((LWContainer) c); upgradeAbsoluteToRelativeCoords(c.getChildren()); } } } private void upgradeLinksToParentRelative(Collection<LWComponent> nodes) { for (LWComponent c : nodes) { if (c instanceof LWLink == false) continue; final LWLink link = (LWLink) c; final LWContainer parent = link.getParent(); if (parent instanceof LWMap) { // okay instanceof check: for explicit backward compat //if (DEBUG.Enabled) System.out.println("LINK ALREADY RELATIVE: " + link); } else { //if (DEBUG.Enabled) System.out.println("MAKING LINK PARENT RELATIVE: " + link); if (DEBUG.Enabled) link.out("MAKING LINK PARENT RELATIVE"); // theoretically the parent could be scaled -- e.g., link is in a group that // is in a node, tho this is rare case... // Parent container should already have been converted to relative, // so we now need to use getMapX/Y here for the offset: link.translate(-parent.getMapX(), -parent.getMapY()); } // Now make sure link is parented to it's common parent: link.run(); } } private void upgradeChildrenFromAbsoluteToRelative(LWContainer container) { if (DEBUG.Enabled) System.out.println("CONVERTING TO RELATIVE in " + this + "; container: " + container); for (LWComponent c : container.getChildren()) { if (c instanceof LWLink) continue; // todo??? use getX/Y or getMapX/Y ? if the component and container are UNCOVERTED ALREADY, // then getX/Y is fine, as these are already in the old absolute form -- but if, e.g., // the container were already converted, we need to use getMapX/Y -- check the order // of operations here... c.takeLocation(c.getX() - container.getX(), c.getY() - container.getY()); } } private final class NoNullsArrayList extends ArrayList<LWComponent> { NoNullsArrayList() { super(Math.max(numChildren(),10)); } @Override public boolean add(LWComponent c) { if (c == null) { Util.printStackTrace("null not allowed in map descendents"); } else { super.add(c); } return true; } } /** * A map will always have at least one layer, and may have any number of layers. * All layers within the map share exactly the same coordinate space -- this is an * inherent property of the layer implementation. Beyond that, how layers appear is * determined by the LWMap and the view controller (e.g., a MapViewer). That said, * generally speaking they're intended to be drawn visually one on top of another. * "Upper" layers appear on top, and will be picked first. Hiding a layer hides all * objects in the layer, and locking a layer locks all objects in a layer. */ public static final class Layer extends LWContainer { /** for persistance only */ public Layer() { initLayer(); setFillColor(null); } public Layer(String name) { initLayer(); setFillColor(null); setLabel(name); } private void initLayer() { // style properties not used on layers (pretty much no properties at all actually) // we disable them mainly to prevent warnings on layers with invalid values disablePropertyTypes(KeyType.STYLE); } @Override public float getX() { return 0; } @Override public float getY() { return 0; } @Override public float getMapX() { return 0; } @Override public float getMapY() { return 0; } @Override protected double getMapXPrecise() { return 0; } @Override protected double getMapYPrecise() { return 0; } //@Override public double getMapScale() { return getParent().getMapScale(); } // fully accurate, in case of maps in maps impl @Override public double getMapScale() { return 1; } // currently always true: for performance @Override protected AffineTransform transformDownA(final AffineTransform a) { return a; } @Override protected void transformDownG(final Graphics2D g) {} @Override public void transformZero(final Graphics2D g) {} @Override protected Rectangle2D transformMapToZeroRect(Rectangle2D mapRect) { return mapRect; } @Override public String getXMLfillColor() { return null; } @Override public String getXMLtextColor() { return null; } @Override public String getXMLstrokeColor() { return null; } @Override public String getXMLfont() { return null; } @Override public java.awt.Color getRenderFillColor(DrawContext dc) { return parent.getRenderFillColor(dc); } @Override public boolean isCollapsed() { return false; } @Override public boolean isAncestorCollapsed() { return false; } /** @return true */ @Override public boolean isTopLevel() { return true; } /** @return this */ @Override public Layer getLayer() { return this; } /** @return null */ @Override public Layer getPersistLayer() {return null; } @Override public boolean isFiltered() { return false; } /** currently a no-op: layers not allowed to be filtered */ @Override public void setFiltered(boolean filtered) { //super.setFiltered(filtered); //Util.printStackTrace("FILTERED=" + filtered + "; " + this); // if we re-enable this, we'll need to fix pathway exlusive display, which was // marking layers as filtered, and leaving them that way even after turning // that mode off, so the layers would be visible, but contents unselectable. if (DEBUG.Enabled) Log.debug("layers ignore filtered; (request=" + filtered + ") " + this); } @Override protected void setParent(LWContainer p) { if (p instanceof LWMap) { super.setParent(p); } else { //throw new IllegalArgumentException("Layers can only be parented to map; attempted to add " + this + " to " + p); Util.printStackTrace("Layers can only be parented to map; attempted to add " + this + " to " + p); } } /** @return true -- to support our UI impl that keeps listening JComponents around */ @Override protected boolean permitZombieEvent(LWCEvent e) { return true; } /** do nothing: when deleting, layers need to keep listeners active for our UI impl */ @Override public void removeAllLWCListeners() {} /** @return false */ @Override public boolean supportsMultiSelection() { return false; } /** @return false -- is editable, but not on the map */ @Override public boolean supportsUserLabel() { return false; } /** @return false */ @Override protected boolean selectedOrParent() { // even if a layer is selected, it's not part of the a "normal" hierarchical selection return false; } //@Override public edu.tufts.vue.metadata.MetadataList getMetadataList() { return null; } @Override public String getComponentTypeLabel() { return "Layer"; } // @Override // protected boolean intersectsImpl(Rectangle2D mapRect) { // // probably better to change LWComponent.requiresPaint // // such that there another API call to check along w/clipOptimized // // so we can skip making the intersects call entirely // // TODO: we can optimize this by having layers track their bounds, // // which they currently don't // return true; // } @Override protected boolean intersectsImpl(Rectangle2D mapRect) { // must always return FALSE otherwise can be picked via a region pick return false; } @Override public boolean requiresPaint(DrawContext dc) { // this is overkill for now: layers don't track their own bounds return isVisible(); } @Override protected Rectangle2D.Float getZeroBounds() { Util.printStackTrace(this + "; Layer getZeroBounds; always empty"); return EmptyBounds; } private void add(LWComponent c) { if (mChildren == NO_CHILDREN) mChildren = new ArrayList(); mChildren.add(c); } @Override protected void setAsChildAndLocalize(LWComponent c) { if (c.getParent() instanceof Layer) { // skip localization -- un-needed as layers share // same coordinate space if (DEBUG.Enabled && getMap() != null && c.getMap() != getMap()) Util.printStackTrace("cross-map reparenting! " + c.getMap() + " -> " + getMap()); c.setParent(this); } else { super.setAsChildAndLocalize(c); } } /** @return null to ensure will never be treated as styleable */ @Override public Object getTypeToken() { return null; } // Commenting out drawImpl/drawChild overrides turns off the fading out of // locked layers [VUE-1429] // @Override // protected void drawImpl(DrawContext dc) { // super.drawImpl(dc); // if (isLocked() && dc.focal != this && getParent().isOnBottom(this)) { // // this a better (easier to read) and faster method of fading out a // // layer, but it only works on the bottom layer. To make work on other // // layers, we'd have to render the layer offscreen first then fade out // // the results, which would be very slow. Also, this has a bug in that // // we're filling the clipRect, which can actually be larger an area than // // we'd ideally like to fade out (e.g., see what happens in the // // MapPanner). // // The problem case is if there are any layers BELOW this one that // // are not locked/faded out, they'd be faded out anyway by any // // locked layers above them. // dc.setAlpha(0.5); // dc.g.setColor(getMap().getFillColor()); // dc.g.fill(dc.g.getClipRect()); // } // } // @Override // protected void drawChild(LWComponent child, DrawContext dc) // { // if (isLocked() && !getParent().isOnBottom(this)) // dc.setAlpha(0.5); // super.drawChild(child, dc); // } /** @return null -- layer contents not persisted with layer for backward compat with old versions of VUE */ @Override public List<LWComponent> getXMLChildList() { return null; } @Override public Layer duplicate(CopyContext cc) { LWComponent c = super.duplicate(cc); if (hasLabel()) c.setLabel(getLabel() + " Copy"); return (Layer) c; } @Override public String toString() { try { return String.format("Layer[%s<%d> \"%s\" %2d%s]", getParent().getDisplayLabel(), getParent().indexOf(this), getDisplayLabel(), numChildren(), describeBits() ); } catch (Throwable t) { return super.toString(); } } } public LWContainer getActiveContainer() { return mActiveLayer == null ? this : mActiveLayer; } public Layer getActiveLayer() { return mActiveLayer; } public Layer addLayer(String name) { Layer layer = new Layer(name); addChild(layer); return layer; } /** @return the internal layer of the given name. Internal layers start at the back, intially locked */ public Layer getInternalLayer(String name) { Layer layer = findLayer(name); if (layer == null) { layer = new Layer(name); //layer.setLocked(true); layer.setFlag(Flag.INTERNAL); addChild(layer); sendToBack(layer); } return layer; } public Layer getOrCreateLayer(String name) { Layer layer = findLayer(name); if (layer == null) { layer = new Layer(name); addChild(layer); } return layer; } /** @return Layer if one by the given name is found */ public Layer findLayer(String name) { for (Layer layer : Util.typeFilter(getChildren(), Layer.class)) { if (name.equals(layer.getLabel())) { return layer; } } return null; } public void setActiveLayer(LWComponent layer) { if (DEBUG.PARENTING) Log.debug("setActiveLayer: " + layer); // Log.debug("setActiveLayer: " + layer, new Throwable("FYI")); if (layer == null || layer instanceof Layer) mActiveLayer = (Layer) layer; else Util.printStackTrace("not a layer: " + Util.tags(layer)); } @Override public java.util.List<LWComponent> getXMLChildList() { if (mXMLRestoreUnderway) { return super.getXMLChildList(); } else { // Layer handling if (hasChildren()) { List childrenInAllLayers = new ArrayList(); //childrenInAllLayers.addAll(mChildren); for (LWComponent c : getChildren()) { if (c instanceof Layer) { childrenInAllLayers.addAll(c.getChildren()); } else { Log.warn("getXMLChildList: non-layer direct child of map; " + this + ": " + c); childrenInAllLayers.add(c); } } Log.debug("getXMLChildList: built integrated list of all layer members, n=" + childrenInAllLayers.size()); return childrenInAllLayers; } else return null; } } public java.util.List<? extends LWComponent> getXMLLayers() { if (mXMLRestoreUnderway) { return mLayers; } else { // Layer handling if (Util.containsOnly(mChildren, Layer.class)) { // this should always be the case unless of model up-leakage return mChildren; } else { // just in case: never let anything that isn't a layer at the // top level be accidentally persisted as a layer Log.warn("getXMLLayers: " + (mChildren.size() - Util.countTypes(mChildren, Layer.class)) + " non-layer children were ignored in producing the layers-list"); return Util.extractType(mChildren, Layer.class); } } // else return null; } private boolean reparentAllToLayers() { boolean addedLayers = false; if (mLayers.size() > 0) { if (DEBUG.Enabled) Log.debug("restoring: found existing layers in " + this); for (Layer layer : mLayers) { for (LWComponent c : mChildren) { if (c.getParent() == layer) layer.add(c); } } // We should never have orphans, but just in case / for debug while testing this: List orphans = new ArrayList(); for (LWComponent c : mChildren) { if (c.getParent() instanceof Layer) { // what we want } else { orphans.add(c); Log.error("Layer orphaned node: " + c); } } mChildren.clear(); mChildren.addAll(orphans); } else { Log.debug("restoring: creating default layers in " + this); installDefaultLayers(); addedLayers = true; } //Log.debug("CHILDREN: " + Util.tags(mChildren)); //Log.debug(" LAYERS: " + Util.tags(mLayers)); mChildren.addAll(mLayers); if (!addedLayers) { // findLayer won't work until the above mChildren.addAll final Layer defaultLayer = findLayer("Default"); if (defaultLayer != null) setActiveLayer(defaultLayer); else if (mLayers.size() > 1) setActiveLayer(mLayers.get(1)); else setActiveLayer(mLayers.get(0)); } // mLayers is only needed during restore mLayers = null; //isLayered = true; return addedLayers; } private void installDefaultLayers() { // TODO: need to handle persistance -- could match via a special name, for maybe persistIsStyle // mInternalLayer = new Layer("*Internal*"); // mInternalLayer.setVisible(false); // mInternalLayer.setFlag(Flag.INTERNAL); // mInternalLayer.setParent(this); final Layer activeLayer; if (true) { final Layer layer0; layer0 = new Layer("Layer 1"); layer0.setParent(this); layer0.mChildren = this.mChildren; for (LWComponent c : this.mChildren) c.setParent(layer0); this.mChildren = new ArrayList(); this.mChildren.add(layer0); activeLayer = layer0; } else { // old style three inital layers final Layer layer0, layer1, layer2; layer0 = new Layer("Background"); layer0.setParent(this); //layer0.setVisible(false); layer1 = new Layer("Default"); layer1.setParent(this); layer1.mChildren = LWMap.this.mChildren; for (LWComponent c : LWMap.this.mChildren) c.setParent(layer1); layer2 = new Layer("Notations"); layer2.setParent(this); //layer2.setVisible(false); LWMap.this.mChildren = new ArrayList(); //mChildren.add(mInternalLayer); mChildren.add(layer0); mChildren.add(layer1); mChildren.add(layer2); activeLayer = layer1; } //isLayered = true; setActiveLayer(activeLayer); } static final String NODE_INIT_LAYOUT = "INIT_NODE_LAYOUT"; static final String LINK_INIT_LAYOUT = "INIT_LINK_LAYOUT"; private static final String INIT_LAYOUT = "<validating-layout>"; /** for debug when this is called on an LWMap only */ @Override public void layoutAll(Object trigger) { Log.info("layoutAll(" + Util.tags(trigger) + ") " + this); super.layoutAll(trigger); } /** to be called on maps that are manually created (e.g., not deserialized) before they're displayed */ public void layoutAndValidateNewMap() { layoutAll(INIT_LAYOUT); // final Collection<LWComponent> all = getAllDescendents(); // todo: probably should do as order-depth // layoutAll(all, INIT_LAYOUT); // will be auto-validated due to initial layout trigger // //validateAll(all); } // private void validateAll(Collection<LWComponent> components) { // for (LWComponent c : components) { // c.validateInitialValues(); // } // } /** note side effect: will clear all mXMLRestoreUnderway flags that are set * This processes all NODES first, then all LINKS. Then normalizes all groups just to be safe. * The components should be provided in depth-first order (Order.DEPTH), so that * children are fully laid out before their parents, which will need the proper size of * the children to lay out correctly. */ private void layoutAllAfterRestore(final Collection<LWComponent> components, final Object key) { Object layoutKey; if (key == INIT_LAYOUT) layoutKey = NODE_INIT_LAYOUT; else layoutKey = key; //----------------------------------------------------------------------------- // First, we layout all NON links, so we can layout the links afterwords, and when // the links recompute, they'll be able to know for certian the borders of what // they're connected to. Note that this means that the layout of a container // should not depend on a link be current yet. Groups depend on knowing the size // of their children, but that's not handled via the layout code -- that's handled // via group normalization. //----------------------------------------------------------------------------- for (LWComponent c : components) { // mark all, including links, now, as when we get to them, links-to-links may // cause cascading recomputes that would warn us they're still being restored otherwise. c.mXMLRestoreUnderway = false; if (c instanceof LWLink) continue; if (DEBUG.LAYOUT||DEBUG.INIT) out("LAYOUT NODE: in " + c.getParent() + ": " + c); try { c.layout(layoutKey); } catch (Throwable t) { Log.warn("LAYOUT-NODE/" + layoutKey + ": " + c, t); } } if (key == INIT_LAYOUT) layoutKey = LINK_INIT_LAYOUT; else layoutKey = key; //----------------------------------------------------------------------------- // Layout links -- will trigger recomputes & layout any link-labels that need it. //----------------------------------------------------------------------------- //if (!tufts.vue.action.SaveAction.PACKAGE_DEBUG) // tmp hack for (LWComponent c : components) { if (c instanceof LWLink == false) continue; if (DEBUG.LAYOUT||DEBUG.INIT) out("LAYOUT LINK: in " + c.getParent() + ": " + c); try { c.layout(layoutKey); } catch (Throwable t) { Log.warn("LAYOUT-LINK/" + layoutKey + ": " + c, t); } } //----------------------------------------------------------------------------- // Just to be sure, re-normalize all groups. This shouldn't be required, except // perhaps if we're updating from an old model version. //----------------------------------------------------------------------------- for (LWComponent c : components) { try { if (c instanceof LWGroup) ((LWGroup)c).normalize(); } catch (Throwable t) { Log.warn("GROUP-NORMALIZE/" + key + ": " + c, t); } } } public void completeXMLRestore(Object context) { if (DEBUG.INIT || DEBUG.IO || DEBUG.XML) Log.debug(getLabel() + ": completing restore..."); if (mChildren == NO_CHILDREN || mChildren == Collections.EMPTY_LIST || mChildren == null) { // If there was NO content in the map, we need to make sure we manually set // the child list to a real list before we do anything else. Note // that the only case we should have to check is NO_CHILDREN, but we // check for other problems just in case. This fixes VUE-1463. mChildren = new ArrayList(); } //----------------------------------------------------------------------------- // We do this every time, as nodes are always saved as children // of the map, so that old versions of VUE can at least // still see the flattened content. final boolean addedLayers = reparentAllToLayers(); //----------------------------------------------------------------------------- if (mPathways != null) { try { mPathways.completeXMLRestore(this); } catch (Throwable t) { tufts.Util.printStackTrace(new Throwable(t), "PATHWAYS RESTORE"); } } // Need to do this after complete XML restore, as getAllDescendents for special // componenets may otherwise not yet be ready to return everything (e.g. MasterSlide) final Collection<LWComponent> allRestored = getAllDescendents(ChildKind.ANY, new NoNullsArrayList(), //new ArrayList(Math.max(numChildren(),10)), Order.DEPTH); //resolvePersistedLinks(allRestored); if (mPathways == null) mPathways = new LWPathwayList(this); mNextID.set(findGreatestID(allRestored) + 1); if (addedLayers) { // if we created any layers, ensure their ID's now, after // we already know the greatest ID. for (LWComponent c : getChildren()) c.ensureID(c, false); } for (LWPathway pathway : mPathways) { // 2006-11-30 14:33.32 SMF: LWPathways now have ID's, // but they didn't used to, so make sure they // have an ID on restore in case it was a save file prior // to 11/30/06. ensureID(pathway); } //---------------------------------------------------------------------------------------- // Now update the model to the most recent data version //---------------------------------------------------------------------------------------- if (getModelVersion() < getCurrentModelVersion()) { // note that these are one-way upgrades that can only be run on old data model: // running them on newer data models will corrupt the location information. if (isGroupAbsolute(getModelVersion())) upgradeAbsoluteToRelativeCoords(getChildren()); if (getCurrentModelVersion() >= 4 && getModelVersion() < 4) upgradeLinksToParentRelative(allRestored); Log.info(this + " Updated from model version " + getModelVersion() + " to " + getCurrentModelVersion()); mModelVersion = getCurrentModelVersion(); } final Collection allResources = getAllResources(); // Note: by this time, some duplicate resources have been removed from the map/ // E.g., "image" nodes, where the LWNode and the the LWImage both point to the // same resource spec, the reference to the instance for the de-serialized // LWNode is pointed to the instance in LWImage. See LWNode.setResource. This // simplifies things, improves performance, and makes debugging easier. // Someday, we may change castor persistance to use Resource references in the // LWComponents, and then persist a separate list of all Resources with the map, // but for now multiple references to the same Resource object are re-persisted // each time in the map. if (isArchiveMap()) { // Archive maps don't currently look for relative resources: just run all final inits //runResourceDeserializeInits(allResources); is run manually by Archive runResourceFinalInits(allResources); } else { // DEFAULT: LOOK FOR MAP-RELATIVE RESOURCES (files in same directory as the map, or below it) // (1) First: load the property maps to we can find @file.relative properties: runResourceDeserializeInits(allResources); // (2) Now, patch up old absolute resource locations that were relative to the map // to the new absolute locations: if (mSaveLocationURI == null) { Log.info("unrooted map (no setFile) -- skipping search for relative resources; " + this); } else { // todo: we should be able to get rid of mSaveLocationURI and use mFile // (and canonicalize it like we do for recordRelativeLocations) // setFile should already have been called as per MapUnmarshalHandler, // unless this is an archive map, in which case we could be here. restoreRelativeLocations(allResources, mSaveLocationURI); } // (3) Then run final inits: runResourceFinalInits(allResources); // Now, we could look for any NEW relative's that weren't recorded before, tho // we shouldn't touch any existing relatives: Need as an update/second pass // tho so we don't touch any we've already determined are relative as they were. // if (getFile() != null) // recordRelativeLocations(getAllResources(), getFile().getParentFile()); } mResourceFactory.loadResources(allResources); Schema.restoreSavedMapSchemas(this, mRestoredSchemas, allRestored); // for now, any restored may is assumed to have already done an auto-cluster setState(State.HAS_AUTO_CLUSTERED); //---------------------------------------------------------------------------------------- // Now lay everything out. allRestored should be in depth-first order for maximum // reliability (the deepest items should lay themselves out first, so parent items // that need to know the size of their children will get accurate results). //---------------------------------------------------------------------------------------- // Do NOT normalize the groups yet: will seriously break old maps. It slighly improves some of our // interim formats (1-2), but makes others a complete mess. // for (LWComponent c : allRestored) // if (c instanceof LWGroup) // ((LWGroup)c).normalize(); // tmp hack: we were geting exceptions when testing just SaveAction on this code? //if (!tufts.vue.action.SaveAction.PACKAGE_DEBUG) layoutAllAfterRestore(allRestored, INIT_LAYOUT); if (DEBUG.INIT || DEBUG.IO || DEBUG.XML) Log.debug("RESTORE COMPLETED; nextID=" + mNextID.get()); mXMLRestoreUnderway = false; markAsSaved(); } class ResourceFactory extends Resource.DefaultFactory { private final Map<String,Resource> resourceMap = new java.util.concurrent.ConcurrentHashMap(); void loadResources(Collection<Resource> resourceBag) { for (Resource r : resourceBag) track(r); } private void track(Resource r) { final Resource already = resourceMap.put(r.getSpec(), r); if (already != null) { // this okay for the moment: we're only using this for keeping // image data up to date if (DEBUG.WORK) Log.debug("duplicate tossed: " + already); if (DEBUG.WORK) Log.debug(" : " + r); } } private Resource trackNew(Resource r) { final Resource already = resourceMap.get(r.getSpec()); // This is not currently a very efficient way to do this -- // override the create methods to first check for a cache // member before going through all this. if (already != null) { if (DEBUG.Enabled) { Log.debug("tossing: " + r); Log.debug("reusing: " + already); } return already; } else { track(r); if (DEBUG.Enabled) Log.debug("keeping: " + r); return r; } } @Override protected Resource postProcess(Resource r, Object source) { Log.info("created: " + r + " from " + Util.tags(source)); final Resource using = trackNew(r); // Of course, do NOT want to call dataHasChanged here, as it will use-up the // update. Since newly dropped objects get auto-selected, our auto-update // code should automatically run for now -- when dropping single objects // that is: multiple drops will fail to trigger an update, as there won't be // a single-selection, so that's why this wants to be refactored further, // and have the update code currently in VUE.java (update of all LWImages) // triggerable from here. // Also, we can probably add to the code to the factory for ensuring that // any new resource added to any map element, is duplicated if it's in any // other map -- may need to have the Resource object itself point back to // it's map. Tho as long as doing that, might as well just make it an // "owner", that could be an LWComponent, or null if a resource not attached // to a map (My Computer browser, etc), or keyed to a special owner, or for // search results (could point back to repository if that's helpful). // if (using != r && using.dataHasChanged()) Log.info("DO AN UPDATE"); return using; } // not turned on yet -- see if can move to Resource.java if we keep. // if (mSaveLocationURI != null) { // //URI curRoot = URLResource.makeURI(mSaveLocation.getParentFile()); // //if (curRoot != null) { // r.updateRootLocation(mSaveLocationURI, null); // //} // } private void recordInode(Resource r, Object source) { if (Util.isMacPlatform() && (source instanceof java.io.File || source instanceof String)) { String inode = Util.getSystemCommandOutput(new String[] { "/usr/bin/stat", "-f", "%i", ""+source }, getSaveLocation()); if (inode != null) r.setHiddenProperty("file.MacOSX.inode", inode); } } } private final ResourceFactory mResourceFactory = new ResourceFactory(); @Override public Resource.Factory getResourceFactory() { return mResourceFactory; } /** * Perform any actions on the map we want to happen just before it is persisted to the given file. * E.g., record the map-relative location of any local file resources. */ public void makeReadyForSaving(File file) { if (file == null) { Log.debug("makeReadyForSaving: null file, must be archive"); return; } Log.debug("makeReadyForSaving to " + file); // if (file == null) { // Util.printStackTrace("makeReadyForSaving: no file"); // return; // } recordRelativeLocations(getAllResources(), file.getParentFile()); } private Collection<Schema> findAllSchemas() { final Set<Schema> allSchemas = new HashSet(); for (LWComponent c : getAllDescendents()) { Schema s = c.getDataSchema(); if (s != null) allSchemas.add(s); } return allSchemas; } @Override public Collection<Schema> getIncludedSchemas() { if (mXMLRestoreUnderway) return mRestoredSchemas; else return findAllSchemas(); } // // TODO: actually, put these right in the Schema? Or redundantly right in the Field??? // public Collection<tufts.vue.ds.Association> getIncludedAssociations() { // if (mXMLRestoreUnderway) // return null; // else // return tufts.vue.ds.Association.getAll(); // } // public void setIncludedSchemas(Collection<Schema> schemas) { // Log.debug("FYI, PERSISTED SCHEMA HANDLES WERE: " + schemas, new Throwable("FYI")); // } private void recordRelativeLocations(Collection<Resource> resources, File mapSaveDirectory) { final URI root = Resource.toCanonicalFile(mapSaveDirectory).toURI(); if (DEBUG.Enabled) Log.debug("relativizing any resources local to: " + root); for (Resource r : resources) { try { r.recordRelativeTo(root); } catch (Throwable t) { Log.warn(this + "; recordRelativeLocations failure " + root + ": " + t + "; " + r, t); } } } private void restoreRelativeLocations(Collection<Resource> resources, URI root) { if (DEBUG.IO || DEBUG.INIT || DEBUG.RESOURCE) { Resource.dumpURI(root, Util.TERM_GREEN + "resolving resources to map root;"); System.out.print(Util.TERM_CLEAR); } for (Resource r : resources) { try { r.restoreRelativeTo(root); } catch (Throwable t) { Log.warn(this + "; restoreRelativeLocations failure " + root + ": " + t + "; " + r, t); } } } // public only for Archive to be able to call us: clean that up public void runResourceDeserializeInits(Collection<Resource> resources) { if (DEBUG.RESOURCE || DEBUG.IO) Log.debug(Util.TERM_CYAN + "initAfterDerserialize for all resources; " + Util.tags(resources) + Util.TERM_CLEAR); for (Resource r : resources) { try { r.initAfterDeserialize(this); } catch (Throwable t) { Log.warn(this + "; failure on: " + r, t); } } } private void runResourceFinalInits(Collection<Resource> resources) { if (DEBUG.Enabled) Log.debug(Util.TERM_CYAN + "initFinal's for all resources; " + Util.tags(resources) + Util.TERM_CLEAR); for (Resource r : resources) { try { r.initFinal(this); } catch (Throwable t) { Log.warn(this + "; failure on: " + r, t); } } } // private void relativizeResources(Collection<LWComponent> nodes, URI root) { // for (LWComponent c : nodes) { // if (!c.hasResource()) // continue; // try { // c.getResource().makeRelativeTo(root); // } catch (Throwable t) { // Log.warn(this + "; relativize failure: " + t + "; " + c.getResource(), t); // //t.printStackTrace(); // } // } // } // private void ensureAllResourcesFoundAndRelative(Collection<LWComponent> nodes, File oldMapLocation) // { // final File oldParentDirectory = oldMapLocation.getParentFile(); // final File newParentDirectory = mFile.getParentFile(); // if (oldParentDirectory == null) { // Util.printStackTrace("Unable to find parent of " + oldMapLocation + "; can't relativize local resources."); // return; // } // Log.info(" SAVED MAP FILE: " + oldMapLocation); // Log.info("SAVED MAP LOCATION: " + oldParentDirectory); // //final URI oldRoot = oldParentDirectory.toURI(); // final URI oldRoot = URLResource.makeURI(oldParentDirectory); // final URI newRoot; // if (oldRoot == null) { // Log.error(this + "; unable to parse old parent directory: " + oldParentDirectory); // return; // } // Resource.dumpURI(oldRoot, "ROOT SAVED"); // if (oldParentDirectory.equals(newParentDirectory)) { // System.err.println("ROOT NEW URI: (same)"); // newRoot = null; // } else { // newRoot = newParentDirectory.toURI(); // Resource.dumpURI(newRoot, "ROOT NEW OPENED"); // } // // Normalize resources // for (LWComponent c : nodes) { // if (!c.hasResource()) // continue; // try { // //Log.info(this + "; relativize: " + c.getResource()); // c.getResource().updateRootLocation(oldRoot, newRoot); // } catch (Throwable t) { // Log.warn(this + "; relativiztion: " + t + "; " + c.getResource()); // } // } // } LWComponent findByID(Collection<LWComponent> allRestored, String id) { for (LWComponent c : allRestored) if (id.equals(c.getID())) return c; tufts.Util.printStackTrace("Failed to child child with id [" + id + "]"); return null; } /** for use during restore */ private int findGreatestID(final Collection<LWComponent> allRestored) { LWComponent mostRecent = findMostRecentlyCreated(allRestored, null); if (mostRecent != null) return mostRecent.getNumericID(); else return -1; } public LWComponent findMostRecentlyCreated(final Collection<LWComponent> components, Object typeToken) { int maxID = -1; LWComponent mostRecent = null; for (LWComponent c : components) { if (c.getID() == null) { if (!(c instanceof LWPathway)) { Log.warn("found a child persisted without an id: " + Util.tags(c)); } continue; } if (typeToken != null && c.getTypeToken() != typeToken) continue; int curID = c.getNumericID(); if (curID > maxID) { maxID = curID; mostRecent = c; } } return mostRecent; } public LWComponent findMostRecentlyCreated() { return findMostRecentlyCreated(getAllDescendents(ChildKind.ANY), null); } public LWComponent findMostRecentlyCreatedType(Object typeToken) { return findMostRecentlyCreated(getAllDescendents(ChildKind.ANY), typeToken); } /** * @return the list of children */ // overridden for performance @Override public final java.util.List<LWComponent> getPickList(PickContext pc, List<LWComponent> stored) { return getChildren(); } // do nothing //void setScale(float scale) { } @Override public void draw(DrawContext dc) { if (DEBUG.SCROLL || DEBUG.CONTAINMENT) { dc.g.setColor(java.awt.Color.green); dc.setAbsoluteStroke(1); dc.g.draw(getBounds()); } /* if (!dc.isInteractive()) { out("FILLING with " + getFillColor() + " " + dc.g.getClipBounds()); //tufts.Util.printStackTrace(); dc.g.setColor(getFillColor()); dc.g.fill(dc.g.getClipBounds()); } */ /* * Draw all the children of the map. * * Note that when the map draws, it does NOT fill the background, * as the background color of the map is usually a special case * property used to completely fill the background of an underlying * GUI component or an image. */ // We don't draw the pathways on top if we're zoomed in: otherwise // they may obscure a pseudo-focal (e.g., a slide) if (dc.zoom > PathwayOnTopZoomThreshold || dc.isPresenting()) { // VUE-1177 drawPathways(dc); super.drawChildren(dc); // draw all layers } else { super.drawChildren(dc); // draw all layers drawPathways(dc); } // if (DEBUG.BOXES) { // dc.g.setColor(java.awt.Color.red); // dc.g.setStroke(STROKE_ONE); // for (LWComponent c : getAllDescendents()) { // if (c.isDrawingSlideIcon()) // dc.g.draw(c.getMapSlideIconBounds()); // } // } } private void drawPathways(DrawContext dc) { if (mPathways != null && dc.drawPathways()) { LWPathway active = getActivePathway(); for (LWPathway path : mPathways) { if (path.isDrawn()) { if (path == active) continue; path.drawPathway(dc.create()); } else if (path == active) active = null; // active isn't being drawn } // Draw the active one last (on top) // If these all have equal transparency, this shouldn't // actually make a visible difference, but if the active // should take on non-transparent value, it definitely // will. if (active != null) { final DrawContext pdc = dc.create(); active.drawPathway(pdc); pdc.dispose(); } } // if (mPathways != null && dc.drawPathways()) { // int pathIndex = 0; // for (LWPathway path : mPathways) { // if (path.isDrawn()) { // dc.setIndex(pathIndex++); // path.drawPathway(dc.create()); // } // } // } } //for peristance public String getPresentationBackground() { return mPresentationColor.asString(); } //for persistance public void setPresentationBackground(String c) { mPresentationColor.setFromString(c); } public java.awt.Color getPresentationBackgroundValue() { return mPresentationColor.get(); } public void setPresentationBackgroundValue(java.awt.Color c) { mPresentationColor.set(c); } public java.awt.image.BufferedImage createImage(double alpha, java.awt.Dimension maxSize, java.awt.Color fillColor, double mapZoom) { return super.createImage(alpha, maxSize, fillColor == null ? getFillColor() : fillColor, mapZoom); } // /** Override default image getter to double the scale on the rendered map */ // @Override // public java.awt.image.BufferedImage getAsImage() { // return getAsImage(OPAQUE, null, 2.0); // } // Actually, as dragged images produce highest-res RAW image data (converted to TIFF if, e.g., dropped // into the Apple Mail application), we don't really need to double-up the resolution here. (And // doing so can produce HUGE 20MB+ tiff attachments). [Addendum: not all apps will create a tiff version tho: e.g., Skitch] /** for viewer to report user origin sets via pan drags */ void setUserOrigin(float x, float y) { if (userOriginX != x || userOriginY != y){ this.userOriginX = x; this.userOriginY = y; //markChange("userOrigin"); } } /** for persistance */ public Point2D.Float getUserOrigin() { return new Point2D.Float(this.userOriginX, this.userOriginY); } /** for persistance */ public void setUserOrigin(Point2D.Float p) { setUserOrigin((float) p.getX(), (float) p.getY()); } /** for persi if(filterTable.isEditing()) { * filterTable.getCellEditor(filterTable.getEditingRow(),filterTable.getEditingColumn()).stopCellEditing(); * System.out.println("Focus Lost: Row="+filterTable.getEditingRow()+ "col ="+ filterTable.getEditingColumn()); * } * filterTable.removeEditor();stance. Note that as maps can be in more than * one viewer, each with it's own zoom, we take on only * the zoom value set in the more recent viewer to change * it's zoom. */ public void setUserZoom(double zoom) { this.userZoom = zoom; } private double tempZoom = 0; private Point2D.Float tempOrigin = null; private Rectangle2D tempBounds = null; public void setTempBounds(Rectangle2D bounds) { tempBounds = bounds; } public Rectangle2D getTempBounds() { return tempBounds; } public void setTempZoom(double zoom){ this.tempZoom = zoom; } public double getTempZoom() { return tempZoom; } public void setTempUserOrigin(Point2D.Float origin) { this.tempOrigin = origin; } public Point2D.Float getTempUserOrigin() { return tempOrigin; } /** for persistance */ public double getUserZoom() { return this.userZoom; } @Override public LWMap getMap() { return this; } /** @return false: maps can't be selected with anything else */ public boolean supportsMultiSelection() { return false; } /** @return false -- maps aren't moveable objects */ @Override public boolean isMoveable() { return false; } /** @return Float.MAX_VALUE: map contains all points, but any contents take priority */ @Override protected final float pickDistance(float x, float y, PickContext pc) { return Float.MAX_VALUE; } /** override of LWContainer: default hit component on the map * is nothing -- we just @return null. */ @Override protected final LWComponent defaultPick(PickContext pc) { //return this; // allow picking of the map // OPTIMIZATION: if embed maps in maps, lose this override (and make LWComponent version final) return null; } /* override of LWComponent: parent == null indicates deleted, * but map parent is always null. For now always returns * false. If need to support tracking deleted map, create * a different internal indicator for LWMap's [OLD] public boolean isDeleted() { return false; } */ /** override of LWComponent: normally, parent == null indicates orphan, * which is considered a problem condition if attempting to deliver * events, but this is normal for the LWMap which as no parent, * so this always returns false. */ @Override public boolean isOrphan() { return false; } /** deprecated */ public LWNode addNode(LWNode c) { addChild(c); return c; } /** deprecated */ public LWLink addLink(LWLink c) { addChild(c); return c; } public LWPathway addPathway(LWPathway p) { ensureID(p); getPathwayList().add(p); return p; } public LWPathway getActivePathway() { if (getPathwayList() != null) return getPathwayList().getActivePathway(); else return null; } @Override public void addChildren(Collection<? extends LWComponent> children, Object context) { // This code is a backward-compat hack for other code that is attempting to // add children to the map. It used to be you could just do this directly, // but now that we have layers, only a Layer should be a direct child of the // map, and we need to divert this to add call to the appropriate layer. if (children.size() == 1 && Util.getFirst(children) instanceof LWMap.Layer) { //isLayered = true; super.addChildren(children, context); } else if (/*isLayered() &&*/ mActiveLayer != null) { mActiveLayer.addChildren(children, context); } else { super.addChildren(children, context); } } @Override protected void addChildImpl(LWComponent c, Object context) { if (c instanceof LWPathway) throw new IllegalArgumentException("LWPathways not added as direct children of map: use addPathway " + c); if (c instanceof Layer == false && !mXMLRestoreUnderway) { if (mActiveLayer != null) { Log.warn("addChildImpl: forcing to active layer: " + mActiveLayer + " in " + this); mActiveLayer.addChildImpl(c, context); } else { Util.printStackTrace("LWMap adding non-layer: " + Util.tags(c)); } } super.addChildImpl(c, context); } public LWComponent add(LWComponent c) { addChild(c); return c; } /* private void removeLWC(LWComponent c) { removeChild(c); } */ /** @return true: maps are always "alive" -- they always generate events */ protected final boolean alive() { return true; } /** * Every single event anywhere in the map will ultimately end up * calling this notifyLWCListners. */ @Override protected void notifyLWCListeners(LWCEvent e) { if (mChangeSupport.eventsDisabled()) { if (DEBUG.EVENTS) System.out.println(e + " SKIPPING (events disabled)"); return; } if (e.isUndoable()) markChange(e); if (mCachedBounds != null) { Rectangle2D.Float outside = null; if (e.key == LWKey.UserActionCompleted) { flushBounds(); // force re-compute / re-cache while user is likely idle, tho getPaintBounds // may well have already been called my something else by then. VUE.invokeAfterAWT(EnsureBoundsCacheIsFilled); // VUE/AWT ref in model: not ideal } // else if (e.key instanceof LWComponent.Key && ((LWComponent.Key)e.key).isColor) { // // ignore [speeds up colors, but slows down everything else: e.g., drags] // } else if (e.component != null && mCachedBounds.contains(outside = e.component.getPaintBounds())) { // Do nothing. Whatever happened to the component, its bounds are still inside the cached // bounds. This works for making sure GROW the bounds when needed, but it won't SHRINK them. // We handle that above by forcing re-compute on UserActionCompleted. Todo: can skip this // check entirely for many events that can't touch bounds: e.g., colors. } else { // Todo: further optimize: could have a bounds event / flag for events that can // touch bounds. Note: we may only need this for the scroll-pane? If so, maybe it // should handle tracking this itself. In any case, may NOT need it when in // full-screen mode. if (outside != null) mCachedBounds.add(outside); else flushBounds(); } } super.notifyLWCListeners(e); } /** javac should be smart enough to automatically create a single instance of these closures ** when used inline for its enclosing class instance (LWMap in this case) when nothing other ** than "this" is used, but it's not, so here it is not inline. (For that matter, it should ** simply have method references, which would erase the need for this construct entirely, but ** i digress). */ private final Runnable EnsureBoundsCacheIsFilled = new Runnable() { public void run() { getMapBounds(); }}; private void flushBounds() { mCachedBounds = null; if (DEBUG.EVENTS&&DEBUG.META) out(this + " flushed cached bounds"); } private void markChange(LWCEvent e) { if (!javax.swing.SwingUtilities.isEventDispatchThread()) { // for now, anything from a non AWT Event Dispatch Thread (EDT) is assumed to not be a // real undoable change -- this mainly to prevent image size sets after the map loads // from leaving the map appearing to have been modified. A more complete solution // might mark all events generated on specific threads known to be behaving this way. // NOTE: VUE features that modify the map OFF the AWT thread will need to compensate // for this by manually marking the map as changed. Better would be to only skip this // if the modification comes from an image load thread, which is why this was // implemented in the first place: to prevent freshly loaded maps with images to // immediately present as having already been modified. This was much more of an issue // when we had a preference for image icon size, as changing this preference between // the saves of a particular map file would force all the image icons in the map to // "modify" themselves when the map was openened. Now it's only an issue if image data // actually changes on disk. And in fact, in that case, we may well want to report it // as modified... if (DEBUG.WORK || DEBUG.EVENTS || DEBUG.INIT) Log.debug("staying clean for non-AWT event: " + e); return; } if (mChanges == 0) { if (DEBUG.EVENTS) out(this + " First Modification Happening on " + e); if (DEBUG.INIT||DEBUG.UNDO||(DEBUG.EVENTS&&DEBUG.META)) { Log.debug("FYI: FIRST MODIFICATION", new Throwable("HERE")); } } mChanges++; mChangeState++; if (DEBUG.UNDO) { //String msg = "MARKED TO +" + mChanges + " WITH OLD VALUE: " + Util.tags(e.oldValue) + "; " + e; String msg = "MARKED TO +" + mChanges + " ON " + e; if (true||mXMLRestoreUnderway) Log.debug(msg); else Log.debug(msg, new Throwable("HERE")); } } @Override protected Rectangle2D.Float getZeroBounds() { //Util.printStackTrace("LWMap getZeroBounds " + this); Log.warn("LWMap getZeroBounds " + this); return getPaintBounds(); } // @Override // public java.awt.geom.Rectangle2D.Float getBounds() { // return getBounds(Short.MAX_VALUE); // } // public java.awt.geom.Rectangle2D.Float getBounds(int maxLayer) { /** * Return the bounds of the entire map. NOTE THAT THE RETURNED OBJECT SHOULD NOT BE MODIFIED. * This is called so often that we keep a single cached object with the current value. */ @Override public Rectangle2D.Float getMapBounds() { // [TODO: OPTIMIZE! This is getting called EIGHT (8) times per event -- e.g. computing the entire // bounds of the map 8 times per mouse drag when moving a component around. All of them seem to be // coming from adjustCanvasSize in MapViewer, which is being called every time we get an event. We may // finally want that isBoundsEvent flag in LWComponent.Key, either that, or have a special info-only // bounds event delivered any time the bounds change, so parties interested in bounds changes // (MapViewer's, LWGroup's) could pay attention to just that event.] [somewhat optimized now -- see // where flushBounds is called] if (mCachedBounds == null) { //mCachedBounds = getBounds(getChildIterator(), maxLayer); //mCachedBounds = getPaintBounds(getChildIterator()); //if (DEBUG.CONTAINMENT && DEBUG.META) Log.debug("COMPUTING BOUNDS..."); // okay, this is actually pretty fast mCachedBounds = getPaintBounds(); takeSize(mCachedBounds.width, mCachedBounds.height); //takeLocation(mCachedBounds.x, mCachedBounds.y); /* try { setEventsSuspended(); setFrame(mCachedBounds); } finally { setEventsResumed(); } */ //System.out.println(getLabel() + " cachedBounds: " + mCachedBounds); //if (!DEBUG.SCROLL && !DEBUG.CONTAINMENT) //mCachedBoundsOld = false; //if (DEBUG.CONTAINMENT && DEBUG.META) if (DEBUG.CONTAINMENT && DEBUG.META) Log.debug("COMPUTED BOUNDS " + Util.fmt(mCachedBounds) + "; for " + this); } //setSize((float)bounds.getWidth(), (float)bounds.getHeight()); //new Throwable("computedBounds").printStackTrace(); return mCachedBounds; } /** this object is returned if a get*Bounds result had no contents / produced no actual bounds */ public static final Rectangle2D.Float EmptyBounds = new Rectangle2D.Float(); // private static final class Rect // extends Rectangle2D.Float // { // private final String type; // for debug // private Rectangle2D.Float rect; // Rect(String type) { // this.type = type; // } // void add(final Rectangle2D.Float r, // final LWComponent debug) // { // try { // if (Util.isBadRect(r)) { // Log.error(String.format("%s%-17s %s%s for %s", // Util.TERM_RED, // "bad " + type + ": ", // Util.fmt(r), // Util.TERM_CLEAR, // debug)); // } else if (rect == null) { // rect = new Rectangle2D.Float(); // rect.setRect(r); // } else { // rect.add(r); // } // } catch (Throwable t) { // Log.error("AccumeRect " + type + " for " + debug, t); // } // } // Rectangle2D.Float result() { // return rect == null ? EmptyBounds : rect; // } // } // /** // * @return the bounds for all LWComponents in the iterator // */ // public static Rectangle2D.Float getBounds(Iterator<LWComponent> i) // { // final Rect rect = new Rect("bounds"); // while (i.hasNext()) { // final LWComponent c = i.next(); // if (c.isDrawn()) // rect.add(c.getBounds(), c); // } // return rect.result(); // } private static void accumulate (final Rectangle2D.Float accume, final Rectangle2D.Float r, final LWComponent debug, final String debugMsg) { try { if (Util.isBadRect(r)) { Log.error(String.format("%s%-17s %s%s for %s", Util.TERM_RED, "bad " + debugMsg + ": ", Util.fmt(r), Util.TERM_CLEAR, debug)); } else if (accume.isEmpty()) { accume.setRect(r); } else { accume.add(r); } } catch (Throwable t) { Log.error("accumulate " + debugMsg + " for " + debug, t); } } @Override public Rectangle2D.Float getPaintBounds() { if (mChildren == NO_CHILDREN) return EmptyBounds; final Rectangle2D.Float bounds = new Rectangle2D.Float(); for (LWComponent layer : getChildren()) { if (layer.isVisible()) { if (layer instanceof Layer) { // this should always be the case accruePaintBounds(layer.getChildren(), bounds); } else { // but in case of error in maintaining the hierarchy, if any // regular components leak up to the top of the map, still // compute bounds correctly. accruePaintBounds(Util.iterable(layer), bounds); } } } return bounds.isEmpty() ? EmptyBounds : bounds; } public static void accruePaintBounds(Iterable<LWComponent> iterable, Rectangle2D.Float rect) { for (LWComponent c : iterable) if (c.isDrawn()) accumulate(rect, c.getPaintBounds(), c, "paintBounds"); } @Override public Rectangle2D.Float getFocalBounds() { return getPaintBounds(); } /** * @return the bounds for all LWComponents in the iterator */ public static Rectangle2D.Float getBounds(Iterator<LWComponent> i) { final Rectangle2D.Float rect = new Rectangle2D.Float(); while (i.hasNext()) { final LWComponent c = i.next(); if (c.isDrawn()) accumulate(rect, c.getBounds(), c, "bounds"); } return rect.isEmpty() ? EmptyBounds : rect; } public static Rectangle2D.Float getBorderBounds(Iterable<LWComponent> iterable) { final Rectangle2D.Float rect = new Rectangle2D.Float(); for (LWComponent c : iterable) { if (c.isDrawn()) accumulate(rect, c.getBorderBounds(), c, "borderBounds"); } return rect.isEmpty() ? EmptyBounds : rect; } public static Rectangle2D.Float getLocalBorderBounds(Iterable<LWComponent> iterable) { final Rectangle2D.Float rect = new Rectangle2D.Float(); for (LWComponent c : iterable) { if (c.isDrawn()) accumulate(rect, c.getLocalBorderBounds(), c, "localBorderBounds"); } return rect.isEmpty() ? EmptyBounds : rect; } public static Rectangle2D.Float getLocalBounds(Iterable<LWComponent> iterable) { final Rectangle2D.Float rect = new Rectangle2D.Float(); for (LWComponent c : iterable) { if (c.isDrawn()) accumulate(rect, c.getLocalBounds(), c, "localBounds"); } return rect.isEmpty() ? EmptyBounds : rect; } public static Rectangle2D.Float getLayoutBounds(Iterable<LWComponent> iterable) { final Rectangle2D.Float rect = new Rectangle2D.Float(); for (LWComponent c : iterable) { if (c.isDrawn()) accumulate(rect, c.getLayoutBounds(), c, "layoutBounds"); } return rect.isEmpty() ? EmptyBounds : rect; } /** returing a bounding rectangle that includes all the upper left * hand corners of the given components */ public static Rectangle2D.Float getULCBounds(java.util.Iterator i) { Rectangle2D.Float rect = new Rectangle2D.Float(); if (i.hasNext()) { LWComponent c = (LWComponent) i.next(); rect.x = c.getX(); rect.y = c.getY(); while (i.hasNext()) rect.add(((LWComponent)i.next()).getLocation()); } return rect; } /** returing a bounding rectangle that includes all the lower right * hand corners of the given components */ public static Rectangle2D.Float getLRCBounds(java.util.Iterator i) { Rectangle2D.Float rect = new Rectangle2D.Float(); if (i.hasNext()) { LWComponent c = (LWComponent) i.next(); rect.x = c.getX() + c.getWidth(); rect.y = c.getY() + c.getHeight(); while (i.hasNext()) { c = (LWComponent) i.next(); rect.add(c.getX() + c.getWidth(), c.getY() + c.getHeight()); } } return rect; } // /** optimized for LWMap: remove if/when embed maps in maps */ // @Override // public AffineTransform getZeroTransform() { // return new AffineTransform(); // } /** optimized LWMap noop: remove if/when embed maps in maps */ @Override protected AffineTransform transformDownA(final AffineTransform a) { return a; } /** optimized LWMap noop: remove if/when embed maps in maps */ @Override protected void transformDownG(final Graphics2D g) {} /** optimized LWMap noop: remove if/when embed maps in maps */ @Override public void transformZero(final Graphics2D g) {} /** optimized LWMap noop: remove if/when embed maps in maps * Just copies mapPoint to zeroPoint if zeroPoint is non null. * @return mapPoint if zeroPoint is null, zeroPoint otherwise */ @Override public final Point2D transformMapToZeroPoint(Point2D.Float mapPoint, Point2D.Float zeroPoint) { if (zeroPoint == null) { return mapPoint; } else { zeroPoint.x = mapPoint.x; zeroPoint.y = mapPoint.y; return zeroPoint; } } @Override protected final Rectangle2D transformMapToZeroRect(Rectangle2D mapRect) { return mapRect; // if (zeroRect == null) // zeroRect = (Rectangle2D) mapRect.clone(); // else // zeroRect.setRect(mapRect); // return zeroRect; } public String toString() { final StringBuilder buf = new StringBuilder("LWMap[v"); buf.append(getSaveFileModelVersion()); buf.append(' '); buf.append(getLabel()); buf.append(" n=" + numChildren()); if (DEBUG.DATA && mFile != null) buf.append(" <" + mFile + ">"); // return "LWMap[" + getLabel() // + " n=" + children.size() // + (file==null?"":" <" + this.file + ">") // + "]"; buf.append(']'); return buf.toString(); } //todo: this method must be re-written. not to save and restore public Object clone() throws CloneNotSupportedException{ try { String prefix = "concept_map"; String suffix = ".vue"; File file = this.getFile(); File tempFile = File.createTempFile(prefix,suffix,VueUtil.getDefaultUserFolder()); tufts.vue.action.ActionUtil.marshallMap(tempFile, this); LWMap cloneMap = tufts.vue.action.OpenAction.loadMap(tempFile.getAbsolutePath()); cloneMap.setLabel(this.getLabel()); tufts.vue.action.ActionUtil.marshallMap(file, this); return cloneMap; }catch(Exception ex) { throw new CloneNotSupportedException(ex.getMessage()); } } private static boolean isGroupRelative(int dm) { return dm == 1 || dm >= 3; } private static boolean isGroupAbsolute(int dm) { return !isGroupRelative(dm); } /** * @return the current data-model version -- how the LWComponent hierarchy is organized and what their coordinates mean * * Model version 0: absolute children: pre-model versions / unknown (assumed all absolute coordinates) * Model version 1: relative children, including groups (excepting link members) * Model version 2: relative children, groups back to absolute (excepting link members -- only a few days this version) * Model version 3: relative children, groups back to relative with crude node-embedding support * Model version 4: relative children, groups relative, link points relative to parent (no longer have absolute map location) * Model version 5: layers added * Model version 6: meta-data persistance change: old versions of VUE can no longer see the meta-data */ public static int getCurrentModelVersion() { return 6; } // Moved KEY_PresentationColor to the bottom of the file -- seems // to be helping with the sporadic javac failures -- SMF 2008-04-09 private final ColorProperty mPresentationColor = new ColorProperty(KEY_PresentationColor, new java.awt.Color(32,32,32)); public static final Key KEY_PresentationColor = new Key("presentation.color", KeyType.STYLE) { final Property getSlot(LWMap c) { return c.mPresentationColor; } }; private List searchArrLst = new ArrayList(); public List getSearchArrLst() { return searchArrLst; } public void setSearchArrLst(List searchArrLst) { if (DEBUG.SEARCH || DEBUG.RDF) Log.debug("setSearchArrLst " + searchArrLst); this.searchArrLst = searchArrLst; } // /** LWMergeMap purge */ public List<LWMap> getMapList() { return null; } // /** LWMergeMap purge */ public void setMapListSelectionType(int choice) {} // /** LWMergeMap purge */ public int getMapListSelectionType(int choice) { return 0; } private int _mergeMapBugCount = 0; /** * Any XML tag found in a save file that does not match a mapping in from the current mapping file shows * up here -- they appear to always be instances of org.exolab.castor.types.AnyNode * * This overrride here checks for *Boundaries XML tags to ignore (that only ocurred at the * LWMap level) that came from persisted LWMergeMaps, that could grow to include over three (3) * million of them in at least one document case. */ @Override public final void addObject(Object o) { if (o instanceof org.exolab.castor.types.AnyNode && ((org.exolab.castor.types.AnyNode)o).getLocalName().endsWith("Boundaries")) { // Christ -- there are more than THREE MILLION of them in our bug-revealing customer map ForcesTrialAlicePulman.vue // Note: we to NOT want to call o.toString() for that many -- it's dramatically slow. if (_mergeMapBugCount++ % 100000 == 0) Log.info("restoring: " + _mergeMapBugCount + " XML <*Boundaries> ignored..."); } else super.addObject(o); } }