/* Copyright 2013 The jeo project. All rights reserved. * * Licensed 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. */ package io.jeo.tile; import static java.lang.Math.ceil; import static java.lang.Math.floor; import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.jeo.geom.Bounds; import io.jeo.util.Pair; import io.jeo.proj.Proj; import org.osgeo.proj4j.CoordinateReferenceSystem; /** * Represents a multi level structure of tiles. * <p> * * </p> * @author Justin Deoliveira, OpenGeo */ public class TilePyramid { /** * Creates a new builder. */ public static TilePyramidBuilder build() { return new TilePyramidBuilder(); } /** * Tile coordinate system origin. */ public static enum Origin { BOTTOM_LEFT, TOP_LEFT, BOTTOM_RIGHT, TOP_RIGHT; } CoordinateReferenceSystem crs = Proj.EPSG_4326; Bounds bounds = new Bounds(-180,180,-90,90); List<TileGrid> grids = new ArrayList<TileGrid>(); Origin origin = Origin.BOTTOM_LEFT; Integer tileWidth = 256; Integer tileHeight = 256; /** * The grids making up the pyramid, sorted by ascending z/zoom value. */ public List<TileGrid> grids() { return grids; } /** * The spatial extent of the tile pyramid. */ public Bounds bounds() { return bounds; } /** * Sets the spatial extent of the tile pyramid. */ public TilePyramid bounds(Bounds bounds) { this.bounds = bounds; return this; } /** * The coordinate reference system of the tile pyramid. */ public CoordinateReferenceSystem crs() { return crs; } /** * Sets the coordinate reference system of the tile pyramid. */ public TilePyramid crs(CoordinateReferenceSystem crs) { this.crs = crs; return this; } /** * The origin of tile coordinate system. * <p> * This value dictates how tile coordinates (x,y) should be interpreted. The defaule value for * this property is {@link Origin#BOTTOM_LEFT}. * </p> */ public Origin origin() { return origin; } /** * Sets origin of tile coordinate system. */ public TilePyramid origin(Origin origin) { this.origin = origin; return this; } /** * The width in pixels of tiles in the pyramid. * <p> * The default value for this property is 256. * </p> */ public Integer tileWidth() { return tileWidth; } /** * Sets the width in pixels of tiles in the pyramid. */ public TilePyramid tileWidth(Integer tileWidth) { this.tileWidth = tileWidth; return this; } /** * The height in pixels of tiles in the pyramid. * <p> * The default value for this property is 256. * </p> */ public Integer tileHeight() { return tileHeight; } /** * Sets the height in pixels of tiles in the pyramid. */ public TilePyramid tileHeight(Integer tileHeight) { this.tileHeight = tileHeight; return this; } /** * Returns the tile grid at the specified zoom level. * * @param z The zoom level * * @return The tile grid, or <code>null</code> if no such grid exists for the specified zoom. */ public TileGrid grid(int z) { for (TileGrid grid : grids) { if (grid.z() == z) { return grid; } } return null; } /** * Returns the spatial extent of the specified tile. * <p> * The tile z value must match a zoom level defined by the pyramid. The tile x/y need not fall * within the bounds of the pyramid. * </p> * @param t The tile. * * @return The spatial extent of the tile. * * @throws IllegalArgumentException If the tile has a z value not defined by the pyramid. */ public Bounds bounds(Tile t) { TileGrid grid = grid(t.z()); if (grid == null) { throw new IllegalArgumentException(String.format(Locale.ROOT,"no grid at zoom %d", t.z())); } int w = grid.width(); int h = grid.height(); Bounds b = bounds; double dx = b.getWidth() / ((double)w); double dy = b.getHeight() / ((double)h); double x,y; switch(origin) { case BOTTOM_LEFT: case TOP_LEFT: x = b.getMinX() + dx*t.x(); break; default: x = b.getMinX() + dx*(w - t.x()); } switch(origin) { case BOTTOM_LEFT: case BOTTOM_RIGHT: y = b.getMinY() + dy*t.y(); break; default: y = b.getMinY() + dy*(h - t.y()); } return new Bounds(x, x+dx, y, y+dy); } /** * Realigns a tile with the pyramid. * * @param t The tile to rebase. * @param o The original origin of the tile. * * @return A newly realigned tile, or null if the t did not map to a grid in the pyramid. */ public Tile realign(Tile t, Origin o) { TileGrid grid = grid(t.z()); if (grid == null) { return null; } int w = grid.width(); int h = grid.height(); Tile u = new Tile(t); switch(origin) { case BOTTOM_LEFT: if (o == Origin.BOTTOM_RIGHT || o == Origin.TOP_RIGHT) { u.x(w - (t.x() + 1)); } if (o == Origin.TOP_LEFT || o == Origin.TOP_RIGHT) { u.y(h - (t.y() + 1)); } break; case BOTTOM_RIGHT: if (o == Origin.BOTTOM_LEFT || o == Origin.TOP_LEFT) { u.x(w - (t.x() + 1)); } if (o == Origin.TOP_LEFT || o == Origin.TOP_RIGHT) { u.y(h - (t.y() + 1)); } break; case TOP_LEFT: if (o == Origin.BOTTOM_RIGHT || o == Origin.TOP_RIGHT) { u.x(w - (t.x() + 1)); } if (o == Origin.BOTTOM_LEFT || o == Origin.BOTTOM_RIGHT) { u.y(h - (t.y() + 1)); } break; case TOP_RIGHT: if (o == Origin.BOTTOM_LEFT || o == Origin.TOP_LEFT) { u.x(w - (t.x() + 1)); } if (o == Origin.BOTTOM_LEFT || o == Origin.BOTTOM_RIGHT) { u.y(h - (t.y() + 1)); } break; } return u; } /** * Creates a tile cover for the specified bounds using the specified width, height to * determine the appropriate tile resolution. */ public TileCover cover(Bounds e, int width, int height) { Pair<Double,Double> res = res(e, width, height); return cover(e, res.first, res.second); } /** * Creates a tile cover for the specified bounds at the specified resolutions. */ public TileCover cover(Bounds e, double resx, double resy) { return cover(e, match(e, resx, resy)); } /** * Creates a tile cover for the specified bounds at the specified zoom level. */ public TileCover cover(Bounds e, int z) { TileGrid grid = grid(z); return grid != null ? cover(e, grid) : null; } /** * Creates a tile cover for the specified bounds at the tile grid. */ public TileCover cover(Bounds e, TileGrid grid) { int[] cov = cov(e, grid); if (cov == null) { return null; } return new TileCover(grid, cov[0], cov[2], cov[1], cov[3]); } Pair<Double,Double> res(Bounds bbox, int width, int height) { double resx = bbox.getWidth() / ((double)width); double resy = bbox.getHeight() / ((double)height); return new Pair<Double,Double>(resx, resy); } TileGrid match(Bounds bbox, double resx, double resy) { TileGrid best = null; double score = Double.MAX_VALUE; for (TileGrid grid : grids) { double res = Math.abs(resx - grid.xres()) + Math.abs(resy - grid.yres()); if (res < score) { score = res; best = grid; } } if (best == null) { return null; } return best; } int[] cov(Bounds bbox, TileGrid grid) { int x1 = (int) floor((((bbox.getMinX() - bounds.getMinX()) / bounds.getWidth()) * grid.width())); int x2 = (int) ceil(((bbox.getMaxX() - bounds.getMinX()) / bounds.getWidth()) * grid.width())-1; int y1 = (int) floor(((bbox.getMinY() - bounds.getMinY()) / bounds.getHeight()) * grid.height()); int y2 = (int) ceil(((bbox.getMaxY() - bounds.getMinY()) / bounds.getHeight()) * grid.height())-1; return new int[]{x1, x2, y1, y2}; } }