Skip to content

Commit b828e26

Browse files
committed
Feature: Add save-to-string primitive
1 parent e8ecc11 commit b828e26

10 files changed

Lines changed: 281 additions & 37 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ There may be rare occasions in which you don't want the NW extension to remember
208208
[`nw:load`](#nwload)
209209
[`nw:load-from-string`](#nwload-from-string)
210210
[`nw:save`](#nwsave)
211+
[`nw:save-to-string`](#nwsave-to-string)
211212

212213
### Centrality Measures
213214

@@ -1398,6 +1399,35 @@ Limitations:
13981399

13991400

14001401

1402+
### `nw:save-to-string`
1403+
1404+
```NetLogo
1405+
nw:save-to-string format
1406+
```
1407+
1408+
1409+
Serialize the current network context to a string in the given format and report it, rather than writing to a file. This is the counterpart to [`nw:load-from-string`](#nwload-from-string), and is useful in NetLogo Web and other contexts where the result needs to go somewhere other than the file system.
1410+
1411+
The `format` argument names the format explicitly and takes the same values as `nw:load-from-string`. It is case-insensitive and an optional leading dot is ignored, so `"gml"`, `"GML"`, and `".gml"` are all equivalent. The supported formats are:
1412+
1413+
- `dl`
1414+
- `gdf`
1415+
- `gexf`
1416+
- `gml`
1417+
- `graphml`
1418+
- `matrix`
1419+
- `vna`
1420+
1421+
Aside from reporting a string instead of writing to a file, `nw:save-to-string` behaves exactly like the corresponding `nw:save-*` primitive, with the same limitations regarding attributes and multigraphs.
1422+
1423+
For example, to save a network and hand it off to a browser download in NetLogo Web:
1424+
1425+
```
1426+
let gml nw:save-to-string "gml"
1427+
```
1428+
1429+
1430+
14011431
## Building
14021432

14031433
The extension is written in Scala (version 2.9.2).

documentation.conf

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,35 @@ Limitations:
12021202
- Multigraphs are not supported. Thus, two turtles can share at most one link. `nw:save-graphml` does support multigraphs, so use that if turtles can have more than one type of link connecting them.
12031203

12041204
`nw:save` determines the file-type of the given file based on the extension and calls the corresponding `save-*` primitive on it. Note that GraphML must be exported with `nw:save-graphml`.
1205+
"""
1206+
},
1207+
1208+
{
1209+
name: save-to-string,
1210+
type: reporter,
1211+
returns: string,
1212+
tags: [ io ],
1213+
arguments: [ {name: format, type: string} ],
1214+
description: """
1215+
Serialize the current network context to a string in the given format and report it, rather than writing to a file. This is the counterpart to [`nw:load-from-string`](#nwload-from-string), and is useful in NetLogo Web and other contexts where the result needs to go somewhere other than the file system.
1216+
1217+
The `format` argument names the format explicitly and takes the same values as `nw:load-from-string`. It is case-insensitive and an optional leading dot is ignored, so `"gml"`, `"GML"`, and `".gml"` are all equivalent. The supported formats are:
1218+
1219+
- `dl`
1220+
- `gdf`
1221+
- `gexf`
1222+
- `gml`
1223+
- `graphml`
1224+
- `matrix`
1225+
- `vna`
1226+
1227+
Aside from reporting a string instead of writing to a file, `nw:save-to-string` behaves exactly like the corresponding `nw:save-*` primitive, with the same limitations regarding attributes and multigraphs.
1228+
1229+
For example, to save a network and hand it off to a browser download in NetLogo Web:
1230+
1231+
```
1232+
let gml nw:save-to-string "gml"
1233+
```
12051234
"""
12061235
}
12071236
]

documentation.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,35 @@ primitives:
12871287
tags:
12881288
- io
12891289
type: command
1290+
- arguments:
1291+
- name: format
1292+
type: string
1293+
description: |2
1294+
1295+
Serialize the current network context to a string in the given format and report it, rather than writing to a file. This is the counterpart to [`nw:load-from-string`](#nwload-from-string), and is useful in NetLogo Web and other contexts where the result needs to go somewhere other than the file system.
1296+
1297+
The `format` argument names the format explicitly and takes the same values as `nw:load-from-string`. It is case-insensitive and an optional leading dot is ignored, so `"gml"`, `"GML"`, and `".gml"` are all equivalent. The supported formats are:
1298+
1299+
- `dl`
1300+
- `gdf`
1301+
- `gexf`
1302+
- `gml`
1303+
- `graphml`
1304+
- `matrix`
1305+
- `vna`
1306+
1307+
Aside from reporting a string instead of writing to a file, `nw:save-to-string` behaves exactly like the corresponding `nw:save-*` primitive, with the same limitations regarding attributes and multigraphs.
1308+
1309+
For example, to save a network and hand it off to a browser download in NetLogo Web:
1310+
1311+
```
1312+
let gml nw:save-to-string "gml"
1313+
```
1314+
name: save-to-string
1315+
returns: string
1316+
tags:
1317+
- io
1318+
type: reporter
12901319
tableOfContents:
12911320
centrality: Centrality Measures
12921321
clusterer: Clusterer/Community Detection

src/main/org/nlogo/extensions/nw/NetworkExtension.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class NetworkExtension extends api.DefaultClassManager with GraphContextManager
102102
add("load-vna", new LoadFileType(".vna"))
103103

104104
add("save", new prim.Save(this))
105+
add("save-to-string", new prim.SaveToString(this))
105106
add("save-dl", new SaveFileType(this, ".dl"))
106107
add("save-gdf", new SaveFileType(this, ".gdf"))
107108
add("save-gexf", new SaveFileType(this, ".gexf"))

src/main/org/nlogo/extensions/nw/NetworkExtensionUtil.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ object NetworkExtensionUtil {
2222
def readerForString(data: String): Reader =
2323
new InputStreamReader(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)
2424

25+
val supportedStringFormats = Seq("dl", "gdf", "gexf", "gml", "graphml", "matrix", "vna")
26+
27+
def normalizeNetworkFormat(rawFormat: String): String =
28+
rawFormat.trim.toLowerCase(Locale.ENGLISH).stripPrefix(".")
29+
30+
def unsupportedFormatMessage(rawFormat: String): String =
31+
s"'$rawFormat' is not a supported network format. Valid formats are: dl, gdf, gexf, gml, graphml, matrix, and vna."
32+
2533
implicit def functionToTransformer[I, O](f: Function1[I, O]): org.apache.commons.collections15.Transformer[I,O] =
2634
new org.apache.commons.collections15.Transformer[I, O] {
2735
override def transform(i: I) = f(i)

src/main/org/nlogo/extensions/nw/gephi/GephiExport.scala

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.nlogo.extensions.nw.gephi
22

3-
import java.io.File
3+
import java.io.{ File, StringWriter }
44

55
import org.nlogo.agent.{ AgentSet, Link, Turtle, World }
66
import org.nlogo.api
@@ -10,7 +10,7 @@ import org.nlogo.core.Breed
1010
import org.gephi.graph.api.{ Column, GraphController }
1111
import org.gephi.io.exporter.api.ExportController
1212
import org.gephi.io.exporter.plugin.{ ExporterCSV, ExporterGraphML }
13-
import org.gephi.io.exporter.spi.Exporter
13+
import org.gephi.io.exporter.spi.{ CharacterExporter, Exporter }
1414
import org.gephi.project.api.ProjectController
1515
import org.gephi.utils.longtask.spi.LongTask
1616
import org.gephi.utils.progress.ProgressTicket
@@ -30,11 +30,29 @@ object GephiExport {
3030
save(context, world, file, exportController.getFileExporter(file))
3131
}
3232

33-
def save(context: GraphContext, world: World, file: File, exporter: Exporter) = GephiUtils.withNWLoaderContext {
34-
implicit val implicitWorld = world;
33+
def save(context: GraphContext, world: World, file: File, exporter: Exporter): Unit = GephiUtils.withNWLoaderContext {
34+
prepareExport(exporter, file.toString)
35+
buildWorkspace(context, world)
36+
exportController.exportFile(file, exporter)
37+
}
38+
39+
def saveToString(context: GraphContext, world: World, extension: String): String = GephiUtils.withNWLoaderContext {
40+
val exporter = exportController.getExporter(extension)
41+
prepareExport(exporter, s"the given $extension string")
42+
buildWorkspace(context, world)
43+
exporter match {
44+
case ce: CharacterExporter =>
45+
val writer = new StringWriter
46+
exportController.exportWriter(writer, ce)
47+
writer.toString
48+
case _ =>
49+
throw new ExtensionException(s"The $extension format cannot be exported to a string.")
50+
}
51+
}
3552

53+
private def prepareExport(exporter: Exporter, sourceName: String): Unit = {
3654
if (exporter == null) {
37-
throw new ExtensionException("Unable to find exporter for " + file)
55+
throw new ExtensionException("Unable to find exporter for " + sourceName)
3856
} else if (exporter.isInstanceOf[ExporterCSV]) {
3957
throw new ExtensionException("Exporting CSV files is not supported.")
4058
} else if (exporter.isInstanceOf[ExporterGraphML]) {
@@ -70,6 +88,11 @@ object GephiExport {
7088
} )
7189
case _ => // ignore
7290
}
91+
}
92+
93+
private def buildWorkspace(context: GraphContext, world: World): Unit = {
94+
implicit val implicitWorld = world;
95+
7396
val program = world.program
7497
val projectController = Lookup.getDefault.lookup(classOf[ProjectController])
7598
projectController.newProject()
@@ -144,7 +167,6 @@ object GephiExport {
144167
graph.addEdge(edge)
145168
}
146169
gephiWorkspace.add(graphModel)
147-
exportController.exportFile(file, exporter)
148170
}
149171

150172
private type JDouble = java.lang.Double

src/main/org/nlogo/extensions/nw/jung/io/GraphMLExport.scala

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package org.nlogo.extensions.nw.jung.io
55
import java.io.BufferedWriter
66
import java.io.FileWriter
77
import java.io.PrintWriter
8+
import java.io.StringWriter
9+
import java.io.Writer
810

911
import org.apache.commons.collections15.Transformer
1012
import org.nlogo.agent
@@ -15,7 +17,23 @@ import org.nlogo.extensions.nw.NetworkExtensionUtil.{ functionToTransformer, usi
1517

1618
object GraphMLExport {
1719

18-
def save(graphContext: GraphContext, filename: String) = {
20+
def save(graphContext: GraphContext, filename: String): Unit =
21+
try {
22+
using(new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { writeGraphML(graphContext, _) }
23+
} catch {
24+
case e: Exception => throw new ExtensionException(e)
25+
}
26+
27+
def saveToString(graphContext: GraphContext): String =
28+
try {
29+
val writer = new StringWriter
30+
writeGraphML(graphContext, writer)
31+
writer.toString
32+
} catch {
33+
case e: Exception => throw new ExtensionException(e)
34+
}
35+
36+
private def writeGraphML(graphContext: GraphContext, writer: Writer): Unit = {
1937
val world = graphContext.world
2038

2139
val graphMLWriter = new GraphMLWriterWithAttribType[agent.Turtle, agent.Link]
@@ -79,13 +97,7 @@ object GraphMLExport {
7997
}
8098
}
8199

82-
try {
83-
using(new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { printWriter =>
84-
graphMLWriter.save(graphContext.asJungGraph, printWriter)
85-
}
86-
} catch {
87-
case e: Exception => throw new ExtensionException(e)
88-
}
100+
graphMLWriter.save(graphContext.asJungGraph, writer)
89101

90102
}
91103

src/main/org/nlogo/extensions/nw/jung/io/Matrix.scala

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
package org.nlogo.extensions.nw.jung.io
44

5-
import java.io.{ BufferedReader, FileNotFoundException, Reader }
5+
import java.io.{ BufferedReader, FileNotFoundException, Reader, StringWriter, Writer }
66

77
import org.nlogo.agent.AgentSet
88
import org.nlogo.agent.Link
@@ -19,24 +19,34 @@ import edu.uci.ics.jung.algorithms.matrix.GraphMatrixOperations
1919

2020
object Matrix {
2121

22-
def save(graph: jung.graph.Graph[Turtle, Link], filename: String): Unit = {
22+
def save(graph: jung.graph.Graph[Turtle, Link], filename: String): Unit =
23+
try {
24+
using(new java.io.FileWriter(filename)) { writeMatrix(graph, _) }
25+
} catch {
26+
case e: Exception => throw new ExtensionException(e)
27+
}
28+
29+
def saveToString(graph: jung.graph.Graph[Turtle, Link]): String =
30+
try {
31+
val writer = new StringWriter
32+
writeMatrix(graph, writer)
33+
writer.toString
34+
} catch {
35+
case e: Exception => throw new ExtensionException(e)
36+
}
37+
38+
private def writeMatrix(graph: jung.graph.Graph[Turtle, Link], writer: Writer): Unit = {
2339
/* This is almost a line for line copy of jung.io.MatrixFile.save, the major
2440
* difference being that it explicitly uses the US locale to make sure entries
2541
* use the dot decimal separator (see issue #69) */
26-
try {
27-
using(new java.io.FileWriter(filename)) { writer =>
28-
val matrix = GraphMatrixOperations.graphToSparseMatrix(graph, null) // TODO: provide weights
29-
for (i <- 0 until matrix.rows) {
30-
for (j <- 0 until matrix.columns) {
31-
val w = matrix.getQuick(i, j)
32-
writer.write("%4.2f".formatLocal(java.util.Locale.US, w))
33-
if (j < matrix.columns - 1) writer.write(" ")
34-
}
35-
writer.write("\n")
36-
}
42+
val matrix = GraphMatrixOperations.graphToSparseMatrix(graph, null) // TODO: provide weights
43+
for (i <- 0 until matrix.rows) {
44+
for (j <- 0 until matrix.columns) {
45+
val w = matrix.getQuick(i, j)
46+
writer.write("%4.2f".formatLocal(java.util.Locale.US, w))
47+
if (j < matrix.columns - 1) writer.write(" ")
3748
}
38-
} catch {
39-
case e: Exception => throw new ExtensionException(e)
49+
writer.write("\n")
4050
}
4151
}
4252

src/main/org/nlogo/extensions/nw/prim/IO.scala

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.nlogo.extensions.nw.prim
22

33
import java.io.File
4-
import java.util.Locale
54

65
import org.nlogo.agent.World
76
import org.nlogo.api
@@ -11,7 +10,7 @@ import org.nlogo.nvm.ExtensionContext
1110
import org.nlogo.extensions.nw.GraphContextProvider
1211
import org.nlogo.extensions.nw.NetworkExtensionUtil._
1312
import org.nlogo.extensions.nw.gephi.{ GephiExport, GephiImport, GephiUtils }
14-
import org.nlogo.extensions.nw.jung.io.{ GraphMLImport, Matrix }
13+
import org.nlogo.extensions.nw.jung.io.{ GraphMLExport, GraphMLImport, Matrix }
1514

1615
class Load extends TurtleAskingCommand {
1716
override def getSyntax = commandSyntax(
@@ -34,22 +33,19 @@ class LoadFromString extends TurtleAskingCommand {
3433
override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext {
3534
implicit val world = context.world.asInstanceOf[World]
3635
val rawFormat = args(0).getString
37-
val format = rawFormat.trim.toLowerCase(Locale.ENGLISH).stripPrefix(".")
3836
val data = args(1).getString
3937
val turtleBreed = args(2).getAgentSet.requireTurtleBreed
4038
val linkBreed = args(3).getAgentSet.requireLinkBreed
4139
val rng = context.getRNG
42-
format match {
40+
normalizeNetworkFormat(rawFormat) match {
4341
case "graphml" =>
4442
askTurtles(context)(GraphMLImport.load(readerForString(data), world, rng, turtleBreed, linkBreed))
4543
case "matrix" =>
4644
askTurtles(context)(Matrix.load(readerForString(data), turtleBreed, linkBreed, world, rng))
47-
case "dl" | "gdf" | "gexf" | "gml" | "vna" =>
45+
case format @ ("dl" | "gdf" | "gexf" | "gml" | "vna") =>
4846
GephiImport.loadString(data, "." + format, world, turtleBreed, linkBreed, askTurtles(context))
4947
case _ =>
50-
throw new api.ExtensionException(
51-
s"'$rawFormat' is not a supported network format. Valid formats are: " +
52-
"dl, gdf, gexf, gml, graphml, matrix, and vna.")
48+
throw new api.ExtensionException(unsupportedFormatMessage(rawFormat))
5349
}
5450
}
5551
}
@@ -87,6 +83,25 @@ class Save(gcp: GraphContextProvider) extends api.Command {
8783
}
8884
}
8985

86+
class SaveToString(gcp: GraphContextProvider) extends api.Reporter {
87+
override def getSyntax = reporterSyntax(right = List(StringType), ret = StringType)
88+
override def report(args: Array[api.Argument], context: api.Context): AnyRef = GephiUtils.withNWLoaderContext {
89+
val world = context.getAgent.world.asInstanceOf[World]
90+
val rawFormat = args(0).getString
91+
val graphContext = gcp.getGraphContext(world)
92+
normalizeNetworkFormat(rawFormat) match {
93+
case "graphml" =>
94+
GraphMLExport.saveToString(graphContext)
95+
case "matrix" =>
96+
Matrix.saveToString(graphContext.asJungGraph)
97+
case format @ ("dl" | "gdf" | "gexf" | "gml" | "vna") =>
98+
GephiExport.saveToString(graphContext, world, "." + format)
99+
case _ =>
100+
throw new api.ExtensionException(unsupportedFormatMessage(rawFormat))
101+
}
102+
}
103+
}
104+
90105
class SaveFileType(gcp: GraphContextProvider, extension: String) extends api.Command {
91106
override def getSyntax = commandSyntax(right = List(StringType))
92107
override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext {

0 commit comments

Comments
 (0)