/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.layout.model; import java.util.Arrays; public class PageBreakPositionList implements PageBreakPositions { private static class PageBreakPositionBackend { public long[] pageHeaderSizes; public long[] masterBreaks; public long[] breakPositions; private PageBreakPositionBackend() { pageHeaderSizes = new long[100]; masterBreaks = new long[100]; breakPositions = new long[100]; } public void copyFrom( final PageBreakPositionBackend parentList ) { final long[] parentBreaks = parentList.breakPositions; if ( parentBreaks.length > this.breakPositions.length ) { this.breakPositions = new long[parentBreaks.length]; } System.arraycopy( parentBreaks, 0, breakPositions, 0, parentBreaks.length ); if ( parentList.masterBreaks.length > this.masterBreaks.length ) { this.masterBreaks = new long[parentList.masterBreaks.length]; } System.arraycopy( parentList.masterBreaks, 0, masterBreaks, 0, parentList.masterBreaks.length ); if ( parentList.pageHeaderSizes.length > this.pageHeaderSizes.length ) { this.pageHeaderSizes = new long[parentList.pageHeaderSizes.length]; } System.arraycopy( parentList.pageHeaderSizes, 0, pageHeaderSizes, 0, parentList.pageHeaderSizes.length ); } private void ensureSize( final int breakSize, final int masterSize ) { if ( breakSize >= breakPositions.length ) { final int newSize = breakSize + Math.min( Math.max( breakSize / 2, 5 ), 512 ); final long[] newBreakPositions = new long[newSize]; System.arraycopy( breakPositions, 0, newBreakPositions, 0, breakPositions.length ); this.breakPositions = newBreakPositions; } if ( masterSize >= masterBreaks.length ) { final int newSize = masterSize + Math.min( Math.max( masterSize / 2, 5 ), 512 ); final long[] newBreakPositions = new long[newSize]; System.arraycopy( masterBreaks, 0, newBreakPositions, 0, masterBreaks.length ); this.masterBreaks = newBreakPositions; } if ( masterSize >= pageHeaderSizes.length ) { final int newSize = masterSize + Math.min( Math.max( masterSize / 2, 5 ), 512 ); final long[] newBreakPositions = new long[newSize]; System.arraycopy( pageHeaderSizes, 0, newBreakPositions, 0, pageHeaderSizes.length ); this.pageHeaderSizes = newBreakPositions; } } public boolean equals( final Object o ) { if ( this == o ) { return true; } if ( o == null || getClass() != o.getClass() ) { return false; } final PageBreakPositionBackend that = (PageBreakPositionBackend) o; if ( !Arrays.equals( breakPositions, that.breakPositions ) ) { return false; } if ( !Arrays.equals( masterBreaks, that.masterBreaks ) ) { return false; } if ( !Arrays.equals( pageHeaderSizes, that.pageHeaderSizes ) ) { return false; } return true; } public int hashCode() { int result = Arrays.hashCode( pageHeaderSizes ); result = 31 * result + Arrays.hashCode( masterBreaks ); result = 31 * result + Arrays.hashCode( breakPositions ); return result; } } private int breakSize; private int masterSize; private int nextFoundIdx; private int prevFoundIdx; private int prevFoundMasterIdx; private int lastCommonBreak; private int lastMasterBreak; private PageBreakPositionBackend backend; private long scaleFactorMasters; private long scaleFactorMinors; private boolean enableQuickLookup; public PageBreakPositionList() { enableQuickLookup = true; // There is always a break at position ZERO. This break is always a master-break. backend = new PageBreakPositionBackend(); breakSize = 1; masterSize = 1; scaleFactorMinors = 0; scaleFactorMasters = 0; } public PageBreakPositionList( final PageBreakPositionList parentList ) { this.enableQuickLookup = true; this.backend = parentList.backend; this.breakSize = parentList.breakSize; this.masterSize = parentList.masterSize; this.prevFoundMasterIdx = parentList.prevFoundMasterIdx; this.prevFoundIdx = parentList.prevFoundIdx; this.nextFoundIdx = parentList.nextFoundIdx; this.lastCommonBreak = parentList.lastCommonBreak; this.lastMasterBreak = parentList.lastMasterBreak; scaleFactorMinors = parentList.scaleFactorMinors; scaleFactorMasters = parentList.scaleFactorMasters; } public void copyFrom( final PageBreakPositionList parentList ) { this.backend.copyFrom( parentList.backend ); this.breakSize = parentList.breakSize; this.masterSize = parentList.masterSize; this.prevFoundMasterIdx = parentList.prevFoundMasterIdx; this.prevFoundIdx = parentList.prevFoundIdx; this.nextFoundIdx = parentList.nextFoundIdx; this.lastCommonBreak = parentList.lastCommonBreak; this.lastMasterBreak = parentList.lastMasterBreak; scaleFactorMinors = parentList.scaleFactorMinors; scaleFactorMasters = parentList.scaleFactorMasters; } public void addMinorBreak( final long position ) { // If this results in a IOBEx, then we made something wrong and deserve the exception. final long lastPosition = backend.breakPositions[this.breakSize - 1]; if ( position < lastPosition ) { // This usually happens if someone tries to pass a page with negative margins. We do not accept that. throw new IllegalArgumentException( "Invalid position error: Unsorted Entry or negative page area." ); } backend.ensureSize( breakSize, masterSize ); if ( position > lastPosition ) { backend.breakPositions[breakSize] = position; breakSize += 1; scaleFactorMinors = position / breakSize; } } public void addMajorBreak( final long position, final long pageHeaderSize ) { // If this results in a IOBEx, then we made something wrong and deserve the exception. final long lastPosition = backend.breakPositions[this.breakSize - 1]; if ( position < lastPosition ) { // This usually happens if someone tries to pass a page with negative margins. We do not accept that. throw new IllegalArgumentException( "Invalid position error: Unsorted Entry or negative page area." ); } backend.ensureSize( breakSize, masterSize ); if ( position > lastPosition ) { backend.breakPositions[breakSize] = position; breakSize += 1; scaleFactorMinors = position / breakSize; } final long lastMaster = backend.masterBreaks[this.masterSize - 1]; if ( position < lastMaster ) { throw new IllegalStateException( "Adding new values to the break-position list must be happen sorted." ); } if ( position > lastMaster ) { backend.masterBreaks[masterSize] = position; backend.pageHeaderSizes[masterSize] = pageHeaderSize; masterSize += 1; scaleFactorMasters = position / masterSize; } } /** * Finds the closest break-position that is larger or equal to the given position. This returns the next pagebreak in * the flow after the given position. If the position given is larger than the largest posible page-break, then this * returns the last pagebreak instead. * * @param position * the position from where to search the next pagebreak. * @return the position. */ public long findNextBreakPosition( final long position ) { final int breakIndex = findBreak( position ); if ( breakIndex < 0 ) { return backend.breakPositions[0]; } if ( breakIndex >= breakSize ) { return backend.breakPositions[breakSize - 1]; } return backend.breakPositions[breakIndex]; } public long findPreviousBreakPosition( final long position ) { final int breakIndex = findPreviousMajorBreak( position ); if ( breakIndex < 0 ) { return backend.breakPositions[0]; } if ( breakIndex >= breakSize ) { return backend.breakPositions[breakSize - 1]; } return backend.breakPositions[breakIndex]; } /** * Returns the page-segment, where a box would be located that ends at the given position. If the position is located * before or on the page start, then this method returns -1 to indicate that this box would not be displayed at all. * If the given position is direcly located on a page-boundary, the segment number of the previous page is returned. * * @param pos * the starting position of the box. * @return -1 or a positive integer denoting the page segment where the box would be displayed. */ private int findNextBreak( final long pos ) { int start = 0; int end = breakSize; if ( nextFoundIdx > 0 ) { final long foundPos = backend.breakPositions[nextFoundIdx]; final long prevPos = backend.breakPositions[nextFoundIdx - 1]; if ( foundPos >= pos && prevPos < pos ) { return nextFoundIdx - 1; } } if ( enableQuickLookup && breakSize > 0 ) { // assume a relatively uniform layout, all rows have roughly the same size .. // this means, we can guess-jump close to the target-position .. final int maxIdx = breakSize - 1; final long lastVal = backend.breakPositions[maxIdx]; if ( lastVal > 0 ) { final int targetIdx = (int) ( pos / scaleFactorMinors ); final int minTgtIdx = Math.max( 0, Math.min( maxIdx, targetIdx - 7 ) ); final int maxTgtIdx = Math.min( maxIdx, targetIdx + 7 ); final long minKey = backend.breakPositions[minTgtIdx]; final long maxKey = backend.breakPositions[maxTgtIdx]; final boolean minLessPos = pos >= minKey; final boolean maxMorePos = pos <= maxKey; if ( minLessPos ) { start = minTgtIdx; } if ( maxMorePos ) { end = maxTgtIdx + 1; } } } final int i = PageBreakPositionList.binarySearch( backend.breakPositions, pos, start, end ); if ( i > -1 ) { nextFoundIdx = ( i - 1 ); return nextFoundIdx; } if ( i == -1 ) { nextFoundIdx = -1; return -1; } final int insertPos = Math.min( -( i + 2 ), breakSize - 1 ); // if greater than last break, return the last break .. nextFoundIdx = insertPos; return insertPos; } /** * Returns the page-segment, where a box would be located that starts at the given position. If the position is * located before the page start, then this method returns -1 to indicate that this box would not be displayed at all. * * @param pos * the starting position of the box. * @return -1 or a positive integer denoting the page segment where the box would be displayed. */ private int findPreviousBreak( final long pos ) { int start = 0; if ( prevFoundIdx >= 0 ) { final long prevFoundPos = backend.breakPositions[prevFoundIdx]; if ( prevFoundPos == pos ) { return prevFoundIdx; } if ( prevFoundPos < pos ) { if ( prevFoundIdx >= ( breakSize - 1 ) ) { // This is behind or directly at the end of the known breaks ... return prevFoundIdx; } // Check, whether the next one would be smaller too final long nextBreak = backend.breakPositions[prevFoundIdx + 1]; if ( nextBreak > pos ) { return prevFoundIdx; } } } int end = breakSize; if ( enableQuickLookup && breakSize > 0 ) { // assume a relatively uniform layout, all rows have roughly the same size .. // this means, we can guess-jump close to the target-position .. final int maxIdx = breakSize - 1; final long lastVal = backend.breakPositions[maxIdx]; if ( lastVal > 0 ) { final int targetIdx = (int) ( pos / scaleFactorMinors ); final int minTgtIdx = Math.max( 0, Math.min( maxIdx, targetIdx - 7 ) ); final int maxTgtIdx = Math.min( maxIdx, targetIdx + 7 ); final long minKey = backend.breakPositions[minTgtIdx]; final long maxKey = backend.breakPositions[maxTgtIdx]; final boolean minLessPos = pos >= minKey; final boolean maxMorePos = pos <= maxKey; if ( minLessPos ) { start = minTgtIdx; } if ( maxMorePos ) { end = maxTgtIdx + 1; } } } final int i = PageBreakPositionList.binarySearch( backend.breakPositions, pos, start, end ); if ( i > -1 ) { prevFoundIdx = i; return prevFoundIdx; } if ( i == -1 ) { prevFoundIdx = -1; return -1; } final int insertPos = Math.min( -( i + 1 ), breakSize ) - 1; // if greater than last break, return the last break .. prevFoundIdx = insertPos; return insertPos; } /** * Returns the page-segment, where a box would be located that starts at the given position. If the position is * located before the page start, then this method returns -1 to indicate that this box would not be displayed at all. * * @param pos * the starting position of the box. * @return -1 or a positive integer denoting the page segment where the box would be displayed. */ private int findPreviousMajorBreak( final long pos ) { if ( prevFoundMasterIdx >= 0 ) { final long prevFoundPos = backend.masterBreaks[prevFoundMasterIdx]; if ( prevFoundPos == pos ) { return prevFoundMasterIdx; } if ( prevFoundPos < pos ) { if ( prevFoundMasterIdx >= ( masterSize - 1 ) ) { // This is behind or directly at the end of the known breaks ... return prevFoundMasterIdx; } // Check, whether the next one would be smaller too final long nextBreak = backend.masterBreaks[prevFoundMasterIdx + 1]; if ( nextBreak > pos ) { return prevFoundMasterIdx; } } } int start = 0; int end = masterSize; if ( enableQuickLookup && masterSize > 0 ) { // assume a relatively uniform layout, all rows have roughly the same size .. // this means, we can guess-jump close to the target-position .. final int maxIdx = masterSize - 1; final long lastVal = backend.masterBreaks[maxIdx]; if ( lastVal > 0 ) { final int targetIdx = (int) ( pos / scaleFactorMasters ); final int minTgtIdx = Math.max( 0, Math.min( maxIdx, targetIdx - 7 ) ); final int maxTgtIdx = Math.min( maxIdx, targetIdx + 7 ); final long minKey = backend.masterBreaks[minTgtIdx]; final long maxKey = backend.masterBreaks[maxTgtIdx]; final boolean minLessPos = pos >= minKey; final boolean maxMorePos = pos <= maxKey; if ( minLessPos ) { start = minTgtIdx; } if ( maxMorePos ) { end = maxTgtIdx + 1; } } } final int i = PageBreakPositionList.binarySearch( backend.masterBreaks, pos, start, end ); if ( i > -1 ) { prevFoundMasterIdx = i; return prevFoundMasterIdx; } if ( i == -1 ) { prevFoundMasterIdx = -1; return -1; } final int insertPos = Math.min( -( i + 1 ), masterSize ) - 1; // if greater than last break, return the last break .. prevFoundMasterIdx = insertPos; return insertPos; } /** * Checks, whether the given box will cross a pagebreak. The box's y-position is shifted by the given amount before * testing the result. A box will cross a pagebreak if its shifted y position and its shifted y2 position (y + height) * are located on different pages. A box with a height of zero cannot cross a pagebreak by definition. * * @param boxY * the box Y, unshifted. * @param boxHeight * the box height. * @param pagebreakShift * the current shift that should be applied for the test * @return true, if the box crosses a pagebreak, false otherwise. */ public boolean isCrossingPagebreak( final long boxY, final long boxHeight, final long pagebreakShift ) { // A box does not cross the break, if both Y1 and Y2 are on the same page. if ( boxHeight == 0 ) { // A box without a height can appear on either side of the pagebreak. // But under no circumstances it can cross it. return false; } // Simple case: No fixed position at all .. final long shiftedStartPos = boxY + pagebreakShift; final int y1 = findPreviousBreak( shiftedStartPos ); final int y2 = findNextBreak( shiftedStartPos + boxHeight ); return y1 != y2; } public boolean isCrossingPagebreakWithFixedPosition( final long shiftedBoxPosition, final long boxHeight, final long fixedPositionResolved ) { // A box does not cross the break, if both Y1 and Y2 are on the same page. if ( boxHeight == 0 ) { // A box without a height can appear on either side of the pagebreak. But under no circumstances it may cross it. return false; } // Only allow positive values. // Make sure that we do not cover the page header area. If so, then correct the value to be // directly below the header-area. // Compute, the distance between the fixed-positioned box and the bottom edge of the page-header. final long shiftedSpaceOnPage = Math.max( 0, ( fixedPositionResolved - getPageHeaderHeight( shiftedBoxPosition ) ) ); // Compute the page-start on the normal flow. final int pageIndex = findPreviousMajorBreak( shiftedBoxPosition ); final long fixedPositionInFlow; if ( pageIndex < 0 ) { fixedPositionInFlow = backend.masterBreaks[0] + shiftedSpaceOnPage; } else { fixedPositionInFlow = backend.masterBreaks[pageIndex] + shiftedSpaceOnPage; } final int y1 = findPreviousBreak( fixedPositionInFlow ); final int y2 = findNextBreak( fixedPositionInFlow + boxHeight ); return y1 != y2; } /** * Computes the box's position in the normal-flow that will fullfill the 'fixed-position' constraint. The result will * be the position on the current page. This position might sit on already processed content, so the caller has to * check whether the return value of this function is less than the shifted box position. In that case, the band must * cause a pagebreak before it can be positioned. * * @param shiftedBoxPosition * @param fixedPositionResolved * @return the computed fixed position, which may be invalid. */ public long computeFixedPositionInFlow( final long shiftedBoxPosition, final long fixedPositionResolved ) { // (1) Compute the local position of the fixed-pos box in the current page. For that, lookup the current pageheader // height and subtract that from the total page size. final long pageHeaderHeight = getPageHeaderHeight( shiftedBoxPosition ); final long positionInPageContentArea = Math.max( 0, ( fixedPositionResolved - pageHeaderHeight ) ); // (2) Compute the page-start position in the normal flow for the current box. final int pageIndex = findPreviousMajorBreak( shiftedBoxPosition ); final long pageStart; if ( pageIndex < 0 ) { pageStart = backend.masterBreaks[0]; } else { pageStart = backend.masterBreaks[pageIndex]; } return pageStart + positionInPageContentArea; } protected long getPageHeaderHeight( final long position ) { final int majorBreak = findNextMajorBreak( position ); if ( isMasterBreak( position ) ) { return backend.pageHeaderSizes[Math.min( majorBreak + 1, masterSize - 1 )]; } return backend.pageHeaderSizes[majorBreak]; } /** * Returns the first break position that is greater than the given position. * * @param pos * @return -1 or a positive integer. */ private int findBreak( final long pos ) { int start = 0; if ( lastCommonBreak >= breakSize ) { // the last known pagebreak. final long lastBreakPos = backend.breakPositions[breakSize - 1]; if ( lastBreakPos > pos ) { // the position behind the last break, so return it return breakSize; } if ( lastBreakPos == pos ) { lastCommonBreak = breakSize - 1; return lastCommonBreak; } // else the position we search is lower. Search from the beginning .. } else if ( lastCommonBreak == 0 ) { final long lastBreakPos = backend.breakPositions[lastCommonBreak]; if ( lastBreakPos >= pos ) { return lastCommonBreak; } } else { final long lastBreakPos = backend.breakPositions[lastCommonBreak]; if ( lastBreakPos >= pos ) { final long prevBreakPos = backend.breakPositions[lastCommonBreak - 1]; if ( prevBreakPos < pos ) { return lastCommonBreak; } } } int end = breakSize; if ( enableQuickLookup && breakSize > 0 ) { // assume a relatively uniform layout, all rows have roughly the same size .. // this means, we can guess-jump close to the target-position .. final int maxIdx = breakSize - 1; final long lastVal = backend.breakPositions[maxIdx]; if ( lastVal > 0 ) { final int targetIdx = (int) ( pos / scaleFactorMinors ); final int minTgtIdx = Math.max( 0, Math.min( maxIdx, targetIdx - 7 ) ); final int maxTgtIdx = Math.min( maxIdx, targetIdx + 7 ); final long minKey = backend.breakPositions[minTgtIdx]; final long maxKey = backend.breakPositions[maxTgtIdx]; final boolean minLessPos = pos >= minKey; final boolean maxMorePos = pos <= maxKey; if ( minLessPos ) { start = minTgtIdx; } if ( maxMorePos ) { end = maxTgtIdx + 1; } } } final int i = PageBreakPositionList.binarySearch( backend.breakPositions, pos, start, end ); if ( i > -1 ) { lastCommonBreak = i; return i; } if ( i == -1 ) { // not found .. lastCommonBreak = 0; return 0; } final int insertPos = -( i + 1 ); lastCommonBreak = insertPos; return insertPos; } /** * Returns the page number that would contain this position. Mapping the page-number into a pagebreak position returns * the page boundary (y + height). This method returns -1 if the given position is *before* the first page-boundary. * * @param pos * @return -1 or a positive integer. */ private int findNextMajorBreak( final long pos ) { int start = 0; if ( lastMasterBreak >= masterSize ) { // the last known pagebreak. final long lastBreakPos = backend.masterBreaks[breakSize - 1]; if ( lastBreakPos > pos ) { // the position behind the last break, so return it return masterSize; } if ( lastBreakPos == pos ) { lastMasterBreak = masterSize - 1; return lastMasterBreak; } // else the position we search is lower. Search from the beginning .. } else if ( lastMasterBreak == 0 ) { final long lastBreakPos = backend.masterBreaks[lastMasterBreak]; if ( lastBreakPos >= pos ) { return lastMasterBreak; } } else { final long lastBreakPos = backend.masterBreaks[lastMasterBreak]; if ( lastBreakPos >= pos ) { final long prevBreakPos = backend.masterBreaks[lastMasterBreak - 1]; if ( prevBreakPos < pos ) { return lastMasterBreak; } } } int end = masterSize; if ( enableQuickLookup && masterSize > 0 ) { // assume a relatively uniform layout, all rows have roughly the same size .. // this means, we can guess-jump close to the target-position .. final int maxIdx = masterSize - 1; final long lastVal = backend.masterBreaks[maxIdx]; if ( lastVal > 0 ) { final int targetIdx = (int) ( pos / scaleFactorMasters ); final int minTgtIdx = Math.max( 0, Math.min( maxIdx, targetIdx - 7 ) ); final int maxTgtIdx = Math.min( maxIdx, targetIdx + 7 ); final long minKey = backend.masterBreaks[minTgtIdx]; final long maxKey = backend.masterBreaks[maxTgtIdx]; final boolean minLessPos = pos >= minKey; final boolean maxMorePos = pos <= maxKey; if ( minLessPos ) { start = minTgtIdx; } if ( maxMorePos ) { end = maxTgtIdx + 1; } } } final int i = PageBreakPositionList.binarySearch( backend.masterBreaks, pos, start, end ); if ( i > -1 ) { lastMasterBreak = i; return i; } if ( i == -1 ) { // not found .. lastMasterBreak = 0; return 0; } final int insertPos = -( i + 1 ); lastMasterBreak = insertPos; return insertPos; } private boolean isMasterBreak( final long pos ) { int start = 0; int end = masterSize; if ( enableQuickLookup && masterSize > 0 ) { // assume a relatively uniform layout, all rows have roughly the same size .. // this means, we can guess-jump close to the target-position .. final int maxIdx = masterSize - 1; final long lastVal = backend.masterBreaks[maxIdx]; if ( lastVal > 0 ) { final int targetIdx = (int) ( pos / scaleFactorMasters ); final int minTgtIdx = Math.max( 0, Math.min( maxIdx, targetIdx - 7 ) ); final int maxTgtIdx = Math.min( maxIdx, targetIdx + 7 ); final long minKey = backend.masterBreaks[minTgtIdx]; final long maxKey = backend.masterBreaks[maxTgtIdx]; final boolean minLessPos = pos >= minKey; final boolean maxMorePos = pos <= maxKey; if ( minLessPos ) { start = minTgtIdx; } if ( maxMorePos ) { end = maxTgtIdx + 1; } } } return ( PageBreakPositionList.binarySearch( backend.masterBreaks, pos, start, end ) > -1 ); } private static int binarySearch( final long[] array, final long key, final int start, final int end ) { int low = start; int high = end - 1; while ( low <= high ) { final int mid = ( low + high ) >>> 1; final long midVal = array[mid]; if ( midVal < key ) { low = mid + 1; } else if ( midVal > key ) { high = mid - 1; } else { return mid; // key found } } return -( low + 1 ); // key not found. } /** * Finds the closest master break-position that is larger or equal to the given position. A master pagebreak is the * boundary of a logical page, which in itself can consist of several physical pages. * <p/> * This returns the next master pagebreak in the flow after the given position. If the position given is larger than * the largest posible page-break, then this returns the last pagebreak instead. * * @param position * the position from where to search the next pagebreak. * @return the position. */ public long findNextMajorBreakPosition( final long position ) { final int majorBreakIndex = findNextMajorBreak( position ); if ( majorBreakIndex < 0 ) { return backend.masterBreaks[0]; } if ( majorBreakIndex >= masterSize ) { return backend.masterBreaks[masterSize - 1]; } return backend.masterBreaks[majorBreakIndex]; } public long getLastMasterBreak() { return backend.masterBreaks[masterSize - 1]; } public int getMasterBreakSize() { return masterSize; } public long getMasterBreak( final int index ) { return backend.masterBreaks[index]; } public String toString() { final StringBuilder retval = new StringBuilder( 100 ); retval.append( "PageBreakPositionList{breakSize=" ); retval.append( breakSize ); retval.append( ", masterSize=" ); retval.append( masterSize ); retval.append( ", prevFoundIdx=" ); retval.append( prevFoundIdx ); retval.append( ", masterBreaks={" ); final int masterBreakCount = masterSize; for ( int i = 0; i < masterBreakCount; i++ ) { if ( i > 0 ) { retval.append( ", " ); } final long aBreak = backend.masterBreaks[i]; retval.append( String.valueOf( aBreak ) ); } retval.append( "}, breakPositions={" ); final int breakPosCount = breakSize; for ( int i = 0; i < breakPosCount; i++ ) { if ( i > 0 ) { retval.append( ", " ); } final long position = backend.breakPositions[i]; retval.append( String.valueOf( position ) ); } retval.append( "}}" ); return retval.toString(); } public long findPageEndForPageStartPosition( final long pageOffset ) { final int masterBreakSize = getMasterBreakSize(); if ( masterBreakSize > 0 ) { final long lastBreak = getMasterBreak( masterBreakSize - 1 ); if ( pageOffset == lastBreak ) { return lastBreak; } for ( int i = masterBreakSize - 2; i >= 0; i -= 1 ) { final long masterBreak = getMasterBreak( i ); if ( masterBreak == pageOffset ) { return getMasterBreak( i + 1 ); } if ( masterBreak < pageOffset ) { break; } } } throw new IllegalStateException( "Unable to locate proper page start for given offset " + pageOffset ); } public long findPageStartPositionForPageEndPosition( final long pageOffset ) { final int masterBreakSize = getMasterBreakSize(); for ( int i = masterBreakSize - 1; i > 0; i -= 1 ) { final long masterBreak = getMasterBreak( i ); if ( masterBreak == pageOffset ) { return getMasterBreak( i - 1 ); } if ( masterBreak < pageOffset ) { throw new IllegalStateException( "Unable to locate proper page start for given offset " + pageOffset ); } } return 0; } public boolean isPageStart( final long position ) { return isMasterBreak( position ); } public boolean equals( final Object o ) { if ( this == o ) { return true; } if ( o == null || getClass() != o.getClass() ) { return false; } final PageBreakPositionList that = (PageBreakPositionList) o; if ( breakSize != that.breakSize ) { return false; } if ( masterSize != that.masterSize ) { return false; } if ( !backend.equals( that.backend ) ) { return false; } return true; } public int hashCode() { int result = breakSize; result = 31 * result + masterSize; result = 31 * result + backend.hashCode(); return result; } }