/* * 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. */ /* * LWMergeMap.java * * Created on January 24, 2007, 1:38 PM * * @author dhelle01 * */ package tufts.vue; import edu.tufts.vue.compare.*; import edu.tufts.vue.style.*; import java.io.File; import java.util.*; import edu.tufts.vue.metadata.VueMetadataElement; /** todo: this needs to turn into an LWMap builder, not a subclass of LWMap */ public class LWMergeMap extends LWMap { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWMergeMap.class); public static final int THRESHOLD_DEFAULT = 20; private static int NumberOfMaps = 0; private LWMap baseMap; private Set baseMapKeys; private int nodeThresholdSliderValue = THRESHOLD_DEFAULT; private int linkThresholdSliderValue = THRESHOLD_DEFAULT; private boolean filterOnBaseMap; private boolean excludeNodesFromBaseMap; /** Actual maps used to generate the most recent merge. **/ private List<LWMap> mapList = new ArrayList<LWMap>(); /** to declare inactive status of individual maps -- if empty or shorter, status defaults to active */ private List<Boolean> activeStatus = new ArrayList<Boolean>(); private List<Double> nodeIntervalBoundaries = new ArrayList<Double>(); private List<Double> linkIntervalBoundaries = new ArrayList<Double>(); // is this used for anything? set/get by MergeMapsControlPanel private int visualizationSelectionType; public static String getTitle() { // todo: will go away with factory return "Merge Map " + (++NumberOfMaps); //+ "*"; } public String getLabel() { return super.getLabel() + " *"; } public LWMergeMap() {} public LWMergeMap(String label) { super(label); } public void setVisualizationSelectionType(int choice) { visualizationSelectionType = choice; } public int getVisualizationSelectionType() { return visualizationSelectionType; } public void setFilterOnBaseMap(boolean doFilter) { filterOnBaseMap = doFilter; } public boolean getFilterOnBaseMap() { return filterOnBaseMap; } public void setExcludeNodesFromBaseMap(boolean doExclude) { excludeNodesFromBaseMap = doExclude; } public boolean getExcludeNodesFromBaseMap() { return excludeNodesFromBaseMap; } public void setNodeThresholdSliderValue(int value) { nodeThresholdSliderValue = value; } public int getNodeThresholdSliderValue() { return nodeThresholdSliderValue; } public void setLinkThresholdSliderValue(int value) { linkThresholdSliderValue = value; } public int getLinkThresholdSliderValue() { return linkThresholdSliderValue; } public void setBaseMap(LWMap baseMap) { this.baseMap = baseMap; } public LWMap getBaseMap() { return baseMap; } /** * todo: rename no-arg version below as "default" setup method. * This method and the next are for legacy persistence * defaults to node, even though now link has separate interval boundaries **/ public void setIntervalBoundaries(List<Double> intervalBoundaries) { this.nodeIntervalBoundaries = intervalBoundaries; } public List<Double> getIntervalBoundaries() { return nodeIntervalBoundaries; } public void setNodeIntervalBoundaries(List<Double> intervalBoundaries) { this.nodeIntervalBoundaries = intervalBoundaries; } public List<Double> getNodeIntervalBoundaries() { return nodeIntervalBoundaries; } public void setLinkIntervalBoundaries(List<Double> intervalBoundaries) { this.linkIntervalBoundaries = intervalBoundaries; } public List<Double> getLinkIntervalBoundaries() { return linkIntervalBoundaries; } public void setMapList(List<LWMap> mapList) { this.mapList = mapList; } public List<LWMap> getMapList() { return mapList; } public void setActiveMapList(List<Boolean> activeMapList) { activeStatus = activeMapList; } /** @return the list of currently active maps (our maps list adjusted by activeStatus) */ private List<LWMap> getActiveMaps() { if (activeStatus == null || activeStatus.size() == 0) { // no status list specified, thus, all maps are active: return mapList; } final List<LWMap> maps = getMapList(); final List<LWMap> activeMaps = new ArrayList(maps.size()); final Iterator<Boolean> statusIter = activeStatus.iterator(); for (LWMap map : maps) { if (statusIter.hasNext()) { if (statusIter.next().booleanValue()) activeMaps.add(map); else /* leave this map out */; } else { activeMaps.add(map); } } return activeMaps; } private boolean isBaseMapActive() { if (activeStatus == null || activeStatus.size() == 0) return true; final Iterator<Boolean> actives = activeStatus.iterator(); final Iterator<LWMap> maps = mapList.iterator(); while (actives.hasNext() && maps.hasNext()) { LWMap map = maps.next(); boolean active = actives.next().booleanValue(); if (map == baseMap && !active) return false; } return true; } private void mergeInNodes(final LWMap sourceMap, final Map<Object,LWComponent> keysMerged, final VoteAggregate voteAggregate) { for (LWComponent srcNode : sourceMap.getAllDescendents(ChildKind.PROPER)) { if (srcNode instanceof LWNode || srcNode instanceof LWImage) ; else continue; if (srcNode.hasFlag(Flag.ICON)) continue; final Object mergeKey = getMergeKey(srcNode); if (mergeKey == null) continue; final LWComponent alreadyMerged = keysMerged.get(mergeKey); if (alreadyMerged != null) { annotateWithSource(sourceMap, srcNode, alreadyMerged); continue; } //----------------------------------------------------------------------------- // weightAggregate was ignored here -- is only used for styling in our caller //----------------------------------------------------------------------------- if (voteAggregate != null && !voteAggregate.isNodeVoteAboveThreshold(mergeKey)) continue; if (!excludeNodesFromBaseMap || !mergeKeyPresentOnBaseMap(mergeKey)) // todo: base-map check needlessly slow keysMerged.put(mergeKey, copyInNode(sourceMap, srcNode)); } } /** copy the source node to this map, returning the new duplicate node */ private LWComponent copyInNode(LWMap sourceMap, LWComponent sourceNode) { final LWComponent node = sourceNode.duplicate(); annotateWithSource(sourceMap, sourceNode, node); super.addChild(node); return node; } /** Called by MergeMapsControlPanel -- REPLACE WITH SINGLE FACTORY CALL WITH VOTE v.s. WEIGHT ARG */ public void fillAsWeightMerge() { final ConnectivityMatrixList<ConnectivityMatrix> cms = new ConnectivityMatrixList<ConnectivityMatrix>(); final Collection<LWMap> activeMaps = getActiveMaps(); final LWMap baseMap = getBaseMap(); if (excludeNodesFromBaseMap) this.baseMapKeys = hashMergeKeys(baseMap); for (LWMap map : activeMaps) { if (DEBUG.MERGE) Log.debug("computing matrix adding: " + map); // old comment had commented out check to skip baseMap... cms.add(new ConnectivityMatrix(map)); } final List<Style> nodeStyles = new ArrayList<Style>(); final List<Style> linkStyles = new ArrayList<Style>(); for(int si=0;si<5;si++) nodeStyles.add(StyleMap.getStyle("node.w" + (si +1))); for(int lsi=0;lsi<5;lsi++) linkStyles.add(StyleMap.getStyle("link.w" + (lsi +1))); final WeightAggregate weightAggregate = WeightAggregate.create(cms); final Map<Object,LWComponent> keysMerged = new HashMap(); if (!excludeNodesFromBaseMap && isBaseMapActive()) { // Don't know if we need to add baseMap 1st, but leaving this in // instead of including in loop below just in case. mergeInNodes(baseMap, keysMerged, null); } if (!getFilterOnBaseMap()) { for (LWMap map : activeMaps) { if (map == baseMap) { Log.debug("addMerge; skipping baseMap " + baseMap); } else { Log.debug("addMerge; adding map " + map); mergeInNodes(map, keysMerged, null); } } } final Collection<LWComponent> allMergedNodes = getAllDescendents(ChildKind.PROPER); for (LWComponent c : allMergedNodes) { if (c instanceof LWNode || c instanceof LWImage) continue; // As we've only done addMergeNodesForMap calls up till now, everything in the map should already // only be LWNodes or LWImages, but we check here just in case... Log.warn("merge results pollution?: " + c); //allMergedNodes.remove(c); // iterator would fail... } // todo: use applyCSS(style) -- need to plug in formatting panel for (LWComponent node : allMergedNodes) { if (node instanceof LWNode == false) continue; double score = 100 * weightAggregate.getNodeCount(getMergeKey(node))/weightAggregate.getCount(); if(score>100) score = 100; else if(score<0) score = 0; final Style currStyle = nodeStyles.get(getNodeInterval(score)-1); //todo: applyCss here instead. node.setFillColor(Style.hexToColor(currStyle.getAttribute("background"))); java.awt.Color strokeColor = null; if(currStyle.getAttribute("font-color") != null) strokeColor = Style.hexToColor(currStyle.getAttribute("font-color")); if (strokeColor != null) node.setTextColor(strokeColor); } for (LWComponent head : allMergedNodes) { final Object headKey = getMergeKey(head); // Again, we're expecting that only LWImages or LWnodes are in allMergedNodes. for (LWComponent tail : allMergedNodes) { if (head == tail) continue; final Object tailKey = getMergeKey(tail); final int c = weightAggregate.getConnection(headKey, tailKey); if (c <= 0) continue; final int c2 = weightAggregate.getConnection(tailKey, headKey); double score = 100 * c / weightAggregate.getCount(); // are either of these ever happenning? If so, why? if (score > 100) score = 100; else if (score < 0) score = 0; final Style currLinkStyle = linkStyles.get(getLinkInterval(score)-1); final LWLink link = new LWLink(head, tail); if (c2 > 0 && !getFilterOnBaseMap()) { link.setArrowState(LWLink.ARROW_BOTH); weightAggregate.setConnection(tailKey, headKey, 0); } // todo: applyCSS here link.setStrokeColor(Style.hexToColor(currLinkStyle.getAttribute("background"))); super.addLink(link); cms.addLinkSourceMapMetadata(headKey, tailKey, link); } } } /** Called by MergeMapsControlPanel */ public void fillAsVoteMerge() { final ConnectivityMatrixList<ConnectivityMatrix> matrixList = new ConnectivityMatrixList<ConnectivityMatrix>(); final List<LWMap> activeMaps = getActiveMaps(); //----------------------------------------------------------------------------- // Create a connectivity matrix for each active map to be fed to the VoteAggregate //----------------------------------------------------------------------------- for (LWMap map : activeMaps) { // if (map != getBaseMap()) // TODO: check -- really create matrix if ignoring baseMap? matrixList.add(new ConnectivityMatrix(map)); } final double nodeThresh = (double) getNodeThresholdSliderValue() / 100.0; final double linkThresh = (double) getLinkThresholdSliderValue() / 100.0; final VoteAggregate voteAggregate = VoteAggregate.create(matrixList, nodeThresh, linkThresh); //----------------------------------------------------------------------------- // compute and create nodes in Merge Map //----------------------------------------------------------------------------- if (excludeNodesFromBaseMap) this.baseMapKeys = hashMergeKeys(getBaseMap()); final Map<Object,LWComponent> keysMerged = new HashMap(); if (!excludeNodesFromBaseMap && isBaseMapActive()) mergeInNodes(baseMap, keysMerged, voteAggregate); if (! getFilterOnBaseMap()) { // TODO: CHECK THIS LOGIC: ONLY ADDING ANY MAPS IF *NOT* "filterOnBaseMap" ? for (LWMap map : activeMaps) if (map != baseMap) mergeInNodes(map, keysMerged, voteAggregate); } //----------------------------------------------------------------------------- // compute and create links in Merge Map //----------------------------------------------------------------------------- final Collection<LWComponent> allComponents = getAllDescendents(ChildKind.PROPER); final List<LWComponent> linkables = new ArrayList(allComponents.size() / 2); for (LWComponent c : allComponents) { // We only generate links between nodes and images -- tho this is a double-check if (c instanceof LWNode || c instanceof LWImage) linkables.add(c); } // Is there a faster way to do this than O(n^2) ? Shouldn't we be able to iterate the // VoteAggregate or track the above threshold relations there? for (LWComponent head : linkables) { final Object headKey = getMergeKey(head); for (LWComponent tail : linkables) { if (head != tail) { final Object tailKey = getMergeKey(tail); if (voteAggregate.isLinkVoteAboveThreshold(headKey, tailKey)) { // Vote for the relation between these two nodes was above link threshold: add a new link final LWLink link = new LWLink(head, tail); super.addLink(link); matrixList.addLinkSourceMapMetadata(headKey, tailKey, link); } } } } } private static Set hashMergeKeys(LWMap map) { final Set hashedKeys = new HashSet(); for (LWComponent c : map.getAllDescendents(ChildKind.PROPER)) { if (c.hasFlag(Flag.ICON)) continue; if (c instanceof LWNode || c instanceof LWImage) { final Object key = getMergeKey(c); if (key != null) hashedKeys.add(key); } } return hashedKeys; } public boolean mergeKeyPresentOnBaseMap(Object key) { return baseMapKeys.contains(key); } // todo: this should become only the default initialization method // for interval boundaries -- in fact: implementing 4/1/2008 public void setIntervalBoundaries() { nodeIntervalBoundaries = new ArrayList<Double>(); for(int vai = 0;vai<6;vai++) { double va = 20*vai + 0.5; nodeIntervalBoundaries.add(new Double(va)); } linkIntervalBoundaries = new ArrayList<Double>(); for(int vai = 0;vai<6;vai++) { double va = 20*vai + 0.5; linkIntervalBoundaries.add(new Double(va)); } } public int getNodeInterval(double score) { int count = 0; for (Double d : nodeIntervalBoundaries) { if (score < d.doubleValue()) return count; count++; } return 0; } private int getLinkInterval(double score) { int count = 0; for (Double d : linkIntervalBoundaries) { if (score < d.doubleValue()) return count; count ++; } return 0; } private static Object getMergeKey(LWComponent c) { return edu.tufts.vue.compare.Util.getMergeProperty(c); } /* for LWComponent.putclientData */ private static final class Counter { int count = 0; } // Note that since clientData is runtime *instance* information, and not copied over on // duplication, we don't have to worry about cleaning these up / having them propagate. /* * FYI: the first time this method sees @param mergeNew during a merge, it happens to be the actual * duplicate of sourceNode, and it wont have any counter set yet. Each time we see it after that, * sourceNode is another node with the same merge-key, possibly from a different source map. (BTW, the * choice of which merge-key matching node to use as the actual duplication source is somewhat random: the * merge process simply uses the first it comes across in the order we merge the maps.) */ private static void annotateWithSource(LWMap sourceMap, LWComponent sourceNode, LWComponent mergeNew) { Counter counter = mergeNew.getClientData(Counter.class); if (counter == null) counter = mergeNew.putClientData(new Counter()); String mapLabel = sourceMap.getDisplayLabel(); if (mapLabel.endsWith(".vue")) mapLabel = mapLabel.substring(0, mapLabel.length()-4); final String annotation = String.format("[in:%d:%s/%s/%s]", ++counter.count, mapLabel, sourceNode.getID(), sourceNode.getDisplayLabel()); if (DEBUG.TEST) { if (mergeNew.hasNotes()) { // note: string manips are slow -- could store a StringBuilder in the client-data // and run a final baking pass at the end of all the merges. mergeNew.setNotes(mergeNew.getNotes() + (counter.count > 1 ? "\n" : "\n----\n") + annotation); } else { mergeNew.setNotes(annotation); } } // This apparently is a special form a meta-data annotation (#TAG?) that only shows up in the UI via a // special Resource icon (which never made it to the preferences) that pulls // MetadataList.getMetadataAsHTML(type) to display its rollover content. It depends on a special // format for the meta-data to be broken up and formatted in the HTML (actually, it seems to mainly // strip "<word>:" off the front). It does NOT show up in the "Keywords" InspectorPane tab. Note that // this data DOES, however, end up appaering in the RDFIndex, and this can be searched on, tho the user // won't have a place to see where the hit came from except the rollover. final VueMetadataElement vme = new VueMetadataElement(); vme.setType(VueMetadataElement.OTHER); final String stripped = annotation.substring(3, annotation.length()-1); vme.setObject("source" + stripped); mergeNew.getMetadataList().getMetadata().add(vme); } // [DAN] old method for recording source nodes code currently does not compile // (its commented out below) - needs adjustment to // LWComponent from LWNode (for both LWImage and LWNode) // a relatively minor fix and probably // also needs to be stored in special metadata of a new type... // (since otherwise this info always appears in the notes and possibly // out of sight) // **new system is to use VueMetadataElement.OTHER (see below)** // public static final boolean RECORD_SOURCE_NODES = false; // edu.tufts.vue.metadata.VueMetadataElement vme = new edu.tufts.vue.metadata.VueMetadataElement(); // vme.setType(edu.tufts.vue.metadata.VueMetadataElement.OTHER); // vme.setObject("source: " + node.getMap().getLabel() + "," + sourceLabel); // c.getMetadataList().getMetadata().add(vme); public String toString() { return "LWMerge" + super.toString().substring(2); } // @Override String getDiagnosticLabel() { return "MergeMap: " + getLabel(); } // [DAN] todo: recover only file names back into the GUI this list will continue to persist to // provide a record of actual map data used for merge (will be overwritten on re-save of file // as merge will be recalculated on GUI load) note: this initializer seems to be required for // Castor libary to actually persist the list. [re: mapList?] // // private File baseMapFile; // public File getBaseMapFile() { return baseMapFile; } // public void setBaseMapFile(File file) { baseMapFile = file; } // public void clearAllElements() { // deleteChildrenPermanently(getChildren()); // } // private String styleFile; // public void setStyleMapFile(String file) { // styleFile = file; // } // public String getStyleMapFile() { // return styleFile; // } // public String getSelectionText() { return selectionText; } // public void setSelectionText(String text) { selectionText = text; } // [DAN?] todo: deprecate and/or remove -- this was for old GUI [SMF: MergeMapsChooser is still calling tho] // public String getSelectChoice() { return selectChoice; } /** @deprecated todo: deprecate and/or remove -- this was for old GUI (MergeMapsChooser is still calling tho) */ //public void setSelectChoice(String choice) { selectChoice = choice; } // /** @deprecated [DAN?] todo: deprecate and/or remove -- this was for old GUI [SMF: MergeMapsChooser is still calling tho] */ // public void setBaseMapSelectionType(int choice) { baseMapSelectionType = choice; } // /** @deprecated: deprecate and/or remove -- this was for old GUI (STILL IN CASTOR MAPPING FILE) */ // public int getBaseMapSelectionType() { return baseMapSelectionType; } // // only called by old MergeMapsChooser // private int mapListSelectionType; // private int baseMapSelectionType; // private List<String> fileList = new ArrayList<String>(); // public void recreateVoteMerge() { // clearAllElements(); // fillAsVoteMerge(); // } // public void refillAsVoteMerge() { // clearAllElements(); // fillAsVoteMerge(); // } // /** // * No guarantee that these filenames were generated from *saved* maps todo: document here the // * current behavior of VUE in this case. GUI should correctly handle any current or future // * nulls or otherwise invalid file names that might be stored here. // **/ // public void setMapFileList(List<String> mapList) { // if(mapList != null) // fileList = mapList; // } // public List<String> getMapFileList() { // return fileList; // } // /** // * no longer relevant. This was for old gui with separation between options to load from file // * or from all open maps todo: deprecate and/or remove // **/ // public void setMapListSelectionType(int choice) { // mapListSelectionType = choice; // } // /** // * no longer relevant. This was for old gui with separation between options to load from file // * or from all open maps todo: deprecate and/or remove // **/ // public int getMapListSelectionType() { // return mapListSelectionType; // } // public List<Boolean> getActiveFileList() { // return activeFiles; // } /** This class is only there to provide something for the old mapping description for LWMergeMap to refer * to, which is NOT, in fact, an LWMergeMap. Eventualy, we can get rid of this and just delete/comment out * the old mapping info, but I'm leaving it for now in case we decide we need it. */ public static final class HIDE_FROM_CASTOR_MAPPING extends LWMergeMap {static{if(true)throw new Error("should-never-init");}} }