blob: 266a7d9b9530196ab365b2029430966fde8f7a7e [file] [log] [blame]
/*
* 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.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fop.area.AreaTreeHandler;
import org.apache.fop.area.BodyRegion;
import org.apache.fop.area.PageViewport;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.pagination.PageProductionException;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.pagination.Region;
import org.apache.fop.fo.pagination.RegionBody;
import org.apache.fop.fo.pagination.SimplePageMaster;
/**
* <p>This class delivers Page instances. It also caches them as necessary.
* </p>
* <p>Additional functionality makes sure that surplus instances that are requested by the
* page breaker are properly discarded, especially in situations where hard breaks cause
* blank pages. The reason for that: The page breaker sometimes needs to preallocate
* additional pages since it doesn't know exactly until the end how many pages it really needs.
* </p>
*/
public class PageProvider implements Constants {
private Log log = LogFactory.getLog(PageProvider.class);
/** Indices are evaluated relative to the first page in the page-sequence. */
public static final int RELTO_PAGE_SEQUENCE = 0;
/** Indices are evaluated relative to the first page in the current element list. */
public static final int RELTO_CURRENT_ELEMENT_LIST = 1;
private int startPageOfPageSequence;
private int startPageOfCurrentElementList;
private int startColumnOfCurrentElementList;
private boolean spanAllForCurrentElementList;
private List<Page> cachedPages = new java.util.ArrayList<Page>();
private int lastPageIndex = -1;
private int indexOfCachedLastPage = -1;
//Cache to optimize getAvailableBPD() calls
private int lastRequestedIndex = -1;
private int lastReportedBPD = -1;
/**
* AreaTreeHandler which activates the PSLM and controls
* the rendering of its pages.
*/
private AreaTreeHandler areaTreeHandler;
/**
* fo:page-sequence formatting object being
* processed by this class
*/
private PageSequence pageSeq;
protected boolean skipPagePositionOnly;
/**
* Main constructor.
* @param ath the area tree handler
* @param ps The page-sequence the provider operates on
*/
public PageProvider(AreaTreeHandler ath, PageSequence ps) {
this.areaTreeHandler = ath;
this.pageSeq = ps;
this.startPageOfPageSequence = ps.getStartingPageNumber();
}
public void initialize() {
cachedPages.clear();
}
/**
* The page breaker notifies the provider about the page number an element list starts
* on so it can later retrieve PageViewports relative to this first page.
* @param startPage the number of the first page for the element list.
* @param startColumn the starting column number for the element list.
* @param spanAll true if the current element list is for a column-spanning section
*/
public void setStartOfNextElementList(int startPage, int startColumn, boolean spanAll) {
if (log.isDebugEnabled()) {
log.debug("start of the next element list is:"
+ " page=" + startPage + " col=" + startColumn
+ (spanAll ? ", column-spanning" : ""));
}
this.startPageOfCurrentElementList = startPage - startPageOfPageSequence + 1;
this.startColumnOfCurrentElementList = startColumn;
this.spanAllForCurrentElementList = spanAll;
//Reset Cache
this.lastRequestedIndex = -1;
this.lastReportedBPD = -1;
}
/**
* Sets the index of the last page. This is done as soon as the position of the last page
* is known or assumed.
* @param index the index relative to the first page in the page-sequence
*/
public void setLastPageIndex(int index) {
this.lastPageIndex = index;
}
/**
* Returns the available BPD for the part/page indicated by the index parameter.
* The index is the part/page relative to the start of the current element list.
* This method takes multiple columns into account.
* @param index zero-based index of the requested part/page
* @return the available BPD
*/
public int getAvailableBPD(int index) {
//Special optimization: There may be many equal calls by the BreakingAlgorithm
if (this.lastRequestedIndex == index) {
if (log.isTraceEnabled()) {
log.trace("getAvailableBPD(" + index + ") -> (cached) " + lastReportedBPD);
}
return this.lastReportedBPD;
}
int pageIndexTmp = index;
int pageIndex = 0;
int colIndex = startColumnOfCurrentElementList;
Page page = getPage(
false, pageIndex, RELTO_CURRENT_ELEMENT_LIST);
while (pageIndexTmp > 0) {
colIndex++;
if (colIndex >= page.getPageViewport().getCurrentSpan().getColumnCount()) {
colIndex = 0;
pageIndex++;
page = getPage(false, pageIndex, RELTO_CURRENT_ELEMENT_LIST);
BodyRegion br = page.getPageViewport().getBodyRegion();
if (!pageSeq.getMainFlow().getFlowName().equals(br.getRegionName())) {
pageIndexTmp++;
}
}
pageIndexTmp--;
}
this.lastRequestedIndex = index;
this.lastReportedBPD = page.getPageViewport().getBodyRegion().getRemainingBPD();
if (log.isTraceEnabled()) {
log.trace("getAvailableBPD(" + index + ") -> " + lastReportedBPD);
}
return this.lastReportedBPD;
}
private static class Column {
final Page page;
final int pageIndex;
final int colIndex;
final int columnCount;
Column(Page page, int pageIndex, int colIndex, int columnCount) {
this.page = page;
this.pageIndex = pageIndex;
this.colIndex = colIndex;
this.columnCount = columnCount;
}
}
private Column getColumn(int index) {
int columnCount = 0;
int colIndex = startColumnOfCurrentElementList + index;
int pageIndex = -1;
Page page;
do {
colIndex -= columnCount;
pageIndex++;
page = getPage(false, pageIndex, RELTO_CURRENT_ELEMENT_LIST);
if (page.getPageViewport().getPage() != null) {
columnCount = page.getPageViewport().getCurrentSpan().getColumnCount();
}
} while (colIndex >= columnCount);
return new Column(page, pageIndex, colIndex, columnCount);
}
/**
* Compares the IPD of the given part with the following one.
*
* @param index index of the current part
* @return a negative integer, zero or a positive integer as the current IPD is less
* than, equal to or greater than the IPD of the following part
*/
public int compareIPDs(int index) {
Column column = getColumn(index);
if (column.colIndex + 1 < column.columnCount) {
// Next part is a column on same page => same IPD
return 0;
} else {
Page nextPage = getPage(false, column.pageIndex + 1, RELTO_CURRENT_ELEMENT_LIST);
return column.page.getPageViewport().getBodyRegion().getColumnIPD()
- nextPage.getPageViewport().getBodyRegion().getColumnIPD();
}
}
/**
* Checks if a break at the passed index would start a new page
* @param index the index of the element before the break
* @return {@code true} if the break starts a new page
*/
boolean startPage(int index) {
return getColumn(index).colIndex == 0;
}
/**
* Checks if a break at the passed index would end a page
* @param index the index of the element before the break
* @return {@code true} if the break ends a page
*/
boolean endPage(int index) {
Column column = getColumn(index);
return column.colIndex == column.columnCount - 1;
}
/**
* Obtain the applicable column-count for the element at the
* passed index
* @param index the index of the element
* @return the number of columns
*/
int getColumnCount(int index) {
return getColumn(index).columnCount;
}
/**
* Returns the part index (0&lt;x&lt;partCount) which denotes the first part on the last page
* generated by the current element list.
* @param partCount Number of parts determined by the breaking algorithm
* @return the requested part index
*/
public int getStartingPartIndexForLastPage(int partCount) {
int lastPartIndex = partCount - 1;
return lastPartIndex - getColumn(lastPartIndex).colIndex;
}
Page getPageFromColumnIndex(int columnIndex) {
return getColumn(columnIndex).page;
}
/**
* Returns a Page.
* @param isBlank true if this page is supposed to be blank.
* @param index Index of the page (see relativeTo)
* @param relativeTo Defines which value the index parameter should be evaluated relative
* to. (One of PageProvider.RELTO_*)
* @return the requested Page
*/
public Page getPage(boolean isBlank, int index, int relativeTo) {
if (relativeTo == RELTO_PAGE_SEQUENCE) {
return getPage(isBlank, index);
} else if (relativeTo == RELTO_CURRENT_ELEMENT_LIST) {
int effIndex = startPageOfCurrentElementList + index;
effIndex += startPageOfPageSequence - 1;
return getPage(isBlank, effIndex);
} else {
throw new IllegalArgumentException(
"Illegal value for relativeTo: " + relativeTo);
}
}
/**
* Returns a Page.
* @param isBlank true if the Page should be a blank one
* @param index the Page's index
* @return a Page instance
*/
protected Page getPage(boolean isBlank, int index) {
boolean isLastPage = (lastPageIndex >= 0) && (index == lastPageIndex);
if (log.isTraceEnabled()) {
log.trace("getPage(" + index + " " + (isBlank ? "blank" : "non-blank")
+ (isLastPage ? " <LAST>" : "") + ")");
}
int intIndex = index - startPageOfPageSequence;
if (log.isTraceEnabled()) {
if (isBlank) {
log.trace("blank page requested: " + index);
}
if (isLastPage) {
log.trace("last page requested: " + index);
}
}
if (intIndex > cachedPages.size()) {
throw new UnsupportedOperationException("Cannot handle holes in page cache");
} else if (intIndex == cachedPages.size()) {
if (log.isTraceEnabled()) {
log.trace("Caching " + index);
}
cacheNextPage(index, isBlank, isLastPage, this.spanAllForCurrentElementList);
}
Page page = cachedPages.get(intIndex);
boolean replace = false;
if (page.getPageViewport().isBlank() != isBlank) {
log.debug("blank condition doesn't match. Replacing PageViewport.");
replace = true;
}
if (page.getPageViewport().getPage() != null
&& page.getPageViewport().getCurrentSpan().getColumnCount() == 1
&& !this.spanAllForCurrentElementList) {
RegionBody rb = (RegionBody)page.getSimplePageMaster().getRegion(Region.FO_REGION_BODY);
int colCount = rb.getColumnCount();
if (colCount > 1) {
log.debug("Span doesn't match. Replacing PageViewport.");
replace = true;
}
}
if ((isLastPage && indexOfCachedLastPage != intIndex)
|| (!isLastPage && indexOfCachedLastPage >= 0)) {
log.debug("last page condition doesn't match. Replacing PageViewport.");
replace = true;
indexOfCachedLastPage = (isLastPage ? intIndex : -1);
}
if (replace) {
discardCacheStartingWith(intIndex);
PageViewport oldPageVP = page.getPageViewport();
page = cacheNextPage(index, isBlank, isLastPage, this.spanAllForCurrentElementList);
PageViewport newPageVP = page.getPageViewport();
newPageVP.replace(oldPageVP);
this.areaTreeHandler.getIDTracker().replacePageViewPort(oldPageVP, newPageVP);
}
return page;
}
protected void discardCacheStartingWith(int index) {
while (index < cachedPages.size()) {
this.cachedPages.remove(cachedPages.size() - 1);
if (!pageSeq.goToPreviousSimplePageMaster()) {
log.warn("goToPreviousSimplePageMaster() on the first page called!");
}
}
}
private Page cacheNextPage(int index, boolean isBlank, boolean isLastPage, boolean spanAll) {
String pageNumberString = pageSeq.makeFormattedPageNumber(index);
boolean isFirstPage = (startPageOfPageSequence == index);
SimplePageMaster spm = pageSeq.getNextSimplePageMaster(
index, isFirstPage, isLastPage, isBlank);
boolean isPagePositionOnly = pageSeq.hasPagePositionOnly() && !skipPagePositionOnly;
if (isPagePositionOnly) {
spm = pageSeq.getNextSimplePageMaster(index, isFirstPage, true, isBlank);
}
Page page = new Page(spm, index, pageNumberString, isBlank, spanAll, isPagePositionOnly);
//Set unique key obtained from the AreaTreeHandler
page.getPageViewport().setKey(areaTreeHandler.generatePageViewportKey());
page.getPageViewport().setForeignAttributes(spm.getForeignAttributes());
page.getPageViewport().setWritingModeTraits(pageSeq);
cachedPages.add(page);
if (isLastPage) {
pageSeq.getRoot().setLastSeq(pageSeq);
} else if (!isFirstPage) {
pageSeq.getRoot().setLastSeq(null);
}
return page;
}
public int getIndexOfCachedLastPage() {
return indexOfCachedLastPage;
}
public int getLastPageIndex() {
return lastPageIndex;
}
public int getLastPageIPD() {
int index = this.cachedPages.size();
boolean isFirstPage = (startPageOfPageSequence == index);
SimplePageMaster spm = pageSeq.getLastSimplePageMaster(index, isFirstPage, false);
Page page = new Page(spm, index, "", false, false, false);
if (pageSeq.getRoot().getLastSeq() != null && pageSeq.getRoot().getLastSeq() != pageSeq) {
return -1;
}
return page.getPageViewport().getBodyRegion().getColumnIPD();
}
public int getCurrentIPD() {
if (startPageOfCurrentElementList == 0) {
return -1;
}
Page page = getPageFromColumnIndex(startColumnOfCurrentElementList);
return page.getPageViewport().getBodyRegion().getColumnIPD();
}
public int getNextIPD() {
pageSeq.setOnlyTryInfinite(true);
try {
int oldSize = cachedPages.size();
Page page = getPageFromColumnIndex(startColumnOfCurrentElementList + 1);
if (oldSize != cachedPages.size()) {
cachedPages.remove(cachedPages.size() - 1);
}
return page.getPageViewport().getBodyRegion().getColumnIPD();
} catch (PageProductionException e) {
return getCurrentIPD();
} finally {
pageSeq.setOnlyTryInfinite(false);
}
}
public int getCurrentColumnCount() {
Page page = getPageFromColumnIndex(startColumnOfCurrentElementList);
return page.getPageViewport().getCurrentSpan().getColumnCount();
}
/**
* Indicates whether the column/page at the given index is on the first page of the page sequence.
*
* @return {@code true} if the given part is on the first page of the sequence
*/
boolean isOnFirstPage(int partIndex) {
Column column = getColumn(partIndex);
return startPageOfCurrentElementList + column.pageIndex == startPageOfPageSequence;
}
}