/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Gabriel Roldan (OpenGeo) 2010
*
*/
package org.geowebcache.diskquota.storage;
import java.math.BigInteger;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.storage.TileRange;
import org.springframework.util.Assert;
/**
* Pyramid of tile pages for a given {@link GridSubset}
* <p>
* This is a support class for {@link TilePageCalculator}, hence package visible.
* </p>
*
* @author groldan
*/
class PagePyramid {
/**
* {@code [level][numTilesPerPageX, numTilesPerPageY, numPagesX, numPagesY]}
*/
private Map<Integer, PageLevelInfo> pageInfo;
private Map<Integer, long[]> gridSubsetCoverages;
private final int zoomStart;
private final int zoomStop;
public static final class PageLevelInfo {
public final int pagesX;
public final int pagesY;
public final int tilesPerPageX;
public final int tilesPerPageY;
public final BigInteger tilesPerPage;
public final long coverageMinX;
public final long coverageMinY;
public final long coverageMaxX;
public final long coverageMaxY;
public PageLevelInfo(int pagesX, int pagesY, int tilesPerPageX, int tilesPerPageY,
long coverageMinX, long coverageMinY, long coverageMaxX, long coverageMaxY) {
this.pagesX = pagesX;
this.pagesY = pagesY;
this.tilesPerPageX = tilesPerPageX;
this.tilesPerPageY = tilesPerPageY;
this.tilesPerPage = BigInteger.valueOf(tilesPerPageX).multiply(
BigInteger.valueOf(tilesPerPageY));
this.coverageMinX = coverageMinX;
this.coverageMinY = coverageMinY;
this.coverageMaxX = coverageMaxX;
this.coverageMaxY = coverageMaxY;
}
@Override
public String toString() {
NumberFormat nf = NumberFormat.getInstance(new Locale("es"));
nf.setGroupingUsed(true);
return "Pages: " + pagesX + " x " + pagesY + " (" + nf.format(pagesX * pagesY) + "), "
+ "tiles:" + tilesPerPageX + " x " + tilesPerPageY + " ("
+ nf.format(tilesPerPageX * (long) tilesPerPageY) + ")";
}
}
/**
* @param gridSubsetCoverages
* grid subset coverage per level, as per {@link GridSubset#getCoverages()}
* @param zoomStop
* @param zoomStart
*/
public PagePyramid(final long[][] gridSubsetCoverages, int zoomStart, int zoomStop) {
this.gridSubsetCoverages = new TreeMap<Integer, long[]>();
for (long[] coverage : gridSubsetCoverages) {
this.gridSubsetCoverages.put(Integer.valueOf((int) coverage[4]), coverage);
}
this.zoomStart = zoomStart;
this.zoomStop = zoomStop;
this.pageInfo = new TreeMap<Integer, PagePyramid.PageLevelInfo>();
}
public int getZoomStart() {
return zoomStart;
}
public int getZoomStop() {
return zoomStop;
}
public PageLevelInfo getPageInfo(final int zoomLevel) {
Assert.isTrue(zoomLevel >= zoomStart);
Assert.isTrue(zoomLevel <= zoomStop);
final Integer key = Integer.valueOf(zoomLevel);
PageLevelInfo levelInfo = pageInfo.get(key);
if (levelInfo == null) {
long[] coverage = this.gridSubsetCoverages.get(key);
levelInfo = calculatePageInfo(coverage);
pageInfo.put(key, levelInfo);
}
return levelInfo;
}
/**
*
* @param coverage
* {@code [minx, miny, maxx, maxy, zoomlevel]} gridsubset coverage for a given zoom
* level
* @return {@code [numTilesPerPageX, numTilesPerPageY, numPagesX, numPagesY]} number of pages in
* both directions for the given coverage
*/
public PageLevelInfo calculatePageInfo(final long[] coverage) {
final int level = (int) coverage[4];
final long coverageMinX = coverage[0];
final long coverageMaxX = coverage[2];
final long coverageMinY = coverage[1];
final long coverageMaxY = coverage[3];
final long coverageTilesWide = 1 + coverageMaxX - coverageMinX;
final long coverageTilesHigh = 1 + coverageMaxY - coverageMinY;
final int tilesPerPageX = calculateNumTilesPerPage(coverageTilesWide);
final int tilesPerPageY = calculateNumTilesPerPage(coverageTilesHigh);
final int numPagesX = (int) Math.ceil((double) coverageTilesWide / tilesPerPageX);
final int numPagesY = (int) Math.ceil((double) coverageTilesHigh / tilesPerPageY);
PageLevelInfo pli = new PageLevelInfo(numPagesX, numPagesY, tilesPerPageX, tilesPerPageY,
coverageMinX, coverageMinY, coverageMaxX, coverageMaxY);
// if (log.isDebugEnabled()) {
// log.debug("Coverage: " + Arrays.toString(coverage) + " (" + coverageTilesWide + "x"
// + coverageTilesHigh + ") tiles. Tiles perpage: " + tilesPerPageX + " x "
// + tilesPerPageY + " for a total of " + numPagesX + " x " + numPagesY
// + " pages and " + (tilesPerPageX * (long) tilesPerPageY) + " tiles per page");
// }
return pli;
}
/**
* Calculates the number of tiles per page for a gridset coverage over one of its axes (this
* method doesn't care which of them, either X or Y), client code knows which zoom level it
* belongs too.
* <p>
* The number of pages for each zoom level is different, and currently calculated on a
* logarithmic basis.
* </p>
*
* @param numTilesInAxis
* number of tiles in either the x or y axis for the {@link GridSubset} coverage at
* one of its zoom levels
* @return the number of pages corresponding to {@code numTilesInAxis}
*/
private int calculateNumTilesPerPage(long numTilesInAxis) {
/*
* Found log base 1.3 gives a pretty decent progression of number of pages for zoom level
*/
final double logBase = 1.1;
// Calculate log base <logBase>
final double log = (Math.log(numTilesInAxis) / Math.log(logBase));
// log(1) == 0, so be careful
final int numTilesPerPage = numTilesInAxis == 1 ? 1 : (int) Math
.ceil((numTilesInAxis / log));
return numTilesPerPage;
}
public int[] pageIndexForTile(long x, long y, int level, int[] pageIndexTarget) {
Assert.notNull(pageIndexTarget);
Assert.isTrue(pageIndexTarget.length >= 3);
PageLevelInfo levelInfo = getPageInfo(level);
final int tilePageX = (int) ((x - levelInfo.coverageMinX) / levelInfo.tilesPerPageX);
final int tilePageY = (int) ((y - levelInfo.coverageMinY) / levelInfo.tilesPerPageY);
pageIndexTarget[0] = tilePageX;
pageIndexTarget[1] = tilePageY;
pageIndexTarget[2] = level;
return pageIndexTarget;
}
/**
* Returns a grid subset coverage range suitable for {@link TileRange}
*
* @param page
* @return {@code [minTileX, minTileY, maxTileX, maxTileY, zoomlevel]}
*/
public long[][] toGridCoverage(int pageX, int pageY, int level) {
final PageLevelInfo pageLevelInfo = getPageInfo(level);
final long coverageMinX = pageLevelInfo.coverageMinX;
final long coverageMinY = pageLevelInfo.coverageMinY;
final int numTilesPerPageX = pageLevelInfo.tilesPerPageX;
final int numTilesPerPageY = pageLevelInfo.tilesPerPageY;
long minTileX = coverageMinX + (long) pageX * numTilesPerPageX;
long minTileY = coverageMinY + (long) pageY * numTilesPerPageY;
long maxTileX = minTileX + numTilesPerPageX - 1;// these are indexes, so rest one
long maxTileY = minTileY + numTilesPerPageY - 1;// same thing
long[] pageCoverage = { minTileX, minTileY, maxTileX, maxTileY, level };
final int numLevels = gridSubsetCoverages.size();
long[][] allLevelsCoverage = new long[numLevels][];
allLevelsCoverage[level] = pageCoverage;
return allLevelsCoverage;
}
public int getTilesPerPageX(int level) {
return getPageInfo(level).tilesPerPageX;
}
public int getTilesPerPageY(int level) {
return getPageInfo(level).tilesPerPageY;
}
public int getPagesPerLevelX(int level) {
return getPageInfo(level).pagesX;
}
public int getPagesPerLevelY(int level) {
return getPageInfo(level).pagesY;
}
}