/* * Copyright 2011 Christian Thiemann <christian@spato.net> * Developed at Northwestern University <http://rocs.northwestern.edu> * * This file is part of the SPaTo Visual Explorer (SPaTo). * * SPaTo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * SPaTo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with SPaTo. If not, see <http://www.gnu.org/licenses/>. */ package net.spato.sve.app; import net.spato.sve.app.layout.*; import processing.core.PApplet; import processing.core.PGraphics; import processing.xml.XMLElement; import de.cthiemann.tGUI.TConsole; public class SPaToView { public static float linkLineWidth = 0.25f; public static boolean fastNodes = false; protected SPaTo_Visual_Explorer app = null; SPaToDocument doc = null; public SPaToView(SPaTo_Visual_Explorer app, SPaToDocument doc) { this.app = app; this.doc = doc; } class Node { String id, label, name; // node ID, short label and full name float poslat, poslong, w; // geographical position and strength // FIXME: remove the node strength stuff and use proper showLabel framework boolean showLabel; // whether or not the node should make cookies on every 2nd Friday of the spring months float x, y, a; // current screen position, and alpha factor (0\u20131) Node(XMLElement node) { id = node.getString("id"); label = node.getString("label", (id == null) ? "" : id); name = node.getString("name", label); w = node.getFloat("strength", 0); showLabel = node.getBoolean("showlabel"); String pieces[] = PApplet.split(node.getString("location", app.random(-1,1) + "," + app.random(-1,1)), ','); poslong = PApplet.parseFloat(pieces[0]); poslat = PApplet.parseFloat(pieces[1]); x = Float.NaN; y = Float.NaN; a = 0; } } class SortedLinkList { int NN, NL; // number of nodes (max value in src[] and dst[]) and number of links int src[] = null, dst[] = null; float value[] = null; float minval, maxval; boolean sorted = false; SortedLinkList(SparseMatrix sm) { this(sm, false, false, false); } SortedLinkList(SparseMatrix sm, boolean logValues) { this(sm, logValues, false, false); } SortedLinkList(SparseMatrix sm, boolean logValues, boolean asyncSort) { this(sm, logValues, asyncSort, false); } SortedLinkList(SparseMatrix sm, boolean logValues, boolean asyncSort, boolean upperTriangleOnly) { NN = sm.N; // create arrays NL = 0; for (int i = 0; i < NN; i++) NL += sm.index[i].length; src = new int[NL]; dst = new int[NL]; value = new float[NL]; // copy values NL = 0; minval = Float.POSITIVE_INFINITY; maxval = Float.NEGATIVE_INFINITY; for (int i = 0; i < NN; i++) { for (int l = 0; l < sm.index[i].length; l++) { if (upperTriangleOnly && (sm.index[i][l] < i)) continue; src[NL] = i; dst[NL] = sm.index[i][l]; value[NL] = logValues ? PApplet.log(sm.value[i][l]) : sm.value[i][l]; minval = PApplet.min(minval, value[NL]); maxval = PApplet.max(maxval, value[NL]); NL++; } } // sort if (asyncSort) app.worker.submit(new Runnable() { public void run() { sort(); } }); else sort(); } public void sort() { // Is it stupid to manually implement a sort algorithm here? // I don't want to use one Object per link and I'd like to have // small synchronized {} brackets so that the drawing routing can // already visualize the full network with partially sorted links. sorted = false; sort(0, NL); sorted = true; } private void sort(int i0, int i1) { // [in-place quicksort from wikipedia] // * will sort sublist from index i0 to i1 (incl.) // * assume no other concurring thread writes to the object, // i.e., concurring read ops are ok, only need to sync write ops; // other threads need to sync read ops as well if (i1 <= i0) return; // only need to sort lists of length > 1 // select pivot value (median of first/middle/last) int ip = i0 + (i1 - i0)/2; // assume pivot is middle element if (((value[i0] > value[ip]) && (value[i0] < value[i1])) || ((value[i0] < value[ip]) && (value[i0] > value[i1]))) ip = i0; // first element is median of first/middle/last else if (((value[i1] > value[ip]) && (value[i1] < value[i0])) || ((value[i1] < value[ip]) && (value[i1] > value[i0]))) ip = i1; // last element is median of first/middle/last // partition float pivot = value[ip]; synchronized (this) { // this could even go inside swap()... swap(ip, i1); // "park" pivot at end ip = i0; // assume pivot will go at the left end for (int i = i0; i < i1; i++) if (value[i] < pivot) // if i-th value is smaller than pivot swap(i, ip++); // then add to left list and move pivot one to the right swap(ip, i1); // put pivot value at proper position } // recurse sort(i0, ip - 1); sort(ip + 1, i1); } private void swap(int i, int j) { int tmpi; float tmpf; tmpi = src[i]; src[i] = src[j]; src[j] = tmpi; tmpi = dst[i]; dst[i] = dst[j]; dst[j] = tmpi; tmpf = value[i]; value[i] = value[j]; value[j] = tmpf; } } // nodes public boolean hasNodes = false; public int NN = -1, ih = -1; // number of nodes, currently hovered node Node nodes[] = null; // links public boolean hasLinks = false; XMLElement xmlLinks = null; SparseMatrix links = null; SortedLinkList loglinks = null; // map layout public boolean hasMapLayout = false; XMLElement xmlProjection = null; Projection projMap = null; // node coloring data public boolean hasData = false; XMLElement xmlData = null; Colormap colormap = null; final int DATA_1D = 0, DATA_2D = 1; int datatype = DATA_1D; float data[][] = null; // slices public boolean hasSlices = false; XMLElement xmlSlices = null; int r = -1; // current root node int pred[][] = null; // predecessor vectors SparseMatrix salience = null; // salience matrix (fraction of slices in which each link participates) // tomogram layouts public boolean hasTomLayout = false; String tomLayouts = null; int NL = -1, l = -1; Layout layouts[] = null; // tomogram distance matrix XMLElement xmlDistMat = null; float D[][] = null; // distance matrix float minD = Float.POSITIVE_INFINITY, maxD = Float.NEGATIVE_INFINITY; // current view parameters public final static int VIEW_MAP = 0; public final static int VIEW_TOM = 1; int viewMode = VIEW_MAP; boolean showNodes = true; boolean showLinks = true; boolean showLinksWithSkeleton = false; boolean showLinksWithNeighbors = false; boolean showLinksWithNetwork = false; boolean showSkeleton = false; boolean showNeighbors = false; boolean showNetwork = false; boolean showLabels = true; float zoom[] = { 1, 1 }; float xoff[] = { 0, 0 }; float yoff[] = { 0, 0 }; float nodeSizeFactor = 0.2f; public void setNodes(XMLElement xmlNodes) { hasNodes = false; XMLElement tmp[] = null; if ((xmlNodes == null) || ((tmp = xmlNodes.getChildren("node")).length < 1)) return; nodes = new Node[NN = tmp.length]; for (int i = 0; i < NN; i++) nodes[i] = new Node(tmp[i]); float maxw = Float.NEGATIVE_INFINITY; for (int i = 0; i < NN; i++) if (nodes[i].w > maxw) { maxw = nodes[i].w; r = i; } for (int i = 0; i < NN; i++) nodes[i].showLabel = nodes[i].w > .4f*maxw; hasNodes = true; } public void setMapProjection(XMLElement xmlProjection) { hasMapLayout = false; if (!hasNodes) return; this.xmlProjection = xmlProjection; if ((xmlProjection == null) || !MapProjectionFactory.canProduce(xmlProjection.getString("name"))) setMapProjection(MapProjectionFactory.getDefaultProduct()); else { projMap = MapProjectionFactory.produce(xmlProjection.getString("name"), NN); projMap.beginData(); for (int i = 0; i < NN; i++) projMap.setPoint(i, nodes[i].poslat, nodes[i].poslong); projMap.endData(); hasMapLayout = true; } } public void setMapProjection(String name) { if (!hasNodes) return; if (xmlProjection == null) xmlProjection = new XMLElement("projection"); xmlProjection.setString("name", name); setMapProjection(xmlProjection); } public void setRootNode(int i) { if (!hasNodes || (i < 0) || (i >= NN)) return; r = i; if (hasTomLayout) layouts[l].updateProjection(r, D); } public void setLinks(XMLElement xmlLinks) { setLinks(xmlLinks, app.console); } public void setLinks(XMLElement xmlLinks, TConsole console) { hasLinks = false; this.xmlLinks = xmlLinks; if (!hasNodes || (xmlLinks == null)) return; BinaryThing blob = doc.getBlob(xmlLinks); if (blob == null) { app.console.logError("Data for links \u201C" + xmlLinks.getString("name", "<unnamed>") + "\u201D is corrupt"); return; } if (!blob.isSparse(NN)) { app.console.logError("Data format error (expected SparseMatrix[" + NN + "]): " + blob); return; } links = blob.getSparseMatrix(); // BEGIN work-around (save_spato.m used to save matrices with 1-based indices in binary files before June 3, 2011) if (xmlLinks.getString("blob") != null) { boolean hasZeroIndex = false, hasIllegalIndices = false; for (int i = 0; i < NN; i++) { for (int l = 0; l < links.index[i].length; l++) { if (links.index[i][l] == 0) hasZeroIndex = true; if (links.index[i][l] >= NN) hasIllegalIndices = true; } } if (!hasZeroIndex && hasIllegalIndices) { app.console.logWarning("Correcting indices in the sparse weight matrix to zero-based"); for (int i = 0; i < NN; i++) for (int l = 0; l < links.index[i].length; l++) links.index[i][l]--; } } // END work-around loglinks = new SortedLinkList(links, true, true, true); hasLinks = true; } public void setSlices(XMLElement xmlSlices) { setSlices(xmlSlices, app.console); } public void setSlices(XMLElement xmlSlices, TConsole console) { hasSlices = false; if (!hasNodes || ((xmlSlices == null) && (!hasLinks))) return; // prepare data structure pred = new int[NN][NN]; D = new float[NN][NN]; // read data if (xmlSlices != null) { BinaryThing blob = doc.getBlob(xmlSlices); if (blob == null) { app.console.logError("Data for slices \u201C" + xmlSlices.getString("name", "<unnamed>") + "\u201D is corrupt"); return; } if (!blob.isInt2(NN)) { app.console.logError("Data format error (expected int[" + NN + "][" + NN + "]): " + blob); return; } pred = blob.getIntArray(); } else { // calculate shortest path trees from scratch boolean inverse = xmlLinks.getBoolean("inverse"); app.console.logProgress("Calculating shortest-path trees"); for (int r = 0; r < NN; r++) { Dijkstra.calculateShortestPathTree(links.index, links.value, r, pred[r], D[r], inverse); app.console.updateProgress(r, NN); } app.console.finishProgress(); // process data minD = Float.POSITIVE_INFINITY; maxD = Float.NEGATIVE_INFINITY; float mean = 0; int meanCount = 0; for (int r = 0; r < NN; r++) { maxD = PApplet.max(maxD, PApplet.max(D[r])); for (int i = 0; i < NN; i++) { if (r == i) continue; minD = PApplet.min(minD, D[r][i]); mean += D[r][i]; meanCount++; } } mean /= meanCount; // add SPTs as slices xmlSlices = doc.getSlices(); if (xmlSlices == null) xmlSlices = doc.getChild("slices[@name=Shortest-Path Trees]"); if (xmlSlices == null) doc.xmlDocument.addChild(xmlSlices = new XMLElement("slices")); xmlSlices.setString("id", "spt"); xmlSlices.setString("name", "Shortest-Path Trees"); while (xmlSlices.hasChildren()) xmlSlices.removeChild(0); doc.setBlob(xmlSlices, pred, true); // add SPD to distance measures dataset XMLElement xmlDataset = doc.getDataset("dist"); if (xmlDataset == null) // try by name xmlDataset = doc.getChild("dataset[@name=Distance Measures]"); if (xmlDataset == null) // create new doc.xmlDocument.addChild(xmlDataset = new XMLElement("dataset")); xmlDataset.setString("id", "dist"); xmlDataset.setString("name", "Distance Measures"); xmlDistMat = doc.getQuantity(xmlDataset, "spd"); if (xmlDistMat == null) xmlDistMat = doc.getChild(xmlDataset, "data[@name=SPD]"); if (xmlDistMat == null) xmlDataset.insertChild(xmlDistMat = new XMLElement("data"), 0); xmlDistMat.setString("id", "spd"); xmlDistMat.setString("name", "SPD"); while (xmlDistMat.getChild("values") != null) xmlDistMat.removeChild(xmlDistMat.getChild("values")); while (xmlDistMat.getChild("colormap") != null) xmlDistMat.removeChild(xmlDistMat.getChild("colormap")); String clog = (meanCount > 0) && (mean < minD + (maxD - minD)/4) ? " log=\"true\"" : ""; xmlDistMat.addChild(XMLElement.parse( String.format("<colormap%s minval=\"%g\" maxval=\"%g\" />", clog, minD, maxD))); doc.setBlob(xmlDistMat, D, true); } // calculate salience matrix app.console.logProgress("Calculating salience matrix"); int abundance[][] = new int[NN][NN]; float salienceFull[][] = new float[NN][NN]; for (int root = 0; root < NN; root++) { for (int i = 0; i < NN; i++) if (pred[root][i] != -1) abundance[i][pred[root][i]]++; app.console.updateProgress(root, 2*NN); } for (int i = 0; i < NN; i++) { for (int j = 0; j < NN; j++) salienceFull[i][j] = salienceFull[j][i] = (float)(abundance[i][j] + abundance[j][i])/NN; app.console.updateProgress(NN+i, 2*NN); } salience = new SparseMatrix(salienceFull); // FIXME: performance? (SparseMatrix uses a lot of append()) app.console.finishProgress(); // done this.xmlSlices = xmlSlices; hasSlices = true; } public void setNodeColoringData(XMLElement xmlData) { hasData = false; data = null; colormap = null; this.xmlData = xmlData; if (xmlData == null) return; // read values BinaryThing blob = doc.getBlob(xmlData); if (blob == null) { app.console.logError("Data for quantity \u201C" + xmlData.getString("name", "<unnamed>") + "\u201D is corrupt"); return; } if (blob.isFloat1(NN)) datatype = DATA_1D; else if (blob.isFloat2(NN)) datatype = DATA_2D; else { app.console.logError("Data format error (expected float[1][" + NN + "] or float[" + NN + "][" + NN + "]): " + blob); return; } data = blob.getFloatArray(); // process values float mindata = Float.POSITIVE_INFINITY; float maxdata = Float.NEGATIVE_INFINITY; for (int root = 0; root < data.length; root++) { for (int j = 0; j < data[root].length; j++) { if (Float.isInfinite(data[root][j]) || Float.isNaN(data[root][j])) continue; mindata = PApplet.min(mindata, data[root][j]); maxdata = PApplet.max(maxdata, data[root][j]); } } // setup colormap XMLElement xmlColormap = doc.getColormap(xmlData); if (xmlColormap == null) // make sure there is a <colormap> tag we can write to later xmlData.addChild(xmlColormap = new XMLElement("colormap")); colormap = new Colormap(app, xmlColormap, mindata, maxdata); //colormap = new Colormap(app, xmlColormap.getString("name", "default"), xmlColormap.getBoolean("log"), mindata, maxdata); // FIXME // finished hasData = true; } public void setTomLayout() { hasTomLayout = false; if (!hasNodes || !hasSlices) return; if (tomLayouts == null) tomLayouts = "radial_id"; String layoutNames[] = PApplet.split(tomLayouts, ' '); layouts = new Layout[NL = layoutNames.length]; for (int l = 0; l < NL; l++) layouts[l] = new Layout(app, pred, layoutNames[l]); layouts[this.l = 0].updateProjection(r, D); hasTomLayout = true; } public void setDistanceMatrix(XMLElement xmlData) { setDistanceMatrix(xmlData, app.console); } public void setDistanceMatrix(XMLElement xmlData, TConsole console) { if (!hasTomLayout) return; xmlDistMat = xmlData; if (xmlData != null) { // read values BinaryThing blob = doc.getBlob(xmlData); if (blob == null) { app.console.logError("Data for quantity \u201C" + xmlData.getString("name", "<unnamed>") + "\u201D is corrupt"); return; } if (!blob.isFloat2(NN)) { app.console.logError("Data format error (expected float[" + NN + "][" + NN + "]): " + blob); return; } D = blob.getFloatArray(); } else D = null; // process values if (D == null) D = new float[NN][NN]; minD = Float.POSITIVE_INFINITY; maxD = Float.NEGATIVE_INFINITY; for (int root = 0; root < NN; root++) { for (int j = 0; j < NN; j++) { if ((pred[root][j] == -1) && (root != j)) // ignore values of disconnected nodes D[root][j] = Float.POSITIVE_INFINITY; if (!Float.isInfinite(D[root][j]) && D[root][j] > 0) // minD = 0 will mess up the log-scale calibration, so ensure minD > 0 minD = PApplet.min(minD, D[root][j]); if (!Float.isInfinite(D[root][j]) && !Float.isNaN(D[root][j])) maxD = PApplet.max(maxD, D[root][j]); } } // update projection etc. String scaling = null; if (xmlData != null) { scaling = xmlData.getString("scaling", null); if (scaling == null) { XMLElement xmlColormap = doc.getColormap(xmlData); if (xmlColormap != null) { scaling = xmlColormap.getBoolean("log") ? "log" : "id"; xmlData.setString("scaling", scaling); // save for later } } } if (scaling == null) scaling = "id"; // default for tree depth distance layouts[l].setupScaling(scaling, minD/1.25f); if (r > -1) layouts[l].updateProjection(r, D); } float a = 0, nodeSize = 0; float[] tmpx = null, tmpy = null; float viewWidth = Float.NaN; float aNodes = 0, aLinks = 0, aSkeleton = 0, aNeighbors = 0, aNetwork = 0, aLabels = 0; protected boolean animate = true; protected float animate(float currentValue, float targetValue) { return (animate && !Float.isNaN(currentValue)) ? currentValue + 3*(targetValue - currentValue)*PApplet.min(app.dt, 1/3.f) : targetValue; } public void draw(PGraphics g) { draw(g, true); } public void draw(PGraphics g, boolean animate) { this.animate = animate; if (Float.isNaN(viewWidth)) viewWidth = g.width; if (!hasNodes || (!hasMapLayout && !hasTomLayout)) return; // nothing to draw if ((showNeighbors || showNetwork) && !hasLinks) { showNeighbors = false; aNeighbors = 0; showNetwork = false; aNetwork = 0; } // can't draw full network Projection p = ((viewMode == VIEW_MAP) || !hasTomLayout) ? projMap : layouts[0].proj; boolean linksVisible = (showLinks && !showSkeleton && !showNeighbors && !showNetwork) || (showSkeleton && showLinksWithSkeleton) || (showNeighbors && showLinksWithNeighbors) || (showNetwork && showLinksWithNetwork); aNodes = animate(aNodes, (showNodes ? 1 : 0)); aLinks = animate(aLinks, (linksVisible ? 1 : 0)); aSkeleton = animate(aSkeleton, (showSkeleton ? 1 : 0)); aNeighbors = animate(aNeighbors, (showNeighbors ? 1 : 0)); aNetwork = animate(aNetwork, (showNetwork ? 1 : 0)); aLabels = animate(aLabels, (showLabels ? 1 : 0)); // get current data float[] val = null; if (hasData) { switch (datatype) { case DATA_1D: val = data[0]; break; case DATA_2D: if (!app.isAltDown && (r > -1)) val = data[r]; if (app.isAltDown && (ih > -1)) val = data[ih]; break; } } if ((xmlDistMat == null) && (viewMode == VIEW_TOM)) { minD = 1; for (int i = 0; i < NN; i++) maxD = PApplet.max(maxD, D[r][i] = (pred[r][i] == -1) ? 0 : D[r][pred[r][i]] + 1); layouts[l].updateProjection(r, D); } // update node positions and determine hovered node boolean wrap = ((viewMode == VIEW_MAP) && xmlProjection.getString("name").equals("LonLat Roll")); if ((tmpx == null) || (tmpx.length != NN)) { tmpx = new float[NN]; tmpy = new float[NN]; } p.setScalingToFitWithin(wrap ? g.width : .9f*g.width, .9f*g.height); if (wrap) { // FIXME: wrapping should be handled by the projection float targetViewWidth = (wrap ? g.width : .9f*g.width)*zoom[viewMode]; // width of the scaled data (used for wrapping) viewWidth = animate(viewWidth, targetViewWidth); while (xoff[viewMode] > +viewWidth) { xoff[viewMode] -= viewWidth; for (int i = 0; i < NN; i++) nodes[i].x -= viewWidth; } while (xoff[viewMode] < -viewWidth) { xoff[viewMode] += viewWidth; for (int i = 0; i < NN; i++) nodes[i].x += viewWidth; } } float mind = g.width*g.height; ih = -1; // currently hovered node for (int i = 0; i < NN; i++) { boolean invis = (viewMode == VIEW_TOM) && (pred[i][r] == -1) && (i != r); float tx = invis ? nodes[i].x : p.sx*(p.x[i] - p.cx)*zoom[viewMode] + xoff[viewMode] + g.width/2; float ty = invis ? nodes[i].y : p.sy*(p.y[i] - p.cy)*zoom[viewMode] + yoff[viewMode] + g.height/2; float ta = invis ? 0 : 1; nodes[i].x = animate(nodes[i].x, tx); nodes[i].y = animate(nodes[i].y, ty); nodes[i].a = animate(nodes[i].a, ta); if (wrap) { tmpx[i] = nodes[i].x; tmpy[i] = nodes[i].y; if (nodes[i].x - g.width/2 > +viewWidth/2) nodes[i].x -= viewWidth; if (nodes[i].x - g.width/2 < -viewWidth/2) nodes[i].x += viewWidth; } float d = PApplet.dist(nodes[i].x, nodes[i].y, app.mouseX, app.mouseY); if (!invis && (app.gui.componentAtMouse == null) && (app.gui.componentMouseClicked == null) && (d < 50) && (d < mind) && (!app.gui.searchMatchesValid || !app.gui.tfSearch.isFocusOwner() || app.gui.searchMatches[i] || (app.gui.searchMatchesChild[i] > 0))) { mind = d; ih = i; } } // draw links if (hasSlices && ((aLinks > 1/192.f) || (aSkeleton > 1/192.f) || (aNeighbors > 1/192.f) || (aNetwork > 1/192.f))) { g.noFill(); g.strokeWeight(linkLineWidth); // update search matches if (app.gui.searchMatchesValid) { // update branch matching flags (would need to be recursive, but we are sloppy here) // 1: node i matches search // 0: node i does not match and we don't know of any children who match // 2: node i does not match but we used to have matching children for (int i = 0; i < NN; i++) app.gui.searchMatchesChild[i] = app.gui.searchMatches[i] ? 1 : 2*app.gui.searchMatchesChild[i]; // if we think that node i has a matching child, tell the parent of node i for (int i = 0; i < NN; i++) if ((app.gui.searchMatchesChild[i] > 0) && (pred[r][i] != -1)) app.gui.searchMatchesChild[pred[r][i]] = 1; // if any of the nodes with status 2 has not been set to 1 by now, there is no matching child for (int i = 0; i < NN; i++) if (app.gui.searchMatchesChild[i] == 2) app.gui.searchMatchesChild[i] = 0; } // visualize salience matrix if (aSkeleton > 1/192.f) { for (int i = 0; i < NN; i++) { for (int l = 0; l < salience.index[i].length; l++) { int j = salience.index[i][l]; if (j >= i) continue; // avoid drawing duplicate links if (salience.value[i][l] == 0) continue; // not a salient link g.stroke(192*(1 - salience.value[i][l]), 192*aSkeleton); // FIXME: the following code is copy'n'pasted... if (!wrap || (PApplet.abs(nodes[i].x - nodes[j].x) < viewWidth/2)) g.line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); else { if (nodes[i].x < nodes[j].x) { g.line(nodes[i].x, nodes[i].y, nodes[j].x - viewWidth, nodes[j].y); g.line(nodes[i].x + viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); } else { g.line(nodes[i].x - viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); g.line(nodes[i].x, nodes[i].y, nodes[j].x + viewWidth, nodes[j].y); } } } } } // show direct neighbors of current root node if (aNeighbors > 1/192.f) synchronized (loglinks) { // sync against SortedLinkList.sort() int i0 = app.isAltDown ? ih : r; // show neighbors of hovered node if Alt is held down for (int l = 0; l < loglinks.NL; l++) { int i = loglinks.src[l], j = loglinks.dst[l]; if ((i != i0) && (j != i0)) continue; g.stroke(192*(1 - (loglinks.value[l] - loglinks.minval)/(loglinks.maxval - loglinks.minval)), 192*aNeighbors); // FIXME: the following code is copy'n'pasted... if (!wrap || (PApplet.abs(nodes[i].x - nodes[j].x) < viewWidth/2)) g.line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); else { if (nodes[i].x < nodes[j].x) { g.line(nodes[i].x, nodes[i].y, nodes[j].x - viewWidth, nodes[j].y); g.line(nodes[i].x + viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); } else { g.line(nodes[i].x - viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); g.line(nodes[i].x, nodes[i].y, nodes[j].x + viewWidth, nodes[j].y); } } } } // show full network if (aNetwork > 1/192.f) synchronized (loglinks) { // sync against SortedLinkList.sort() for (int l = 0; l < loglinks.NL; l++) { int i = loglinks.src[l], j = loglinks.dst[l]; g.stroke(192*(1 - (loglinks.value[l] - loglinks.minval)/(loglinks.maxval - loglinks.minval)), 192*aNetwork); // FIXME: the following code is copy'n'pasted... if (!wrap || (PApplet.abs(nodes[i].x - nodes[j].x) < viewWidth/2)) g.line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); else { if (nodes[i].x < nodes[j].x) { g.line(nodes[i].x, nodes[i].y, nodes[j].x - viewWidth, nodes[j].y); g.line(nodes[i].x + viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); } else { g.line(nodes[i].x - viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); g.line(nodes[i].x, nodes[i].y, nodes[j].x + viewWidth, nodes[j].y); } } } } // draw links in selected slice if (aLinks > 1/192.f) { float aSNN = PApplet.max(aSkeleton, PApplet.max(aNeighbors, aNetwork)); if (!showLinksWithSkeleton && !showLinksWithNeighbors && !showLinksWithNetwork && !(aSNN > 1 - 1/192.f)) aSNN = 0; float aN = (showLinksWithNetwork || (aNetwork > 1 - 1/192.f)) ? aNetwork : 0; g.strokeWeight(PApplet.min(1, linkLineWidth + aN*aLinks)); // strongly emphasize slice if drawing over full network g.stroke(64 + 128*aSNN, 64*(1 - aSNN), 64*(1 - aSNN), 192*aLinks + 63*aSNN); for (int i = 0; i < NN; i++) { int j = pred[r][i]; if (j == -1) continue; // don't draw links from disconnected (or root) nodes if (app.gui.searchMatchesValid) { float alphafactor = (app.gui.searchMatches[i] || (app.gui.searchMatchesChild[i] > 0)) ? 1.25f : 0.25f; g.stroke(64 + 128*aSNN, 64*(1 - aSNN), 64*(1 - aSNN), PApplet.min(255, (192*aLinks + 63*aSNN)*alphafactor)); g.strokeWeight((alphafactor > 1) ? 1 : 0.25f); } if (!wrap || (PApplet.abs(nodes[i].x - nodes[j].x) < viewWidth/2)) g.line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); else { if (nodes[i].x < nodes[j].x) { g.line(nodes[i].x, nodes[i].y, nodes[j].x - viewWidth, nodes[j].y); g.line(nodes[i].x + viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); } else { g.line(nodes[i].x - viewWidth, nodes[i].y, nodes[j].x, nodes[j].y); g.line(nodes[i].x, nodes[i].y, nodes[j].x + viewWidth, nodes[j].y); } } } } g.strokeWeight(1); } // draw nodes g.rectMode(PApplet.CENTER); float nodeSize_target = nodeSizeFactor*PApplet.sqrt(PApplet.min(g.width, g.height))*PApplet.sqrt(zoom[viewMode]); nodeSize = animate(nodeSize, nodeSize_target); if (aNodes > 1/192.f) { g.noStroke(); for (int i = 0; i < NN; i++) { float alphafactor = !app.gui.searchMatchesValid ? 1 : (app.gui.searchMatches[i] ? 1.25f : 0.125f); g.fill((val == null) ? 127 : colormap.getColor(val[i]), 192*aNodes*nodes[i].a*alphafactor); if (fastNodes) g.rect(nodes[i].x + .5f, nodes[i].y + .5f, 3*nodeSize/4, 3*nodeSize/4); else g.ellipse(nodes[i].x, nodes[i].y, nodeSize, nodeSize); } } // mark root node and hovered node (these are shown even if showNodes is false) if (r > -1) { g.stroke(255, 0, 0); g.noFill(); if (fastNodes) g.rect(nodes[r].x, nodes[r].y, 3*nodeSize/4 + .5f, 3*nodeSize/4 + .5f); else g.ellipse(nodes[r].x, nodes[r].y, nodeSize + .5f, nodeSize + .5f); } if (ih > -1) { g.stroke(200, 0, 0); g.noFill(); if (fastNodes) g.rect(nodes[ih].x, nodes[ih].y, 3*nodeSize/4 + .5f, 3*nodeSize/4 + .5f); else g.ellipse(nodes[ih].x, nodes[ih].y, nodeSize + .5f, nodeSize + .5f); } g.rectMode(PApplet.CORNER); // draw labels g.textFont(/*fnSmall*/app.gui.fnMedium); g.textAlign(PApplet.LEFT, PApplet.BASELINE); g.noStroke(); for (int i = 0; i < NN; i++) { if ((i == r) || (i == ih) || ((aLabels > 1/255.f) && nodes[i].showLabel && (!app.gui.searchMatchesValid || app.gui.searchMatches[i]))) { g.fill(0, 255*aLabels*nodes[i].a); g.text(nodes[i].label + (((i == ih) && (val != null)) ? " (" + format(val[i]) + ")" : ""), nodes[i].x + nodeSize/4, nodes[i].y - nodeSize/4); } } // reset nodes.x/y if wrapping is on if (wrap) for (int i = 0; i < NN; i++) { nodes[i].x = tmpx[i]; nodes[i].y = tmpy[i]; } } public String format(float val) { if (val == 0) return "0"; else if ((val > 1 - 1e-7f) && (val < 100000)) { int nd = 3; if (val >= 10) nd--; if (val >= 100) nd--; if (val >= 1000) nd--; String res = PApplet.nfc(val, nd); if (nd > 0) res = res.replaceAll("0+$", "").replaceAll("\\.$", ""); return res; } else return String.format("%.4g", val); } public void writeLayout(String filename) { String lines[] = new String[NN]; Projection p = ((viewMode == VIEW_MAP) || !hasTomLayout) ? projMap : layouts[0].proj; if (p == null) lines = new String[] { "Error! No valid node layout available..." }; else for (int i = 0; i < NN; i++) lines[i] = new String(p.x[i] + "\t" + p.y[i] + "\t" + nodes[i].label); app.saveStrings(filename, lines); } }