From d2ba22d3c581852f2d86ee37a8ca0bbc3062ebe5 Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Wed, 21 Aug 2024 15:41:31 +0200 Subject: [PATCH 1/8] Update to graphANNIS 3.4.0 --- CHANGELOG.md | 6 +++--- pom.xml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4194777f0f..bb30e08976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 when the "layer" is not specified in the resolver configuration. Before this fix, the layer name was not checked, but the node still needed to be part of a layer. -- Updated to graphANNIS 3.3.3 which fixes an issue where a backup folder in the - database might not be loaded correctly and an issue where impossible precedence - queries created a "File too large" error. - Allow to show text visualizations for documents where the token are not connected to the textual data source. - Resolver entries with the "element" type "node" but no layer are now always shown. - Make the audio and video visualizer a little bit more robust when they are not pre-loaded. +- Upgraded to graphANNIS 3.4.0 which includes fixes for an issue where a backup + folder in the database might not be loaded correctly and an issue where + impossible precedence queries created a "File too large" error ## [4.12.2] - 2024-06-04 diff --git a/pom.xml b/pom.xml index 3e2d3a118a..52139a20fd 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 8.14.3 true org.corpus_tools.annis.gui.AnnisUiApplication - 3.3.3 + 3.4.0 4.7.2 server 1.5.24 @@ -261,7 +261,7 @@ https://github.com/korpling/graphANNIS/releases/download/v${graphannis.version}/graphannis-webservice-x86_64-unknown-linux-gnu.tar.xz ${project.build.directory}/native/ - d8f66653be3cae861a4505804bbb6fd85eed527a5d4941445c5579ae4a4671d6 + dff1c36dc7e11a0e585dc89e52996ab339f9e1f1c1624ca285f5ecb83be882b5 true @@ -276,7 +276,7 @@ https://github.com/korpling/graphANNIS/releases/download/v${graphannis.version}/graphannis-webservice-x86_64-pc-windows-msvc.zip ${project.build.directory}/native/win32-x86-64/ - b52d34ade7c9ba2db8150a7236f48f923b13e59832a0232bd46f65b15889e44b + 4438ce0aa88b3f9484bd058c26ef4a71fdfedb6509888a15ecb93e6a27c2a017 true @@ -291,7 +291,7 @@ https://github.com/korpling/graphANNIS/releases/download/v${graphannis.version}/graphannis-webservice-x86_64-apple-darwin.tar.xz ${project.build.directory}/native/ - c06dec468e72b9bd1eeb524eede42874ac78e9ca6e034939395f5e8dbfd67072 + 9d149ed447c76eb9eedb85b2f8b6bf6393431f11978ffc31b3d868985bb1c59d true From c5a7a8a089aee621193cc5665595779e8b3c014b Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Wed, 21 Aug 2024 16:34:13 +0200 Subject: [PATCH 2/8] Start to extend the firstPass function to have access to the "data" section. --- .../gui/graphml/DocumentGraphMapper.java | 137 +++++++++++++++--- 1 file changed, 115 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java index 87c9859d8a..d48761f7f4 100644 --- a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java +++ b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java @@ -58,6 +58,7 @@ public class DocumentGraphMapper extends AbstractGraphMLMapper { private final Set hasOutgoingCoverageEdge; private final Set hasOutgoingDominanceEdge; private final Set> hasNonEmptyDominanceEdge; + private final Multimap isPartOf; @@ -66,6 +67,7 @@ protected DocumentGraphMapper() { this.hasOutgoingCoverageEdge = new HashSet<>(); this.hasOutgoingDominanceEdge = new HashSet<>(); this.hasNonEmptyDominanceEdge = new HashSet<>(); + this.isPartOf = HashMultimap.create(); } @@ -78,33 +80,123 @@ public static SDocumentGraph map(File inputFile) throws IOException, XMLStreamEx @Override protected void firstPass(XMLEventReader reader) throws XMLStreamException { + Map keys = new TreeMap<>(); + int level = 0; + boolean inGraph = false; + Optional currentNodeId = Optional.empty(); + Optional currentDataKey = Optional.empty(); + Optional currentSourceId = Optional.empty(); + Optional currentTargetId = Optional.empty(); + Optional currentComponent = Optional.empty(); + + Map data = new HashMap<>(); + while (reader.hasNext()) { XMLEvent event = reader.nextEvent(); - if (event.isStartElement()) { - StartElement element = event.asStartElement(); - if ("edge".equals(element.getName().getLocalPart())) { - Attribute source = element.getAttributeByName(new QName("source")); - Attribute target = element.getAttributeByName(new QName("target")); - Attribute label = element.getAttributeByName(new QName("label")); - if (label != null) { - Component c = parseComponent(label.getValue()); - if (source != null) { - if (c.getType() == AnnotationComponentType.COVERAGE) { - hasOutgoingCoverageEdge.add(source.getValue()); - } else if (c.getType() == AnnotationComponentType.DOMINANCE) { - hasOutgoingDominanceEdge.add(source.getValue()); - } else if (c.getType() == AnnotationComponentType.PARTOF) { - isPartOf.put(Helper.addSaltPrefix(source.getValue()), - Helper.addSaltPrefix(target.getValue())); + switch (event.getEventType()) { + case XMLEvent.START_ELEMENT: + level++; + StartElement startElement = event.asStartElement(); + // create all new nodes + switch (startElement.getName().getLocalPart()) { + case "graph": + if (level == 2) { + inGraph = true; } - if (target != null && c.getType() == AnnotationComponentType.DOMINANCE - && !c.getName().isEmpty()) { - hasNonEmptyDominanceEdge.add(Pair.of(source.getValue(), target.getValue())); + break; + case "key": + if (level == 2) { + addAnnotationKey(keys, startElement); + } + break; + case "node": + if (inGraph && level == 3) { + Attribute id = startElement.getAttributeByName(new QName("id")); + if (id != null) { + currentNodeId = Optional.ofNullable(id.getValue()); + } } + break; + case "edge": + if (inGraph && level == 3) { + // Get the source and target node IDs + Attribute source = startElement.getAttributeByName(new QName("source")); + Attribute target = startElement.getAttributeByName(new QName("target")); + Attribute label = startElement.getAttributeByName(new QName("label")); + if (label != null) { + Component c = parseComponent(label.getValue()); + if (source != null) { + if (c.getType() == AnnotationComponentType.COVERAGE) { + hasOutgoingCoverageEdge.add(source.getValue()); + } else if (c.getType() == AnnotationComponentType.DOMINANCE) { + hasOutgoingDominanceEdge.add(source.getValue()); + } else if (c.getType() == AnnotationComponentType.PARTOF) { + isPartOf.put(Helper.addSaltPrefix(source.getValue()), + Helper.addSaltPrefix(target.getValue())); + } + if (target != null && c.getType() == AnnotationComponentType.DOMINANCE + && !c.getName().isEmpty()) { + hasNonEmptyDominanceEdge.add(Pair.of(source.getValue(), target.getValue())); + } + } + } + } + break; + case "data": + Attribute key = startElement.getAttributeByName(new QName("key")); + if (key != null) { + currentDataKey = Optional.ofNullable(key.getValue()); + } + break; + } + break; + case XMLEvent.CHARACTERS: + if (currentDataKey.isPresent() && inGraph && level == 4) { + String annoKey = keys.get(currentDataKey.get()); + if (annoKey != null) { + // Copy all data attributes into our own map + data.put(annoKey, event.asCharacters().getData()); } } - } + break; + case XMLEvent.END_ELEMENT: + EndElement endElement = event.asEndElement(); + switch (endElement.getName().getLocalPart()) { + case "graph": + inGraph = false; + break; + case "node": + String nodeType = data.get("annis::node_type"); + if (nodeType == "datasource") { + // TODO: check if this data source is a timeline + } + + currentNodeId = Optional.empty(); + data.clear(); + break; + case "edge": + // add edge + currentSourceId = Optional.empty(); + currentTargetId = Optional.empty(); + currentComponent = Optional.empty(); + data.clear(); + break; + case "data": + if (currentDataKey.isPresent()) { + String annoKey = keys.get(currentDataKey.get()); + // Add an empty data entry if this element did not have any character child + if (annoKey != null && !data.containsKey(annoKey)) { + data.put(annoKey, ""); + } + } + currentDataKey = Optional.empty(); + break; + } + + level--; + break; } + } } @@ -385,7 +477,7 @@ private void recreateTextForTokenRoots(SDocumentGraph graph, Map // traverse the token chain using the order relations SToken currentToken = rootForText; while (currentToken != null) { - addToken(currentToken, text, token2Range); + addTokenToText(currentToken, text, token2Range); SToken previousToken = currentToken; currentToken = nextToken.get(previousToken); @@ -416,7 +508,8 @@ private void recreateTextForTokenRoots(SDocumentGraph graph, Map } - private void addToken(SToken token, StringBuilder text, Map> token2Range) { + private void addTokenToText(SToken token, StringBuilder text, + Map> token2Range) { SFeature featTokWhitespaceBefore = token.getFeature("annis::tok-whitespace-before"); if (featTokWhitespaceBefore != null) { text.append(featTokWhitespaceBefore.getValue().toString()); From 5ed5cb4e4b442e059b1a658495a562b263d65c3d Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Thu, 22 Aug 2024 10:48:21 +0200 Subject: [PATCH 3/8] Use first pass to determine if the base token layer is a timeline --- .../gui/graphml/DocumentGraphMapper.java | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java index d48761f7f4..a1fe671dcd 100644 --- a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java +++ b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java @@ -4,6 +4,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Range; +import com.google.common.collect.TreeMultimap; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -15,6 +16,7 @@ import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; @@ -61,6 +63,8 @@ public class DocumentGraphMapper extends AbstractGraphMLMapper { private final Multimap isPartOf; + private boolean hasTimeline; + protected DocumentGraphMapper() { this.graph = SaltFactory.createSDocumentGraph(); @@ -69,6 +73,7 @@ protected DocumentGraphMapper() { this.hasNonEmptyDominanceEdge = new HashSet<>(); this.isPartOf = HashMultimap.create(); + this.hasTimeline = false; } @@ -91,6 +96,9 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { Map data = new HashMap<>(); + Multimap tokenIdByComponentName = TreeMultimap.create(); + Map tokenToValue = new HashMap<>(); + while (reader.hasNext()) { XMLEvent event = reader.nextEvent(); switch (event.getEventType()) { @@ -123,6 +131,7 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { Attribute source = startElement.getAttributeByName(new QName("source")); Attribute target = startElement.getAttributeByName(new QName("target")); Attribute label = startElement.getAttributeByName(new QName("label")); + Attribute component = startElement.getAttributeByName(new QName("label")); if (label != null) { Component c = parseComponent(label.getValue()); if (source != null) { @@ -140,6 +149,11 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { } } } + if (source != null && target != null && component != null) { + currentSourceId = Optional.ofNullable(source.getValue()); + currentTargetId = Optional.ofNullable(target.getValue()); + currentComponent = Optional.ofNullable(component.getValue()); + } } break; case "data": @@ -166,16 +180,24 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { inGraph = false; break; case "node": - String nodeType = data.get("annis::node_type"); - if (nodeType == "datasource") { - // TODO: check if this data source is a timeline + String tokValue = data.get("annis::tok"); + if (tokValue != null && currentNodeId.isPresent()) { + tokenToValue.put(tokValue, currentNodeId.get()); } currentNodeId = Optional.empty(); data.clear(); break; case "edge": - // add edge + if (currentComponent.isPresent() && currentSourceId.isPresent() + && currentTargetId.isPresent()) { + Component component = parseComponent(currentComponent.get()); + if (component.getType() == AnnotationComponentType.ORDERING) { + tokenIdByComponentName.put(component.getName(), currentSourceId.get()); + tokenIdByComponentName.put(component.getName(), currentTargetId.get()); + } + } + currentSourceId = Optional.empty(); currentTargetId = Optional.empty(); currentComponent = Optional.empty(); @@ -196,8 +218,28 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { level--; break; } + } + + // Check if this GraphML file has a timeline. + this.hasTimeline = false; + if (tokenIdByComponentName.keySet().size() > 1) { + boolean hasNonEmptyBaseToken = false; + Pattern whitespacePattern = Pattern.compile("\\s*"); + + for (String tokId : tokenIdByComponentName.get("")) { + String tokValue = tokenToValue.getOrDefault(tokId, ""); + // Check if this base token value is non-empty + if (!whitespacePattern.matcher(tokValue).matches()) { + hasNonEmptyBaseToken = true; + break; + } + } + if (!hasNonEmptyBaseToken) { + this.hasTimeline = true; + } } + } @Override @@ -372,8 +414,12 @@ private void addAnnotationKey(Map keys, StartElement event) { private SNode mapNode(String nodeName, Map labels) { SNode newNode; - if ((labels.containsKey(ANNIS_TOK)) && !hasOutgoingCoverageEdge.contains(nodeName)) { - newNode = SaltFactory.createSToken(); + if ((labels.containsKey(ANNIS_TOK))) { + if (!this.hasTimeline && hasOutgoingCoverageEdge.contains(nodeName)) { + newNode = SaltFactory.createSSpan(); + } else { + newNode = SaltFactory.createSToken(); + } } else if (hasOutgoingDominanceEdge.contains(nodeName)) { newNode = SaltFactory.createSStructure(); } else { From 635992edf318cbc9654d94acee2dea99bd5fb9d6 Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Thu, 22 Aug 2024 12:14:23 +0200 Subject: [PATCH 4/8] Add STimeline and timeline relations --- .../gui/graphml/DocumentGraphMapper.java | 138 +++++++++++++++--- 1 file changed, 117 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java index a1fe671dcd..314d817494 100644 --- a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java +++ b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java @@ -8,16 +8,21 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; +import java.util.TreeSet; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.annotation.CheckForNull; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLStreamException; @@ -36,6 +41,8 @@ import org.corpus_tools.salt.common.SSpan; import org.corpus_tools.salt.common.STextualDS; import org.corpus_tools.salt.common.STextualRelation; +import org.corpus_tools.salt.common.STimeline; +import org.corpus_tools.salt.common.STimelineRelation; import org.corpus_tools.salt.common.SToken; import org.corpus_tools.salt.core.GraphTraverseHandler; import org.corpus_tools.salt.core.SAnnotation; @@ -60,10 +67,11 @@ public class DocumentGraphMapper extends AbstractGraphMLMapper { private final Set hasOutgoingCoverageEdge; private final Set hasOutgoingDominanceEdge; private final Set> hasNonEmptyDominanceEdge; - + private final Map gapEdges; private final Multimap isPartOf; - - private boolean hasTimeline; + private Optional timeline; + private final Map timelineIndex; + private final Multimap coveredTlis; protected DocumentGraphMapper() { @@ -71,9 +79,11 @@ protected DocumentGraphMapper() { this.hasOutgoingCoverageEdge = new HashSet<>(); this.hasOutgoingDominanceEdge = new HashSet<>(); this.hasNonEmptyDominanceEdge = new HashSet<>(); - + this.gapEdges = new HashMap<>(); this.isPartOf = HashMultimap.create(); - this.hasTimeline = false; + this.timeline = Optional.empty(); + this.timelineIndex = new HashMap<>(); + this.coveredTlis = HashMultimap.create(); } @@ -83,6 +93,27 @@ public static SDocumentGraph map(File inputFile) throws IOException, XMLStreamEx return mapper.graph; } + private static boolean isConnected(String a, String b, Multimap outgoingEdges) { + + HashSet visited = new HashSet<>(); + Deque stack = new LinkedList<>(); + stack.add(a); + + while (!stack.isEmpty()) { + String current = stack.removeLast(); + if (visited.add(current)) { + for (String out : outgoingEdges.get(current)) { + if (Objects.equal(b, out)) { + return true; + } + stack.add(out); + } + } + } + + return false; + } + @Override protected void firstPass(XMLEventReader reader) throws XMLStreamException { Map keys = new TreeMap<>(); @@ -98,6 +129,7 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { Multimap tokenIdByComponentName = TreeMultimap.create(); Map tokenToValue = new HashMap<>(); + Multimap outgoingOrderingEdges = HashMultimap.create(); while (reader.hasNext()) { XMLEvent event = reader.nextEvent(); @@ -142,6 +174,10 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { } else if (c.getType() == AnnotationComponentType.PARTOF) { isPartOf.put(Helper.addSaltPrefix(source.getValue()), Helper.addSaltPrefix(target.getValue())); + } else if (c.getType() == AnnotationComponentType.ORDERING + && "annis".equals(c.getLayer()) + && "".equals(c.getName()) && target != null) { + outgoingOrderingEdges.put(source.getValue(), target.getValue()); } if (target != null && c.getType() == AnnotationComponentType.DOMINANCE && !c.getName().isEmpty()) { @@ -221,7 +257,6 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { } // Check if this GraphML file has a timeline. - this.hasTimeline = false; if (tokenIdByComponentName.keySet().size() > 1) { boolean hasNonEmptyBaseToken = false; Pattern whitespacePattern = Pattern.compile("\\s*"); @@ -236,7 +271,24 @@ protected void firstPass(XMLEventReader reader) throws XMLStreamException { } if (!hasNonEmptyBaseToken) { - this.hasTimeline = true; + STimeline timeline = graph.createTimeline(); + + // Order the timeline tokens by the ordering edges + ArrayList tlis = new ArrayList<>(tokenIdByComponentName.get("")); + tlis.sort((a, b) -> { + if (isConnected(a, b, outgoingOrderingEdges)) { + return -1; + } else if (isConnected(b, a, outgoingOrderingEdges)) { + return 1; + } else { + return 0; + } + }); + timeline.setData(tlis.size()); + for (int i = 0; i < tlis.size(); i++) { + this.timelineIndex.put(tlis.get(i), i); + } + this.timeline = Optional.of(timeline); } } @@ -255,7 +307,6 @@ protected void secondPass(XMLEventReader reader) throws XMLStreamException { Optional currentComponent = Optional.empty(); SortedMap datasourcesInGraphMl = new TreeMap<>(); - Map gapEdges = new HashMap<>(); Map data = new HashMap<>(); @@ -327,7 +378,9 @@ protected void secondPass(XMLEventReader reader) throws XMLStreamException { if ("node".equals(nodeType)) { // Map node and add it SNode n = mapNode(currentNodeId.get(), data); - graph.addNode(n); + if (n != null) { + graph.addNode(n); + } } else if ("datasource".equals(nodeType)) { // Create a textual datasource of this name STextualDS ds = SaltFactory.createSTextualDS(); @@ -345,7 +398,7 @@ protected void secondPass(XMLEventReader reader) throws XMLStreamException { if (currentSourceId.isPresent() && currentTargetId.isPresent() && currentComponent.isPresent()) { mapAndAddEdge(currentSourceId.get(), currentTargetId.get(), currentComponent.get(), - data, gapEdges); + data); } currentSourceId = Optional.empty(); @@ -370,9 +423,26 @@ protected void secondPass(XMLEventReader reader) throws XMLStreamException { } } + if (timeline.isPresent()) { + // Add a timeline relation to all token + for (Map.Entry> entry : this.coveredTlis.asMap().entrySet()) { + + TreeSet sortedTlis = new TreeSet<>(entry.getValue()); + + STimelineRelation timeRel = SaltFactory.createSTimelineRelation(); + timeRel.setSource(entry.getKey()); + timeRel.setTarget(this.timeline.get()); + timeRel.setStart(sortedTlis.first()); + timeRel.setEnd(sortedTlis.last() + 1); + graph.addRelation(timeRel); + + } + + } + // Always create own own data sources from the tokens. Get all real token roots (ignore gaps) // and create a data source for each of them. - recreateTextForTokenRoots(graph, gapEdges, datasourcesInGraphMl); + recreateTextForTokenRoots(graph, datasourcesInGraphMl); // Create the text annotation for the segmentation nodes Multimap orderRoots = graph.getRootsByRelationType(SALT_TYPE.SORDER_RELATION); @@ -411,11 +481,26 @@ private void addAnnotationKey(Map keys, StartElement event) { } } + @CheckForNull private SNode mapNode(String nodeName, Map labels) { SNode newNode; if ((labels.containsKey(ANNIS_TOK))) { - if (!this.hasTimeline && hasOutgoingCoverageEdge.contains(nodeName)) { + if (this.timeline.isPresent()) { + if (hasOutgoingCoverageEdge.contains(nodeName)) { + newNode = SaltFactory.createSToken(); + } else { + // Do not map timeline items as token + return null; + } + } else { + if (hasOutgoingCoverageEdge.contains(nodeName)) { + newNode = SaltFactory.createSSpan(); + } else { + newNode = SaltFactory.createSToken(); + } + } + if (!this.timeline.isPresent() && hasOutgoingCoverageEdge.contains(nodeName)) { newNode = SaltFactory.createSSpan(); } else { newNode = SaltFactory.createSToken(); @@ -433,7 +518,7 @@ private SNode mapNode(String nodeName, Map labels) { } private void mapAndAddEdge(String sourceId, String targetId, String componentRaw, - Map labels, Map gapEdges) { + Map labels) { SNode source = graph.getNode(Helper.addSaltPrefix(sourceId)); SNode target = graph.getNode(Helper.addSaltPrefix(targetId)); @@ -441,9 +526,15 @@ private void mapAndAddEdge(String sourceId, String targetId, String componentRaw // Split the component description into its parts Component component = parseComponent(componentRaw); - if (source != null && target != null && source != target) { - + if (this.timeline.isPresent() && component.getType() == AnnotationComponentType.COVERAGE + && target == null + && source instanceof SToken) { + // The coverage edge describe to which timeline item the token belongs to. + // At this point, we are only interested in the index of the TLI. + this.coveredTlis.put((SToken) source, this.timelineIndex.get(targetId)); + } else if (source != null && target != null && source != target) { SRelation rel = null; + switch (component.getType()) { case DOMINANCE: if ((component.getName() == null || component.getName().isEmpty()) @@ -463,9 +554,14 @@ private void mapAndAddEdge(String sourceId, String targetId, String componentRaw if ("annis".equals(component.getLayer()) && "datasource-gap".equals(component.getName())) { if (source instanceof SToken && target instanceof SToken) { - gapEdges.put((SToken) source, (SToken) target); + this.gapEdges.put((SToken) source, (SToken) target); } - } else { + } else if (this.timeline.isPresent() && "annis".equals(component.getLayer()) + && "".equals(component.getName())) { + // Do not map ordering edges for the timeline + rel = null; + } + else { rel = graph.createRelation(source, target, SALT_TYPE.SORDER_RELATION, null); } @@ -490,14 +586,14 @@ private void mapAndAddEdge(String sourceId, String targetId, String componentRaw } } - private void recreateTextForTokenRoots(SDocumentGraph graph, Map gapEdges, + private void recreateTextForTokenRoots(SDocumentGraph graph, SortedMap datasourcesInGraphMl) { Map nextToken = new HashMap<>(); Map incomingOrderingEdgesWithGaphs = new HashMap<>(); for (SOrderRelation rel : graph.getOrderRelations()) { - if ((rel.getType() == null || "".equals(rel.getType())) && rel.getSource() instanceof SToken + if (rel.getSource() instanceof SToken && rel.getTarget() instanceof SToken) { SToken source = (SToken) rel.getSource(); SToken target = (SToken) rel.getTarget(); @@ -507,7 +603,7 @@ private void recreateTextForTokenRoots(SDocumentGraph graph, Map } } - for (Map.Entry rel : gapEdges.entrySet()) { + for (Map.Entry rel : this.gapEdges.entrySet()) { incomingOrderingEdgesWithGaphs.put(rel.getValue(), rel.getKey()); } @@ -529,7 +625,7 @@ private void recreateTextForTokenRoots(SDocumentGraph graph, Map currentToken = nextToken.get(previousToken); // Step over the possible gap if (currentToken == null) { - currentToken = gapEdges.get(previousToken); + currentToken = this.gapEdges.get(previousToken); } } From 4b6e0fbf0f2da7bf92489a282cb66730b4d7a085 Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Thu, 22 Aug 2024 12:22:53 +0200 Subject: [PATCH 5/8] Use ordering type as text name --- .../annis/gui/graphml/DocumentGraphMapper.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java index 314d817494..13cd8374df 100644 --- a/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java +++ b/src/main/java/org/corpus_tools/annis/gui/graphml/DocumentGraphMapper.java @@ -634,6 +634,13 @@ private void recreateTextForTokenRoots(SDocumentGraph graph, if (datasourcesInGraphMl.size() == 1) { STextualDS origDs = datasourcesInGraphMl.get(datasourcesInGraphMl.firstKey()); ds.setName(origDs.getName()); + } else { + Optional orderingType = + rootForText.getOutRelations().stream().filter(rel -> rel instanceof SOrderRelation) + .map(rel -> (SOrderRelation) rel).map(rel -> rel.getType()).findFirst(); + if (orderingType.isPresent()) { + ds.setName(orderingType.get()); + } } // add all relations From f0df1420efff5cbb460f274c19170eaa5aa49b52 Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Thu, 22 Aug 2024 16:43:43 +0200 Subject: [PATCH 6/8] Collect gaps and islands using the timelien --- .../component/grid/EventExtractor.java | 214 +++++++++++------- .../component/grid/GridVisualizer.java | 7 +- .../component/grid/SingleGridComponent.java | 91 +++++--- .../component/grid/TimelineSpanCollector.java | 89 ++++++++ 4 files changed, 283 insertions(+), 118 deletions(-) create mode 100644 src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/TimelineSpanCollector.java diff --git a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/EventExtractor.java b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/EventExtractor.java index 9119d21e38..65067adfc1 100644 --- a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/EventExtractor.java +++ b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/EventExtractor.java @@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.corpus_tools.annis.gui.Helper; import org.corpus_tools.annis.gui.PDFPageHelper; @@ -50,6 +51,7 @@ import org.corpus_tools.salt.common.SSpan; import org.corpus_tools.salt.common.SSpanningRelation; import org.corpus_tools.salt.common.STextualDS; +import org.corpus_tools.salt.common.STimeline; import org.corpus_tools.salt.common.SToken; import org.corpus_tools.salt.core.SAnnotation; import org.corpus_tools.salt.core.SFeature; @@ -87,10 +89,18 @@ private static void addAnnotationsForNode(SNode node, SDocumentGraph graph, } // calculate the left and right values of a span - Range overlappedSpan = Helper.getLeftRightSpan(node, graph, token2index); - int left = overlappedSpan.lowerEndpoint(); + STimeline timeline = graph.getTimeline(); + Range overlappedSpan; + if (timeline != null) { + overlappedSpan = TimelineSpanCollector.getRange(graph, node); + } else { + // Use the token to get the node span + overlappedSpan = Helper.getLeftRightSpan(node, graph, token2index); + } + int left = overlappedSpan.lowerEndpoint(); int right = overlappedSpan.upperEndpoint(); + for (SAnnotation anno : node.getAnnotations()) { ArrayList rows = rowsByAnnotation.get(anno.getQName()); if (rows == null) { @@ -120,35 +130,46 @@ else if (matchedQualifiedAnnoName.equals(anno.getQName())) { } } - if (node instanceof SSpan) { - // calculate overlapped SToken - - List> outEdges = - graph.getOutRelations(node.getId()); - if (outEdges != null) { - for (SRelation e : outEdges) { - if (e instanceof SSpanningRelation) { - SSpanningRelation spanRel = (SSpanningRelation) e; - - SToken tok = spanRel.getTarget(); - event.getCoveredIDs().add(tok.getId()); - - // get the STextualDS of this token and add it to the event - String textID = Helper.getTextualDSForNode(tok, graph).getId(); - if (textID != null) { - event.setTextID(textID); + + if(timeline != null) { + for(Range range : TimelineSpanCollector.getAllRanges(graph, node)) { + for(int i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) { + event.getCoveredIDs().add("" + i); + + } + } + } else { + if (node instanceof SSpan) { + // calculate overlapped SToken + List> outEdges = + graph.getOutRelations(node.getId()); + if (outEdges != null) { + for (SRelation e : outEdges) { + if (e instanceof SSpanningRelation) { + SSpanningRelation spanRel = (SSpanningRelation) e; + + SToken tok = spanRel.getTarget(); + event.getCoveredIDs().add(tok.getId()); + + // get the STextualDS of this token and add it to the event + String textID = Helper.getTextualDSForNode(tok, graph).getId(); + if (textID != null) { + event.setTextID(textID); + } } } + } // end if span has out edges + } else if (node instanceof SToken) { + event.getCoveredIDs().add(node.getId()); + // get the STextualDS of this token and add it to the event + String textID = Helper.getTextualDSForNode(node, graph).getId(); + if (textID != null) { + event.setTextID(textID); } - } // end if span has out edges - } else if (node instanceof SToken) { - event.getCoveredIDs().add(node.getId()); - // get the STextualDS of this token and add it to the event - String textID = Helper.getTextualDSForNode(node, graph).getId(); - if (textID != null) { - event.setTextID(textID); - } + } } + + // try to get time annotations if (mediaLayer == null || mediaLayer.contains(anno.getQName())) { @@ -184,17 +205,6 @@ else if (matchedQualifiedAnnoName.equals(anno.getQName())) { } // end for each annotation of span } - private static long clip(long value, long min, long max) { - if (value > max) { - return max; - } else if (value < min) { - return min; - } else { - return value; - } - - } - /** * Returns the annotations to display according to the mappings configuration. * @@ -466,6 +476,7 @@ public static LinkedHashMap> parseSalt(VisualizerInput in PDFPageHelper pageNumberHelper = new PDFPageHelper(input); + if (showSpanAnnos) { for (SSpan span : graph.getSpans()) { if (text == null || text == Helper.getTextualDSForNode(span, graph)) { @@ -475,6 +486,7 @@ public static LinkedHashMap> parseSalt(VisualizerInput in } // end for each span } + if (showTokenAnnos) { for (SToken tok : graph.getTokens()) { if (text == null || text == Helper.getTextualDSForNode(tok, graph)) { @@ -510,6 +522,7 @@ public static LinkedHashMap> parseSalt(VisualizerInput in } } + return rowsByAnnotation; } @@ -617,58 +630,90 @@ private static void sortEventsByTokenIndex(Row row) { private static void splitRowsOnGaps(Row row, final SDocumentGraph graph, Map token2index) { ListIterator itEvents = row.getEvents().listIterator(); + STimeline timeline = graph.getTimeline(); while (itEvents.hasNext()) { GridEvent event = itEvents.next(); - int lastTokenIndex = -1; - - // sort the coveredIDs - LinkedList sortedCoveredToken = new LinkedList<>(event.getCoveredIDs()); - Collections.sort(sortedCoveredToken, (o1, o2) -> { - SToken node1 = (SToken) graph.getNode(o1); - SToken node2 = (SToken) graph.getNode(o2); + List gaps = new LinkedList<>(); + if (timeline != null) { + // Calculate the gaps using the covered timeline items + List coveredTlis = event.getCoveredIDs().stream().map(id -> Integer.parseInt(id)) + .sorted() + .collect(Collectors.toList()); + int lastTli = -1; + for (int tli : coveredTlis) { + + // sanity check + if (tli >= event.getLeft() && tli <= event.getRight()) { + int diff = tli - lastTli; + + if (lastTli >= 0 && diff > 1) { + // we detected a gap + GridEvent gap = new GridEvent(event.getId() + "_gap_" + gaps.size(), + lastTli + 1, tli - 1, ""); + gap.setGap(true); + gaps.add(gap); + } - if (node1 == node2) { - return 0; - } - if (node1 == null) { - return -1; - } - if (node2 == null) { - return +1; + lastTli = tli; + } else { + // reset gap search when discovered there were token we use for + // highlighting but do not actually cover + lastTli = -1; + } } - long tokenIndex1 = token2index.get(node1); - long tokenIndex2 = token2index.get(node2); + } else { + // Calculate the gaps using the covered token + int lastTokenIndex = -1; - return ((Long) (tokenIndex1)).compareTo(tokenIndex2); - }); + // sort the coveredIDs + LinkedList sortedCoveredToken = new LinkedList<>(event.getCoveredIDs()); + Collections.sort(sortedCoveredToken, (o1, o2) -> { + SToken node1 = (SToken) graph.getNode(o1); + SToken node2 = (SToken) graph.getNode(o2); - // first calculate all gaps - List gaps = new LinkedList<>(); - for (String id : sortedCoveredToken) { - SToken node = (SToken) graph.getNode(id); - int tokenIndex = token2index.get(node); - - // sanity check - if (tokenIndex >= event.getLeft() && tokenIndex <= event.getRight()) { - int diff = tokenIndex - lastTokenIndex; - - if (lastTokenIndex >= 0 && diff > 1) { - // we detected a gap - GridEvent gap = new GridEvent(event.getId() + "_gap_" + gaps.size(), lastTokenIndex + 1, - tokenIndex - 1, ""); - gap.setGap(true); - gaps.add(gap); + if (node1 == node2) { + return 0; + } + if (node1 == null) { + return -1; + } + if (node2 == null) { + return +1; } - lastTokenIndex = tokenIndex; - } else { - // reset gap search when discovered there were token we use for - // hightlighting but do not actually cover - lastTokenIndex = -1; - } - } // end for each covered token id + long tokenIndex1 = token2index.get(node1); + long tokenIndex2 = token2index.get(node2); + + return ((Long) (tokenIndex1)).compareTo(tokenIndex2); + }); + + // Actually calculate the gaps + for (String id : sortedCoveredToken) { + SToken node = (SToken) graph.getNode(id); + int tokenIndex = token2index.get(node); + + // sanity check + if (tokenIndex >= event.getLeft() && tokenIndex <= event.getRight()) { + int diff = tokenIndex - lastTokenIndex; + + if (lastTokenIndex >= 0 && diff > 1) { + // we detected a gap + GridEvent gap = new GridEvent(event.getId() + "_gap_" + gaps.size(), + lastTokenIndex + 1, tokenIndex - 1, ""); + gap.setGap(true); + gaps.add(gap); + } + + lastTokenIndex = tokenIndex; + } else { + // reset gap search when discovered there were token we use for + // highlighting but do not actually cover + lastTokenIndex = -1; + } + } // end for each covered token id + } ListIterator itGaps = gaps.listIterator(); // remember the old right value @@ -722,6 +767,7 @@ private static void splitRowsOnGaps(Row row, final SDocumentGraph graph, private static void splitRowsOnIslands(Row row, final SDocumentGraph graph, STextualDS text, Map token2index) { + STimeline timeline = graph.getTimeline(); BitSet tokenCoverage = new BitSet(); // get the sorted token List sortedTokenList = graph.getSortedTokenByText(); @@ -729,7 +775,13 @@ private static void splitRowsOnIslands(Row row, final SDocumentGraph graph, STex ListIterator itToken = sortedTokenList.listIterator(); while (itToken.hasNext()) { SToken t = itToken.next(); - if (text == null || text == Helper.getTextualDSForNode(t, graph)) { + if (timeline != null) { + Range coveredRange = TimelineSpanCollector.getRange(graph, t); + for (int i = coveredRange.lowerEndpoint(); i <= coveredRange.upperEndpoint(); i++) { + tokenCoverage.set(i); + } + } + else if (text == null || text == Helper.getTextualDSForNode(t, graph)) { int tokenIndex = token2index.get(t); tokenCoverage.set(tokenIndex); } diff --git a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/GridVisualizer.java b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/GridVisualizer.java index 3db59ccb5d..ebc80e008a 100644 --- a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/GridVisualizer.java +++ b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/GridVisualizer.java @@ -19,6 +19,7 @@ import org.corpus_tools.annis.gui.media.PDFController; import org.corpus_tools.annis.gui.visualizers.AbstractVisualizer; import org.corpus_tools.annis.gui.visualizers.VisualizerInput; +import org.corpus_tools.salt.common.SDocumentGraph; import org.corpus_tools.salt.common.STextualDS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,9 +65,9 @@ public GridComponent createComponent(VisualizerInput visInput, VisualizationTogg PDFController pdfController = visInput.getUI().getSession().getAttribute(PDFController.class); GridComponent component = null; try { - - List texts = visInput.getDocument().getDocumentGraph().getTextualDSs(); - if (texts.size() == 1) { + SDocumentGraph documentGraph = visInput.getDocument().getDocumentGraph(); + List texts = documentGraph.getTextualDSs(); + if (texts.size() == 1 || documentGraph.getTimeline() != null) { component = new SingleGridComponent(visInput, mediaController, pdfController, true, null); } else { component = new MultipleGridComponent(visInput, mediaController, pdfController, true); diff --git a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java index 61ac4fbb05..627472b982 100644 --- a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java +++ b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java @@ -44,6 +44,7 @@ import org.corpus_tools.salt.common.SOrderRelation; import org.corpus_tools.salt.common.SSpan; import org.corpus_tools.salt.common.STextualDS; +import org.corpus_tools.salt.common.STimeline; import org.corpus_tools.salt.common.SToken; import org.corpus_tools.salt.core.SAnnotation; import org.corpus_tools.salt.core.SFeature; @@ -227,6 +228,18 @@ && hasSegmentation(t, this.segmentationName)) { return tokenRow; } + + private Row computeTimelineRow(STimeline timeline) { + + + Row timelineRow = new Row(); + for (int i = timeline.getStart(); i < timeline.getEnd(); i++) { + GridEvent event = new GridEvent(timeline.getId() + "-" + i, i, i, "" + i); + timelineRow.addEvent(event); + } + + return timelineRow; + } private boolean createAnnotationGrid() { String resultID = input.getId(); @@ -303,8 +316,6 @@ private boolean createAnnotationGrid() { if (origValue.equals(targetValue)) { ev.setValue(unit_split[1]); } - // String newValue = unit_split[1].replaceAll("%%value%%",origValue); - } } @@ -314,47 +325,59 @@ private boolean createAnnotationGrid() { } } - // add tokens as row - Row tokenRow = computeTokenRow(sortedSegmentationNodes, graph, rowsByAnnotation, token2index); - - String tokenRowCaption = "tok"; - if (isHidingToken()) { + boolean tokenRowIsEmpty = true; + STimeline timeline = graph.getTimeline(); + if (timeline != null) { + Row timelineRow = computeTimelineRow(timeline); + // timelineRow.setStyle("invisible_token"); - // We have to add the invisible token row avoid issues with the layout - // (see https://github.com/korpling/ANNIS/issues/524) - // but we don't want the invisible token layer to override an actual "tok" - // annotation layer (see https://github.com/korpling/ANNIS/issues/596) - tokenRow.setStyle("invisible_token"); - tokenRowCaption = ""; + tokenRowIsEmpty = false; grid.setTokRowKey(""); - } + rowsByAnnotation.put("", Lists.newArrayList(timelineRow)); + // TODO: also calculate *all* token as rows and display them aligned by the timeline + } else { + // add tokens as row + Row tokenRow = computeTokenRow(sortedSegmentationNodes, graph, rowsByAnnotation, token2index); + + String tokenRowCaption = "tok"; + if (isHidingToken()) { + + // We have to add the invisible token row avoid issues with the layout + // (see https://github.com/korpling/ANNIS/issues/524) + // but we don't want the invisible token layer to override an actual "tok" + // annotation layer (see https://github.com/korpling/ANNIS/issues/596) + tokenRow.setStyle("invisible_token"); + tokenRowCaption = ""; + grid.setTokRowKey(""); + } - if (isTokenFirst()) { - // copy original list but add token row at the beginning - LinkedHashMap> newList = new LinkedHashMap<>(); + if (isTokenFirst()) { + // copy original list but add token row at the beginning + LinkedHashMap> newList = new LinkedHashMap<>(); - newList.put(tokenRowCaption, Lists.newArrayList(tokenRow)); - newList.putAll(rowsByAnnotation); - rowsByAnnotation = newList; + newList.put(tokenRowCaption, Lists.newArrayList(tokenRow)); + newList.putAll(rowsByAnnotation); + rowsByAnnotation = newList; - } else { - // just add the token row to the end of the list - rowsByAnnotation.put(tokenRowCaption, Lists.newArrayList(tokenRow)); - } + } else { + // just add the token row to the end of the list + rowsByAnnotation.put(tokenRowCaption, Lists.newArrayList(tokenRow)); + } - EventExtractor.removeEmptySpace(rowsByAnnotation, tokenRow); + EventExtractor.removeEmptySpace(rowsByAnnotation, tokenRow); - // check if the token row only contains empty values - boolean tokenRowIsEmpty = true; - for (GridEvent tokenEvent : tokenRow.getEvents()) { - if (tokenEvent.getValue() != null && !tokenEvent.getValue().trim().isEmpty()) { - tokenRowIsEmpty = false; - break; + // check if the token row only contains empty values + for (GridEvent tokenEvent : tokenRow.getEvents()) { + if (tokenEvent.getValue() != null && !tokenEvent.getValue().trim().isEmpty()) { + tokenRowIsEmpty = false; + break; + } + } + if (!isHidingToken() && canShowEmptyTokenWarning()) { + lblEmptyToken.setVisible(tokenRowIsEmpty); } } - if (!isHidingToken() && canShowEmptyTokenWarning()) { - lblEmptyToken.setVisible(tokenRowIsEmpty); - } + grid.setRowsByAnnotation(rowsByAnnotation); return !tokenRowIsEmpty; diff --git a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/TimelineSpanCollector.java b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/TimelineSpanCollector.java new file mode 100644 index 0000000000..5def2166bb --- /dev/null +++ b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/TimelineSpanCollector.java @@ -0,0 +1,89 @@ +package org.corpus_tools.annis.gui.visualizers.component.grid; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Range; +import java.util.Arrays; +import java.util.HashSet; +import java.util.OptionalInt; +import java.util.Set; +import org.corpus_tools.salt.common.SDocumentGraph; +import org.corpus_tools.salt.common.STimeOverlappingRelation; +import org.corpus_tools.salt.common.STimeline; +import org.corpus_tools.salt.common.STimelineRelation; +import org.corpus_tools.salt.core.GraphTraverseHandler; +import org.corpus_tools.salt.core.SGraph.GRAPH_TRAVERSE_TYPE; +import org.corpus_tools.salt.core.SNode; +import org.corpus_tools.salt.core.SRelation; + +public class TimelineSpanCollector implements GraphTraverseHandler { + + + + public static Range getRange(SDocumentGraph graph, SNode node) { + STimeline timeline = graph.getTimeline(); + Preconditions.checkNotNull(timeline); + + TimelineSpanCollector collector = new TimelineSpanCollector(); + graph.traverse(Arrays.asList(node), GRAPH_TRAVERSE_TYPE.TOP_DOWN_DEPTH_FIRST, + "TimelineSpanCollector", collector); + + OptionalInt start = + collector.collectedRanges.stream().mapToInt((range) -> range.lowerEndpoint()).min(); + OptionalInt end = + collector.collectedRanges.stream().mapToInt((range) -> range.upperEndpoint()).max(); + + if (start.isPresent() && end.isPresent()) { + return Range.closed(start.getAsInt(), end.getAsInt()); + } else { + // Use the whole timeline as a fallback + return Range.closed(timeline.getStart(), timeline.getEnd() - 1); + } + } + + public static Set> getAllRanges(SDocumentGraph graph, SNode node) { + STimeline timeline = graph.getTimeline(); + Preconditions.checkNotNull(timeline); + + TimelineSpanCollector collector = new TimelineSpanCollector(); + graph.traverse(Arrays.asList(node), GRAPH_TRAVERSE_TYPE.TOP_DOWN_DEPTH_FIRST, + "TimelineSpanCollector", collector); + + return collector.collectedRanges; + + } + + private final HashSet> collectedRanges; + + private TimelineSpanCollector() { + collectedRanges = new HashSet<>(); + } + + @Override + public void nodeReached(GRAPH_TRAVERSE_TYPE traversalType, String traversalId, SNode currNode, + @SuppressWarnings("rawtypes") SRelation relation, SNode fromNode, long order) { + if (relation instanceof STimelineRelation) { + STimelineRelation timelineRelation = (STimelineRelation) relation; + this.collectedRanges + .add(Range.closed(timelineRelation.getStart(), timelineRelation.getEnd() - 1)); + } + + } + + @Override + public void nodeLeft(GRAPH_TRAVERSE_TYPE traversalType, String traversalId, SNode currNode, + SRelation relation, SNode fromNode, long order) { + + } + + @Override + public boolean checkConstraint(GRAPH_TRAVERSE_TYPE traversalType, String traversalId, + SRelation relation, SNode currNode, long order) { + if (relation == null) { + return true; + } else if (relation instanceof STimeOverlappingRelation) { + return true; + } + return false; + } + +} From cbcc7a7c834be6a04a906e46de49ec91acae10d1 Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Thu, 22 Aug 2024 17:17:23 +0200 Subject: [PATCH 7/8] Add token to grid when timeline is active --- .../component/grid/SingleGridComponent.java | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java index 627472b982..22c8d5d918 100644 --- a/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java +++ b/src/main/java/org/corpus_tools/annis/gui/visualizers/component/grid/SingleGridComponent.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.regex.Pattern; import org.corpus_tools.annis.gui.AnnisUI; import org.corpus_tools.annis.gui.Helper; @@ -50,6 +51,7 @@ import org.corpus_tools.salt.core.SFeature; import org.corpus_tools.salt.core.SNode; import org.corpus_tools.salt.core.SRelation; +import org.corpus_tools.salt.util.DataSourceSequence; /** * @@ -230,8 +232,6 @@ && hasSegmentation(t, this.segmentationName)) { } private Row computeTimelineRow(STimeline timeline) { - - Row timelineRow = new Row(); for (int i = timeline.getStart(); i < timeline.getEnd(); i++) { GridEvent event = new GridEvent(timeline.getId() + "-" + i, i, i, "" + i); @@ -329,12 +329,62 @@ private boolean createAnnotationGrid() { STimeline timeline = graph.getTimeline(); if (timeline != null) { Row timelineRow = computeTimelineRow(timeline); - // timelineRow.setStyle("invisible_token"); + timelineRow.setStyle("invisible_token"); tokenRowIsEmpty = false; grid.setTokRowKey(""); rowsByAnnotation.put("", Lists.newArrayList(timelineRow)); - // TODO: also calculate *all* token as rows and display them aligned by the timeline + if (!isHidingToken()) { + TreeMap allTokenRows = new TreeMap<>(); + // also calculate tokens from *all* texts as rows and display them aligned by the timeline + for (STextualDS ds : graph.getTextualDSs()) { + Row tokenRow = new Row(); + + final DataSourceSequence seq = new DataSourceSequence<>(); + seq.setDataSource(ds); + seq.setStart(0); + seq.setEnd(ds.getText() != null ? ds.getText().length() : 0); + List tokensForDs = graph.getTokensBySequence(seq); + + if (tokensForDs != null) { + for (SToken t : tokensForDs) { + Range tokenRange = TimelineSpanCollector.getRange(graph, t); + GridEvent event = new GridEvent(t.getId(), tokenRange.lowerEndpoint(), + tokenRange.upperEndpoint(), graph.getText(t)); + event.setTextID(ds.getId()); + for (Range coveredRange : TimelineSpanCollector.getAllRanges(graph, t)) { + for (int i = coveredRange.lowerEndpoint(); i <= coveredRange.upperEndpoint(); i++) { + event.getCoveredIDs().add(timeline.getId() + "-" + i); + } + } + tokenRow.addEvent(event); + } + } + + allTokenRows.put(ds.getName(), tokenRow); + } + + if (isTokenFirst()) { + // copy original list but add token row at the beginning + LinkedHashMap> newList = new LinkedHashMap<>(); + + for (Map.Entry entry : allTokenRows.entrySet()) { + newList.put(entry.getKey(), Lists.newArrayList(entry.getValue())); + } + newList.putAll(rowsByAnnotation); + rowsByAnnotation = newList; + } else { + for (Map.Entry entry : allTokenRows.entrySet()) { + rowsByAnnotation.put(entry.getKey(), Lists.newArrayList(entry.getValue())); + } + } + + for (Row tokenRow : allTokenRows.values()) { + EventExtractor.removeEmptySpace(rowsByAnnotation, tokenRow); + } + } + + } else { // add tokens as row Row tokenRow = computeTokenRow(sortedSegmentationNodes, graph, rowsByAnnotation, token2index); From 5249c16eb73f1e3b25d66010b6734b3ed978347b Mon Sep 17 00:00:00 2001 From: Thomas Krause Date: Fri, 17 Jan 2025 13:34:59 +0100 Subject: [PATCH 8/8] Remove additional lines left-over from the merge commit --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f36d8c580a..31c556118d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 shown. - Make the audio and video visualizer a little bit more robust when they are not pre-loaded. - folder in the database might not be loaded correctly and an issue where - impossible precedence queries created a "File too large" error ## [4.12.2] - 2024-06-04