/* * 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 edu.tufts.vue.compare; import java.util.*; import java.io.*; import tufts.vue.DEBUG; import tufts.vue.LWComponent; import tufts.vue.LWComponent.ChildKind; import tufts.vue.LWMap; import tufts.vue.LWNode; import tufts.vue.LWLink; import tufts.vue.LWImage; /** * @author akumar03 * @author Scott Fraize re-write 2012 * * The class creates a connectivity Matrix for a VUE map. * * Further information on connectivity matrix can be found: * @see http://w3.antd.nist.gov/wctg/netanal/netanal_netmodels.html * * The matrix can be used to assess the connectivity among a given set of nodes. A value of * connetion is 1 if there is a connection between nodes: * connection(a,b) = 1 implies there is a link from a to b * connection(b,a) = 1 implies there is a link from b to a * connection(b,a) may not be equal to connection(a,b) * connection(a,b) = connection(b,a) implies the link between a and b is not directed. */ public class ConnectivityMatrix { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ConnectivityMatrix.class); public static final int INDEX_ERROR = -1; protected final LWMap map; protected final IndexedCountingSet keys; protected final int cx[][]; protected int scanCount = 0; protected int hitCount = 0; /** * An object set that provides a fixed index value for each unique object, a count for repeat * objects, and fast constant lookup times for all queries. Note that there is no error * checking with indexOf or count (in typical usage, we want exceptions if we don't know what * we're asking for). Use findIndex to see -1 return value for objects not in the set. * Indicies and iteration order occur in the preserved insertion order. */ protected static class IndexedCountingSet<T> implements Iterable<T> { private static final int INDEX = 0, COUNT = 1; // redundant data-structures for x-way lookups: private final Map<T,int[]> map = new HashMap<T,int[]>(); private final List<T> values = new ArrayList<T>(); private int unique = 0; private int maxLen = 0; void add(T s) { if (s == null) { if (DEBUG.Enabled) Log.warn("null value ignored in IndexedCountingSet"); return; } int[] entry = map.get(s); if (entry != null) { entry[COUNT]++; } else { entry = new int[2]; entry[INDEX] = unique++; entry[COUNT] = 1; map.put(s, entry); values.add(s); // values.size() == unique String ts = s.toString(); if (ts.length() > maxLen) maxLen = ts.length(); } } //int count(T s) { try{return map.get(s)[COUNT];}catch(Throwable t){return 0;}} int count(T s) { return map.get(s)[COUNT]; } int indexOf(T s) { return map.get(s)[INDEX]; } T get(int atIndex) { return values.get(atIndex); } int size() { return unique; } boolean contains(T s) { return map.containsKey(s); } /** @return index, or -1 if not found */ int findIndex(T s) { final int[] entry = map.get(s); return entry == null ? INDEX_ERROR : entry[INDEX]; } void addAll(IndexedCountingSet<T> mergeIn) { for (T val : mergeIn.values) add(val); } int maxLength() { return maxLen; } public Iterator<T> iterator() { return values.iterator(); } } protected ConnectivityMatrix(IndexedCountingSet preComputedSet) { this.map = null; this.keys = preComputedSet; this.cx = new int[keys.size()][keys.size()]; if (DEBUG.Enabled) Log.debug(this + " created from pre-computed."); } public ConnectivityMatrix(LWMap map) { this.map = map; this.keys = new IndexedCountingSet(); final Collection<LWComponent> allInMap = map.getAllDescendents(ChildKind.PROPER); indexMergeKeys(allInMap); // after adding all keys, we know exactly how big to make the matrix this.cx = new int[keys.size()][keys.size()]; generateMatrix(allInMap); if (DEBUG.Enabled) Log.debug(this + " created."); } public IndexedCountingSet getKeys() { return keys; } public boolean containsKey(String key) { return keys.contains(key); } public int size() { return keys.size(); } public LWMap getMap() { return map; } public int[][] getMatrix() { return cx; } public static final boolean isValidTarget(LWComponent c) { if (c != null) { if (c.hasFlag(LWComponent.Flag.ICON)) { return false; } else { final Class clazz = c.getClass(); return clazz == tufts.vue.LWNode.class || clazz == tufts.vue.LWImage.class; } } else { return false; } } /** find all merge keys based on the global merge-property set, and load them into the counting set */ private void indexMergeKeys(final Collection<LWComponent> allInMap) { if (DEBUG.MERGE) Log.debug("indexing [" + Util.getMergeProperty() + "] merge keys for map " + map); for (LWComponent c : allInMap) { if (isValidTarget(c)) { final Object key = getMergeKey(c); if (key != null) { keys.add(key); hitCount++; } scanCount++; } } if (DEBUG.MERGE) Log.debug(String.format("merge keys: %d valid targets scanned, %d keys found, %d unique.", scanCount, hitCount, size())); } /** set connection values for merge-key components that have a VUE map-link between them */ private void generateMatrix(final Collection<LWComponent> allInMap) { Log.info("generateMatrix for " + tufts.Util.tags(allInMap)); for (LWComponent c : allInMap) { if (c instanceof LWLink) { final LWLink link = (LWLink) c; final LWComponent head = link.getHead(); // note: will be null if pruned (or use getPersistHead) final LWComponent tail = link.getTail(); // note: will be null if pruned (or use getPersistHead) if (isValidTarget(head) && isValidTarget(tail)) { try { final int arrowState = link.getArrowState(); final Object headKey = getMergeKey(head); final Object tailKey = getMergeKey(tail); if (headKey == null || tailKey == null) { // Just because both are valid targets does *not* mean a valid // merge-key will be found for them. continue; } final int headIndex = keys.findIndex(headKey); final int tailIndex = keys.findIndex(tailKey); if (arrowState == LWLink.ARROW_BOTH || arrowState == LWLink.ARROW_NONE) { cx[headIndex][tailIndex] = 1; cx[tailIndex][headIndex] = 1; } else if (arrowState == LWLink.ARROW_HEAD) { cx[tailIndex][headIndex] = 1; } else if (arrowState == LWLink.ARROW_TAIL) { cx[headIndex][tailIndex] = 1; } } catch (Throwable t) { // Should never happen, but theoretically could get NPE or ArrayOutOfBounds Log.debug("exception: skipping link: " + link + "; " + t); } } } } //Log.info("generateMatrix complete."); } public int getConnection(int i, int j) { return cx[i][j]; } /** @return connection value found for these two keys, if any, otherwise 0 */ public int getConnection(Object key1, Object key2) { final int row = keys.findIndex(key1); final int col = keys.findIndex(key2); if (row >= 0 && col >=0) return this.cx[row][col]; else return 0; } public void setConnection(Object key1, Object key2, int value) { final int index1 = keys.findIndex(key1); final int index2 = keys.findIndex(key2); if (index1 >= 0 && index2 >=0) this.cx[index1][index2] = value; } /** * Compares a connectivity matrix to input connectivity matrix * returns truf if both are same * @param c2 ConnectivityMatrix to be compared to * @return boolean * */ public boolean compare(ConnectivityMatrix c2) { final int size = size(); if(c2.size() != size) return false; for(int i=0;i<size;i++) { for(int j=0;j<size;j++) { if (this.cx[i][j] != c2.cx[i][j]) return false; } } return true; } // public void store(OutputStream out) { // try { // out.write(this.asString().getBytes()); // }catch(IOException ex) { // System.out.println("ConnectivityMatrix.store:"+ex); // } // } public String toString() { return getClass().getSimpleName() + "[" + size() + "^2=" + (size()*size()) + " for map " + (map==null?"<aggregate>":map) + "]"; } private static final String NewLine = System.getProperty("line.separator"); private static final char TAB = '\t'; private static final char SPACE = ' '; private static final boolean LABELS_TOP = true; private static final boolean LABELS_TOP_CHAR = true; // 1st-char of label only private static final boolean LABELS_TOP_BIG = (LABELS_TOP && !LABELS_TOP_CHAR); private static final boolean LABELS_LEFT = true; public String asString() { final int size = keys.size(); int capacity = size * size * 2 + size; // grid + newlines final int maxLeftLen = Math.min(keys.maxLength(), 42); // if using tabs, better multiple of 8 less 1 // note: only really need to have have this generate to a Writer (buffered) // instead of having to guess capacity to get performance. This is // normally only used to write out to a file. if (LABELS_TOP) capacity += size * (LABELS_TOP_CHAR ? 2 : 8); if (LABELS_LEFT) capacity += size * (maxLeftLen+1); if (DEBUG.MERGE) { Log.debug("toString: cx.size=" + cx.length + " (max alloc matrix)"); Log.debug("toString: nodesSeen=" + scanCount); Log.debug("toString: keysFound=" + hitCount); Log.debug("toString: keys.size=" + keys.size() + " (unique property values)"); Log.debug("toString: capacity=" + capacity); } final StringBuilder b = new StringBuilder(capacity); if (LABELS_TOP) { if (LABELS_LEFT) for (int i = 0; i <= maxLeftLen; i++) b.append(' '); for (Object key : this.keys) { if (key != null) { if (LABELS_TOP_CHAR) { final String txt = key.toString().trim(); b.append(txt.length() > 0 ? txt.charAt(0) : '?'); b.append(' '); } else { final String txt = key.toString().replace('\n', ' ').trim(); if (txt.length() > 7) b.append(txt, 0, 7); else b.append(txt); b.append(TAB); } } } b.append(NewLine); } // Note: this used to use the input-size count, which would end up generating an unlabeled // column with all zeros in them for ever node that had the a same merge-property (e.g, // nodes with same label). for (int row = 0; row < size; row++) { if (LABELS_LEFT) { // b.append(String.format("%*.*s", maxLeftLen, maxLeftLen, labels.get(i).replace('\n', ' ').trim())); final String txt = this.keys.get(row).toString().replace('\n', ' ').trim(); if (txt.length() > maxLeftLen) { b.append(txt, 0, maxLeftLen); } else { for (int i = txt.length(); i < maxLeftLen; i++) b.append(SPACE); b.append(txt); } // I'd be nice to have an invisible char separator at end of label here in case // anyone wants to script process this data (e.g., even a tab). Tho I suppose they // could get the label-field length from the 1st index of the top-char label. // (A further problem would be that labels themselves can have special chars in them) b.append(SPACE); } for (int col = 0; col < size; col++) { if (col != 0) b.append(LABELS_TOP_BIG ? TAB : SPACE); final int val = cx[row][col]; if (val == 0) b.append((row==col) ? '0' : '.'); else b.append(val); } b.append(NewLine); } if (DEBUG.MERGE) Log.debug("toString: length=" + b.length()); return b.toString(); } public static final Object getMergeKey(LWComponent node) { // Log.debug("getMergeKey: " + Util.getMergeKey(node)); return Util.getMergeProperty(node); } }