/** * 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 Arne Kepp, OpenGeo, Copyright 2009 */ package org.geowebcache.grid; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; import org.geowebcache.GeoWebCacheException; import org.geowebcache.util.ServletUtils; /** * A GridSubSet is a GridSet + a coverage area */ public class GridSubset { private final GridSet gridSet; // {level}{minx,miny,maxx,maxy,z} private final Map<Integer, GridCoverage> gridCoverageLevels; private final boolean fullGridSetCoverage; private final BoundingBox subSetExtent; private final Integer minCachedZoom; private final Integer maxCachedZoom; protected GridSubset(GridSet gridSet, Map<Integer, GridCoverage> coverages, BoundingBox originalExtent, boolean fullCoverage) { this(gridSet, coverages, originalExtent, fullCoverage, null, null); } public GridSubset(GridSet gridSet, Map<Integer, GridCoverage> coverages, BoundingBox originalExtent, boolean fullCoverage, Integer minCachedZoom, Integer maxCachedZoom) { this.gridSet = gridSet; this.gridCoverageLevels = coverages; this.subSetExtent = originalExtent; this.fullGridSetCoverage = fullCoverage; this.minCachedZoom = minCachedZoom; this.maxCachedZoom = maxCachedZoom; } public BoundingBox boundsFromIndex(long[] tileIndex) { return gridSet.boundsFromIndex(tileIndex); } /** * Finds the spatial bounding box of a rectangular group of tiles. * @param rectangleExtent the rectangle of tiles. {minx, miny, maxx, maxy} in tile coordinates * @return the spatial bounding box in the coordinates of the SRS used by the GridSet */ public BoundingBox boundsFromRectangle(long[] rectangleExtent) { return gridSet.boundsFromRectangle(rectangleExtent); } public long[] closestIndex(BoundingBox tileBounds) throws GridMismatchException { return gridSet.closestIndex(tileBounds); } public long[] closestRectangle(BoundingBox rectangleBounds) { return gridSet.closestRectangle(rectangleBounds); } /** * Indicates whether this gridsubset coverage contains the given tile * * @param index * the tile index to check for coverage inclusion * @return {@code true} if {@code index} is inside this grid subset's coverage, {@code false} * otherwise */ public boolean covers(long[] index) { final int level = (int) index[2]; final long[] coverage = getCoverage(level); if (coverage == null) { return false; } if (index[0] >= coverage[0] && index[0] <= coverage[2] && index[1] >= coverage[1] && index[1] <= coverage[3]) { return true; } return false; } public void checkCoverage(long[] index) throws OutsideCoverageException { if (covers(index)) { return; } if (index[2] < getZoomStart() || index[2] > getZoomStop()) { throw new OutsideCoverageException(index, getZoomStart(), getZoomStop()); } long[] coverage = getCoverage((int) index[2]); throw new OutsideCoverageException(index, coverage); } public void checkTileDimensions(int width, int height) throws TileDimensionsMismatchException { if (width != gridSet.getTileWidth() || height != gridSet.getTileHeight()) { throw new TileDimensionsMismatchException(width, height, gridSet.getTileWidth(), gridSet.getTileWidth()); } } public long[][] expandToMetaFactors(final long[][] coverages, final int[] metaFactors) { long[][] ret = ServletUtils.arrayDeepCopy(coverages); for (long[] cov : ret) { final int z = (int) cov[4]; final Grid grid = this.gridSet.getGrid(z); final long numTilesWide = grid.getNumTilesWide(); final long numTilesHigh = grid.getNumTilesHigh(); cov[0] = cov[0] - (cov[0] % metaFactors[0]); cov[1] = cov[1] - (cov[1] % metaFactors[1]); cov[2] = cov[2] - (cov[2] % metaFactors[0]) + (metaFactors[0] - 1); if (cov[2] > numTilesWide) { cov[2] = numTilesWide; } cov[3] = cov[3] - (cov[3] % metaFactors[1]) + (metaFactors[1] - 1); if (cov[3] > numTilesHigh) { cov[3] = numTilesHigh; } } return ret; } public long[] getCoverage(int level) { GridCoverage gridCoverage = gridCoverageLevels.get(Integer.valueOf(level)); if (gridCoverage == null) { return null; } long[] coverage = gridCoverage.coverage.clone(); return coverage; } public long[][] getCoverages() { long[][] ret = new long[gridCoverageLevels.size()][5]; final int zoomStart = getZoomStart(); final int zoomStop = getZoomStop(); for (int level = zoomStart, i = 0; level <= zoomStop; level++, i++) { long[] cov = getCoverage(level); ret[i] = cov; } return ret; } /** * Convert pixel size to dots per inch * * @return */ public double getDotsPerInch() { return (0.0254 / this.gridSet.getPixelSize()); } public BoundingBox getCoverageBounds(int level) { long[] coverage = getCoverage(level); return gridSet.boundsFromRectangle(coverage); } // Returns the tightest rectangle that covers the data public long[] getCoverageBestFit() { int level; long[] cov = null; final int zoomStart = getZoomStart(); final int zoomStop = getZoomStop(); for (level = zoomStop; level > zoomStart; level--) { cov = getCoverage(level); if (cov[0] == cov[2] && cov[1] == cov[3]) { break; } } cov = getCoverage(level); return cov; } public BoundingBox getCoverageBestFitBounds() { return boundsFromRectangle(getCoverageBestFit()); } public long[] getCoverageIntersection(long[] reqRectangle) { final int level = (int) reqRectangle[4]; GridCoverage gridCov = gridCoverageLevels.get(Integer.valueOf(level)); return gridCov.getIntersection(reqRectangle); } public long[][] getCoverageIntersections(BoundingBox reqBounds) { final int zoomStart = getZoomStart(); final int zoomStop = getZoomStop(); long[][] ret = new long[1 + zoomStop - zoomStart][5]; for (int level = zoomStart; level <= zoomStop; level++) { ret[level - zoomStart] = getCoverageIntersection(level, reqBounds); } return ret; } /** * Find the area that covers the given rectangle with tiles from the subset. * @param level integer zoom level at which to consider the tiles * @param reqBounds BoundingBox to try to cover. * @return Array of long, the rectangle in tile coordinates, {minx, miny, maxx, maxy} */ public long[] getCoverageIntersection(int level, BoundingBox reqBounds) { long[] reqRectangle = gridSet.closestRectangle(level, reqBounds); GridCoverage gridCoverage = gridCoverageLevels.get(Integer.valueOf(level)); return gridCoverage.getIntersection(reqRectangle); } public long getGridIndex(String gridId) { final int zoomStart = getZoomStart(); final int zoomStop = getZoomStop(); for (int index = zoomStart; index <= zoomStop; index++) { if (gridSet.getGrid(index).getName().equals(gridId)) { return index; } } return -1L; } public String[] getGridNames() { List<String> ret = new ArrayList<String>(gridCoverageLevels.size()); final int zoomStart = getZoomStart(); final int zoomStop = getZoomStop(); for (int i = zoomStart; i <= zoomStop; i++) { ret.add(gridSet.getGrid(i).getName()); } return ret.toArray(new String[ret.size()]); } public GridSet getGridSet() { return gridSet; } public BoundingBox getGridSetBounds() { return gridSet.getBounds(); } public long getNumTilesWide(int zoomLevel) { return gridSet.getGridLevels()[zoomLevel].getNumTilesWide(); } public long getNumTilesHigh(int zoomLevel) { return gridSet.getGridLevels()[zoomLevel].getNumTilesHigh(); } public String getName() { return gridSet.getName(); } public BoundingBox getOriginalExtent() { if (this.subSetExtent == null) { return gridSet.getOriginalExtent(); } return this.subSetExtent; } public double[] getResolutions() { double[] ret = new double[gridCoverageLevels.size()]; final int zoomStart = getZoomStart(); final int zoomStop = getZoomStop(); final Grid[] gridLevels = gridSet.getGridLevels(); for (int z = zoomStart, i = 0; z <= zoomStop; z++, i++) { Grid grid = gridLevels[z]; ret[i] = grid.getResolution(); } return ret; } // TODO: this is specific to KML service, move it somewhere on the kml module public long[][] getSubGrid(long[] gridLoc) throws GeoWebCacheException { final int firstLevel = getZoomStart(); final int zoomStop = getZoomStop(); int idx = (int) gridLoc[2]; long[][] ret = { { -1, -1, -1 }, { -1, -1, -1 }, { -1, -1, -1 }, { -1, -1, -1 } }; if ((idx - firstLevel + 1) <= zoomStop) { // Check whether this grid is doubling double resolutionCheck = gridSet.getGridLevels()[idx].getResolution() / 2 - gridSet.getGridLevels()[idx + 1].getResolution(); if (Math.abs(resolutionCheck) > gridSet.getGridLevels()[idx + 1].getResolution() * 0.025) { throw new GeoWebCacheException( "The resolution is not decreasing by a factor of two for " + this.getName()); } else { long[] coverage = getCoverage(idx + 1); long baseX = gridLoc[0] * 2; long baseY = gridLoc[1] * 2; long baseZ = idx + 1; long[] xOffset = { 0, 1, 0, 1 }; long[] yOffset = { 0, 0, 1, 1 }; for (int i = 0; i < 4; i++) { if (baseX + xOffset[i] >= coverage[0] && baseX + xOffset[i] <= coverage[2] && baseY + yOffset[i] >= coverage[1] && baseY + yOffset[i] <= coverage[3]) { ret[i][0] = baseX + xOffset[i]; ret[i][1] = baseY + yOffset[i]; ret[i][2] = baseZ; } } } } return ret; } /** * @return whether the scale is based on CRS84, even though it may not be */ public boolean getScaleWarning() { return gridSet.isScaleWarning(); } public SRS getSRS() { return gridSet.getSrs(); } public int getTileHeight() { return gridSet.getTileHeight(); } public int getTileWidth() { return gridSet.getTileWidth(); } /** * WMTS is indexed from top left hand corner. We will still return {minx,miny,maxx,maxy}, but * note that the y positions have been reversed * * @return */ // TODO: this is specific to WMTS, move it somewhere on the wmts module // TODO: Does this need to be public? public long[][] getWMTSCoverages() { long[][] ret = new long[gridCoverageLevels.size()][4]; final int zoomStop = getZoomStop(); int zoomStart = getZoomStart(); for (int i = zoomStart; i <= zoomStop; i++) { Grid grid = gridSet.getGrid(i); long[] coverage = getCoverage(i); /* * Both internal and WMTS coordinates start at 0 and run to 1 less than the number of * tiles. In internal coordinates, row 0 is the bottommost, and in WMTS it's the * topmost. So subtract the row number from 1 less than the height to convert. */ long bottomRow = grid.getNumTilesHigh()-1; // The WMST row number for the bottom row long[] cur = { coverage[0], // minX bottomRow - coverage[3], // minY coverage[2], // maxX bottomRow - coverage[1] // maxY }; ret[i - zoomStart] = cur; } return ret; } public int getZoomStart() { Integer firstLevel = Collections.min(gridCoverageLevels.keySet()); return firstLevel.intValue(); } public int getZoomStop() { Integer maxLevel = Collections.max(gridCoverageLevels.keySet()); return maxLevel.intValue(); } public Integer getMinCachedZoom() { return minCachedZoom; } public Integer getMaxCachedZoom() { return maxCachedZoom; } /** * Whether the Grid Subset equals or exceeds the extent of the Grid Set * * @return */ public boolean fullGridSetCoverage() { return fullGridSetCoverage; } public boolean shouldCacheAtZoom(long zoom) { boolean shouldCache = true; if (minCachedZoom != null) { shouldCache = zoom >= minCachedZoom; } if (shouldCache && maxCachedZoom != null) { shouldCache = zoom <= maxCachedZoom; } return shouldCache; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } }