/** * 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.Arrays; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.builder.ReflectionToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; public class GridSet { private String name; private SRS srs; private int tileWidth; private int tileHeight; /** * Whether the y-coordinate of {@link #tileOrigin()} is at the top (true) or at the bottom * (false) */ protected boolean yBaseToggle = false; /** * By default the coordinates are {x,y}, this flag reverses the output for WMTS getcapabilities */ private boolean yCoordinateFirst = false; private boolean scaleWarning = false; private double metersPerUnit; private double pixelSize; private BoundingBox originalExtent; private Grid[] gridLevels; private String description; /** * {@code true} if the resolutions are preserved and the scaleDenominators calculated, * {@code false} if the resolutions are calculated based on the sacale denominators. */ private boolean resolutionsPreserved; protected GridSet() { // Blank } /** * @return the originalExtent */ public BoundingBox getOriginalExtent() { return originalExtent; } /** * @param originalExtent * the originalExtent to set */ void setOriginalExtent(BoundingBox originalExtent) { this.originalExtent = originalExtent; } /** * @return {@code true} if the resolutions are preserved and the scaleDenominators calculated, * {@code false} if the resolutions are calculated based on the sacale denominators. */ public boolean isResolutionsPreserved() { return resolutionsPreserved; } /** * @param resolutionsPreserved * {@code true} if the resolutions are preserved and the scaleDenominators * calculated, {@code false} if the resolutions are calculated based on the sacale * denominators. */ void setResolutionsPreserved(boolean resolutionsPreserved) { this.resolutionsPreserved = resolutionsPreserved; } protected BoundingBox boundsFromIndex(long[] tileIndex) { final int tileZ = (int) tileIndex[2]; Grid grid = getGrid(tileZ); final long tileX = tileIndex[0]; final long tileY; if (yBaseToggle) { tileY = tileIndex[1] - grid.getNumTilesHigh(); } else { tileY = tileIndex[1]; } double width = grid.getResolution() * getTileWidth(); double height = grid.getResolution() * getTileHeight(); final double[] tileOrigin = tileOrigin(); BoundingBox tileBounds = new BoundingBox(tileOrigin[0] + width * tileX, tileOrigin[1] + height * (tileY), tileOrigin[0] + width * (tileX + 1), tileOrigin[1] + height * (tileY + 1)); return tileBounds; } /** * 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 */ protected BoundingBox boundsFromRectangle(long[] rectangleExtent) { Grid grid = getGridLevels()[(int) rectangleExtent[4]]; double width = grid.getResolution() * getTileWidth(); double height = grid.getResolution() * getTileHeight(); long bottomY = rectangleExtent[1]; long topY = rectangleExtent[3]; if (yBaseToggle) { bottomY = bottomY - grid.getNumTilesHigh(); topY = topY - grid.getNumTilesHigh(); } double[] tileOrigin = tileOrigin(); double minx = tileOrigin[0] + width * rectangleExtent[0]; double miny = tileOrigin[1] + height * (bottomY); double maxx = tileOrigin[0] + width * (rectangleExtent[2] + 1); double maxy = tileOrigin[1] + height * (topY + 1); BoundingBox rectangleBounds = new BoundingBox(minx, miny, maxx, maxy); return rectangleBounds; } protected long[] closestIndex(BoundingBox tileBounds) throws GridMismatchException { double wRes = tileBounds.getWidth() / getTileWidth(); double bestError = Double.MAX_VALUE; int bestLevel = -1; double bestResolution = -1.0; for (int i = 0; i < getGridLevels().length; i++) { Grid grid = getGridLevels()[i]; double error = Math.abs(wRes - grid.getResolution()); if (error < bestError) { bestError = error; bestResolution = grid.getResolution(); bestLevel = i; } else { break; } } if (Math.abs(wRes - bestResolution) > (0.1 * wRes)) { throw new ResolutionMismatchException(wRes, bestResolution); } return closestIndex(bestLevel, tileBounds); } protected long[] closestIndex(int level, BoundingBox tileBounds) throws GridAlignmentMismatchException { Grid grid = getGridLevels()[level]; double width = grid.getResolution() * getTileWidth(); double height = grid.getResolution() * getTileHeight(); double x = (tileBounds.getMinX() - tileOrigin()[0]) / width; double y = (tileBounds.getMinY() - tileOrigin()[1]) / height; long posX = (long) Math.round(x); long posY = (long) Math.round(y); if (x - posX > 0.1 || y - posY > 0.1) { throw new GridAlignmentMismatchException(x, posX, y, posY); } if (yBaseToggle) { posY = posY + grid.getNumTilesHigh(); } long[] ret = { posX, posY, level }; return ret; } public long[] closestRectangle(BoundingBox rectangleBounds) { double rectWidth = rectangleBounds.getWidth(); double rectHeight = rectangleBounds.getHeight(); double bestError = Double.MAX_VALUE; int bestLevel = -1; // Now we loop over the resolutions until for (int i = 0; i < getGridLevels().length; i++) { Grid grid = getGridLevels()[i]; double countX = rectWidth / (grid.getResolution() * getTileWidth()); double countY = rectHeight / (grid.getResolution() * getTileHeight()); double error = Math.abs(countX - Math.round(countX)) + Math.abs(countY - Math.round(countY)); if (error < bestError) { bestError = error; bestLevel = i; } else if (error >= bestError) { break; } } return closestRectangle(bestLevel, rectangleBounds); } /** * Find the rectangle of tiles that most closely covers the given rectangle * @param level integer zoom level to consider tiles at * @param rectangeBounds rectangle to match * @return Array of long, the rectangle of tiles in tile coordinates: {minx, miny, maxx, maxy, level} */ protected long[] closestRectangle(int level, BoundingBox rectangeBounds) { Grid grid = getGridLevels()[level]; double width = grid.getResolution() * getTileWidth(); double height = grid.getResolution() * getTileHeight(); long minX = (long) Math.floor((rectangeBounds.getMinX() - tileOrigin()[0]) / width); long minY = (long) Math.floor((rectangeBounds.getMinY() - tileOrigin()[1]) / height); long maxX = (long) Math.ceil(((rectangeBounds.getMaxX() - tileOrigin()[0]) / width)); long maxY = (long) Math.ceil(((rectangeBounds.getMaxY() - tileOrigin()[1]) / height)); if (yBaseToggle) { minY = minY + grid.getNumTilesHigh(); maxY = maxY + grid.getNumTilesHigh(); } // We substract one, since that's the tile at that position long[] ret = { minX, minY, maxX - 1, maxY - 1, level }; return ret; } @Override public boolean equals(Object obj) { if (!(obj instanceof GridSet)) return false; GridSet other = (GridSet) obj; if (this == other) return true; boolean equals = ObjectUtils.equals(getSrs(), other.getSrs()) && ObjectUtils.equals(getName(), other.getName()) && ObjectUtils.equals(getDescription(), other.getDescription()) && ObjectUtils.equals(getTileWidth(), other.getTileWidth()) && ObjectUtils.equals(getTileHeight(), other.getTileHeight()) && ObjectUtils.equals(isTopLeftAligned(), other.isTopLeftAligned()) && ObjectUtils.equals(isyCoordinateFirst(), other.isyCoordinateFirst()) && ObjectUtils.equals(getOriginalExtent(), other.getOriginalExtent()) && Arrays.equals(getGridLevels(), other.getGridLevels()); return equals; } @Override public int hashCode() { int hashCode = HashCodeBuilder.reflectionHashCode(this); return hashCode; } public BoundingBox getBounds() { int i; long tilesWide, tilesHigh; for (i = (getGridLevels().length - 1); i > 0; i--) { tilesWide = getGridLevels()[i].getNumTilesWide(); tilesHigh = getGridLevels()[i].getNumTilesHigh(); if (tilesWide == 1 && tilesHigh == 0) { break; } } tilesWide = getGridLevels()[i].getNumTilesWide(); tilesHigh = getGridLevels()[i].getNumTilesHigh(); long[] ret = { 0, 0, tilesWide - 1, tilesHigh - 1, i }; return boundsFromRectangle(ret); } /** * Returns the top left corner of the grid in the order used by the coordinate system. (Bad * idea) * * Used for WMTS GetCapabilities * * @param gridIndex * @return */ public double[] getOrderedTopLeftCorner(int gridIndex) { // First we will find the x,y pair, then we'll flip it if necessary double[] leftTop = new double[2]; if (yBaseToggle) { leftTop[0] = tileOrigin()[0]; leftTop[1] = tileOrigin()[1]; } else { // We don't actually store the top coordinate, need to calculate it Grid grid = getGridLevels()[gridIndex]; double dTileHeight = getTileHeight(); double dGridExtent = grid.getNumTilesHigh(); double top = tileOrigin()[1] + dTileHeight * grid.getResolution() * dGridExtent; // Round off if we are within 0.5% of an integer value if (Math.abs(top - Math.round(top)) < (top / 200)) { top = Math.round(top); } leftTop[0] = tileOrigin()[0]; leftTop[1] = top; } // Y coordinate first? if (isyCoordinateFirst()) { double[] ret = { leftTop[1], leftTop[0] }; return ret; } return leftTop; } public String guessMapUnits() { if (113000 > getMetersPerUnit() && getMetersPerUnit() > 110000) { return "degrees"; } else if (1100 > getMetersPerUnit() && getMetersPerUnit() > 900) { return "kilometers"; } else if (1.1 > getMetersPerUnit() && getMetersPerUnit() > 0.9) { return "meters"; } else if (0.4 > getMetersPerUnit() && getMetersPerUnit() > 0.28) { return "feet"; } else if (0.03 > getMetersPerUnit() && getMetersPerUnit() > 0.02) { return "inches"; } else if (0.02 > getMetersPerUnit() && getMetersPerUnit() > 0.005) { return "centimeters"; } else if (0.002 > getMetersPerUnit() && getMetersPerUnit() > 0.0005) { return "millimeters"; } else { return "unknown"; } } public boolean isTopLeftAligned() { return this.yBaseToggle; } void setTopLeftAligned(boolean yBaseToggle) { this.yBaseToggle = yBaseToggle; } public int getNumLevels() { return gridLevels.length; } /** * @return the gridLevels * @deprecated use {@link #getGrid(int)} */ @Deprecated public Grid[] getGridLevels() { return gridLevels; } public Grid getGrid(final int zLevel) { return gridLevels[zLevel]; } /** * @param gridLevels * the gridLevels to set */ void setGridLevels(Grid[] gridLevels) { this.gridLevels = gridLevels; } /** * The base cordinates in x/y order, used to map tile indexes to coordinate bounding boxes. * These can either be top left or bottom left, so must be kept private. * <p> * This is a derived property of {@link #getOriginalExtent()} and {@link #isTopLeftAligned()}. * </p> */ public double[] tileOrigin() { BoundingBox extent = getOriginalExtent(); double[] tileOrigin = { extent.getMinX(), yBaseToggle ? extent.getMaxY() : extent.getMinY() }; return tileOrigin; } /** * @return the yCoordinateFirst */ public boolean isyCoordinateFirst() { return yCoordinateFirst; } /** * @param yCoordinateFirst * the yCoordinateFirst to set */ void setyCoordinateFirst(boolean yCoordinateFirst) { this.yCoordinateFirst = yCoordinateFirst; } /** * @return the scaleWarning */ public boolean isScaleWarning() { return scaleWarning; } /** * @param scaleWarning * the scaleWarning to set */ void setScaleWarning(boolean scaleWarning) { this.scaleWarning = scaleWarning; } /** * @return the metersPerUnit */ public double getMetersPerUnit() { return metersPerUnit; } /** * @param metersPerUnit * the metersPerUnit to set */ void setMetersPerUnit(double metersPerUnit) { this.metersPerUnit = metersPerUnit; } /** * @return the pixelSize */ public double getPixelSize() { return pixelSize; } /** * @param pixelSize * the pixelSize to set */ void setPixelSize(double pixelSize) { this.pixelSize = pixelSize; } /** * @return the name */ public String getName() { return name; } /** * @param name * the name to set */ void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } /** * @return the srs */ public SRS getSrs() { return srs; } /** * @param srs * the srs to set */ void setSrs(SRS srs) { this.srs = srs; } /** * @return the tileWidth */ public int getTileWidth() { return tileWidth; } /** * @param tileWidth * the tileWidth to set */ void setTileWidth(int tileWidth) { this.tileWidth = tileWidth; } /** * @return the tileHeight */ public int getTileHeight() { return tileHeight; } /** * @param tileHeight * the tileHeight to set */ void setTileHeight(int tileHeight) { this.tileHeight = tileHeight; } /** * Evaluates wheter this GridSet is different engouh from {@code another} so that if this * GridSet were replaced by {@code another} all layers referencing this GridSet should be * truncated. * <p> * The rule is, if any of the following properties differ: {@link #getBounds()}, * {@link #isTopLeftAligned()}, {@link #getTileHeight()}, {@link #getTileWidth()}, * {@link #getSrs()}, OR none of the previously mentiond properties differ and * {@link #getGrids()} are different, except if both the grids of {@code another} are a superset * of the grids of this gridset (i.e. they are all the same but {@code another} just has more * zoom levels}. * </p> * * @param oldGridSet * @param another * @return {@code true} if */ public boolean shouldTruncateIfChanged(final GridSet another) { boolean needsTruncate = !getBounds().equals(another.getBounds()); needsTruncate |= isTopLeftAligned() != another.isTopLeftAligned(); needsTruncate |= getTileWidth() != another.getTileWidth(); needsTruncate |= getTileHeight() != another.getTileHeight(); needsTruncate |= !getSrs().equals(another.getSrs()); if (needsTruncate) { return true; } // now check the zoom levels Grid[] myGrids = getGridLevels(); Grid[] otherGrids = another.getGridLevels(); if (myGrids.length > otherGrids.length) { return true; } for (int i = 0; i < myGrids.length; i++) { if (!myGrids[i].equals(otherGrids[i])) { return true; } } return false; } @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); } }