Skip to content

Commit

Permalink
feat (core): GEXF! (re. #502)
Browse files Browse the repository at this point in the history
  • Loading branch information
vorburger committed Aug 11, 2024
1 parent f39327e commit 562f5d6
Show file tree
Hide file tree
Showing 21 changed files with 363 additions and 75 deletions.
6 changes: 3 additions & 3 deletions docs/concepts/other.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ Enola 🕵🏾‍♀️ visualizes (TBD) the relationships of its _Entities_ usi
* [yEd](https://www.yworks.com) (yFiles) #commercial #sdk #freemium
* [Graphviz](https://graphviz.org) #available
* [Gephi](https://gephi.org), with [Gephi Lite](https://gephi.org/gephi-lite/) which uses [SigmaJS](https://www.sigmajs.org) on [Graphology](https://graphology.github.io) for JS #[FOSDEM](https://github.com/vorburger/vorburger.ch-Notes/blob/develop/conferences/FOSDEM-2024.md) #dynamicGraph #web #active #planned #ToDo
* [D3js.org](https://d3js.org) also has [Graphs](https://observablehq.com/@d3/force-directed-graph-component?collection=@d3/charts) (and [d3rdf](https://github.com/Rathachai/d3rdf))
* [vis.js](https://visjs.org)
* [D3js.org](https://d3js.org), and [Observable Plot](https://observablehq.com/plot/) has #[graph](https://observablehq.com/@d3/force-directed-graph-component?collection=@d3/charts) (and [d3rdf](https://github.com/Rathachai/d3rdf)) #[timeline](https://observablehq.com/@observablehq/plot-civilizations-timeline?intent=fork)
* [vis.js](https://visjs.org) #opensource #graph #timeline #web
* [Cytoscape](https://cytoscape.org) #opensource #graph #desktop #[plugins](https://apps.cytoscape.org/)
* [Cytoscape.js](https://js.cytoscape.org/) #graph #library #web
* [Mermaid](https://mermaid.js.org) #opensource
* [Mermaid](https://mermaid.js.org) #graph #[timeline](https://mermaid.js.org/syntax/timeline.html) #opensource
* [PlantUML](https://plantuml.com) #opensource
* [GraphStream](https://graphstream-project.org) #opensource #dynamicGraph #java #swing #desktop #inactive
* [Vega Lite](https://vega.github.io/vega-lite/) & [Vega](https://vega.github.io/vega/) #opensource
Expand Down
3 changes: 3 additions & 0 deletions java/dev/enola/core/rosetta/Rosetta.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import dev.enola.datatype.DatatypeRepository;
import dev.enola.datatype.Datatypes;
import dev.enola.rdf.RdfResourceConverter;
import dev.enola.thing.gen.gexf.GexfGenerator;
import dev.enola.thing.gen.gexf.GexfResourceConverter;
import dev.enola.thing.gen.graphviz.GraphvizGenerator;
import dev.enola.thing.gen.graphviz.GraphvizResourceConverter;
import dev.enola.thing.metadata.ThingMetadataProvider;
Expand Down Expand Up @@ -96,6 +98,7 @@ public Rosetta(ResourceProvider rp) {
messageResourceConverter,
new YamlJsonResourceConverter(),
new GraphvizResourceConverter(new GraphvizGenerator(tmp)),
new GexfResourceConverter(new GexfGenerator(tmp)),
new CharResourceConverter()));
}

Expand Down
24 changes: 12 additions & 12 deletions java/dev/enola/core/rosetta/RosettaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@
import dev.enola.common.io.iri.namespace.NamespaceConverter;
import dev.enola.common.io.iri.namespace.NamespaceConverterWithRepository;
import dev.enola.common.io.iri.namespace.NamespaceRepositoryEnolaDefaults;
import dev.enola.common.io.resource.ClasspathResource;
import dev.enola.common.io.resource.MemoryResource;
import dev.enola.common.io.resource.ResourceProvider;
import dev.enola.common.io.resource.StringResource;
import dev.enola.common.io.resource.*;
import dev.enola.rdf.RdfMediaTypes;
import dev.enola.thing.Thing;
import dev.enola.thing.gen.gexf.GexfMediaType;
import dev.enola.thing.gen.graphviz.GraphvizMediaType;
import dev.enola.thing.impl.ImmutableThing;
import dev.enola.thing.io.ThingMediaTypes;
Expand Down Expand Up @@ -175,10 +173,8 @@ public void testYamlToTurtle() throws Exception {
}

@Test
public void testGraphviz() throws Exception {
var in = rp.get("classpath:/graphviz.ttl");
var out = new MemoryResource(GraphvizMediaType.GV);

public void testGraphvizAndGexf() throws Exception {
var in = rp.get("classpath:/graph.ttl");
try (var ctx = TLC.open()) {
// This tests that StackedThingProvider in GraphvizGenerator works;
// if it did not "shadow", then we would have an empty Salutation.
Expand All @@ -192,10 +188,14 @@ public void testGraphviz() throws Exception {
var namespaceConverter = new NamespaceConverterWithRepository(namespaceRepo);
ctx.push(NamespaceConverter.class, namespaceConverter);

rosetta.convertInto(in, out);
}
var gexf = new MemoryResource(GexfMediaType.GEXF);
rosetta.convertInto(in, gexf);
assertThat(gexf)
.hasCharsEqualTo(rp.get("classpath:/graph.expected.gexf.xml?charset=UTF-8"));

var expectedOut = rp.get("classpath:/graphviz.expected.gv");
assertThat(out).hasCharsEqualTo(expectedOut);
var gv = new MemoryResource(GraphvizMediaType.GV);
rosetta.convertInto(in, gv);
assertThat(gv).hasCharsEqualTo(rp.get("classpath:/graph.expected.gv"));
}
}
}
1 change: 1 addition & 0 deletions java/dev/enola/thing/gen/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ java_library(
"//java/dev/enola/common/convert",
"//java/dev/enola/common/function",
"//java/dev/enola/common/io",
"//java/dev/enola/common/time",
"//java/dev/enola/common/tree",
"//java/dev/enola/data",
"//java/dev/enola/datatype",
Expand Down
40 changes: 40 additions & 0 deletions java/dev/enola/thing/gen/ThingsIntoAppendableConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache 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
*
* https://www.apache.org/licenses/LICENSE-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 dev.enola.thing.gen;

import dev.enola.common.convert.ConversionException;
import dev.enola.common.convert.ConverterIntoAppendable;
import dev.enola.common.io.metadata.Metadata;
import dev.enola.common.io.resource.WritableResource;
import dev.enola.thing.Thing;

import java.io.IOException;

public interface ThingsIntoAppendableConverter extends ConverterIntoAppendable<Iterable<Thing>> {

default void convertIntoOrThrow(Iterable<Thing> things, WritableResource into)
throws ConversionException, IOException {
try (var out = into.charSink().openBufferedStream()) {
convertIntoOrThrow(things, out);
}
}

default String label(Metadata metadata) {
return metadata.emoji() + metadata.label();
}
}
130 changes: 126 additions & 4 deletions java/dev/enola/thing/gen/gexf/GexfGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,139 @@
*/
package dev.enola.thing.gen.gexf;

import com.google.common.collect.Iterables;
import com.google.common.escape.Escaper;
import com.google.common.xml.XmlEscapers;

import dev.enola.common.context.TLC;
import dev.enola.common.convert.ConversionException;
import dev.enola.common.convert.ConverterIntoAppendable;
import dev.enola.common.io.metadata.MetadataProvider;
import dev.enola.common.time.Interval;
import dev.enola.thing.Thing;
import dev.enola.thing.gen.ThingsIntoAppendableConverter;
import dev.enola.thing.metadata.ThingHierarchyProvider;
import dev.enola.thing.metadata.ThingTimeProvider;
import dev.enola.thing.repo.StackedThingProvider;
import dev.enola.thing.repo.ThingProvider;

import java.io.IOException;

public class GexfGenerator implements ConverterIntoAppendable<Iterable<Thing>> {
public class GexfGenerator implements ThingsIntoAppendableConverter {

// TODO Use ThingHierarchyProvider to cluster
// TODO Write Attributes
// TODO How to treat blank nodes? Attributes, or Nodes, with parent?
// TODO Custom Node color, shape & size
// TODO Custom Edge color, thickness, shape

private final MetadataProvider metadataProvider;
private final ThingTimeProvider timeProvider = new ThingTimeProvider();
private final ThingHierarchyProvider hierarchyProvider = new ThingHierarchyProvider();

public GexfGenerator(MetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider;
}

@Override
public boolean convertInto(Iterable<Thing> from, Appendable into)
public boolean convertInto(Iterable<Thing> from, Appendable out)
throws ConversionException, IOException {
return false;
out.append(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<gexf xmlns=\"http://gexf.net/1.3\""
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+ " xsi:schemaLocation=\"http://gexf.net/1.3 http://gexf.net/1.3/gexf.xsd\""
+ " version=\"1.3\">\n");
out.append(" <meta><creator>Enola.dev</creator></meta>\n");
out.append(
" <graph defaultedgetype=\"directed\" mode=\"dynamic\" timeformat=\"dateTime\""
+ " timerepresentation=\"interval\">\n");

try (var ctx = TLC.open()) {
ctx.push(ThingProvider.class, new StackedThingProvider(from));

out.append(" <nodes>\n");
for (Thing thing : from) printThingNode(thing, out);
out.append(" </nodes>\n");

out.append(" <edges>\n");
for (Thing thing : from) printThingEdges(thing, out);
out.append(" </edges>\n");
}

out.append(" </graph>\n</gexf>\n");
return true;
}

private void printThingNode(Thing thing, Appendable out) throws IOException {
var id = thing.iri();
var metadata = metadataProvider.get(thing, thing.iri());
var intervals = timeProvider.existance(thing);

out.append(" <node id=\"");
xmlAttribute(id, out);
out.append("\" label=\"");
xmlAttribute(label(metadata), out);
out.append("\"");
if (!Iterables.isEmpty(intervals)) {
if (Iterables.size(intervals) == 1) {
var interval = intervals.iterator().next();
printInterval(interval, out);

} else {
out.append(">\n");
out.append(" <spells>\n");
for (var interval : intervals) {
out.append(" <spell");
printInterval(interval, out);
}
out.append(" </spells>\n");
out.append(" </node>");
}
} else out.append("/>\n");
}

private void printInterval(Interval interval, Appendable out) throws IOException {
if (!interval.isUnboundedStart()) {
out.append(" start=\"");
out.append(interval.start().toString());
out.append("\"");
}
if (!interval.isUnboundedEnd()) {
out.append(" end=\"");
out.append(interval.end().toString());
out.append("\"");
}
out.append("/>\n");
}

private void printThingEdges(Thing thing, Appendable out) throws IOException {
for (var linkPropertyIRI : thing.predicateIRIs()) {
if (!thing.isLink(linkPropertyIRI)) continue;
var linkIRI = thing.getString(linkPropertyIRI);
var linkMetadata = metadataProvider.get(linkPropertyIRI);

var source = thing.iri();
var target = linkIRI;
var kind = linkPropertyIRI;
var label = label(linkMetadata);

out.append(" <edge source=\"");
xmlAttribute(source, out);
out.append("\" target=\"");
xmlAttribute(target, out);
out.append("\" kind=\"");
xmlAttribute(kind, out);
out.append("\" label=\"");
xmlAttribute(label, out);
out.append("\"/>\n");
}
}

private void printThingEdge(String source, String target, String kind, String label) {}

private void xmlAttribute(String text, Appendable out) throws IOException {
out.append(XML_ATTRIBUTE_ESCAPER.escape(text));
}

private static final Escaper XML_ATTRIBUTE_ESCAPER = XmlEscapers.xmlAttributeEscaper();
}
4 changes: 2 additions & 2 deletions java/dev/enola/thing/gen/gexf/GexfMediaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
@AutoService(MediaTypeProvider.class)
public class GexfMediaType implements MediaTypeProvider {

public static final MediaType GV =
public static final MediaType GEXF =
MediaType.create("application", "gexf+xml").withCharset(StandardCharsets.UTF_8);

@Override
public Map<String, MediaType> extensionsToTypes() {
return ImmutableMap.of("gexf", GV);
return ImmutableMap.of("gexf", GEXF);
}
}
45 changes: 45 additions & 0 deletions java/dev/enola/thing/gen/gexf/GexfResourceConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache 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
*
* https://www.apache.org/licenses/LICENSE-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 dev.enola.thing.gen.gexf;

import dev.enola.common.io.mediatype.MediaTypes;
import dev.enola.common.io.resource.ReadableResource;
import dev.enola.common.io.resource.WritableResource;
import dev.enola.common.io.resource.convert.CatchingResourceConverter;
import dev.enola.thing.io.Loader;
import dev.enola.thing.io.UriIntoThingConverters;

public class GexfResourceConverter implements CatchingResourceConverter {

private final GexfGenerator gexfGenerator;

public GexfResourceConverter(GexfGenerator gexfGenerator) {
this.gexfGenerator = gexfGenerator;
}

@Override
public boolean convertIntoThrows(ReadableResource from, WritableResource into)
throws Exception {
if (!MediaTypes.normalizedNoParamsEquals(into.mediaType(), GexfMediaType.GEXF))
return false;

var things = new Loader(new UriIntoThingConverters()).loadAtLeastOneThing(from.uri());
gexfGenerator.convertIntoOrThrow(things, into);
return true;
}
}
38 changes: 38 additions & 0 deletions java/dev/enola/thing/gen/gexf/jaxb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--
SPDX-License-Identifier: Apache-2.0
Copyright 2024 The Enola <https://enola.dev> Authors
Licensed under the Apache 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
https://www.apache.org/licenses/LICENSE-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.
-->

# GEXF Java API

The classes in this directory are a Java API for the
[Graph Exchange XML Format](https://gexf.net) (GEXF).

These were generated using `xjc` from JAXB RI Tools like this:

xjc ???

## How to get `xjc`

git clone https://github.com/eclipse-ee4j/jaxb-ri.git && cd jaxb-ri/jaxb-ri
git checkout 4.0.5-RI
mvn package

???

## ToDo

1. Add link to this on https://github.com/francesco-ficarola/gexf4j/tree/master (which is for GEXF 1.2 whereas we want 1.3)
Loading

0 comments on commit 562f5d6

Please sign in to comment.