use of eu.transkribus.core.model.beans.pagecontent_trp.TrpTextLineType in project TranskribusCore by Transkribus.
the class PageXmlUtils method createPcGtsTypeFromText.
public static PcGtsType createPcGtsTypeFromText(final String imgFileName, Dimension dim, String text, TranscriptionLevel level, boolean skipEmptyLines) throws IOException {
// create empty page
PcGtsType pcGtsType = createEmptyPcGtsType(imgFileName, dim);
TrpPageType page = (TrpPageType) pcGtsType.getPage();
// create and add text region with size of image
Rectangle r = new Rectangle(0, 0, page.getImageWidth(), page.getImageHeight());
String defaultCoords = PointStrUtils.pointsToString(r);
TrpTextRegionType region = new TrpTextRegionType((TrpPageType) page);
region.setId("region_1");
region.setCoordinates(defaultCoords, null);
page.getTextRegionOrImageRegionOrLineDrawingRegion().add(region);
if (level == null) {
level = TranscriptionLevel.LINE_BASED;
}
if (level != TranscriptionLevel.REGION_BASED && level != TranscriptionLevel.LINE_BASED && level != TranscriptionLevel.WORD_BASED) {
throw new IOException("Invalide TranscriptionLevel: " + level);
}
if (level == TranscriptionLevel.REGION_BASED) {
region.setUnicodeText(text, null);
} else {
String splitRegex = skipEmptyLines ? "[\\r\\n]+" : "\\r?\\n";
String[] lines = text.split(splitRegex);
logger.debug("nr of lines = " + lines.length);
int lc = 1;
for (String lineText : lines) {
TrpTextLineType line = new TrpTextLineType(region);
line.setId("line_" + (lc++));
line.setCoordinates(defaultCoords, null);
region.getTextLine().add(line);
if (level == TranscriptionLevel.LINE_BASED) {
line.setUnicodeText(lineText, null);
} else if (level == TranscriptionLevel.WORD_BASED) {
int wc = 1;
for (String wordText : lineText.split(" ")) {
// TODO: better word splitting??
TrpWordType word = new TrpWordType(line);
word.setId("word_" + (wc++));
word.setCoordinates(defaultCoords, null);
word.setUnicodeText(wordText, null);
line.getWord().add(word);
}
}
}
}
return pcGtsType;
}
use of eu.transkribus.core.model.beans.pagecontent_trp.TrpTextLineType in project TranskribusCore by Transkribus.
the class TextStyleTypeUtils method addTextStyleTag.
/**
* Add this text style tag to the given shape. Also checks if the resulting text style tag is covering the whole area and then sets the global text style also if so.
*/
public static void addTextStyleTag(ITrpShapeType shape, TextStyleTag s, String addOnlyThisProperty, /*boolean recursive,*/
Object who) {
if (!(shape instanceof TrpTextRegionType || shape instanceof TrpTextLineType || shape instanceof TrpWordType))
return;
// add text style tag to custom list:
shape.getCustomTagList().addOrMergeTag(s, addOnlyThisProperty);
logger.debug("customtaglist=" + shape.getCustomTagList());
// apply text style tag to global text style if the text style tag is a single index tag over the whole range of the shape:
boolean isS = shape.getCustomTagList().isSingleIndexedTagOverShapeRange(TextStyleTag.TAG_NAME);
logger.debug("isSingleIndexedTagOverShapeRange: " + isS);
// deactivate observer to avoid excessive events...
boolean isActive = shape.getObservable().isActive();
shape.getObservable().setActive(false);
// if (USE_GLOBAL_TEXT_STYLE) {
if (isS) {
// logger.debug("HERE");
shape.setTextStyle(s.getTextStyle(), false, shape);
} else {
shape.setTextStyle(null, false, shape);
}
// }
// else {
// // shape.setTextStyle(null);
// shape.setTextStyle(null, false, shape); // erase global text style
// }
logger.debug("CUSTOM AFTER: " + shape.getCustom());
shape.getObservable().setActive(isActive);
// apply recursively:
// if (recursive) {
// for (ITrpShapeType c : shape.getChildren(recursive)) {
// c.getObservable().setActive(false);
// c.addTextStyleTag(s, addOnlyThisProperty, /*recursive,*/ who);
// c.getObservable().setActive(true);
// }
// }
// send text style changed event:
shape.getObservable().setChangedAndNotifyObservers(new TrpTextStyleChangedEvent(who));
}
use of eu.transkribus.core.model.beans.pagecontent_trp.TrpTextLineType in project TranskribusCore by Transkribus.
the class CustomTagListTest method testSimpleAddOrMergeTagWithTextStyles.
// @Ignore
@Test
public void testSimpleAddOrMergeTagWithTextStyles() {
TrpTextLineType line = new TrpTextLineType(new TrpTextRegionType(new TrpPageType()));
line.setUnicodeText("Hello world!", null);
CustomTagList tl = new CustomTagList(line);
TextStyleTag tst = new TextStyleTag(0, 10);
tst.setFontFamily("testFont");
tl.addOrMergeTag(tst, null);
TextStyleTag ts1 = new TextStyleTag(2, 5);
ts1.setBold(true);
tl.addOrMergeTag(ts1, null);
logger.trace(tl.toString());
Assert.assertEquals("Nr of text styles must be 3!", 3, tl.getTags().size());
TextStyleTag ts2 = new TextStyleTag(3, 4);
ts2.setItalic(true);
tl.addOrMergeTag(ts2, null);
Assert.assertEquals("Nr of text styles must be 4!", 4, tl.getTags().size());
logger.trace(tl.toString());
// Assert.assertEquals("Nr of text styles must be 5!", 5, tl.getTags().size());
Assert.assertTrue("offset = 0", tl.getTags().get(0).getOffset() == 0);
CustomTag last = tl.getTags().get(tl.getTags().size() - 1);
Assert.assertTrue("offset+length = 10", (last.getOffset() + last.getLength()) == 10);
}
use of eu.transkribus.core.model.beans.pagecontent_trp.TrpTextLineType in project TranskribusCore by Transkribus.
the class Pdf2TrpDoc method main.
public static void main(String[] args) {
if (args.length != 1) {
return;
}
File in = new File(args[0]);
final String name = in.getName();
File outDir = new File("/tmp/");
outDir.mkdirs();
try {
// PageImageWriter imgWriter = new PageImageWriter();
// String imgDirPath = imgWriter.extractImages(in.getAbsolutePath(), outDir.getAbsolutePath());
String imgDirPath = "/tmp/Kurzgefaßte_Geschichte_Statistik_und_Topographie_von_Tirol";
File pageDir = new File(imgDirPath + File.separator + "page");
pageDir.mkdirs();
TreeMap<String, File> imgs = LocalDocReader.findImgFiles(new File(imgDirPath));
ArrayList<PDFPage> pages = PDFTextExtractor.processPDF(in.getAbsolutePath());
if (imgs.size() != pages.size()) {
logger.error("Nr. of image files does not match nr. of text pages!");
return;
}
int i = 0;
for (Entry<String, File> img : imgs.entrySet()) {
PDFPage pdfPage = pages.get(i++);
Dimension dim = ImgUtils.readImageDimensions(img.getValue());
PcGtsType pc = PageXmlUtils.createEmptyPcGtsType(img.getValue(), dim);
final File xmlOut = new File(pageDir.getAbsolutePath() + File.separator + img.getKey() + ".xml");
Rectangle printspace = pdfPage.getContentRect();
if (printspace != null) {
TrpPrintSpaceType psType = new TrpPrintSpaceType();
psType.setCoords(rect2Coords(printspace));
TrpPageType pageType = (TrpPageType) pc.getPage();
// ((ITrpShapeType) pageType).getObservable().setActive(false);
pageType.setPrintSpace(psType);
for (PDFRegion r : pdfPage.regions) {
TrpTextRegionType rType = new TrpTextRegionType(pageType);
rType.setCoords(rect2Coords(r.getRect()));
rType.setUnicodeText(r.getText(), null);
for (PDFLine l : r.lines) {
TrpTextLineType lType = new TrpTextLineType(rType);
lType.setCoords(rect2Coords(l.getRect()));
lType.setUnicodeText(l.getText(), null);
for (PDFString s : l.strings) {
TrpWordType wType = new TrpWordType(lType);
wType.setCoords(rect2Coords(s.getRect()));
wType.setUnicodeText(s.value, null);
lType.getWord().add(wType);
}
rType.getTextLine().add(lType);
}
pageType.getRegions().add(rType);
}
}
PageXmlUtils.marshalToFile(pc, xmlOut);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
use of eu.transkribus.core.model.beans.pagecontent_trp.TrpTextLineType in project TranskribusCore by Transkribus.
the class TrpPdfDocument method addUniformTextFromTextRegion.
private void addUniformTextFromTextRegion(final TextRegionType tr, final PdfContentByte cb, int cutoffLeft, int cutoffTop, BaseFont bf, float lineStartX, ExportCache cache) throws IOException, DocumentException {
List<TextLineType> lines = tr.getTextLine();
if (lines != null && !lines.isEmpty()) {
int i = 0;
float lineStartY = 0;
// sort according to reading order
Collections.sort(lines, new TrpElementReadingOrderComparator<TextLineType>(true));
double minY = 0;
double maxY = 0;
// get min and max values of region y direction for later calculation of textline height
// java.awt.Rectangle regionRect = PageXmlUtils.buildPolygon(tr.getCoords().getPoints()).getBounds();
int maxIdx = lines.size() - 1;
// java.awt.Rectangle firstLineRectOld = PageXmlUtils.buildPolygon(lines.get(0).getCoords().getPoints()).getBounds();
// logger.debug("OLDDDDD: firstLineRectOld minX = " + firstLineRectOld.getMinX());
java.awt.Rectangle firstLineRect = ((TrpTextLineType) lines.get(0)).getBoundingBox();
// logger.debug("NEWWWWW: firstLineRect minX = " + firstLineRect.getMinX());
// java.awt.Rectangle lastLineRect = PageXmlUtils.buildPolygon(lines.get(maxIdx).getCoords().getPoints()).getBounds();
java.awt.Rectangle lastLineRect = ((TrpTextLineType) lines.get(maxIdx)).getBoundingBox();
double firstLineRotation = computeRotation((TrpBaselineType) lines.get(0).getBaseline());
double lastLineRotation = computeRotation((TrpBaselineType) lines.get(maxIdx).getBaseline());
boolean isVerticalRegion = false;
// use X coords to compute the total line gap
if (firstLineRotation == 90 && lastLineRotation == 90) {
// since the reading order is not clear if the text is vertically -> could be right to left or vice versa
double tmpMinX1 = firstLineRect.getMinX();
double tmpMinX2 = lastLineRect.getMinX();
double tmpMaxX1 = firstLineRect.getMaxX();
double tmpMaxX2 = lastLineRect.getMaxX();
minY = Math.min(tmpMinX1, tmpMinX2);
maxY = Math.max(tmpMaxX1, tmpMaxX2);
isVerticalRegion = true;
} else {
minY = firstLineRect.getMinY();
maxY = lastLineRect.getMaxY();
}
/*
* if start of line is too tight on the upper bound - set to the first 1/12 of t page from above
* BUT: Is not good since page number and other informations are often in this section
*/
// if (minY < twelfthPoints[1][1]){
// minY = twelfthPoints[1][1];
// }
// for(TextLineType lt : lines){
//
// TrpTextLineType l = (TrpTextLineType)lt;
// java.awt.Rectangle lineRect = PageXmlUtils.buildPolygon(l.getCoords().getPoints()).getBounds();
//
//
//
// if (lines.size() == 1){
// minY = lineRect.getMinY();
// maxY = lineRect.getMaxY();
//
// }
// else if (l.getIndex() == 0){
// minY = lineRect.getMinY();
// }
// else if (l.getIndex() == lines.size()-1){
// maxY = lineRect.getMaxY();
// }
//
// }
double lineGap = (maxY - minY) / lines.size();
// use default values if only one line and no previous line mean height computed
if (lines.size() == 1) {
lineMeanHeight = (prevLineMeanHeight != 0 ? prevLineMeanHeight : lineMeanHeight);
} else if (lines.size() > 1) {
lineMeanHeight = (float) (2 * (lineGap / 3));
leading = (int) (lineGap / 3);
prevLineMeanHeight = lineMeanHeight;
// logger.debug("Line Mean Height for Export " + lineMeanHeight);
// overallLineMeanHeight = ( (overallLineMeanHeight != 0) ? overallLineMeanHeight+lineMeanHeight/2 : lineMeanHeight);
}
for (TextLineType lt : lines) {
wordOffset = 0;
TrpTextLineType l = (TrpTextLineType) lt;
TrpBaselineType baseline = (TrpBaselineType) l.getBaseline();
// PageXmlUtils.buildPolygon(l.getCoords().getPoints()).getBounds();
java.awt.Rectangle lineRect = l.getBoundingBox();
// PageXmlUtils.buildPolygon(baseline.getPoints()).getBounds();
java.awt.Rectangle baseLineRect = baseline == null ? null : baseline.getBoundingBox();
if (baseLineRect == null) {
logger.debug("Baseline is null - ignore this line");
continue;
}
float tmpLineStartX = lineStartX;
// PageXmlUtils.buildPolygon(tr.getCoords().getPoints()).getBounds().getMinX();
float regionStartMinX = (float) tr.getBoundingBox().getMinX();
double regionWidth = tr.getBoundingBox().getWidth();
// first line
if (i == 0) {
lineStartY = (float) (minY + lineMeanHeight);
/*
* if first line of a text region is indented then take this into account in printed text
*/
if (lineRect.getMinX() > regionStartMinX) {
if (lineRect.getMinX() - regionStartMinX > regionWidth / 4) {
// tmpLineStartX = (float) lineStartX + twelfthPoints[1][0];
tmpLineStartX = (float) baseLineRect.getMinX();
}
}
} else // for subsequent lines
{
if (lineRect.getMinX() > regionStartMinX) {
if (lineRect.getMinX() - regionStartMinX > regionWidth / 4) {
// tmpLineStartX = (float) lineStartX + twelfthPoints[1][0];
tmpLineStartX = (float) baseLineRect.getMinX();
}
}
// tmpLineStartX = getLinePositionInTextregionGrid(twelfthRegion, lineRect.getMinX());
lineStartY = lineStartY + lineMeanHeight + leading;
// for (TrpTextRegionType region : tr.getPage().getTextRegions(true)){
//
// double regionMinX = PageXmlUtils.buildPolygon(region.getCoords().getPoints()).getBounds().getMinX();
// double regionMaxX = PageXmlUtils.buildPolygon(region.getCoords().getPoints()).getBounds().getMaxX();
// Rectangle rec = PageXmlUtils.buildPolygon(region.getCoords().getPoints()).getBounds();
//
// if (rec.contains(tmpLineStartX, lineStartY) && !tr.getId().equals(region.getId()) && tmpLineStartX < regionMaxX){
// logger.debug("region contains point " + tr.getId() + " region ID " + region.getId());
// tmpLineStartX = (float) regionMaxX;
// break;
// }
//
//
// }
// if (lineRect.getMinX() > lineStartX){
// if (lineRect.getMinX() - lineStartX > twelfthPoints[1][0]){
// tmpLineStartX = (float) lineRect.getMinX();
// }
// }
}
if (baseLineRect != null && regionStartMinX < baseLineRect.getMinX() && (baseLineRect.getMinX() - regionStartMinX) > twelfthPoints[1][0]) {
// logger.debug("try to find smaller region for baseline !!!!!!! " );
for (TrpTextRegionType region : tr.getPage().getTextRegions(false)) {
if (!region.getId().equals(tr.getId())) {
// PageXmlUtils.buildPolygon(region.getCoords().getPoints()).getBounds().getMinX();
double regionMinX = region.getBoundingBox().getMinX();
double regionMaxX = region.getBoundingBox().getMaxX();
double regionMinY = region.getBoundingBox().getMinY();
double regionMaxY = region.getBoundingBox().getMaxY();
double meanX = regionMinX + (regionMaxX - regionMinX) / 2;
// another region before the lines
if (meanX > regionStartMinX && meanX < baseLineRect.getMinX() && baseLineRect.getMinY() < regionMaxY && baseLineRect.getMinY() > regionMinY) {
tmpLineStartX = (float) regionMaxX + lineMeanHeight;
logger.debug("region " + region.getId() + " overlaps this other region " + tr.getId());
// logger.debug("new tmplineStartX is " + regionMaxX);
break;
}
}
}
// tmpLineStartX = (float) baseLineRect.getMinX();
}
i++;
/*
* word level bei uniform output nicht sinnvoll?
* besser nur ganze lines ausgeben
*/
// if(useWordLevel && !l.getWord().isEmpty()){
// List<WordType> words = l.getWord();
// for(WordType wt : words){
// TrpWordType w = (TrpWordType)wt;
// if(!w.getUnicodeText().isEmpty()){
// java.awt.Rectangle boundRect = PageXmlUtils.buildPolygon(w.getCoords()).getBounds();
//
// addUniformString(boundRect, lineMeanHeight, lineStartX, lineStartY, w.getUnicodeText(), cb, cutoffLeft, cutoffTop, bf);
// } else {
// logger.info("No text content in word: " + w.getId());
// }
// }
// } else if(!l.getUnicodeText().isEmpty()){
/*
* make chunks out of the lineText
* so it is possible to have differnt fonts, underlines and other text styles in one line
*
* possible text styles are:
* new CustomTagAttribute("fontFamily", true, "Font family", "Font family"),
new CustomTagAttribute("serif", true, "Serif", "Is this a serif font?"),
new CustomTagAttribute("monospace",true, "Monospace", "Is this a monospace (i.e. equals width characters) font?"),
new CustomTagAttribute("fontSize", true, "Font size", "The size of the font in points"),
new CustomTagAttribute("kerning", true, "Kerning", "The kerning of the font, see: http://en.wikipedia.org/wiki/Kerning"),
new CustomTagAttribute("textColour", true, "Text colour", "The foreground colour of the text"),
new CustomTagAttribute("bgColour", true, "Background colour", "The background colour of the text"),
new CustomTagAttribute("reverseVideo", true, "Reverse video", "http://en.wikipedia.org/wiki/Reverse_video"),
new CustomTagAttribute("bold", true, "Bold", "Bold font"),
new CustomTagAttribute("italic", true, "Italic", "Italic font"),
new CustomTagAttribute("underlined", true, "Underlined", "Underlined"),
new CustomTagAttribute("subscript", true, "Subscript", "Subscript"),
new CustomTagAttribute("superscript", true, "Superscript", "Superscript"),
new CustomTagAttribute("strikethrough", true, "Strikethrough", "Strikethrough"),
new CustomTagAttribute("smallCaps", true, "Small caps", "Small capital letters at the height as lowercase letters, see: http://en.wikipedia.org/wiki/Small_caps"),
new CustomTagAttribute("letterSpaced", true, "Letter spaced", "Equals distance between characters, see: http://en.wikipedia.org/wiki/Letter-spacing"),
*/
List<Chunk> chunkList = new ArrayList<Chunk>();
/*
* if line is empty -> use the words of this line as line text
* otherwise take the text in the line
*/
List<TextStyleTag> styleTags = new ArrayList<TextStyleTag>();
String shapeText = "";
if (l.getUnicodeText().isEmpty() || useWordLevel) {
// logger.debug("in word based path " + useWordLevel);
List<WordType> words = l.getWord();
int chunkIndex = 0;
for (WordType wt : words) {
TrpWordType w = (TrpWordType) wt;
String wordText = "";
// add empty space after each word
if (chunkIndex > 0) {
chunkList.add(chunkIndex, new Chunk(" "));
chunkIndex++;
}
if (!w.getUnicodeText().isEmpty()) {
// remember all style tags for text formatting later on
styleTags.addAll(w.getTextStyleTags());
if (!shapeText.equals("")) {
shapeText = shapeText.concat(" ");
}
wordText = wordText.concat(w.getUnicodeText());
shapeText = shapeText.concat(w.getUnicodeText());
for (int j = 0; j < wordText.length(); ++j) {
String currentCharacter = wordText.substring(j, j + 1);
chunkList.add(chunkIndex, formatText(currentCharacter, styleTags, j, w, cache));
chunkIndex++;
}
styleTags.clear();
}
}
} else if (!l.getUnicodeText().isEmpty()) {
String lineText = l.getUnicodeText();
shapeText = lineText;
// logger.debug("line Text is " + lineText);
styleTags.addAll(l.getTextStyleTags());
for (int j = 0; j < lineText.length(); ++j) {
String currentCharacter = lineText.substring(j, j + 1);
chunkList.add(j, formatText(currentCharacter, styleTags, j, l, cache));
}
} else // empty shape
{
logger.debug("empty shape ");
continue;
}
Phrase phrase = new Phrase();
// trim is important to get the 'real' first char for rtl definition
boolean rtl = textIsRTL(shapeText.trim());
if (rtl) {
logger.debug("&&&&&&&& STRING IS RTL : ");
}
for (int j = chunkList.size() - 1; j >= 0; j--) {
if (rtl) {
phrase.add(chunkList.get(j));
} else {
phrase.addAll(chunkList);
break;
}
}
// phrase.addAll(chunkList);
// logger.debug("curr phrase is: " + phrase.getContent());
// compute rotation of text, if rotation higher PI/16 than rotate otherwise even text
/*
* No rotation for single lines in a overall horizontal text region
* Reason: Vertical line uses too much space - calculated for horizontal
*/
double rotation = 0;
if (isVerticalRegion) {
rotation = (baseline != null ? computeRotation(baseline) : 0);
if (rotation != 0) {
/*
* if we rotate e.g. 90° than we should use the actual x location of the line
* so vertical text must be treated different than horizontal text
*/
if (baseLineRect != null) {
if (rtl) {
tmpLineStartX = (float) baseLineRect.getMaxX();
} else {
tmpLineStartX = (float) baseLineRect.getMinX();
}
lineStartY = (float) baseLineRect.getMaxY();
} else if (lineRect != null) {
tmpLineStartX = lineRect.x;
lineStartY = (float) lineRect.getMaxY();
}
}
}
// blacken Strings if wanted
// Set<Entry<CustomTag, String>> blackSet = CustomTagUtils.getAllTagsOfThisTypeForShapeElement(l, RegionTypeUtil.BLACKENING_REGION.toLowerCase()).entrySet();
//
// if (!lineText.equals("") && doBlackening && blackSet.size() > 0){
//
// //for all blackening regions replace text with ****
// for (Map.Entry<CustomTag, String> currEntry : blackSet){
//
// if (!currEntry.getKey().isIndexed()){
// //logger.debug("line not indexed : " + lineText);
// lineText = lineText.replaceAll(".", "*");
// }
// else{
// lineText = blackenString(currEntry, lineText);
// //logger.debug("lineText after blackened : " + lineText);
// }
// }
// }
// for rtl export
float lineEndX = 0;
float width = 0;
if (baseLineRect != null) {
lineEndX = (float) baseLineRect.getMaxX();
width = (float) baseLineRect.getWidth();
// this leads to an extra start for each line instead of having a combined start for all lines in a region
// tmpLineStartX = (float) (lineEndX - baseLineRect.getWidth());
} else if (lineRect != null) {
lineEndX = lineRect.x + lineRect.width;
width = (float) lineRect.getWidth();
}
// mainly for very small regions at the very left of a page
if (tmpLineStartX > lineEndX) {
lineEndX = tmpLineStartX + width;
}
// logger.debug("width " + width);
// logger.debug("lineEndX " + lineEndX);
// first add uniform String (=line), ,after that eventaully highlight the tags in this line using the current line information like x/y position,
// addUniformString(lineMeanHeight, tmpLineStartX, lineStartY, lineText, cb, cutoffLeft, cutoffTop, bf, twelfthPoints[1][0], false, null, rotation);
addUniformString(tr.getBoundingBox(), lineMeanHeight, tmpLineStartX, lineStartY, lineEndX, phrase, cb, cutoffLeft, cutoffTop, bf, twelfthPoints[1][0], false, null, rotation, rtl);
/*
* old:
* highlight all tags of this text line if property is set
* no highlighting is done during chunk formatting and not in an extra step
*/
// if (highlightTags){
//
//
// Set<Entry<CustomTag, String>> entrySet = CustomTagUtils.getAllTagsForShapeElement(l).entrySet();
//
// highlightUniformString(entrySet, tmpLineStartX, lineStartY, l, cb, cutoffLeft, cutoffTop, bf);
//
// List<WordType> words = l.getWord();
// for(WordType wt : words){
// TrpWordType w = (TrpWordType)wt;
//
// Set<Entry<CustomTag, String>> entrySet2 = CustomTagUtils.getAllTagsForShapeElement(w).entrySet();
//
// highlightUniformString(entrySet2, tmpLineStartX, lineStartY, l, cb, cutoffLeft, cutoffTop, bf);
// }
//
// }
}
}
}
Aggregations