/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2015, Open Source Geospatial Foundation (OSGeo) * (C) 2004-2010, Refractions Research Inc. * * This library 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; * version 2.1 of the License. * * This library 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. */ package org.geotools.tile; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.tile.impl.ScaleZoomLevelMatcher; import org.geotools.tile.impl.ZoomLevel; import org.geotools.util.ObjectCache; import org.geotools.util.ObjectCaches; import org.geotools.util.logging.Logging; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; /** * A TileService represent the class of objects that serve map tiles. * TileServices must at least have a name and a base URL. * * @author to.srwn * @author Ugo Taddei * @since 12 * @source $URL: * http://svn.osgeo.org/geotools/trunk/modules/unsupported/tile-client * /src/main/java/org/geotools/tile/TileService.java $ */ public abstract class TileService { private static final Logger LOGGER = Logging.getLogger(TileService.class .getPackage().getName()); /** * This WeakHashMap acts as a memory cache. Because we are using * SoftReference, we won't run out of Memory, the GC will free space. **/ private ObjectCache tiles = ObjectCaches.create("soft", 50); //$NON-NLS-1$ private String baseURL; private String name; /** * Create a new TileService with a name and a base URL * * @param name the name. Cannot be null. * @param baseURL the base URL. This is a string representing the common * part of the URL for all this service's tiles. Cannot be null. Note * that this constructor doesn't ensure that the URL is well-formed. */ protected TileService(String name, String baseURL) { setName(name); setBaseURL(baseURL); } private void setBaseURL(String baseURL) { if (baseURL == null || baseURL.isEmpty()) { throw new IllegalArgumentException("Base URL cannot be null"); } this.baseURL = baseURL; } private void setName(String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Name cannot be null"); } this.name = name; } public String getName() { return name; } public int getTileWidth() { return 256; } public int getTileHeight() { return 256; } /** * Returns the prefix of an tile-url, e.g.: http://tile.openstreetmap.org/ * * @return */ public String getBaseUrl() { return this.baseURL; } /** * The CRS that is used when the extent is cut in tiles. * * @return */ public CoordinateReferenceSystem getTileCrs() { return DefaultGeographicCRS.WGS84; } /** * Translates the map scale into a zoom-level for the map services. The * scale-factor (0-100) decides whether the tiles will be scaled down (100) * or scaled up (0). * * @param renderJob Contains all the needed information * @param scaleFactor Scale-factor (0-100) * @return Zoom-level */ public int getZoomLevelFromMapScale(ScaleZoomLevelMatcher zoomLevelMatcher, int scaleFactor) { // fallback scale-list double[] scaleList = getScaleList(); // during the calculations this list caches already calculated scales double[] tempScaleList = new double[scaleList.length]; Arrays.fill(tempScaleList, Double.NaN); assert (scaleList != null && scaleList.length > 0); int zoomLevel = zoomLevelMatcher.getZoomLevelFromScale(this, tempScaleList); // Now apply the scale-factor if (zoomLevel == 0) { return zoomLevel; } else { int upperScaleIndex = zoomLevel - 1; int lowerScaleIndex = zoomLevel; double deltaScale = tempScaleList[upperScaleIndex] - tempScaleList[lowerScaleIndex]; double rangeScale = (scaleFactor / 100d) * deltaScale; double limitScale = tempScaleList[lowerScaleIndex] + rangeScale; if (zoomLevelMatcher.getScale() > limitScale) { return upperScaleIndex; } else { return lowerScaleIndex; } } } /** * Returns the zoom-level that should be used to fetch the tiles. * * @param scale * @param scaleFactor * @param useRecommended always use the calculated zoom-level, do not use * the one the user selected * @return */ public int getZoomLevelToUse(ScaleZoomLevelMatcher zoomLevelMatcher, int scaleFactor, boolean useRecommended) { if (useRecommended) { return getZoomLevelFromMapScale(zoomLevelMatcher, scaleFactor); } boolean selectionAutomatic = true; int zoomLevel = -1; // check if the zoom-level is valid if (!selectionAutomatic && ((zoomLevel >= getMinZoomLevel()) && (zoomLevel <= getMaxZoomLevel()))) { // the zoom-level from the properties is valid, so let's take it return zoomLevel; } else { // No valid property values or automatic selection of the zoom-level return getZoomLevelFromMapScale(zoomLevelMatcher, scaleFactor); } } /** * Returns the lowest zoom-level number from the scaleList. * * @param scaleList * @return */ public int getMinZoomLevel() { double[] scaleList = getScaleList(); int minZoomLevel = 0; while (Double.isNaN(scaleList[minZoomLevel]) && (minZoomLevel < scaleList.length)) { minZoomLevel++; } return minZoomLevel; } /** * Returns the highest zoom-level number from the scaleList. * * @param scaleList * @return */ public int getMaxZoomLevel() { double[] scaleList = getScaleList(); int maxZoomLevel = scaleList.length - 1; while (Double.isNaN(scaleList[maxZoomLevel]) && (maxZoomLevel >= 0)) { maxZoomLevel--; } return maxZoomLevel; } public Set<Tile> findTilesInExtent(ReferencedEnvelope _mapExtent, int scaleFactor, boolean recommendedZoomLevel, int maxNumberOfTiles) { ReferencedEnvelope mapExtent = createSafeEnvelopeInWGS84(_mapExtent); ReferencedEnvelope extent = normalizeExtent(mapExtent); // only continue, if we have tiles that cover the requested extent if (!extent.intersects((Envelope) getBounds())) { return Collections.emptySet(); } TileFactory tileFactory = getTileFactory(); // TODO CRS ScaleZoomLevelMatcher zoomLevelMatcher = null; try { zoomLevelMatcher = new ScaleZoomLevelMatcher(getTileCrs(), getProjectedTileCrs(), CRS.findMathTransform(getTileCrs(), getProjectedTileCrs()), CRS.findMathTransform( getProjectedTileCrs(), getTileCrs()), mapExtent, mapExtent, scaleFactor); } catch (FactoryException e) { throw new RuntimeException(e); } // TODO understand the minus 1 below int zoomLevelA = getZoomLevelToUse(zoomLevelMatcher, scaleFactor, recommendedZoomLevel) - 1; ZoomLevel zoomLevel = tileFactory.getZoomLevel(zoomLevelA, this); long maxNumberOfTilesForZoomLevel = zoomLevel.getMaxTileNumber(); // Map<String, Tile> tileList = new HashMap<String, Tile>(); Set<Tile> tileList = new HashSet<Tile>(100); // Let's get the first tile which covers the upper-left corner Tile firstTile = tileFactory.findTileAtCoordinate(extent.getMinX(), extent.getMaxY(), zoomLevel, this); addTileToCache(firstTile); tileList.add(firstTile); Tile firstTileOfRow = firstTile; Tile movingTile = firstTile; // Loop column do { // Loop row do { // get the next tile right of this one // Tile rightNeighbour = movingTile.getRightNeighbour(); Tile rightNeighbour = tileFactory.findRightNeighbour( movingTile, this);// movingTile.getRightNeighbour(); // Check if the new tile is still part of the extent and // that we don't have the first tile again if (extent.intersects((Envelope) rightNeighbour.getExtent()) && !firstTileOfRow.equals(rightNeighbour)) { // System.out.printf("N: %s %s", rightNeighbour.getId(), // addTileToList(rightNeighbour)); addTileToCache(rightNeighbour); tileList.add(rightNeighbour); movingTile = rightNeighbour; } else { break; } if (tileList.size() > maxNumberOfTiles) { LOGGER.warning("Reached tile limit of " + maxNumberOfTiles + ". Returning an empty collection."); return Collections.emptySet(); } } while (tileList.size() < maxNumberOfTilesForZoomLevel); // get the next tile under the first one of the row // Tile lowerNeighbour = firstTileOfRow.getLowerNeighbour(); Tile lowerNeighbour = tileFactory.findLowerNeighbour( firstTileOfRow, this); // Check if the new tile is still part of the extent if (extent.intersects((Envelope) lowerNeighbour.getExtent()) && !firstTile.equals(lowerNeighbour)) { // System.out.printf("N: %s %s", lowerNeighbour.getId(), // addTileToList(lowerNeighbour)); addTileToCache(lowerNeighbour); tileList.add(lowerNeighbour); firstTileOfRow = movingTile = lowerNeighbour; } else { break; } } while (tileList.size() < maxNumberOfTilesForZoomLevel); return tileList; } private boolean listContainsTile(String tileId) { return !(tiles.peek(tileId) == null || tiles.get(tileId) == null); } private Tile addTileToCache(Tile tile) { if (listContainsTile(tile.getId())) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.fine("Tile already in cache: " + tile.getId()); } return (Tile) tiles.get(tile.getId()); } else { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.fine("Tile added to cache: " + tile.getId()); } tiles.put(tile.getId(), tile); return tile; } } /** * Returns a list that represents a mapping between zoom-levels and map * scale. Array index: zoom-level Value at index: map scale High zoom-level * -> more detailed map Low zoom-level -> less detailed map * * @return mapping between zoom-levels and map scale */ public abstract double[] getScaleList(); public abstract ReferencedEnvelope getBounds(); /** * The projection the tiles are drawn in. * * @return */ public abstract CoordinateReferenceSystem getProjectedTileCrs(); /** * Returns the TileFactory which is used to call the method * getTileFromCoordinate(). */ public abstract TileFactory getTileFactory(); public static final ReferencedEnvelope createSafeEnvelopeInWGS84( ReferencedEnvelope _mapExtent) { try { return _mapExtent.transform(DefaultGeographicCRS.WGS84, true); } catch (TransformException e) { throw new RuntimeException(e); } catch (FactoryException e) { throw new RuntimeException(e); } } /** * The extent from the viewport may look like this: MaxY: 110° (=-70°) MinY: * -110° MaxX: 180° MinX: -180° But cutExtentIntoTiles(..) requires an * extent that looks like this: MaxY: 85° (or 90°) MinY: -85° (or -90°) * MaxX: 180° MinX: -180° * * @param envelope * @return */ private ReferencedEnvelope normalizeExtent(ReferencedEnvelope envelope) { ReferencedEnvelope bounds = getBounds(); if (envelope.getMaxY() > bounds.getMaxY() || envelope.getMinY() < bounds.getMinY() || envelope.getMaxX() > bounds.getMaxX() || envelope.getMinX() < bounds.getMinX()) { double maxY = (envelope.getMaxY() > bounds.getMaxY()) ? bounds .getMaxY() : envelope.getMaxY(); double minY = (envelope.getMinY() < bounds.getMinY()) ? bounds .getMinY() : envelope.getMinY(); double maxX = (envelope.getMaxX() > bounds.getMaxX()) ? bounds .getMaxX() : envelope.getMaxX(); double minX = (envelope.getMinX() < bounds.getMinX()) ? bounds .getMinX() : envelope.getMinX(); ReferencedEnvelope newEnvelope = new ReferencedEnvelope(minX, maxX, minY, maxY, envelope.getCoordinateReferenceSystem()); return newEnvelope; } return envelope; } // endregion public String toString() { return getName(); } }