package eu.transkribus.core.model.beans.customtags; import eu.transkribus.core.model.beans.customtags.search.CustomTagSearchFacets; import eu.transkribus.core.model.beans.pagecontent_trp.ITrpShapeType; import eu.transkribus.core.model.beans.pagecontent_trp.observable.TrpObserveEvent.TrpTagsChangedEvent; import eu.transkribus.core.model.beans.pagecontent_trp.observable.TrpObserveEvent.TrpTagsChangedEvent.Type; import eu.transkribus.core.util.IntRange; import eu.transkribus.core.util.OverlapType; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationTargetException; import java.util.*; /** * Utility class that manages multiple CustomTag objects for a given * ITrpShapeType object It merges indexed tags according to their start and end * indices and removes empty valued tags. Non indexed tags are not added again * if already present.<br> * <em>Warning:</em> the code in this class is pretty complicated and was the * effort of <a * href="http://en.wikipedia.org/wiki/Blood,_toil,_tears,_and_sweat" * >"Blood, toil, tears and sweat"</a><br> * I hereby suggest not to make any blindfold changes to it * */ public class CustomTagList { private final static Logger logger = LoggerFactory.getLogger(CustomTagList.class); ITrpShapeType shape; List<CustomTag> tags = new ArrayList<CustomTag>(); public CustomTagList(ITrpShapeType shape) { Objects.requireNonNull(shape); this.shape = shape; initFromCustomTagString(shape.getCustom(), false); // List<CustomTag> cts = CustomTagUtil.getCustomTags(shape.getCustom()); // logger.trace("nr of custom tags: " + cts.size() + " id: " + shape.getId()); // for (CustomTag ct : CustomTagUtil.getCustomTags(shape.getCustom())) { // logger.trace("adding custom tag: " + ct); // try { // addOrMergeTag(ct, null); // } catch (IndexOutOfBoundsException e) { // logger.error(e.getMessage(), e); // } // } } public void initFromCustomTagString(String customTag, boolean registerNewTags) { if (shape == null) { // cannot happen, but just to be sure... return; } if (shape.getObservable()!=null) { // is this check really needed?? shape.getObservable().setActive(false); } if (!tags.isEmpty()) { logger.warn("Warning - taglist not empty on initFromCustomTagString -> this can cause major trouble!"); removeTags(); } List<CustomTag> cts = CustomTagUtil.getCustomTags(shape.getCustom()); logger.trace("nr of custom tags: " + cts.size() + " id: " + shape.getId()); for (CustomTag ct : cts) { if (registerNewTags) { try { boolean canBeEmpty = ct.isEmpty(); logger.trace("t = "+ct+" canBeEmpty = "+canBeEmpty); CustomTagFactory.addToRegistry(ct, null, canBeEmpty, false); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e) { logger.error("Could not register the tag: "+ct.getCssStr()+", reason: "+e.getMessage(), e); } } logger.trace("adding custom tag: " + ct); try { addOrMergeTag(ct, null); } catch (IndexOutOfBoundsException e) { logger.error(e.getMessage(), e); } } if (shape.getObservable()!=null) { shape.getObservable().setActive(true); } } // public CustomTagList(CustomTagList src) { // this.tags = new ArrayList<>(src.tags.size()); // Collections.copy(this.tags, src.tags); // setShape(src.shape); // } // void setShape(ITrpShapeType shape) { // Assert.assertNotNull(shape); // this.shape = shape; // } // public void update(Observable obj, Object arg) { // TrpObserveEvent e = (TrpObserveEvent) arg; // if (e instanceof TrpTextChangedEvent) { // logger.trace("text edited, who = "+e.who); // onTextEdited((TrpTextChangedEvent) e); // } // } public ITrpShapeType getShape() { return shape; } public int getTextLength() { return shape.getUnicodeText() != null ? shape.getUnicodeText().length() : 0; } public Pair<CustomTagList, CustomTag> getNextContinuedCustomTag(CustomTag ct) { if (!hasTag(ct) || !ct.isContinued()) // tag is not in the list return null; if (ct.getEnd() != getTextLength()) // tag does not reach the end return null; // get next shape: ITrpShapeType nextShape = shape.getSiblingShape(false); if (nextShape == null) return null; CustomTag nextTag = nextShape.getCustomTagList().getOverlappingTag(ct.getTagName(), 0); if (nextTag == null || !nextTag.isContinued()) return null; return Pair.of(nextShape.getCustomTagList(), nextTag); } public Pair<CustomTagList, CustomTag> getPreviousContinuedCustomTag(CustomTag ct) { if (!hasTag(ct) || !ct.isContinued()) // tag is not in the list return null; if (ct.getOffset() != 0) // tag does not start at beginning return null; // get previous shape: ITrpShapeType prevShape = shape.getSiblingShape(true); if (prevShape == null) return null; int lastIndex = Math.max(0, prevShape.getCustomTagList().getTextLength() - 1); CustomTag prevTag = prevShape.getCustomTagList().getOverlappingTag(ct.getTagName(), lastIndex); logger.trace("prevTag = " + prevTag); if (prevTag == null || !prevTag.isContinued()) return null; return Pair.of(prevShape.getCustomTagList(), prevTag); } public List<Pair<CustomTagList, CustomTag>> getCustomTagAndContinuations(CustomTag tag) { LinkedList<Pair<CustomTagList, CustomTag>> allTags = new LinkedList<>(); if (!hasTag(tag)) return allTags; allTags.add(Pair.of(this, tag)); if (!tag.isContinued()) return allTags; // previous tags: Pair<CustomTagList, CustomTag> c = getPreviousContinuedCustomTag(tag); while (c != null) { allTags.addFirst(c); c = c.getLeft().getPreviousContinuedCustomTag(c.getRight()); } // next tags: c = getNextContinuedCustomTag(tag); while (c != null) { allTags.addLast(c); c = c.getLeft().getNextContinuedCustomTag(c.getRight()); } return allTags; } public void deleteTagAndContinuations(CustomTag tag) { List<Pair<CustomTagList, CustomTag>> tags = getCustomTagAndContinuations(tag); logger.debug(tag + " tags and continuations: "); for (Pair<CustomTagList, CustomTag> t : tags) { logger.debug("1shape: " + t.getLeft().getShape().getId() + " tag: " + t.getRight()); t.getLeft().removeTag(t.getRight()); } } // public List<CustomTag> getCustomTagAndContinuations(String tagName, int // offset) { // FIXME?? // CustomTag startTag = getOverlappingTag(tagName, offset); // if (startTag == null) // return null; // // CustomTag ct = startTag; // ITrpShapeType cs = shape; // // LinkedList<CustomTag> allTags = new LinkedList<>(); // allTags.add(ct); // // // look at previous shapes: // while (ct!=null && ct.isContinued() && ct.getOffset()==0) { // ct = null; // cs = cs.getSiblingShape(true); // if (cs != null) { // CustomTagList prevCtl = cs.getCustomTagList(); // // ct = prevCtl.getOverlappingTag(tagName, prevCtl.getLength()-1 >= 0 ? // prevCtl.getLength()-1 : 0); // if (ct!=null) // allTags.addFirst(ct); // } // } // // look at next shapes: // ct = startTag; // cs = shape; // while (ct!=null && ct.getEnd()==cs.getCustomTagList().getLength()) { // ct = null; // cs = cs.getSiblingShape(false); // if (cs != null) { // CustomTagList nextCtl = cs.getCustomTagList(); // // ct = nextCtl.getOverlappingTag(tagName, 0); // if (ct!=null && ct.isContinued()) // allTags.addLast(ct); // } // } // // return allTags; // } public boolean hasTag(CustomTag ct) { return tags.contains(ct); } // public CustomTagList(String customTag) { // for (CustomTag ct : CustomTagUtil.getCustomTags(customTag)) { // addOrMergeTag(ct, null); // } // } void checkRange(CustomTag tag) throws IndexOutOfBoundsException { if (!tag.isIndexed()) return; int tl = getTextLength(); IntRange shapeRange = new IntRange(0, tl); OverlapType ot = shapeRange.getOverlapType(tag.getOffset(), tag.getLength()); if (ot != OverlapType.INSIDE ) { if (!tag.isEmpty() || tag.getOffset()!=tl) { throw new IndexOutOfBoundsException("Tag does not fit into shape text of size " + tl + " (offset/length of tag: " + tag.getOffset() + "/" + tag.getLength() + ") tag: " + tag + " shape: " + shape.getId()); } } } void checkAllTagRanges() throws IndexOutOfBoundsException { for (CustomTag t : tags) { if (t.isIndexed()) checkRange(t); } } public void addOrMergeTag(CustomTag givenTag, String addOnlyThisProperty) throws IndexOutOfBoundsException { addOrMergeTag(givenTag, addOnlyThisProperty, true); } /** * Adds or merge the givenTag into this CustomTagList. If * addOnlyThisProperty is not null, only this property of the givenTag is * merged into the current list! This is useful e.g. when a user wants to * add a certain style (bold, italic, ...) from the UI and does not want to * overwrite other styles at this position. * * @param givenTag * The tag to add or merge * @param addOnlyThisProperty * If not null, only this property is added * @param sendSignal * If true, a signal is sent that tags have changed */ public void addOrMergeTag(CustomTag givenTag, String addOnlyThisProperty, boolean sendSignal) throws IndexOutOfBoundsException { if (givenTag == null) return; logger.trace("adding/merging tag: " + givenTag + " addOnlyThisPropery: " + addOnlyThisProperty); // tryRegisterTag(givenTag); // non-indexed tag: if (!givenTag.isIndexed()) { CustomTag existing = getNonIndexedTag(givenTag.getTagName()); if (existing != null) removeCustomTagFromList(existing); addCustomTagToList(givenTag); sortTags(); notifyTagsChanged(); return; } // indexed-tag: // check if tag fits into bounds of shape: checkRange(givenTag); if (tags.isEmpty()) { addCustomTagToList(givenTag); notifyTagsChanged(); return; } // determine overlapping tags and compute splittings: List<CustomTag> overlapping = getOverlappingTagsOfType(givenTag); boolean hasOverlaps = overlapping.size() > 0; if (!hasOverlaps) { addCustomTagToList(givenTag); } int currentPosition = 0; final int N = overlapping.size(); List<CustomTag> newTags = new ArrayList<>(); for (int i = 0; i < overlapping.size(); ++i) { CustomTag overlapTag = overlapping.get(i); CustomTag left = null, middle = null, right = null; // the // overlapping // parts OverlapType overlapType = givenTag.getOverlapType(overlapTag); logger.trace(overlapType.toString() + " overlap!"); switch (overlapType) { case NONE: break; case LEFT: if (i != 0) { throw new RuntimeException("For the LEFT overlap type, this must be the first element!"); } left = overlapTag.copy(); left.setOffset(overlapTag.getOffset()); left.setLength(givenTag.getOffset() - overlapTag.getOffset()); middle = overlapTag.copy(); middle.setOffset(givenTag.getOffset()); middle.setLength(overlapTag.getEnd() - givenTag.getOffset()); currentPosition = middle.getEnd(); // if this is the last overlapping tag: add right side carry // over: if ((i + 1) == N) { logger.trace("last overlap tag!"); right = givenTag.copy(); right.setOffset(currentPosition); right.setLength(givenTag.getEnd() - currentPosition); } removeCustomTagFromList(overlapTag); break; case INSIDE: int o = Math.max(currentPosition, givenTag.getOffset()); int l = overlapTag.getOffset() - o; if (l > 0) { left = givenTag.copy(); left.setOffset(o); left.setLength(l); } middle = overlapTag.copy(); middle.setOffset(overlapTag.getOffset()); middle.setLength(overlapTag.getLength()); currentPosition = middle.getEnd(); // if this is the last overlapping tag: add right side carry // over: if ((i + 1) == N) { logger.trace("last overlap tag!"); right = givenTag.copy(); right.setOffset(currentPosition); right.setLength(givenTag.getEnd() - currentPosition); } removeCustomTagFromList(overlapTag); break; case RIGHT: if (i + 1 != N) { throw new RuntimeException("For the LEFT overlap type, this must be the first element!"); } int o1 = Math.max(currentPosition, givenTag.getOffset()); int l1 = overlapTag.getOffset() - o1; if (l1 > 0) { left = givenTag.copy(); left.setOffset(Math.max(currentPosition, givenTag.getOffset())); left.setLength(l1); } middle = overlapTag.copy(); middle.setOffset(overlapTag.getOffset()); middle.setLength(givenTag.getEnd() - overlapTag.getOffset()); right = overlapTag.copy(); right.setOffset(middle.getEnd()); right.setLength(overlapTag.getLength() - middle.getLength()); currentPosition = right.getEnd(); // not really here necessary i // guess.. removeCustomTagFromList(overlapTag); break; case BOTH: logger.trace("N={}", N); if (N != 1) { throw new RuntimeException("For the BOTH overlap type, nr of overlapping elements must be 1!"); } if (givenTag.getOffset() - overlapTag.getOffset() > 0) { left = overlapTag.copy(); left.setOffset(overlapTag.getOffset()); left.setLength(givenTag.getOffset() - overlapTag.getOffset()); } middle = overlapTag.copy(); middle.setOffset(givenTag.getOffset()); middle.setLength(givenTag.getLength()); if (overlapTag.getEnd() - givenTag.getEnd() > 0) { right = overlapTag.copy(); right.setOffset(middle.getEnd()); right.setLength(overlapTag.getEnd() - givenTag.getEnd()); } if (right != null) currentPosition = right.getEnd(); // not really necessary // here i guess.. else currentPosition = middle.getEnd(); // not really necessary // here i guess.. removeCustomTagFromList(overlapTag); break; } // middle is the overlapping part whose fields shall be merged if (middle != null) { if (addOnlyThisProperty != null) { // set only this property if // not null! String propValue; try { propValue = BeanUtils.getProperty(givenTag, addOnlyThisProperty); logger.trace("setting prop / value = " + addOnlyThisProperty + "/" + propValue); BeanUtils.setProperty(middle, addOnlyThisProperty, propValue); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { logger.warn("Could not retrieve or set property '" + addOnlyThisProperty + "' from bean, message: " + e.getMessage()); } } else { // or if no property string given --> merge fields from // given tag into middle tag middle.setAttributes(givenTag, false); } } // now add the newly created tags: if (left != null) { newTags.add(left); addCustomTagToList(left); } if (middle != null) { newTags.add(middle); addCustomTagToList(middle); } if (right != null) { newTags.add(right); addCustomTagToList(right); } // for (CustomTag t : newTags){ // logger.debug(" newly created tags : " + t.getOffset()); // logger.debug(" newly created tags : " + t.getContainedText()); // } logger.trace("overlapping tag, i=" + i); } // end for all overlapping tags i // now sort tags, merge neighours that are equals and remove empty // valued tags: sortTags(); mergeNeighboringEqualValuedIndexedTags(newTags, false); // <--- TEST removeEmptyAndEmptyValuedTags(); if (sendSignal) notifyTagsChanged(); logger.trace("ctl: " + this); } void notifyTagsChanged() { // CustomTagUtil.writeCustomTagListToCustomTag(shape); // FIXME: may not be a good idea here!! --> probably in TranscriptObserver! shape.getObservable().setChangedAndNotifyObservers(new TrpTagsChangedEvent(shape, this, Type.CHANGED)); } public void removeNonIndexedTags() { for (CustomTag t : getNonIndexedTags()) { removeTag(t); } } public void removeIndexedTags() { for (CustomTag t : getIndexedTags()) { removeTag(t); } } public void removeTag(CustomTag t) { List<CustomTag> tagsCopy = new ArrayList<>(tags); for (CustomTag ct : tagsCopy) { if (ct.equals(t)) { removeCustomTagFromList(t); } } sortTags(); notifyTagsChanged(); } /** * Remove all tags with the given tagName */ public void removeTags(String tagName) { List<CustomTag> tagsCopy = new ArrayList<>(tags); for (CustomTag t : tagsCopy) { if (t.getTagName().equals(tagName)) { removeCustomTagFromList(t); } } sortTags(); notifyTagsChanged(); } public void removeTags() { List<CustomTag> tagsCopy = new ArrayList<>(tags); for (CustomTag t : tagsCopy) { removeCustomTagFromList(t); } sortTags(); notifyTagsChanged(); } /** * Return the custom tag as a list of css-styled tags */ public String getCustomTag() { if (getTags().isEmpty()) { return null; } String custom = ""; for (CustomTag t : getTags()) { custom += t.getCssStr() + " "; } return custom.trim(); } public List<CustomTag> getTags() { return tags; } public <T extends CustomTag> T getNonIndexedTag(String tagName) { for (CustomTag t : tags) { if (!t.isIndexed() && t.getTagName().equals(tagName)) return (T) t; } return null; } public boolean containsParagraphTag() { for (CustomTag t : tags) { if (!t.isIndexed() && t.getTagName().equals(StructureTag.TAG_NAME)){ StructureTag st = (StructureTag) t; if (st.type.equals("paragraph")){ return true; } } } return false; } public <T extends CustomTag> List<T> getIndexedTags(String tagName) { List<T> it = new ArrayList<>(); for (CustomTag t : tags) { if (t.isIndexed() && t.getTagName().equals(tagName)) it.add((T) t); } return it; } public List<CustomTag> getIndexedTags() { List<CustomTag> indexedTags = new ArrayList<>(); for (CustomTag t : tags) { if (t.isIndexed()) { indexedTags.add(t); } } return indexedTags; } // /** // * @deprecated Set removes messes up the order of the tags --> do not use! // */ // public Set<CustomTag> getIndexedTags() { // Set<CustomTag> indexedTags = new HashSet<>(); // for (CustomTag t : tags) { // if (t.isIndexed()) // indexedTags.add(t); // } // return indexedTags; // } public Set<String> getIndexedTagNames() { Set<String> indexedTagNames = new HashSet<>(); for (CustomTag t : tags) { if (t.isIndexed()) indexedTagNames.add(t.getTagName()); } return indexedTagNames; } public Set<CustomTag> getNonIndexedTags() { Set<CustomTag> indexedTags = new HashSet<>(); for (CustomTag t : tags) { if (!t.isIndexed()) indexedTags.add(t); } return indexedTags; } public Set<String> getNonIndexedTagNames() { Set<String> indexedTagNames = new HashSet<>(); for (CustomTag t : tags) { if (!t.isIndexed()) indexedTagNames.add(t.getTagName()); } return indexedTagNames; } /** * Returns a CustomTag of type T that covers the given range between * (offset, offset+length] and with attributes that are equal along this * range. If attributes do not match over the whole range, its default value * is set. Null is returned, if there are any gaps in the given range where * no tag is defined.<br> * Constructed especially for {@link TextStyleTag} objects where a user * wants to determine the common style for a certain range in the text. */ public <T extends CustomTag> T getCommonIndexedCustomTag(String tagName, int offset, int length) { List<T> cts = getIndexedTags(tagName); logger.trace("getCommonIndexedCustomTag: " + offset + "/" + length + " n-tags = " + cts.size()); // no tags --> return null if (cts.isEmpty()) return null; boolean sneakToLeft = cts.get(0).sneakToLeft(); List<CustomTag> overlapping; if (sneakToLeft && offset > 1) { overlapping = getOverlappingTags(tagName, offset - 1, length + 1); } else { overlapping = getOverlappingTags(tagName, offset, length); } if (overlapping.isEmpty()) return null; if (overlapping.get(0).getOffset() > offset) // gap between first tag // and given offset! return null; if (overlapping.get(overlapping.size() - 1).getEnd() < (offset + length)) // gap // between // last // tag // and // end // of // given // range! return null; logger.trace("nr of overlapping = " + overlapping.size()); // check if all overlapping tags are consecutive and determine their // common attributes: CustomTag commonTag = overlapping.get(0).copy(); logger.trace("commonTag, start = " + commonTag); for (int i = 1; i < overlapping.size(); ++i) { CustomTag next = overlapping.get(i); if (next.getOffset() != commonTag.getEnd()) // non-consecutive tags! return null; else { // consecutive --> adjust offset and merge equals fields commonTag.setLength(next.getEnd() - commonTag.getOffset()); commonTag.mergeEqualAttributes(next, false); } logger.trace("commonTag, " + i + " = " + commonTag); } // now set offset and length according to the given range and return: commonTag.setOffset(offset); commonTag.setLength(length); logger.trace("commonTag final = " + commonTag); return (T) commonTag; } // public void clearAllTags() { // tags.clear(); // notifyTagsChanged(); // } public boolean isSingleIndexedTagOverShapeRange(String tagName) { return isSingleIndexedTagOverRange(tagName, 0, getTextLength()); } public boolean isSingleIndexedTagOverRange(String tagName, int offset, int length) { List<CustomTag> tags = getIndexedTags(tagName); return (tags.size() == 1) ? (tags.get(0).hasRange(offset, length)) : false; } private void mergeNeighboringEqualValuedIndexedTags(List<CustomTag> alwaysMergeTheseTags, boolean forceMerge) { logger.trace("merging tags, currently: " + tags.size()); logger.trace("always merge tags: " + alwaysMergeTheseTags.size()); List<CustomTag> mergedTags = new ArrayList<>(); List<Integer> skip = new ArrayList<Integer>(); for (int i = 0; i < tags.size(); ++i) { if (skip.contains(i)) continue; CustomTag t = tags.get(i); if (t.isIndexed()) { boolean isAlwaysMergeTag1 = alwaysMergeTheseTags.contains(t); for (int j = i + 1; j < tags.size(); ++j) { if (skip.contains(j)) continue; CustomTag n = tags.get(j); if (!n.getTagName().equals(t.getTagName())) continue; boolean isNeighborAndEqualValues = n.getOffset() == t.getEnd() && t.equalsEffectiveValues(n, false); // ... if (!isNeighborAndEqualValues) { break; } else { boolean isAlwaysMergeTag2 = alwaysMergeTheseTags.contains(n); logger.trace("i1 = " + isAlwaysMergeTag1 + " i2 = " + isAlwaysMergeTag2); if ( (isAlwaysMergeTag1 && isAlwaysMergeTag2) || (t.mergeWithEqualValuedNeighbor() || forceMerge) ) { t.setLength(t.getLength() + n.getLength()); // i = j; skip.add(j); } else break; } } } mergedTags.add(t); } tags = mergedTags; logger.trace("merged tags, now: " + tags.size()); } private void removeEmptyAndEmptyValuedTags() { List<CustomTag> copyOfTags = new ArrayList<>(tags); for (CustomTag t : copyOfTags) { if (t.canBeEmpty()) continue; if (t.isEmptyValued() || t.isEmpty()) { removeCustomTagFromList(t); } } } /** * Returns all <em>common</em> indexed tags for the given range */ public List<CustomTag> getCommonIndexedTags(int start, int length) { List<CustomTag> indexedTags = new ArrayList<>(); for (String tagName : getIndexedTagNames()) { CustomTag ct = getCommonIndexedCustomTag(tagName, start, length); if (ct != null) indexedTags.add(ct); } return indexedTags; } // FIXME: Test, Test, Test... public void onTextEdited(int start, int end, String replacement) { if (false) return; // int start = e.start; // int end = e.end; // String replacement = e.text; logger.trace("on text edited: start, end, replacement: " + start + ", " + end + ", " + replacement + " id = " + shape.getId()); // get tags from start index List<CustomTag> startIndexTags = null; final boolean isEmptyText = replacement.isEmpty(); if (!isEmptyText) { // only needed later when replacement not empty! startIndexTags = getCommonIndexedTags(start, 0); // the new shit -> // get *common* // tags at index // startIndexTags = getOverlappingTags(null, start, 0); // the old // shit -> get *all* overlapping tags for (CustomTag t : startIndexTags) { logger.trace("start-index tag: " + t); } } // delete tags in edit-range: deleteTagsInRange(start, end - start, false); // adjust indices according to edit position: final int adjust = -(end - start) + replacement.length(); for (CustomTag t : tags) { logger.trace("tag from tags" + t.getContainedText()); if (!t.isIndexed()) continue; if (t.getOffset() < start && t.getEnd() > start) { // edit on right side of tag start t.setLength(t.getLength() + adjust); } else if (t.getOffset() == start && !t.isEmpty()) { // edit on tag start -> exclude empty tags to prevent copying of them! t.setOffset(t.getOffset() + adjust); } else if (t.getOffset() > start) { // edit on left side of tag start t.setOffset(t.getOffset() + adjust); } } // add new tags if new text was inserted: /* * if the user edit text which is tagged then the next block produces * e.g. 3 tags out of one * but the tag should just be adapted to the new text * so without the next block this should be fine - the existent tag is adjusted with the code above * and no new tags get created */ // if (!isEmptyText) { // for (CustomTag t : startIndexTags) { // CustomTag newT = t.copy(); // newT.setOffset(start); // newT.setLength(replacement.length()); // // // addOrMergeTag(newT, null, false); // logger.debug("new tag " + newT.getContainedText()); // // tags.add(newT); // } // } sortTags(); checkAllTagRanges(); mergeNeighboringEqualValuedIndexedTags(new ArrayList<CustomTag>(), true); // DELETED HERE, ADDED IN deleteTagsInRange // removeEmptyAndEmptyValuedTags(); // not necessary here I guess... logger.trace("ctl: " + this); if (true) // TODO: check above, if tags have really changed (maybe some // time...) notifyTagsChanged(); } // FIXME: Test, Test, Test... public void deleteTagsInRange(int offset, int length, boolean sendSignal) { // if (offset < 0 || (offset+length) > shape.getUnicodeText().length()) // throw new // IndexOutOfBoundsException("Cannot delete range: "+offset+"/"+(offset+length)+", text size is: "+shape.getUnicodeText().length()); if (length == 0) return; List<CustomTag> overlapping = getOverlappingTags(null, offset, length); logger.trace("overlapping tags are: "+overlapping.size()); for (CustomTag t : overlapping) { logger.trace("t = "+t); } IntRange range = new IntRange(offset, length); List<CustomTag> newTags = new ArrayList<CustomTag>(); for (CustomTag overlapTag : overlapping) { OverlapType type = range.getOverlapType(overlapTag.getOffset(), overlapTag.getLength()); CustomTag left = null, right = null; // compute left and right overlaps that must be retained after // deleting: switch (type) { case BOTH: left = overlapTag.copy(); left.setOffset(overlapTag.getOffset()); left.setLength(range.getOffset() - overlapTag.getOffset()); right = overlapTag.copy(); right.setOffset(range.getEnd()); right.setLength(overlapTag.getEnd() - range.getEnd()); break; case INSIDE: // a tag that was inside the range will only be removed! break; case LEFT: left = overlapTag.copy(); left.setOffset(overlapTag.getOffset()); left.setLength(range.getOffset() - overlapTag.getOffset()); break; case RIGHT: right = overlapTag.copy(); right.setOffset(range.getEnd()); right.setLength(overlapTag.getEnd() - range.getEnd()); break; case NONE: throw new RuntimeException("Fatal error: tag overlap cannot be == NONE here, overlapTag = "+overlapTag); } // remove the overlapTag and add the left and right overlaps if // present: removeCustomTagFromList(overlapTag); if (left != null) { addCustomTagToList(left); newTags.add(left); } if (right != null) { addCustomTagToList(right); newTags.add(right); } } // now sort tags, merge neighours that are equals and remove empty and // empty valued tags: sortTags(); // mergeNeighboringEqualValuedIndexedTags(newTags); // not necessary here I // guess... removeEmptyAndEmptyValuedTags(); // also not necessary but cannot hurt // (I guess) if (sendSignal) notifyTagsChanged(); } private List<CustomTag> getOverlappingTagsOfType(CustomTag givenSt) { return getOverlappingTags(givenSt.getTagName(), givenSt.getOffset(), givenSt.getLength()); } public CustomTag getOverlappingTag(String tagName, int offset) { List<CustomTag> tagsAtOffset = getOverlappingTags(tagName, offset, 0); if (tagsAtOffset.isEmpty()) return null; // should never happen! if (tagsAtOffset.size() != 1) { throw new RuntimeException("Nr of tags at position " + offset + " is greater 1: " + tagsAtOffset.size()); } CustomTag firstTag = tagsAtOffset.get(0); return firstTag; } /** * Returns all overlapping tags for the given range and the specified * tagName. If tagName is null, all tags are considered. */ public List<CustomTag> getOverlappingTags(String tagName, int offset, int length) { List<CustomTag> overlapping = new ArrayList<>(); for (CustomTag st : tags) { if (!st.isIndexed()) continue; if (st.getOverlapType(offset, length) == OverlapType.NONE) continue; if (tagName == null || st.getTagName().equals(tagName)) overlapping.add(st); } return overlapping; } private void sortTags() { Collections.sort(tags); } @Override public String toString() { String str = "CustomTagList: nr of tags: " + tags.size() + "; "; for (CustomTag t : tags) { str += t + "; "; } return str; } // public void writeToCustomTag() { // logger.debug("writeToCustomTag, shape = " + shape); // shape.setCustom(this.getCustomTag()); // } private void addCustomTagToList(CustomTag tag) { tags.add(tag); tag.customTagList = this; } private void removeCustomTagFromList(CustomTag tag) { tags.remove(tag); tag.customTagList = null; } public void printTags() { logger.info("Custom tags for shape " + getShape().getId() + ":"); for (CustomTag t : tags) { logger.info(t.toString()); } logger.info("----------------"); } // public List<CustomTag> findText(TextSearchFacets facets, boolean stopOnFirst, int startOffset, boolean previous) { // List<CustomTag> ft = new ArrayList<>(); // //// sortTags(); // should be sorted, just to be sure... //// printTags(); // // String textRegex = facets.getText(true); //// if (facets.isWholeWord()) { //// textRegex = "\\b"+textRegex+"\\b"; //// } // // String txt = getShape().getUnicodeText(); // logger.debug("searching for text: "+textRegex+" in line: "+txt); // //// Pattern p = Pattern.compile(textRegex, facets.isCaseSensitive() ? 0 : Pattern.CASE_INSENSITIVE); // Pattern p = Pattern.compile(textRegex); // // Matcher m = p.matcher(txt); // while (m.find()) { // if (!previous && startOffset!=-1 && m.start() < startOffset) // continue; // else if (previous && startOffset!=-1 && m.end() >= startOffset) // continue; // // String s = m.group(); // logger.debug("found matching text: "+s); // // CustomTag t = new CustomTag("textSearch"); // t.setOffset(m.start()); // t.setLength(s.length()); // t.customTagList = this; // // ft.add(t); // if (stopOnFirst) // return ft; // // logger.debug("textSearch tag: "+t); // } // // return ft; // } /** * Finds custom tags in this list for given the facets - every facet can * include the wildcards '*' for multiple unknown characters and '?' for one * single unknown character * * @param facets * ? * @param stopOnFirst * Stop on the first tag matching * @param startOffset * The offset in the text to start searching from * @param previous * True to search for tags before the given offset * @return The list of tags fulfilling the search criteria */ public List<CustomTag> findTags(CustomTagSearchFacets facets, boolean stopOnFirst, int startOffset, boolean previous) { List<CustomTag> ft = new ArrayList<>(); // sortTags(); // should be sorted, just to be sure... // printTags(); int inc = previous ? -1 : 1; logger.debug("searching for tags in shape="+shape.getId()+", startOffset = "+startOffset); // FIXME ? for (int i=previous?tags.size()-1:0; previous && i>=0 || !previous && i<tags.size(); i+=inc) { CustomTag t = tags.get(i); if (!previous && startOffset!=-1 && t.getOffset() < startOffset) continue; else if (previous && startOffset!=-1 && t.getEnd() >= startOffset) continue; if (!t.showInTagWidget()) { continue; } String tn = facets.getTagName(true); String tv = facets.getTagValue(true); logger.debug("tagNameRegex = " + tn + " tag: " + t.getTagName()); if (!tn.isEmpty() && !t.getTagName().matches(tn)) { logger.debug("tag name '" + t.getTagName() + "' does not match!"); continue; } logger.debug("tagValue = " + tv + " tag value: " + t.getContainedText()); if (!tv.isEmpty() && !t.getContainedText().matches(tv)) { logger.debug(t.getContainedText() + " does not match!"); continue; } Set<String> propertiesToSearch = facets.getProperties(true); boolean hasAttributeMatch = propertiesToSearch.isEmpty(); for (String a : facets.getProperties(true)) { Object v = facets.getPropValue(a, true); logger.debug("searching for prop: " + a + " v: " + v); for (String an : t.getAttributeNames()) { if (!an.matches(a)) // attribute does not match name continue; // if attribute value is not null, check for a match: if (v != null) { // TODO: check for attribute value! String vStr = v.toString(); Object av = t.getAttributeValue(an); String aVStr = (av == null) ? "" : av.toString(); logger.debug("vStr = " + vStr + " aVStr = " + aVStr); if (aVStr.matches(vStr)) { hasAttributeMatch = true; break; } } // end if v != null else { hasAttributeMatch = true; break; } } if (hasAttributeMatch) break; } // end check of all attributes // if all 'tests' passed -> add to list of found tags! if (hasAttributeMatch) { ft.add(t); if (stopOnFirst) break; } } return ft; } }