/*
Copyright 2008-2010 Gephi
Authors : Vojtech Bardiovsky <vojtech.bardiovsky@gmail.com>
Website : http://www.gephi.org
This file is part of Gephi.
Gephi is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
Gephi 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Gephi. If not, see <http://www.gnu.org/licenses/>.
*/
package org.gephi.io.importer.plugin.file;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.gephi.data.attributes.api.AttributeColumn;
import org.gephi.data.attributes.api.AttributeType;
import org.gephi.io.importer.api.ContainerLoader;
import org.gephi.io.importer.api.EdgeDraft;
import org.gephi.io.importer.api.ImportUtils;
import org.gephi.io.importer.api.Issue;
import org.gephi.io.importer.api.NodeDraft;
import org.gephi.io.importer.api.Report;
import org.gephi.io.importer.spi.FileImporter;
import org.gephi.utils.longtask.spi.LongTask;
import org.gephi.utils.progress.Progress;
import org.gephi.utils.progress.ProgressTicket;
/**
* Netdraw .vna files importer implemented as a simple state machine due to very
* loose specification of .vna format (http://netwiki.amath.unc.edu/DataFormats/NetDrawVna).
*
* @author Vojtech Bardiovsky
*/
public class ImporterVNA implements FileImporter, LongTask {
//Architecture
private Reader reader;
private ContainerLoader container;
private Report report;
private ProgressTicket progressTicket;
private boolean cancel = false;
private EdgeWidthFunction edgeWidthFunction;
Pattern pattern;
/**
* States for the state machine.
*/
private enum State {DEFAULT, NODE_DATA, NODE_PROPERTIES, TIE_DATA,
NODE_DATA_DEF, NODE_PROPERTIES_DEF, TIE_DATA_DEF};
/**
* Attributes defined by the VNA file: VNA files allow some or no properties
* to be defined for nodes and edges.
*/
private enum Attributes {OTHER, NODE_X, NODE_Y, NODE_COLOR, NODE_SIZE,
NODE_SHAPE, NODE_SHORT_LABEL, EDGE_STRENGTH};
/**
* Declared column labels for all sections.
*/
private AttributeColumn[] nodeDataColumns, tieDataColumns;
private String[] nodePropertiesLabels;
/**
* Declared attributes for the node properties declaration section.
*/
private Attributes[] nodeDataAttributes;
/**
* Declared attributes for the edge declaration section.
*/
private Attributes[] tieAttributes;
@Override
public boolean execute(ContainerLoader container) {
this.container = container;
this.report = new Report();
LineNumberReader lineReader = ImportUtils.getTextReader(reader);
try {
importData(lineReader);
} catch (Exception e) {
report.logIssue(new Issue(e, Issue.Level.SEVERE));
}
return !cancel;
}
private void importData(LineNumberReader reader) throws Exception {
List<String> lines = new ArrayList<String>();
while (reader.ready()) {
String line = reader.readLine();
if (line != null && !line.isEmpty()) {
lines.add(line);
}
}
State state = State.DEFAULT;
Progress.start(progressTicket, lines.size());
String[] split;
final Pattern nodeDataPattern = Pattern.compile("^\\*node data\\s*", Pattern.CASE_INSENSITIVE);
final Pattern nodePropertiesPattern = Pattern.compile("^\\*node properties\\s*", Pattern.CASE_INSENSITIVE);
final Pattern tieDataPattern = Pattern.compile("^\\*tie data\\s*", Pattern.CASE_INSENSITIVE);
for (String line : lines) {
if (cancel) {
return;
}
if (nodeDataPattern.matcher(line).matches()) {
state = State.NODE_DATA_DEF;
continue;
} else if (nodePropertiesPattern.matcher(line).matches()) {
state = State.NODE_PROPERTIES_DEF;
continue;
} else if (tieDataPattern.matcher(line).matches()) {
state = State.TIE_DATA_DEF;
continue;
}
switch (state) {
case NODE_DATA_DEF:
String[] nodeDataLabels = line.split("[\\s,]+");
nodeDataColumns = new AttributeColumn[nodeDataLabels.length];
for (int i = 1; i < nodeDataLabels.length; i++) {
nodeDataColumns[i] = container.getAttributeModel().getNodeTable().addColumn(nodeDataLabels[i], AttributeType.STRING);
}
state = State.NODE_DATA;
break;
case NODE_PROPERTIES_DEF:
// Initialize node properties labels and fill nodeAttributes
// if some attributes can be used for NodeDraft
nodePropertiesLabels = line.split("[\\s,]+");
nodeDataAttributes = new Attributes[nodePropertiesLabels.length];
for (int i = 1; i < nodePropertiesLabels.length; i++) {
if (nodePropertiesLabels[i].equalsIgnoreCase("x")) {
nodeDataAttributes[i] = Attributes.NODE_X;
} else if (nodePropertiesLabels[i].equalsIgnoreCase("y")) {
nodeDataAttributes[i] = Attributes.NODE_Y;
} else if (nodePropertiesLabels[i].equalsIgnoreCase("color")) {
nodeDataAttributes[i] = Attributes.NODE_COLOR;
} else if (nodePropertiesLabels[i].equalsIgnoreCase("size")) {
nodeDataAttributes[i] = Attributes.NODE_SIZE;
} else if (nodePropertiesLabels[i].equalsIgnoreCase("shortlabel")) {
nodeDataAttributes[i] = Attributes.NODE_SHORT_LABEL;
} else if (nodePropertiesLabels[i].equalsIgnoreCase("shape")) {
nodeDataAttributes[i] = Attributes.NODE_SHAPE;
} else {
throw new RuntimeException("Unexpected node parameter at line '" + line + "';");
}
}
state = State.NODE_PROPERTIES;
break;
case TIE_DATA_DEF:
String tieDataLabels[] = line.split("[\\s,]+");
tieDataColumns = new AttributeColumn[tieDataLabels.length];
tieAttributes = new Attributes[tieDataColumns.length];
if (tieDataColumns.length < 2) {
throw new RuntimeException("Edge data labels definition does not contain two necessary variables ('from' and 'to').");
}
// Initialize edge labels and fill edgeAttributes if some
// attributes can be used for EdgeDraft
for (int i = 2; i < tieDataColumns.length; i++) {
if (tieDataLabels[i].equalsIgnoreCase("strength")) {
tieAttributes[i] = Attributes.EDGE_STRENGTH;
} else {
tieAttributes[i] = Attributes.OTHER;
tieDataColumns[i] = container.getAttributeModel().getEdgeTable().addColumn(tieDataLabels[i], AttributeType.STRING);
}
}
state = State.TIE_DATA;
break;
case NODE_DATA:
// new node
split = split(line);
if (split.length != nodeDataColumns.length) {
report.logIssue(new Issue("Number of labels and number of data mismatch in: '" + line + "'", Issue.Level.WARNING));
break;
}
addNode(split);
// parse - if parse error => LOG error
break;
case NODE_PROPERTIES:
split = split(line);
if (split.length != nodePropertiesLabels.length) {
report.logIssue(new Issue("Number of labels and number of data mismatch in: '" + line + "'", Issue.Level.WARNING));
break;
}
addNodeProperties(split);
// parse - if parse error => LOG error
break;
case TIE_DATA:
split = split(line);
if (split.length != tieDataColumns.length) {
report.logIssue(new Issue("Number of labels and number of data mismatch in: '" + line + "'", Issue.Level.WARNING));
break;
}
addEdge(split);
// parse - if parse error => LOG error
break;
}
Progress.progress(progressTicket);
}
}
/**
* Splits the line using space separator, but respecting quotes.
*/
private String[] split(String line) {
// Pattern for splitting by spaces but respecting quotes.
if (pattern == null) {
pattern = Pattern.compile("[^\\s\"]+|\"([^\"]*)\"");
}
List<String> tokens = new ArrayList<String>();
Matcher patternMatcher = pattern.matcher(line);
while (patternMatcher.find()) {
if ((patternMatcher.group(1)) != null) {
tokens.add(patternMatcher.group(1));
} else {
tokens.add(patternMatcher.group());
}
}
return tokens.toArray(new String[]{});
}
private void addNode(String[] nodeData) {
NodeDraft node;
String id = nodeData[0];
if (!container.nodeExists(id)) {
node = container.factory().newNodeDraft();
node.setId(id);
container.addNode(node);
} else {
node = container.getNode(id);
}
for (int i = 1; i < nodeDataColumns.length; i++) {
node.addAttributeValue(nodeDataColumns[i], nodeData[i]);
}
}
private void addNodeProperties(String[] nodeProperties) {
NodeDraft node;
String id = nodeProperties[0];
if (!container.nodeExists(id)) {
node = container.factory().newNodeDraft();
node.setId(id);
container.addNode(node);
} else {
node = container.getNode(id);
}
int i = 0;
try {
for (i = 1; i < nodeProperties.length; i++) {
switch (nodeDataAttributes[i]) {
case NODE_X:
node.setX(Float.parseFloat(nodeProperties[i]));
break;
case NODE_Y:
node.setY(Float.parseFloat(nodeProperties[i]));
break;
case NODE_COLOR:
// Add just shades of red as NetDraw VNA is not specific
// about color.
node.setColor(Integer.parseInt(nodeProperties[i]), 0, 0);
break;
case NODE_SIZE:
node.setSize(Float.parseFloat(nodeProperties[i]));
break;
case NODE_SHORT_LABEL:
node.setLabel(nodeProperties[i]);
break;
}
}
} catch (NumberFormatException e) {
report.logIssue(new Issue("Error parsing numerical value at '" + nodeProperties[i] + "'.", Issue.Level.WARNING));
}
}
private void addEdge(String[] edgeData) {
NodeDraft sourceNode;
if (!container.nodeExists(edgeData[0])) {
sourceNode = container.factory().newNodeDraft();
sourceNode.setId(edgeData[0]);
container.addNode(sourceNode);
} else {
sourceNode = container.getNode(edgeData[0]);
}
NodeDraft targetNode;
if (!container.nodeExists(edgeData[1])) {
targetNode = container.factory().newNodeDraft();
targetNode.setId(edgeData[1]);
container.addNode(targetNode);
} else {
targetNode = container.getNode(edgeData[1]);
}
EdgeDraft edge = container.getEdge(sourceNode, targetNode);
if (edge == null) {
edge = container.factory().newEdgeDraft();
edge.setSource(sourceNode);
edge.setTarget(targetNode);
int i = 0;
try {
for (i = 2; i < edgeData.length; i++) {
switch (tieAttributes[i]) {
case EDGE_STRENGTH:
float weight = Float.parseFloat(edgeData[i]);
if (edgeWidthFunction != null) {
weight = edgeWidthFunction.computeTransformation(weight);
}
edge.setWeight(weight);
break;
case OTHER:
edge.addAttributeValue(tieDataColumns[i], edgeData[i]);
break;
}
}
} catch (NumberFormatException e) {
report.logIssue(new Issue("Error parsing numerical value at '" + edgeData[i] + "'.", Issue.Level.WARNING));
}
container.addEdge(edge);
}
}
public void setEdgeWidthFunction(EdgeWidthFunction function) {
this.edgeWidthFunction = function;
}
@Override
public void setReader(Reader reader) {
this.reader = reader;
}
@Override
public ContainerLoader getContainer() {
return container;
}
@Override
public Report getReport() {
return report;
}
@Override
public boolean cancel() {
cancel = true;
return true;
}
@Override
public void setProgressTicket(ProgressTicket progressTicket) {
this.progressTicket = progressTicket;
}
public static class EdgeWidthFunction {
public enum Function {LINEAR, SQUARE_ROOT, LOGARITHMIC};
public final Function function;
public final float coefficient;
public EdgeWidthFunction(Function function, float coefficient) {
this.function = function;
this.coefficient = coefficient;
}
public float computeTransformation(float value) {
switch (function) {
case LINEAR:
return value * coefficient;
case LOGARITHMIC:
return (float) Math.log(value + 10);
case SQUARE_ROOT:
return (float) Math.sqrt(value);
}
return 0;
}
@Override
public String toString() {
switch (function) {
case LINEAR:
return "Linear";
case LOGARITHMIC:
return "Logartihmic";
case SQUARE_ROOT:
return "Square root";
}
return null;
}
}
}