From: Karen Lease Date: Fri, 9 Nov 2001 21:57:47 +0000 (+0000) Subject: First versions of LayoutManager classes X-Git-Tag: fop-0_20_4-doc~224 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=76a43e26132766467c569b7938ed27cac5354dd2;p=xmlgraphics-fop.git First versions of LayoutManager classes git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@194539 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/org/apache/fop/layoutmgr/AbstractLayoutManager.java b/src/org/apache/fop/layoutmgr/AbstractLayoutManager.java new file mode 100644 index 000000000..d09ca9c4d --- /dev/null +++ b/src/org/apache/fop/layoutmgr/AbstractLayoutManager.java @@ -0,0 +1,110 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.fo.FObj; +import org.apache.fop.area.Area; + +import java.util.Iterator; + +/** + * The base class for all LayoutManagers. + */ +public abstract class AbstractLayoutManager implements LayoutManager { + protected LayoutManager parentLM; + protected FObj fobj; + + + public AbstractLayoutManager(FObj fobj) { + this.fobj = fobj; + this.parentLM = null; + } + + public void setParentLM(LayoutManager lm) { + this.parentLM = lm; + } + + + /** + * Propagates to lower level layout managers. It iterates over the + * children of its FO, asks each for its LayoutManager and calls + * its generateAreas method. + */ + public void generateAreas() { + Iterator children = fobj.getChildren(); + while (children.hasNext()) { + LayoutManager lm = ((FObj)children.next()).getLayoutManager(); + lm.setParentLM(this); + if (lm != null) { + lm.generateAreas(); + } + } + flush(); // Add last area to parent + } + +// /** +// * Ask the parent LayoutManager to add the current (full) area to the +// * appropriate parent area. +// * @param bFinished If true, this area is finished, either because it's +// * completely full or because there is no more content to put in it. +// * If false, we are in the middle of this area. This can happen, +// * for example, if we find floats in a line. We stop the current area, +// * and add it (temporarily) to its parent so that we can see if there +// * is enough space to place the float(s) anchored in the line. +// */ +// protected void flush(Area area, boolean bFinished) { +// if (area != null) { +// // area.setFinished(true); +// parentLM.addChild(area, bFinished); // ???? +// if (bFinished) { +// setCurrentArea(null); +// } +// } +// } + + /** + * Force current area to be added to parent area. + */ + abstract protected void flush(); + + + /** + * Return an Area which can contain the passed childArea. The childArea + * may not yet have any content, but it has essential traits set. + * In general, if the LayoutManager already has an Area it simply returns + * it. Otherwise, it makes a new Area of the appropriate class. + * It gets a parent area for its area by calling its parent LM. + * Finally, based on the dimensions of the parent area, it initializes + * its own area. This includes setting the content IPD and the maximum + * BPD. + */ + abstract public Area getParentArea(Area childArea); + + + public boolean generatesInlineAreas() { + return false; + } + + public boolean generatesLineAreas() { + return false; + } + + /** + * Add a child area to the current area. If this causes the maximum + * dimension of the current area to be exceeded, the parent LM is called + * to add it. + */ + abstract public void addChild(Area childArea) ; + + /** Do nothing */ + public boolean splitArea(Area areaToSplit, SplitContext context) { + context.nextArea = areaToSplit; + return false; + } + +} diff --git a/src/org/apache/fop/layoutmgr/BlockStackingLayoutManager.java b/src/org/apache/fop/layoutmgr/BlockStackingLayoutManager.java new file mode 100644 index 000000000..8a550b96c --- /dev/null +++ b/src/org/apache/fop/layoutmgr/BlockStackingLayoutManager.java @@ -0,0 +1,198 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.fo.FObj; +import org.apache.fop.area.Area; +import org.apache.fop.area.BlockParent; +import org.apache.fop.area.Block; +import org.apache.fop.area.MinOptMax; + +import java.util.Iterator; + +/** + * Base LayoutManager class for all areas which stack their child + * areas in the block-progression direction, such as Flow, Block, ListBlock. + */ +public abstract class BlockStackingLayoutManager + extends AbstractLayoutManager { + /** Reference to FO whose areas it's managing or to the traits + * of the FO. + */ + LayoutManager curChildLM = null; + BlockParent parentArea = null; + + public BlockStackingLayoutManager(FObj fobj) { + super(fobj); + } + + + + public boolean splitArea(Area area, SplitContext splitContext) { + // Divide area so that it will be within targetLength if possible + // If not, it can be shorter, but not longer. + /* Iterate over contents of the area. */ + + // Need to figure out if we can do this generically + // Logically a BlockStacking LM only handles Block-type areas + if (!(area instanceof BlockParent)) { + return false; + } + Iterator areaIter = ((BlockParent)area).getChildAreas().iterator(); + + + BreakCost minBreakCost = null; + MinOptMax remainBPD = splitContext.targetBPD; + splitContext.nextArea = area; + + while (areaIter.hasNext()) { + Area childArea = (Area)areaIter.next(); + if (remainBPD.max < childArea.getAllocationBPD().min) { + // Past the end point: try to break it + // TODO: get a LayoutManager to do the split of the child + // area, either Area => LM or Area => gen FO => LM + LayoutManager childLM = childArea.getGeneratingFObj(). + getLayoutManager(); + splitContext.targetBPD = remainBPD; + if (childLM.splitArea(childArea, splitContext) == false) { + // Can't split, so must split this area before childArea + // Can we pass the iter? + // If already saw several a potential break, use it + if (minBreakCost != null) { + /* Split 'area', placing all children after + * minBreakCost.getArea() into a new area, + * which we store in the splitContext. + */ + // splitContext.nextArea = area.splitAfter(minBreakCost.getArea()); + } + else { + /* This area will be shorter than the desired minimum. + * Split before the current childArea (which will be + * the first area in the newly created Area. + */ + //splitContext.nextArea = area.splitBefore(childArea); + } + } + else return true; // childLM has done the work for us! + // Set cost, dimension ??? + break; + } + else { + remainBPD.subtract(childArea.getAllocationBPD()); + if (remainBPD.min < 0) { + // Potential breakpoint: remember break Position and + // break "cost" (constraint violation) + BreakCost breakCost = evaluateBreakCost(area, childArea); + minBreakCost = breakCost.chooseLowest(minBreakCost); + } + } + //Note: size of area when split can depend on conditional + // space, border and padding of the split area!!! + } + // True if some part of area can be placed, false if none is placed + return (splitContext.nextArea != area); + + } + + private BreakCost evaluateBreakCost(Area parent, Area child) { + return new BreakCost(child,0); + } + + /** return current area being filled + */ + protected BlockParent getCurrentArea() { + return this.parentArea; + } + + + /** + * Set the current area being filled. + */ + protected void setCurrentArea(BlockParent parentArea) { + this.parentArea = parentArea; + } + + + + protected MinOptMax resolveSpaceSpecifier(Area nextArea) { + SpaceSpecifier spaceSpec = new SpaceSpecifier(); +// Area prevArea = getCurrentArea().getLast(); +// if (prevArea != null) { +// spaceSpec.addSpace(prevArea.getSpaceAfter()); +// } +// spaceSpec.addSpace(nextArea.getSpaceBefore()); + return spaceSpec.resolve(); + } + + /** + * Add the childArea to the passed area. + * Called by child LayoutManager when it has filled one of its areas. + * The LM should already have an Area in which to put the child. + * See if the area will fit in the current area. + * If so, add it. Otherwise initiate breaking. + * @param childArea the area to add: will be some block-stacked Area. + * @param parentArea the area in which to add the childArea + */ + protected void addChildToArea(Area childArea, BlockParent parentArea) { + // This should be a block-level Area (Block in the generic sense) + if (!(childArea instanceof Block)) { + System.err.println("Child not a Block in BlockStackingLM!"); + return; + } + + // See if the whole thing fits, including space before + // Calculate space between last child in curFlow and childArea + MinOptMax targetDim = parentArea.getAvailBPD(); + MinOptMax spaceBefore = resolveSpaceSpecifier(childArea) ; + targetDim.subtract(spaceBefore); + if (targetDim.max >= childArea.getAllocationBPD().min) { + //parentArea.addBlock(new InterBlockSpace(spaceBefore)); + parentArea.addBlock((Block)childArea); + return; + } + else { + // Probably need something like max BPD so we don't get into + // infinite loops with large unbreakable chunks + SplitContext splitContext = new SplitContext(targetDim); + + LayoutManager childLM = childArea.getGeneratingFObj(). + getLayoutManager(); + if (childLM.splitArea(childArea, splitContext)) { + //parentArea.addBlock(new InterBlockSpace(spaceBefore)); + parentArea.addBlock((Block)childArea); + } + flush(); // hand off current area to parent + getParentArea(splitContext.nextArea); + // Check that reference IPD hasn't changed!!! + // If it has, we must "reflow" the content + addChild(splitContext.nextArea); + } + } + + + /** + * Add the childArea to the current area. + * Called by child LayoutManager when it has filled one of its areas. + * The LM should already have an Area in which to put the child. + * See if the area will fit in the current area. + * If so, add it. Otherwise initiate breaking. + * @param childArea the area to add: will be some block-stacked Area. + */ + public void addChild(Area childArea) { + addChildToArea((Block)childArea, getCurrentArea()); + } + + /** + * Force current area to be added to parent area. + */ + protected void flush() { + parentLM.addChild(getCurrentArea()); + } + + +} diff --git a/src/org/apache/fop/layoutmgr/BreakCost.java b/src/org/apache/fop/layoutmgr/BreakCost.java new file mode 100644 index 000000000..66d513fc1 --- /dev/null +++ b/src/org/apache/fop/layoutmgr/BreakCost.java @@ -0,0 +1,33 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.area.Area; + + +/** + * Evaluate and store the cost of breaking an Area at a given point. + */ +public class BreakCost { + private Area breakArea; + + private int cost; // Will be more complicated than this! + + public BreakCost(Area breakArea, int cost) { + this.breakArea = breakArea; + this.cost = cost; + } + + Area getArea() { + return breakArea; + } + + public BreakCost chooseLowest(BreakCost otherCost) { + return this; + } +} diff --git a/src/org/apache/fop/layoutmgr/FlowLayoutManager.java b/src/org/apache/fop/layoutmgr/FlowLayoutManager.java new file mode 100644 index 000000000..f68d693da --- /dev/null +++ b/src/org/apache/fop/layoutmgr/FlowLayoutManager.java @@ -0,0 +1,56 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.apps.FOPException; +import org.apache.fop.fo.FObj; +import org.apache.fop.fo.properties.Constants; +import org.apache.fop.area.*; + +/** + * LayoutManager for an fo:flow object. + * Its parent LM is the PageLayoutManager. + * This LM is responsible for getting columns of the appropriate size + * and filling them with block-level areas generated by its children. + */ +public class FlowLayoutManager extends BlockStackingLayoutManager { + + /** Array of areas currently being filled stored by area class */ + private BlockParent[] currentAreas = new BlockParent[Area.CLASS_MAX]; + + /** + * This is the top level layout manager. + * It is created by the PageSequence FO. + */ + public FlowLayoutManager(FObj fobj) { + super(fobj); + } + + + + /** + * Add child area to a the correct container, depending on its + * area class. A Flow can fill at most one area container of any class + * at any one time. The actual work is done by BlockStackingLM. + */ + public void addChild(Area childArea) { + addChildToArea(childArea, + this.currentAreas[childArea.getAreaClass()]); + } + + public Area getParentArea(Area childArea) { + // Get an area from the Page + BlockParent parentArea = + (BlockParent)parentLM.getParentArea(childArea); + this.currentAreas[parentArea.getAreaClass()] = parentArea; + setCurrentArea(parentArea); + return parentArea; + } + + +} diff --git a/src/org/apache/fop/layoutmgr/LayoutManager.java b/src/org/apache/fop/layoutmgr/LayoutManager.java new file mode 100644 index 000000000..078e11899 --- /dev/null +++ b/src/org/apache/fop/layoutmgr/LayoutManager.java @@ -0,0 +1,22 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + + +import org.apache.fop.area.Area; + +/** + * The interface for all LayoutManagers. + */ +public interface LayoutManager { + public void generateAreas(); + public Area getParentArea (Area childArea); + public void addChild (Area childArea); + public boolean splitArea(Area areaToSplit, SplitContext context); + public void setParentLM(LayoutManager lm); +} diff --git a/src/org/apache/fop/layoutmgr/PageLayoutManager.java b/src/org/apache/fop/layoutmgr/PageLayoutManager.java new file mode 100644 index 000000000..a38c5d3d9 --- /dev/null +++ b/src/org/apache/fop/layoutmgr/PageLayoutManager.java @@ -0,0 +1,343 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.apps.FOPException; +import org.apache.fop.area.*; +import org.apache.fop.fo.pagination.PageSequence; +import org.apache.fop.fo.properties.Constants; + +/** + * LayoutManager for a PageSequence and its flow. + * It manages all page-related layout. + */ +public class PageLayoutManager extends AbstractLayoutManager + implements Runnable{ + + private PageSequence pageseq; + /** True if haven't yet laid out any pages.*/ + private boolean bFirstPage; + /** Current page being worked on. */ + private PageViewport curPage; + + /** Body region of the current page */ + private BodyRegion curBody; + + /** Current span being filled */ + private Span curSpan; + + /** Number of columns in current span area. */ + private int curSpanColumns; + + + /** Current flow-reference-area (column) being filled. */ + private Flow curFlow; + + /** Manager which handles a queue of all pages which are completely + * laid out and ready for rendering, except for resolution of ID + * references? + */ + private AreaTree areaTree; + + /** + * This is the top level layout manager. + * It is created by the PageSequence FO. + */ + public PageLayoutManager(AreaTree areaTree, PageSequence pageseq) { + super( pageseq); + this.areaTree = areaTree; + } + + + /** + * The layout process is designed to be able to be run in a thread. + * In theory it can run at the same + * time as FO tree generation, once the layout-master-set has been read. + * We can arrange it so that the iterator over the fobj children waits + * until the next child is available. + * As it produces pages, it adds them to the AreaTree, where the + * rendering process can also run in a parallel thread. + */ + public void run() { + generateAreas(); + } + + + /** + * For now, only handle normal flow areas. + */ + public void addChild(Area childArea) { + if (childArea.getAreaClass() == Area.CLASS_NORMAL) { + placeFlowRefArea(childArea); + } + else ; // TODO: all the others! + } + + /** + * Place a FlowReferenceArea into the current span. The FlowLM is + * responsible for making sure that it will actually fit in the + * current span area. In fact the area has already been added to the + * current span, so we are just checking to see if the span is "full", + * possibly moving to the next column or to the next page. + */ + protected void placeFlowRefArea(Area area) { + // assert (curSpan != null); + // assert (area == curFlow); + // assert (curFlow == curSpan.getFlow(curSpan.getColumnCount()-1)); + // assert (area.getBPD().min < curSpan.getHeight()); + // Last column on this page is filled + // See if the flow is full. The Flow LM can add an area before + // it's full in the case of a break or a span. + // Also in the case of a float to be placed. In that case, there + // may be further material added later. + // The Flow LM sets the "finished" flag on the Flow Area if it has + // completely filled it. In this case, if on the last column + // end the page. + + // Alternatively the child LM indicates to parent that it's full? + if (area.getAllocationBPD().max >= curSpan.getMaxBPD().min) { + // Consider it filled + if (curSpan.getColumnCount() == curSpanColumns) { + finishPage(); + } + else curFlow = null; // Create new flow on next getParentArea() + } + } + + + protected void placeAbsoluteArea(Area area) { + } + + + protected void placeBeforeFloat(Area area) { + } + + + protected void placeSideFloat(Area area) { + } + + protected void placeFootnote(Area area) { + // After doing this, reduce available space on the curSpan. + // This has to be propagated to the curFlow (FlowLM) so that + // it can adjust its limit for composition (or it just asks + // curSpan for BPD before doing the break?) + // If multi-column, we may have to balance to find more space + // for a float. When? + } + + + + private PageViewport makeNewPage(boolean bIsBlank, boolean bIsLast) { + finishPage(); + try { + curPage = pageseq.createPage(bIsBlank, bIsLast); + } catch (FOPException fopex) { /* ???? */ } + curBody = (BodyRegion) curPage.getPage(). + getRegion(RegionReference.BODY).getRegion(); + return curPage; + } + + private void finishPage() { + if (curPage != null) { + // Layout static content into the regions + // Need help from pageseq for this + // Queue for ID resolution and rendering + areaTree.addPage(curPage); + curPage = null; + curBody = null; + curSpan=null; + curFlow=null; + } + } + + /** + * This is called from FlowLayoutManager when it needs to start + * a new flow container (while generating areas). + * @param area The area for which a container is needed. It must be + * some kind of block-level area. It must have area-class, break-before + * and span properties set. + */ + public Area getParentArea(Area childArea) { + int aclass = childArea.getAreaClass() ; + if (aclass == Area.CLASS_NORMAL) { + // TODO: how to get properties from the Area??? + // Need span, break + int breakVal = Constants.AUTO; // childArea.getBreakBefore(); + if (breakVal != Constants.AUTO) { + // We may be forced to make new page + handleBreak(breakVal); + } + else if (curPage == null) { + makeNewPage(false, false); + } + // Now we should be on the right kind of page + boolean bNeedSpan = false; + int span = Constants.NONE; // childArea.getSpan() + int numCols=1; + if (span == Constants.ALL) { + // Assume the number of columns is stored on the curBody object. + //numCols = curBody.getProperty(NUMBER_OF_COLUMNS); + } + if (curSpan == null) { + createBodyMainReferenceArea(); + bNeedSpan = true; + } + else if (numCols != curSpanColumns) { + // TODO: BALANCE EXISTING COLUMNS + if (curSpanColumns > 1) { + // balanceColumns(); + } + bNeedSpan = true; + } + if (bNeedSpan) { + // Make a new span and the first flow + createSpan(numCols); + } + else if (curFlow == null) { + createFlow(); + } + return curFlow; + } + else { + if (curPage == null) { + makeNewPage(false, false); + } + // Now handle different kinds of areas + if (aclass == Area.CLASS_BEFORE_FLOAT) { + BeforeFloat bf = curBody.getBeforeFloat(); + if (bf == null) { + bf = new BeforeFloat(); + curBody.setBeforeFloat(bf); + } + return bf; + } + else if (aclass == Area.CLASS_FOOTNOTE) { + Footnote fn = curBody.getFootnote(); + if (fn == null) { + fn = new Footnote(); + curBody.setFootnote(fn); + } + return fn; + } + // TODO!!! other area classes (side-float, absolute, fixed) + return null; + } + } + + + /** + * Depending on the kind of break condition, make new column + * or page. May need to make an empty page if next page would + * not have the desired "handedness". + */ + protected void handleBreak(int breakVal) { + if (breakVal == Constants.COLUMN) { + if (curSpan != null && + curSpan.getColumnCount() != curSpanColumns) { + // Move to next column + createFlow(); + return; + } + // else need new page + breakVal = Constants.PAGE; + } + if (needEmptyPage(breakVal)) { + curPage = makeNewPage(true, false); + } + if (needNewPage(breakVal)) { + curPage = makeNewPage(false, false); + } + } + + + /** + * If we have already started to layout content on a page, + * and there is a forced break, see if we need to generate + * an empty page. + * Note that if not all content is placed, we aren't sure whether + * it will flow onto another page or not, so we'd probably better + * block until the queue of layoutable stuff is empty! + */ + private boolean needEmptyPage(int breakValue) { + return false; +// if (breakValue == Constants.PAGE || curPage.isEmpty()) { +// // any page is OK or we already have an empty page +// return false; +// } +// else { +// /* IF we are on the kind of page we need, we'll need a new page. */ +// if (curPage.getPageNumber()%2 != 0) { +// // Current page is odd +// return (breakValue == Constants.ODD_PAGE); +// } +// else { +// return (breakValue == Constants.EVEN_PAGE); +// } +// } + } + + /** + * See if need to generate a new page for a forced break condition. + * TODO: methods to see if the current page is empty and to get + * its number. + */ + private boolean needNewPage(int breakValue) { + return false; +// if (curPage.isEmpty()) { +// if (breakValue == Constants.PAGE) { +// return false; +// } +// else if (curPage.getPageNumber()%2 != 0) { +// // Current page is odd +// return (breakValue == Constants.EVEN_PAGE); +// } +// else { +// return (breakValue == Constants.ODD_PAGE); +// } +// } +// else { +// return true; +// } + } + + + private void createBodyMainReferenceArea() { + curBody.setMainReference(new MainReference()); + } + + + + private Flow createFlow() { + curFlow = new Flow(); + // Set IPD and max BPD on the curFlow from curBody + curSpan.addFlow(curFlow); + return curFlow; + } + + private void createSpan(int numCols) { + // check number of columns (= all in Body or 1) + // If already have a span, get its size and position (as MinMaxOpt) + // This determines the position of the new span area + // Attention: space calculation between the span areas. + +// MinOptMax newpos ; +// if (curSpan != null) { +// newpos = curSpan.getPosition(BPD); +// newpos.add(curSpan.getDimension(BPD)); +// } +// else newpos = new MinOptMax(); + curSpan = new Span(numCols); + //curSpan.setPosition(BPD, newpos); + curBody.getMainReference().addSpan(curSpan); + createFlow(); + } + + // See finishPage... + protected void flush() {} + +} diff --git a/src/org/apache/fop/layoutmgr/SpaceSpecifier.java b/src/org/apache/fop/layoutmgr/SpaceSpecifier.java new file mode 100644 index 000000000..ca33c07a1 --- /dev/null +++ b/src/org/apache/fop/layoutmgr/SpaceSpecifier.java @@ -0,0 +1,31 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.area.Area; +import org.apache.fop.area.MinOptMax; +import org.apache.fop.datatypes.Space; + + +/** + * Accumulate a sequence of space-specifiers (XSL space type) on + * areas with a stacking constraint. Provide a way to resolve these into + * a single MinOptMax value. + */ +public class SpaceSpecifier { + + /** + * Combine passed space property value with any existing space. + */ + public void addSpace(Space moreSpace) { + } + + public MinOptMax resolve() { + return new MinOptMax(); + } +} diff --git a/src/org/apache/fop/layoutmgr/SplitContext.java b/src/org/apache/fop/layoutmgr/SplitContext.java new file mode 100644 index 000000000..d798af5d1 --- /dev/null +++ b/src/org/apache/fop/layoutmgr/SplitContext.java @@ -0,0 +1,23 @@ +/* + * $Id$ + * Copyright (C) 2001 The Apache Software Foundation. All rights reserved. + * For details on use and redistribution please refer to the + * LICENSE file included with these sources. + */ + +package org.apache.fop.layoutmgr; + +import org.apache.fop.area.Area; +import org.apache.fop.area.MinOptMax; + +public class SplitContext { + + Area nextArea; + MinOptMax targetBPD; + + public SplitContext(MinOptMax targetBPD) { + this.targetBPD = targetBPD; + nextArea = null; + } + +}