From: Vincent Hennebert Date: Tue, 8 Apr 2014 19:30:37 +0000 (+0000) Subject: Added support for multiple multi-switch appearing on the same page. X-Git-Tag: fop-2_0~117^2~7 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8c3e6a5e1545f69560c17e9845d60e38c59bab72;p=xmlgraphics-fop.git Added support for multiple multi-switch appearing on the same page. Modified MultiSwitchLM to have its standard behaviour by default. Moved BestFitLayoutUtils into MultiSwitchLM. Code clean-up and javadoc. Patch by Seifeddine Dridi, applied with some modifications. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_WhitespaceManagement@1585822 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/java/org/apache/fop/fo/flow/MultiSwitch.java b/src/java/org/apache/fop/fo/flow/MultiSwitch.java index 6c4333cff..f681f1695 100644 --- a/src/java/org/apache/fop/fo/flow/MultiSwitch.java +++ b/src/java/org/apache/fop/fo/flow/MultiSwitch.java @@ -38,9 +38,7 @@ public class MultiSwitch extends FObj { // private CommonAccessibility commonAccessibility; // End of property values - private FONode currentlyVisibleMultiCase; private String autoToggle; - private String fittingStrategy; /** * Base constructor @@ -97,18 +95,6 @@ public class MultiSwitch extends FObj { return FO_MULTI_SWITCH; } - public void setCurrentlyVisibleNode(FONode node) { - currentlyVisibleMultiCase = node; - } - - public FONode getCurrentlyVisibleNode() { - return currentlyVisibleMultiCase; - } - - public String getFittingStrategy() { - return fittingStrategy; - } - public String getAutoToggle() { return autoToggle; } diff --git a/src/java/org/apache/fop/layoutmgr/BestFitLayoutUtils.java b/src/java/org/apache/fop/layoutmgr/BestFitLayoutUtils.java deleted file mode 100644 index 148a83eba..000000000 --- a/src/java/org/apache/fop/layoutmgr/BestFitLayoutUtils.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.fop.layoutmgr; - -import java.util.LinkedList; -import java.util.List; - -import org.apache.fop.layoutmgr.BestFitPenalty.Variant; - -/** - * Utility class used in {@link MultiSwitchLayoutManager} - * to handle the best-fit property value if specified in {@link org.apache.fop.fo.flow.MultiSwitch} - */ -public final class BestFitLayoutUtils { - - private BestFitLayoutUtils() { } - - static class BestFitPosition extends Position { - - private List knuthList; - - public BestFitPosition(LayoutManager lm) { - super(lm); - } - - public List getPositionList() { - List positions = new LinkedList(); - if (knuthList != null && !knuthList.isEmpty()) { - SpaceResolver.performConditionalsNotification(knuthList, 0, knuthList.size() - 1, -1); - for (ListElement el : knuthList) { - if (el.getPosition() != null) { - positions.add(el.getPosition()); - } - } - } - return positions; - } - - public void setKnuthList(List knuthList) { - this.knuthList = knuthList; - } - - } - - public static List getKnuthList(LayoutManager lm, - List> childrenLists) { - - List knuthList = new LinkedList(); - - BestFitPenalty bestFitPenalty = new BestFitPenalty(new BestFitPosition(lm)); - for (List childList : childrenLists) { - // TODO Doing space resolution here is not correct. - // Space elements will simply be nulled. - SpaceResolver.resolveElementList(childList); - int contentLength = ElementListUtils.calcContentLength(childList); - bestFitPenalty.addVariant(new Variant(childList, contentLength)); - } - // TODO Adding two enclosing boxes is definitely a dirty hack. - // The first box forces the breaking algorithm to consider the penalty - // in case there are no elements preceding it - // and the last box prevents the glue and penalty from getting deleted - // when they are at the end of the Knuth list. - knuthList.add(new KnuthBox(0, new Position(lm), false)); - knuthList.add(bestFitPenalty); - knuthList.add(new KnuthBox(0, new Position(lm), false)); - return knuthList; - } - - public static List getPositionList(LayoutManager lm, PositionIterator posIter) { - - // "unwrap" the NonLeafPositions stored in parentIter - // and put them in a new list; - LinkedList positionList = new LinkedList(); - while (posIter.hasNext()) { - Position pos = posIter.next(); - if (pos instanceof BestFitPosition) { - positionList.addAll(((BestFitPosition) pos).getPositionList()); - } else { - positionList.add(pos); - } - } - return positionList; - } - -} diff --git a/src/java/org/apache/fop/layoutmgr/BestFitPenalty.java b/src/java/org/apache/fop/layoutmgr/BestFitPenalty.java deleted file mode 100644 index 692cc19cb..000000000 --- a/src/java/org/apache/fop/layoutmgr/BestFitPenalty.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* $Id$ */ - -package org.apache.fop.layoutmgr; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.fop.layoutmgr.BestFitLayoutUtils.BestFitPosition; - -/** - * A type of penalty used to specify a set of alternatives for the layout engine - * to choose from. The chosen alternative must have an occupied size - * less than the remaining BPD on the active page. - */ -public class BestFitPenalty extends KnuthPenalty { - - public static class Variant { - - public final List knuthList; - public final int width; - public int penaltyIndex; - - public Variant(List knuthList, int width) { - this.knuthList = knuthList; - this.width = width; - this.penaltyIndex = -1; - } - - public KnuthElement toPenalty() { - return new KnuthPenalty(width, 0, false, null, false); - } - - } - - private final BestFitPosition bestFitPosition; - private final List variantList; - - public BestFitPenalty(BestFitPosition pos) { - super(0, 0, false, pos, false); - this.bestFitPosition = pos; - variantList = new ArrayList(); - } - - public void addVariant(Variant variant) { - variantList.add(variant); - } - - public void setActiveVariant(Variant bestVariant) { - bestFitPosition.setKnuthList(bestVariant.knuthList); - } - - public List getVariants() { - return variantList; - } - - @Override - public String toString() { - String str = super.toString(); - StringBuffer buffer = new StringBuffer(64); - buffer.append(" number of variants = " + variantList.size()); - return str + buffer.toString(); - } - -} diff --git a/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java b/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java index 2ffb48675..e92a4e681 100644 --- a/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java +++ b/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java @@ -451,18 +451,8 @@ public class LayoutManagerMapping implements LayoutManagerMaker { @Override public void make(FONode node, List lms) { - MultiSwitch multiSwitch = (MultiSwitch) node; - MultiSwitchLayoutManager mslm = new MultiSwitchLayoutManager(multiSwitch); - FONode multiCase = multiSwitch.getCurrentlyVisibleNode(); - if (multiCase != null) { - FONodeIterator childIter = multiCase.getChildNodes(); - while (childIter.hasNext()) { - FONode child = (FONode) childIter.next(); - makeLayoutManagers(child, lms); - } - } else { - lms.add(mslm); - } + MultiSwitchLayoutManager mslm = new MultiSwitchLayoutManager((MultiSwitch) node); + lms.add(mslm); } } diff --git a/src/java/org/apache/fop/layoutmgr/MultiSwitchLayoutManager.java b/src/java/org/apache/fop/layoutmgr/MultiSwitchLayoutManager.java index a2b0e90b0..7c810754a 100644 --- a/src/java/org/apache/fop/layoutmgr/MultiSwitchLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/MultiSwitchLayoutManager.java @@ -17,38 +17,123 @@ package org.apache.fop.layoutmgr; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.apache.fop.area.Area; import org.apache.fop.fo.FObj; +import org.apache.fop.fo.flow.MultiSwitch; public class MultiSwitchLayoutManager extends BlockStackingLayoutManager { + static class WhitespaceManagementPosition extends Position { + + private List knuthList; + + public WhitespaceManagementPosition(LayoutManager lm) { + super(lm); + } + + public List getPositionList() { + List positions = new LinkedList(); + if (knuthList != null && !knuthList.isEmpty()) { + SpaceResolver.performConditionalsNotification(knuthList, 0, knuthList.size() - 1, -1); + for (ListElement el : knuthList) { + if (el.getPosition() != null) { + positions.add(el.getPosition()); + } + } + } + return positions; + } + + public void setKnuthList(List knuthList) { + this.knuthList = knuthList; + } + + public List getKnuthList() { + return knuthList; + } + + } + + private interface KnuthElementsGenerator { + List getKnuthElement(LayoutContext context, int alignment); + } + + private class DefaultKnuthListGenerator implements KnuthElementsGenerator { + + public List getKnuthElement(LayoutContext context, int alignment) { + + List knuthList = new LinkedList(); + LayoutManager childLM; + while ((childLM = getChildLM()) != null) { + if (!childLM.isFinished()) { + LayoutContext childLC = makeChildLayoutContext(context); + List childElements = childLM.getNextKnuthElements(childLC, alignment); + if (childElements != null) { + List newList = new LinkedList(); + wrapPositionElements(childElements, newList); + knuthList.addAll(newList); + } + } + } + return knuthList; + } + + } + + private class WhitespaceManagement implements KnuthElementsGenerator { + + public List getKnuthElement(LayoutContext context, int alignment) { + + MultiSwitchLayoutManager mslm = MultiSwitchLayoutManager.this; + List knuthList = new LinkedList(); + WhitespaceManagementPenalty penalty = new WhitespaceManagementPenalty( + new WhitespaceManagementPosition(mslm)); + LayoutManager childLM; + while ((childLM = getChildLM()) != null) { + if (!childLM.isFinished()) { + LayoutContext childLC = makeChildLayoutContext(context); + List childElements = childLM.getNextKnuthElements(childLC, alignment); + if (childElements != null) { + List newList = new LinkedList(); + wrapPositionElements(childElements, newList); + // TODO Doing space resolution here is wrong. + SpaceResolver.resolveElementList(newList); + int contentLength = ElementListUtils.calcContentLength(newList); + penalty.addVariant(penalty.new Variant(newList, contentLength)); + } + } + } + // Prevent the penalty from being ignored if it is at the beginning of the content + knuthList.add(new KnuthBox(0, new Position(mslm), false)); + knuthList.add(penalty); + // Prevent the penalty from being ignored if it is at the end of the content + knuthList.add(new KnuthBox(0, new Position(mslm), false)); + return knuthList; + } + + } + + private KnuthElementsGenerator knuthGen; + public MultiSwitchLayoutManager(FObj node) { super(node); + MultiSwitch multiSwitchNode = (MultiSwitch) node; + if (multiSwitchNode.getAutoToggle().equals("best-fit")) { + knuthGen = new WhitespaceManagement(); + } else { + knuthGen = new DefaultKnuthListGenerator(); + } } @Override public List getNextKnuthElements(LayoutContext context, int alignment) { - referenceIPD = context.getRefIPD(); - List> childrenLists = new ArrayList>(); - LayoutManager childLM; - while ((childLM = getChildLM()) != null) { - if (!childLM.isFinished()) { - LayoutContext childLC = makeChildLayoutContext(context); - List childElements = childLM.getNextKnuthElements(childLC, alignment); - if (childElements != null) { - List newList = new LinkedList(); - wrapPositionElements(childElements, newList); - childrenLists.add(newList); - } - } - } + List knuthList = knuthGen.getKnuthElement(context, alignment); setFinished(true); - return BestFitLayoutUtils.getKnuthList(this, childrenLists); + return knuthList; } @Override @@ -63,11 +148,16 @@ public class MultiSwitchLayoutManager extends BlockStackingLayoutManager { @Override public void addAreas(PositionIterator posIter, LayoutContext context) { - - List positionList = BestFitLayoutUtils.getPositionList(this, posIter); - PositionIterator newPosIter = new PositionIterator( - positionList.listIterator()); - + LinkedList positionList = new LinkedList(); + while (posIter.hasNext()) { + Position pos = posIter.next(); + if (pos instanceof WhitespaceManagementPosition) { + positionList.addAll(((WhitespaceManagementPosition) pos).getPositionList()); + } else { + positionList.add(pos); + } + } + PositionIterator newPosIter = new PositionIterator(positionList.listIterator()); AreaAdditionUtil.addAreas(this, newPosIter, context); flush(); } diff --git a/src/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java b/src/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java index 8c587bcc6..64757854e 100644 --- a/src/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java +++ b/src/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java @@ -30,7 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.fop.fo.Constants; import org.apache.fop.fo.FObj; import org.apache.fop.layoutmgr.AbstractBreaker.PageBreakPosition; -import org.apache.fop.layoutmgr.BestFitPenalty.Variant; +import org.apache.fop.layoutmgr.WhitespaceManagementPenalty.Variant; import org.apache.fop.traits.MinOptMax; import org.apache.fop.util.ListUtil; @@ -153,10 +153,13 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { /** Index of the last inserted element of the last inserted footnote. */ public int footnoteElementIndex; - /** Current active variant attached to this node */ - public final Variant variant; - /** Pending variant to be assigned to all descending nodes */ - public Variant pendingVariant; + /** + * Pending variants of dynamic contents that were evaluated WRT this node. + * When computing page difference for a break element, the total width of these variants + * will be added to 'actualWidth'. + */ + private final List pendingVariants = new ArrayList(); + private int totalVariantsWidth; public KnuthPageNode(int position, int line, int fitness, @@ -164,7 +167,7 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { int insertedFootnotes, int totalFootnotes, int footnoteListIndex, int footnoteElementIndex, double adjustRatio, int availableShrink, int availableStretch, - int difference, double totalDemerits, KnuthNode previous, Variant variant) { + int difference, double totalDemerits, KnuthNode previous) { super(position, line, fitness, totalWidth, totalStretch, totalShrink, adjustRatio, availableShrink, availableStretch, @@ -173,7 +176,11 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { this.insertedFootnotes = insertedFootnotes; this.footnoteListIndex = footnoteListIndex; this.footnoteElementIndex = footnoteElementIndex; - this.variant = variant; + } + + public void addVariant(Variant variant) { + pendingVariants.add(variant); + totalVariantsWidth += variant.width; } } @@ -188,7 +195,6 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { private final int[] bestTotalFootnotesLength = new int[4]; private final int[] bestFootnoteListIndex = new int[4]; private final int[] bestFootnoteElementIndex = new int[4]; - private final Variant[] bestVariant = new Variant[4]; @Override public void addRecord(double demerits, KnuthNode node, double adjust, @@ -201,7 +207,6 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { bestTotalFootnotesLength[fitness] = totalFootnotesLength; bestFootnoteListIndex[fitness] = footnoteListIndex; bestFootnoteElementIndex[fitness] = footnoteElementIndex; - bestVariant[fitness] = ((KnuthPageNode) node).pendingVariant; } public int getInsertedFootnotesLength(int fitness) { @@ -220,9 +225,6 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { return bestFootnoteElementIndex[fitness]; } - public Variant getVariant(int fitness) { - return bestVariant[fitness]; - } } @@ -316,8 +318,7 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { insertedFootnotesLength, totalFootnotesLength, footnoteListIndex, footnoteElementIndex, adjustRatio, availableShrink, availableStretch, - difference, totalDemerits, previous, - (previous != null) ? ((KnuthPageNode)previous).pendingVariant : null); + difference, totalDemerits, previous); } /** {@inheritDoc} */ @@ -332,8 +333,7 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { ((BestPageRecords) best).getFootnoteElementIndex(fitness), best.getAdjust(fitness), best.getAvailableShrink(fitness), best.getAvailableStretch(fitness), best.getDifference(fitness), - best.getDemerits(fitness), best.getNode(fitness), - ((BestPageRecords) best).getVariant(fitness)); + best.getDemerits(fitness), best.getNode(fitness)); } /** @@ -527,15 +527,12 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { int actualWidth = totalWidth - pageNode.totalWidth; int footnoteSplit; boolean canDeferOldFN; - if (element.isPenalty()) { - if (element instanceof BestFitPenalty) { - actualWidth += handleBestFitPenalty(pageNode, (BestFitPenalty) element, elementIndex); - } else { - actualWidth += element.getWidth(); - if (pageNode.pendingVariant != null) { - actualWidth += ((KnuthPageNode) activeNode).pendingVariant.width; - } - } + actualWidth += pageNode.totalVariantsWidth; + if (element instanceof WhitespaceManagementPenalty) { + actualWidth += handleWhitespaceManagementPenalty(pageNode, + (WhitespaceManagementPenalty) element, elementIndex); + } else if (element.isPenalty()) { + actualWidth += element.getWidth(); } if (footnotesPending) { // compute the total length of the footnotes not yet inserted @@ -595,13 +592,18 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { } } - private int handleBestFitPenalty(KnuthPageNode activeNode, BestFitPenalty penalty, int elementIndex) { + /** + * Evaluates the variants corresponding to the given penalty until one that + * leads to an acceptable adjustment ratio is found. That variant will + * be added to the list of pending variants in the given active node. + */ + private int handleWhitespaceManagementPenalty(KnuthPageNode activeNode, + WhitespaceManagementPenalty penalty, int elementIndex) { for (Variant var : penalty.getVariants()) { int difference = computeDifference(activeNode, var.toPenalty(), elementIndex); double r = computeAdjustmentRatio(activeNode, difference); if (r >= -1.0) { - var.penaltyIndex = elementIndex; - activeNode.pendingVariant = var; + activeNode.addVariant(var); return var.width; } } @@ -1022,9 +1024,14 @@ class PageBreakingAlgorithm extends BreakingAlgorithm { // ? bestActiveNode.difference : bestActiveNode.difference + fillerMinWidth; // Check if the given node has an attached variant of a dynamic content KnuthPageNode pageNode = (KnuthPageNode) bestActiveNode; - if (pageNode.variant != null) { - BestFitPenalty penalty = (BestFitPenalty) par.get(pageNode.variant.penaltyIndex); - penalty.setActiveVariant(pageNode.variant); + KnuthPageNode previousPageNode = ((KnuthPageNode) pageNode.previous); + for (Variant var : previousPageNode.pendingVariants) { + WhitespaceManagementPenalty penalty = var.getBestFitPenalty(); + int penaltyIndex = this.par.indexOf(penalty); + // Make sure penalty is inside the range of the current page node + if (penaltyIndex <= pageNode.position) { + penalty.setActiveVariant(var); + } } int difference = bestActiveNode.difference; if (difference + bestActiveNode.availableShrink < 0) { diff --git a/src/java/org/apache/fop/layoutmgr/WhitespaceManagementPenalty.java b/src/java/org/apache/fop/layoutmgr/WhitespaceManagementPenalty.java new file mode 100644 index 000000000..967df2d6a --- /dev/null +++ b/src/java/org/apache/fop/layoutmgr/WhitespaceManagementPenalty.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.layoutmgr; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.fop.layoutmgr.MultiSwitchLayoutManager.WhitespaceManagementPosition; + +/** + * A special penalty used to specify content having multiple variants. At most + * only one variant will be inserted into the final document. If none of the + * variants fit into the remaining space on the current page, the dynamic + * content will be completely ignored. + */ +public class WhitespaceManagementPenalty extends KnuthPenalty { + + public class Variant { + + public final List knuthList; + public final int width; + + public Variant(List knuthList, int width) { + this.knuthList = knuthList; + this.width = width; + } + + public KnuthElement toPenalty() { + return new KnuthPenalty(width, 0, false, null, false); + } + + public WhitespaceManagementPenalty getBestFitPenalty() { + return WhitespaceManagementPenalty.this; + } + + } + + private final WhitespaceManagementPosition whitespaceManagementPosition; + private final List variantList; + + public WhitespaceManagementPenalty(WhitespaceManagementPosition pos) { + super(0, 0, false, pos, false); + this.whitespaceManagementPosition = pos; + variantList = new ArrayList(); + } + + public void addVariant(Variant variant) { + variantList.add(variant); + } + + public void setActiveVariant(Variant bestVariant) { + whitespaceManagementPosition.setKnuthList(bestVariant.knuthList); + } + + public boolean isActivated() { + return whitespaceManagementPosition.getKnuthList() != null; + } + + public List getVariants() { + return variantList; + } + + @Override + public String toString() { + String str = super.toString(); + StringBuffer buffer = new StringBuffer(64); + buffer.append(" number of variants = " + variantList.size()); + return str + buffer.toString(); + } + +} diff --git a/test/layoutengine/standard-testcases/multi-switch_best-fit_multiple_dynamic_contents.xml b/test/layoutengine/standard-testcases/multi-switch_best-fit_multiple_dynamic_contents.xml new file mode 100644 index 000000000..df4c59f74 --- /dev/null +++ b/test/layoutengine/standard-testcases/multi-switch_best-fit_multiple_dynamic_contents.xml @@ -0,0 +1,98 @@ + + + + +

+ Test that multiple whitespace managment elements on the same page are handled nicely. +

+
+ + + + + + + + + + + + MS 1 Variant 1 + + + + + MS 2 Variant 1 + + + + + MS 3 Variant 1 + + + + + + + + + + MS 1 Variant 1 + + + + + MS 2 Variant 1 + + + + + MS 3 Variant 1 + + + MS 3 Variant 2 + + + + + MS 4 Variant 1 + + + + + + + + + + + + + + + + + + + + + + +