diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/multistring/MultiValueStringFeatureSupport.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/multistring/MultiValueStringFeatureSupport.java index 01b569598b5..85b1a4f2ece 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/multistring/MultiValueStringFeatureSupport.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/multistring/MultiValueStringFeatureSupport.java @@ -242,7 +242,7 @@ public List lookupLazyDetails(AnnotationFeature aFeature, Obje var value = (String) v; var tag = schemaService.getTag(value, aFeature.getTagset()); - if (aFeature.getTagset() != null && tag.isEmpty()) { + if (aFeature.getTagset() != null && !tag.isEmpty()) { results.addDetail(new VLazyDetail(value, "Tag not in tagset")); } diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java index 4c3f8df1c64..f2564616228 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/string/StringFeatureSupport.java @@ -236,7 +236,7 @@ public List lookupLazyDetails(AnnotationFeature aFeature, Obje if (aValue instanceof String) { var value = (String) aValue; var tag = schemaService.getTag(value, aFeature.getTagset()); - if (aFeature.getTagset() != null && tag.isEmpty()) { + if (aFeature.getTagset() != null && !tag.isEmpty()) { return asList(new VLazyDetailGroup(new VLazyDetail(value, "Tag not in tagset"))); } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index 60feb0d37f1..961c06a3a5a 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -23,7 +23,6 @@ import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.ANNOTATION; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.MAIN_EDITOR; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.wicket.event.Broadcast.BREADTH; @@ -386,22 +385,24 @@ public List lookupLazyDetails(SourceDocument aDocument, User a var sortedByScore = group.get().bestSuggestionsByFeatureAndLabel(pref, aFeature.getName(), label); - var details = new VLazyDetailGroup(); + var detailGroups = new ArrayList(); for (var ao : sortedByScore) { - var items = new ArrayList(); + var detailGroup = new VLazyDetailGroup(ao.getRecommenderName()); + // detailGroup.addDetail(new VLazyDetail("Age", String.valueOf(ao.getAge()))); if (ao.getScore() > 0.0d) { - items.add(String.format("Score: %.2f", ao.getScore())); + detailGroup + .addDetail(new VLazyDetail("Score", String.format("%.2f", ao.getScore()))); } if (ao.getScoreExplanation().isPresent()) { - items.add("Explanation: " + ao.getScoreExplanation().get()); + detailGroup + .addDetail(new VLazyDetail("Explanation", ao.getScoreExplanation().get())); } if (pref.isShowAllPredictions() && !ao.isVisible()) { - items.add("Hidden: " + ao.getReasonForHiding()); + detailGroup.addDetail(new VLazyDetail("Hidden", ao.getReasonForHiding())); } - details.addDetail( - new VLazyDetail(ao.getRecommenderName(), "\n" + String.join("\n", items))); + detailGroups.add(detailGroup); } - return asList(details); + return detailGroups; } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 333680731df..5fd413a41c7 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -1435,7 +1435,7 @@ private void computePredictions(LazyCas aOriginalCas, // Make sure we have the latest recommender config from the DB - the one // from the active recommenders list may be outdated - Recommender recommender = aEvaluatedRecommender.getRecommender(); + var recommender = aEvaluatedRecommender.getRecommender(); try { recommender = getRecommender(recommender.getId()); } @@ -1454,7 +1454,7 @@ private void computePredictions(LazyCas aOriginalCas, return; } - Optional context = getContext(aSessionOwner.getUsername(), recommender); + var context = getContext(aSessionOwner.getUsername(), recommender); if (!context.isPresent()) { aPredictions.log(LogMessage.info(recommender.getName(), @@ -1465,7 +1465,7 @@ private void computePredictions(LazyCas aOriginalCas, return; } - Optional> maybeFactory = getRecommenderFactory(recommender); + var maybeFactory = getRecommenderFactory(recommender); if (maybeFactory.isEmpty()) { LOG.warn("{}[{}]: No factory found - skipping recommender", aSessionOwner, @@ -1473,7 +1473,7 @@ private void computePredictions(LazyCas aOriginalCas, return; } - RecommendationEngineFactory factory = maybeFactory.get(); + var factory = maybeFactory.get(); // Check that configured layer and feature are accepted // by this type of recommender @@ -1488,13 +1488,13 @@ private void computePredictions(LazyCas aOriginalCas, // We lazily load the CAS only at this point because that allows us to skip // loading the CAS entirely if there is no enabled layer or recommender. // If the CAS cannot be loaded, then we skip to the next document. - CAS originalCas = aOriginalCas.get(); + var originalCas = aOriginalCas.get(); predictionBegin = aPredictionBegin < 0 ? 0 : aPredictionBegin; predictionEnd = aPredictionEnd < 0 ? originalCas.getDocumentText().length() : aPredictionEnd; try { - RecommendationEngine engine = factory.build(recommender); + var engine = factory.build(recommender); if (!engine.isReadyForPrediction(context.get())) { aPredictions.log(LogMessage.info(recommender.getName(), @@ -1808,11 +1808,11 @@ void generateSuggestions(Predictions aIncomingPredictions, PredictionContext aCt static ReconciliationResult reconcile(Predictions aActivePredictions, SourceDocument aDocument, Recommender recommender, Range predictedRange, - List aNewProtoSuggesitons) + List aNewProtoSuggestions) { if (aActivePredictions == null) { - return new ReconciliationResult(aNewProtoSuggesitons.size(), 0, 0, - aNewProtoSuggesitons); + return new ReconciliationResult(aNewProtoSuggestions.size(), 0, 0, + aNewProtoSuggestions); } var reconciledSuggestions = new LinkedHashSet(); @@ -1826,7 +1826,7 @@ static ReconciliationResult reconcile(Predictions aActivePredictions, SourceDocu .filter(s -> s.coveredBy(predictedRange)) // .collect(groupingBy(AnnotationSuggestion::getPosition)); - for (var newSuggestion : aNewProtoSuggesitons) { + for (var newSuggestion : aNewProtoSuggestions) { var existingSuggestions = existingSuggestionsByPosition .getOrDefault(newSuggestion.getPosition(), emptyList()).stream() // .filter(s -> Objects.equals(s.getLabel(), newSuggestion.getLabel()) && // @@ -1837,7 +1837,6 @@ static ReconciliationResult reconcile(Predictions aActivePredictions, SourceDocu if (existingSuggestions.isEmpty()) { addedSuggestions.add(newSuggestion); - reconciledSuggestions.add(newSuggestion); continue; } @@ -1847,18 +1846,22 @@ static ReconciliationResult reconcile(Predictions aActivePredictions, SourceDocu } var existingSuggestion = existingSuggestions.get(0); - existingSuggestion.incrementAge(); - agedSuggestionsCount++; - reconciledSuggestions.add(existingSuggestion); + if (!reconciledSuggestions.contains(existingSuggestion)) { + existingSuggestion.incrementAge(); + agedSuggestionsCount++; + reconciledSuggestions.add(existingSuggestion); + } } var removedSuggestions = predictionsByRecommenderAndDocument.stream() // .filter(s -> s.coveredBy(predictedRange)) // .filter(s -> !reconciledSuggestions.contains(s)) // - .collect(toList()); + .toList(); + var finalSuggestions = new ArrayList<>(reconciledSuggestions); + finalSuggestions.addAll(addedSuggestions); return new ReconciliationResult(addedSuggestions.size(), removedSuggestions.size(), - agedSuggestionsCount, new ArrayList<>(reconciledSuggestions)); + agedSuggestionsCount, finalSuggestions); } @Override diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index c0ffb1fb3ac..8079a989275 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -21,7 +21,6 @@ import static java.lang.Math.max; import static java.lang.Math.min; import static java.util.Comparator.comparingInt; -import static java.util.stream.Collectors.toList; import static org.apache.uima.cas.text.AnnotationPredicates.colocated; import static org.apache.uima.fit.util.CasUtil.getType; import static org.apache.uima.fit.util.CasUtil.select; @@ -198,7 +197,7 @@ public void calculateSuggestionVisibility(Strin return AnnotationPredicates.coveredBy(offset.getBegin(), offset.getEnd(), aWindowBegin, aWindowEnd); }) // - .collect(toList()); + .toList(); // Get all the skipped/rejected entries for the current layer var recordedAnnotations = learningRecordService.listLearningRecords(aSessionOwner, @@ -262,77 +261,72 @@ private void hideSpanSuggestionsThatOverlapWithAnnotations( // This iterator gives us pairs of annotations and suggestions. Note that both lists // must be sorted in the same way. The suggestion offsets are sorted because they are // the keys in a TreeSet - and the annotation offsets are sorted in the same way manually - var oi = new OverlapIterator(sortedAnnotationKeys, new ArrayList<>(suggestions.keySet())); + var oi = new OverlapIterator(new ArrayList<>(suggestions.keySet()), sortedAnnotationKeys); // Bulk-hide any groups that overlap with existing annotations on the current layer // and for the current feature var hiddenForOverlap = new ArrayList(); while (oi.hasNext()) { - if (oi.getA().overlaps(oi.getB())) { - // Fetch the current suggestion and annotation - var group = suggestions.get(oi.getB()); - for (var annotation : annotations.get(oi.getA())) { - var label = annotation.getFeatureValueAsString(feat); - for (var suggestion : group) { - // The suggestion would just create an annotation and not set any - // feature - boolean colocated = colocated(annotation, suggestion.getBegin(), - suggestion.getEnd()); - if (suggestion.getLabel() == null) { - // If there is already an annotation, then we hide any suggestions - // that would just trigger the creation of the same annotation and - // not set any new feature. This applies whether stacking is allowed - // or not. - if (colocated) { - suggestion.hide(FLAG_OVERLAP); - hiddenForOverlap.add(suggestion); - continue; - } - - // If stacking is enabled, we do allow suggestions that create an - // annotation with no label, but only if the offsets differ - if (feature.getLayer().isAllowStacking() && !colocated) { - suggestion.hide(FLAG_OVERLAP); - hiddenForOverlap.add(suggestion); - continue; - } + var pair = oi.next(); + var suggestionOffset = pair.getKey(); + var annotationOffset = pair.getValue(); + // Fetch the current suggestion and annotation + var group = suggestions.get(suggestionOffset); + for (var annotation : annotations.get(annotationOffset)) { + var label = annotation.getFeatureValueAsString(feat); + for (var suggestion : group) { + // The suggestion would just create an annotation and not set any + // feature + boolean colocated = colocated(annotation, suggestion.getBegin(), + suggestion.getEnd()); + if (suggestion.getLabel() == null) { + // If there is already an annotation, then we hide any suggestions + // that would just trigger the creation of the same annotation and + // not set any new feature. This applies whether stacking is allowed + // or not. + if (colocated) { + suggestion.hide(FLAG_OVERLAP); + hiddenForOverlap.add(suggestion); + continue; } - // The suggestion would merge the suggested feature value into an - // existing annotation or create a new annotation with the feature if - // stacking were enabled. - else { - // Is the feature still unset in the current annotation - i.e. would - // accepting the suggestion merge the feature into it? If yes, we do - // not hide - if (label == null && colocated) { - continue; - } - - // Does the suggested label match the label of an existing annotation - // at the same position then we hide - if (label != null && label.equals(suggestion.getLabel()) && colocated) { - suggestion.hide(FLAG_OVERLAP); - hiddenForOverlap.add(suggestion); - continue; - } - - // Would accepting the suggestion create a new annotation but - // stacking is not enabled - then we need to hide - if (!feature.getLayer().isAllowStacking()) { - suggestion.hide(FLAG_OVERLAP); - hiddenForOverlap.add(suggestion); - continue; - } + + // If stacking is enabled, we do allow suggestions that create an + // annotation with no label, but only if the offsets differ + if (!(feature.getLayer().isAllowStacking() && !colocated)) { + suggestion.hide(FLAG_OVERLAP); + hiddenForOverlap.add(suggestion); + continue; } } - } + // The suggestion would merge the suggested feature value into an + // existing annotation or create a new annotation with the feature if + // stacking were enabled. + else { + // Is the feature still unset in the current annotation - i.e. would + // accepting the suggestion merge the feature into it? If yes, we do + // not hide + if (label == null && suggestion.getLabel() != null && colocated) { + continue; + } - // Do not want to process the annotation again since the relevant suggestions are - // already hidden - oi.ignoreA(); - } + // Does the suggested label match the label of an existing annotation + // at the same position then we hide + if (label != null && label.equals(suggestion.getLabel()) && colocated) { + suggestion.hide(FLAG_OVERLAP); + hiddenForOverlap.add(suggestion); + continue; + } - oi.step(); + // Would accepting the suggestion create a new annotation but + // stacking is not enabled - then we need to hide + if (!feature.getLayer().isAllowStacking()) { + suggestion.hide(FLAG_OVERLAP); + hiddenForOverlap.add(suggestion); + continue; + } + } + } + } } if (LOG.isTraceEnabled()) { diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/util/OverlapIterator.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/util/OverlapIterator.java index c0e7810ddf6..9d0bbb21cbd 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/util/OverlapIterator.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/util/OverlapIterator.java @@ -1,10 +1,4 @@ /* - * Copyright (c) 2004-2009 Richard Eckart de Castilho. - * - * This file was originally part of AnnoLab by the name DoubleIterator - * The file was adapted to use Offset instead of Interval and to use SLF4J - * instead of Commons Logging. - * * Licensed to the Technische Universität Darmstadt under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -23,10 +17,13 @@ */ package de.tudarmstadt.ukp.inception.recommendation.util; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; import java.util.List; -import java.util.ListIterator; -import java.util.NoSuchElementException; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,214 +31,99 @@ public class OverlapIterator { - private final Logger log = LoggerFactory.getLogger(getClass()); - - /** The A/B lists */ - private final List la; - private final List lb; - - /** A values we do not want to see again (after a rewind). */ - private final boolean[] ignorea; - - /** List iterators for the A/B lists */ - private final ListIterator ia; - private final ListIterator ib; + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - /** Indices of _cura/_curb within the lists */ - private int na; - private int nb; + private final Iterator ia; + private final Iterator ib; - /** Maximum A/B index within the lists */ - private final int maxa; - private final int maxb; + private List openB; + private List nextOpenB; + private Iterator iob; - /** Current A/B item */ - private Offset cura; - private Offset curb; + private Offset a; + private Offset b; - private int last_b_step_na; private boolean done; - private int stepCount; - - public OverlapIterator(final List aList, final List bList) - { - done = !((aList.size() > 0) && (bList.size() > 0)); - - // Initialize A - la = aList; - maxa = la.size() - 1; // Up until here and no further - ia = la.listIterator(); // Where we are now - na = ia.nextIndex(); // Index of _cura within _la - cura = ia.next(); // The current object. - ignorea = new boolean[la.size()]; - - // Initialize B - lb = bList; - maxb = lb.size() - 1; - ib = lb.listIterator(); - nb = ib.nextIndex(); - curb = ib.next(); - - last_b_step_na = na; - } - - public int getStepCount() + public OverlapIterator(List aList, List bList) { - return stepCount; + ia = aList.iterator(); + ib = bList.iterator(); + openB = Collections.emptyList(); + nextOpenB = new ArrayList<>(); + iob = openB.iterator(); + done = aList.isEmpty() || bList.isEmpty(); + step(); } - public Offset getA() - { - return cura; - } - - public Offset getB() - { - return curb; - } - - public void ignoreA() + private void stepA() { - ignorea[na] = true; - } + LOG.trace("Stepping A"); - public boolean hasNext() - { - return !done; + if (!ia.hasNext()) { + done = true; + return; + } + a = ia.next(); + + LOG.trace("Resetting B"); + b = null; + // When moving to the next A, we can forget all open intervals + // that end before the new A + openB = nextOpenB; + openB.removeIf(o -> o.getEnd() < a.getBegin()); + iob = openB.iterator(); + nextOpenB = new ArrayList<>(); } - public void step() + private void stepB() { - if (done) { - throw new NoSuchElementException(); - } - - // Peek ahead in the A list. - Offset nexta = null; - if (na < maxa) { - nexta = ia.next(); - ia.previous(); + if (iob.hasNext()) { + LOG.trace("Stepping B from open Bs"); + // Step to the next B from open intervals list + b = iob.next(); } - - final boolean nexta_starts_before_curb_ends = (nexta != null) - && (nexta.getBegin() <= curb.getEnd()); - final boolean cura_ends_before_or_with_curb = cura.getEnd() <= curb.getEnd(); - - if (log.isTraceEnabled()) { - log.trace("---"); - log.trace(" A : " + na + "/" + maxa + " " + cura - + " peek: " + nexta); - log.trace(" B : " + nb + "/" + maxb + " " + curb); - log.trace(" nexta starts before curb ends: " + nexta_starts_before_curb_ends); - log.trace(" cura ends before or with curb: " + cura_ends_before_or_with_curb); - } - - // Which one to step up A or B? - if (nexta_starts_before_curb_ends || cura_ends_before_or_with_curb) { - // Can A be stepped up any more? - if (na < maxa) { - stepA(); - // if not, try stepping up B - } - else if (nb < maxb) { - stepB(); - // if both are at the end, bail out - } - else { - done = true; - } + else if (ib.hasNext()) { + LOG.trace("Stepping B from source Bs"); + // Step to the next B from the source list + b = ib.next(); } else { - // Can B be stepped up any more? - if (nb < maxb) { - stepB(); - // if not, try stepping up A - } - else if (na < maxa) { - stepA(); - // if both are at the end, bail out - } - else { - done = true; - } - } - - if (log.isTraceEnabled() && done) { - log.trace(" -> Both lists at the end."); + // Prepare to step to the next A + a = null; } } - private void stepA() + private void step() { - stepCount++; - na = ia.nextIndex(); - cura = ia.next(); + while (!done) { + if (a == null) { + stepA(); + } - if (log.isTraceEnabled()) { - log.trace(" -> A: " + na + "/" + maxa + " " + cura); - } - } + stepB(); - private void stepBackA() - { - na = ia.previousIndex(); - cura = ia.previous(); + if (b != null) { + nextOpenB.add(b); + } - if (log.isTraceEnabled()) { - log.trace(" <- A: " + na + "/" + maxa + " " + cura); + if (a != null && b != null && a.overlaps(b)) { + LOG.trace("Found overlap {} {}", a, b); + // next() should return this combo + break; + } } } - private void stepB() + public boolean hasNext() { - stepCount++; - nb = ib.nextIndex(); - curb = ib.next(); - - if (log.isTraceEnabled()) { - log.trace(" -> B: " + nb + "/" + maxb + " " + curb); - } - - if (curb.getBegin() < cura.getEnd()) { - // Rewind A to the point where it was when we last stepped - // up B. - rewindA(); - } - else { - last_b_step_na = na; - } + return !done; } - private void rewindA() + public Pair next() { - if (log.isTraceEnabled()) { - log.trace(" <- rewinding A"); - } - - // Seek back to the first segment that does not overlap - // with curb and at most until the last b step we made. - boolean steppedBack = false; - while ((na > last_b_step_na) && (cura.getEnd() > curb.getBegin())) { - stepBackA(); - steppedBack = true; - } - - // Correct pointer - if (steppedBack) { - // Make sure the next peek really peeks ahead. - na = ia.nextIndex(); - cura = ia.next(); - } - - // Skip over the A's we do not want to see again. - while (ignorea[na] && (na < maxa)) { - stepA(); - } - - // If we skipped some As those we skip will always be skipped, so we - // can as well update the _last_b_step_na so we don't have to skip them - // every time. - last_b_step_na = na; + var result = Pair.of(a, b); + step(); + return result; } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/exporter/OverlapIteratorTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/exporter/OverlapIteratorTest.java index 0100cf54e1f..cad22de5d99 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/exporter/OverlapIteratorTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/exporter/OverlapIteratorTest.java @@ -23,13 +23,13 @@ */ package de.tudarmstadt.ukp.inception.recommendation.exporter; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toList; +import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; @@ -256,15 +256,36 @@ public void testStep11() test(a, b, true); } + @Test + public void testStep11a() + { + var a = asList(new Offset(28, 71), new Offset(36, 39)); + var b = asList(new Offset(66, 95), new Offset(68, 73)); + + test(a, b, true); + } + + @Test + public void testStep12() + { + var b = asList(new Offset(0, 14), new Offset(7, 14), new Offset(114, 116), + new Offset(114, 124), new Offset(135, 154), new Offset(160, 167)); + var a = asList(new Offset(0, 14), new Offset(114, 124), new Offset(135, 154), + new Offset(160, 167)); + + test(a, b, true); + } + private void test(final List a, final List b, final boolean debug) { - List r1 = overlappingRef(a, b); - List r2 = overlapping(a, b, debug, false); + var r1 = overlappingRef(a, b); + var r2 = overlapping(a, b, debug, false); + + r1 = r1.stream().sorted().distinct().toList(); + r2 = r2.stream().sorted().distinct().toList(); - r1 = r1.stream().sorted(comparing(Offset::getBegin).thenComparing(Offset::getEnd)) - .distinct().collect(toList()); - r2 = r2.stream().sorted(comparing(Offset::getBegin).thenComparing(Offset::getEnd)) - .distinct().collect(toList()); + System.out.println("Expected overlapping pairs: " + r1); + System.out.println("Actual overlapping pairs : " + r2); if (!r1.equals(r2)) { System.out.println("Comparing... Mismatch!"); @@ -279,14 +300,14 @@ private void test(final List a, final List b, final boolean debu } } - private List overlappingRef(final List a, final List b) + private List> overlappingRef(final List a, final List b) { - final ArrayList result = new ArrayList<>(); + var result = new ArrayList>(); - for (final Offset ia : a) { - for (final Offset ib : b) { + for (var ia : a) { + for (var ib : b) { if (ia.overlaps(ib)) { - result.add(ia); + result.add(Pair.of(ia, ib)); } } } @@ -294,29 +315,15 @@ private List overlappingRef(final List a, final List b) return result; } - private List overlapping(final List a, final List b, + private List> overlapping(final List a, final List b, final boolean debug, final boolean showSteps) { - final List result = new ArrayList<>(); + var result = new ArrayList>(); - final OverlapIterator it = new OverlapIterator(a, b); + var it = new OverlapIterator(a, b); while (it.hasNext()) { - final boolean overlaps = it.getA().overlaps(it.getB()); - - if (debug) { - System.out.println(" ->A:" + it.getA() + " B:" + it.getB() + " :: " + overlaps); - } - - if (overlaps) { - result.add(it.getA()); - it.ignoreA(); - } - it.step(); - } - - if (showSteps) { - System.out.println("- Steps : " + it.getStepCount()); + result.add(it.next()); } return result;