/** * Given a tiled request, builds a key that can be used to access the cache looking for a specific * meta-tile, and also as a synchronization tool to avoid multiple requests to trigger parallel * computation of the same meta-tile * * @param request * @return */ public MetaTileKey getMetaTileKey(GetMapRequest request) { String mapDefinition = buildMapDefinition(request.getHttpServletRequest()); Envelope bbox = request.getBbox(); Point2D origin = request.getTilesOrigin(); MapKey mapKey = new MapKey(mapDefinition, normalize(bbox.getWidth() / request.getWidth()), origin); Point tileCoords = getTileCoordinates(bbox, origin); Point metaTileCoords = getMetaTileCoordinates(tileCoords); Envelope metaTileEnvelope = getMetaTileEnvelope(request, tileCoords, metaTileCoords); MetaTileKey key = new MetaTileKey(mapKey, metaTileCoords, metaTileEnvelope); // since this will be used for thread synchronization, we have to make // sure two thread asking for the same meta tile will get the same key // object return (MetaTileKey) metaTileKeys.unique(key); }
public class QuickTileCache implements TransactionListener { /** * Set of parameters that we can ignore, since they do not define a map, are either unrelated, or * define the tiling instead */ private static final Set ignoredParameters; static { ignoredParameters = new HashSet(); ignoredParameters.add("REQUEST"); ignoredParameters.add("TILED"); ignoredParameters.add("BBOX"); ignoredParameters.add("WIDTH"); ignoredParameters.add("HEIGHT"); ignoredParameters.add("SERVICE"); ignoredParameters.add("VERSION"); ignoredParameters.add("EXCEPTIONS"); } /** Canonicalizer used to return the same object when two threads ask for the same meta-tile */ private CanonicalSet<MetaTileKey> metaTileKeys = CanonicalSet.newInstance(MetaTileKey.class); private WeakHashMap tileCache = new WeakHashMap(); public QuickTileCache(GeoServer geoServer) { geoServer.addListener( new ConfigurationListenerAdapter() { public void handleGlobalChange( GeoServerInfo global, List<String> propertyNames, List<Object> oldValues, List<Object> newValues) { tileCache.clear(); } public void handleServiceChange( ServiceInfo service, List<String> propertyNames, List<Object> oldValues, List<Object> newValues) { tileCache.clear(); } public void reloaded() { tileCache.clear(); } }); } /** For testing only */ QuickTileCache() {} /** * Given a tiled request, builds a key that can be used to access the cache looking for a specific * meta-tile, and also as a synchronization tool to avoid multiple requests to trigger parallel * computation of the same meta-tile * * @param request * @return */ public MetaTileKey getMetaTileKey(GetMapRequest request) { String mapDefinition = buildMapDefinition(request.getHttpServletRequest()); Envelope bbox = request.getBbox(); Point2D origin = request.getTilesOrigin(); MapKey mapKey = new MapKey(mapDefinition, normalize(bbox.getWidth() / request.getWidth()), origin); Point tileCoords = getTileCoordinates(bbox, origin); Point metaTileCoords = getMetaTileCoordinates(tileCoords); Envelope metaTileEnvelope = getMetaTileEnvelope(request, tileCoords, metaTileCoords); MetaTileKey key = new MetaTileKey(mapKey, metaTileCoords, metaTileEnvelope); // since this will be used for thread synchronization, we have to make // sure two thread asking for the same meta tile will get the same key // object return (MetaTileKey) metaTileKeys.unique(key); } private Envelope getMetaTileEnvelope( GetMapRequest request, Point tileCoords, Point metaTileCoords) { Envelope bbox = request.getBbox(); double minx = bbox.getMinX() + (metaTileCoords.x - tileCoords.x) * bbox.getWidth(); double miny = bbox.getMinY() + (metaTileCoords.y - tileCoords.y) * bbox.getHeight(); double maxx = minx + bbox.getWidth() * 3; double maxy = miny + bbox.getHeight() * 3; return new Envelope(minx, maxx, miny, maxy); } /** * Given a tile, returns the coordinates of the meta-tile that contains it (where the meta-tile * coordinate is the coordinate of its lower left subtile) * * @param tileCoords * @return */ Point getMetaTileCoordinates(Point tileCoords) { int x = tileCoords.x; int y = tileCoords.y; int rx = x % 3; int ry = y % 3; int mtx = (rx == 0) ? x : ((x >= 0) ? (x - rx) : (x - 3 - rx)); int mty = (ry == 0) ? y : ((y >= 0) ? (y - ry) : (y - 3 - ry)); return new Point(mtx, mty); } /** * Given an envelope and origin, find the tile coordinate (row,col) * * @param env * @param origin * @return */ Point getTileCoordinates(Envelope env, Point2D origin) { // this was using the low left corner and Math.round, but turned // out to be fragile when fairly zoomed in. Using the tile center // and then flooring the division seems to work much more reliably. double centerx = env.getMinX() + env.getWidth() / 2; double centery = env.getMinY() + env.getHeight() / 2; int x = (int) Math.floor((centerx - origin.getX()) / env.getWidth()); int y = (int) Math.floor((centery - origin.getY()) / env.getWidth()); return new Point(x, y); } /** * This is tricky. We need to have doubles that can be compared by equality because resolution and * origin are doubles, and are part of a hashmap key, so we have to normalize them somehow, in * order to make the little differences disappear. Here we take the mantissa, which is made of 52 * bits, and throw away the 20 more significant ones, which means we're dealing with 12 * significant decimal digits (2^40 -> more or less one billion million). See also <a * href="http://en.wikipedia.org/wiki/IEEE_754">IEEE 754</a> on Wikipedia. * * @param d * @return */ static double normalize(double d) { if (Double.isInfinite(d) || Double.isNaN(d)) { return d; } return Math.round(d * 10e6) / 10e6; } /** * Turns the request back into a sort of GET request (not url-encoded) for fast comparison * * @param request * @return */ private String buildMapDefinition(HttpServletRequest request) { StringBuffer sb = new StringBuffer(); Enumeration en = request.getParameterNames(); while (en.hasMoreElements()) { String paramName = (String) en.nextElement(); if (ignoredParameters.contains(paramName.toUpperCase())) { continue; } // we don't have multi-valued parameters afaik, otherwise we would // have to use getParameterValues and deal with the returned array sb.append(paramName).append('=').append(request.getParameter(paramName)); if (en.hasMoreElements()) { sb.append('&'); } } return sb.toString(); } /** Key defining a tiling layer in a map */ static class MapKey { String mapDefinition; double resolution; Point2D origin; public MapKey(String mapDefinition, double resolution, Point2D origin) { super(); this.mapDefinition = mapDefinition; this.resolution = resolution; this.origin = origin; } public int hashCode() { return new HashCodeBuilder() .append(mapDefinition) .append(resolution) .append(resolution) .append(origin) .toHashCode(); } public boolean equals(Object obj) { if (!(obj instanceof MapKey)) { return false; } MapKey other = (MapKey) obj; return new EqualsBuilder() .append(mapDefinition, other.mapDefinition) .append(resolution, other.resolution) .append(origin, other.origin) .isEquals(); } public String toString() { return mapDefinition + "\nw:" + "\nresolution:" + resolution + "\norig:" + origin.getX() + "," + origin.getY(); } } /** Key that identifies a certain meta-tile in a tiled map layer */ static class MetaTileKey { MapKey mapKey; Point metaTileCoords; Envelope metaTileEnvelope; public MetaTileKey(MapKey mapKey, Point metaTileCoords, Envelope metaTileEnvelope) { super(); this.mapKey = mapKey; this.metaTileCoords = metaTileCoords; this.metaTileEnvelope = metaTileEnvelope; } public Envelope getMetaTileEnvelope() { // This old code proved to be too much unstable, numerically wise, to be used // when very much zoomed in, so we moved to a local meta tile envelope computation // based on the requested tile bounds instead // double minx = mapKey.origin.getX() + (mapKey.resolution * 256 * // metaTileCoords.x); // double miny = mapKey.origin.getY() + (mapKey.resolution * 256 * // metaTileCoords.y); // // return new Envelope(minx, minx + (mapKey.resolution * 256 * 3), miny, miny // + (mapKey.resolution * 256 * 3)); return metaTileEnvelope; } public int hashCode() { return new HashCodeBuilder().append(mapKey).append(metaTileCoords).toHashCode(); } public boolean equals(Object obj) { if (!(obj instanceof MetaTileKey)) { return false; } MetaTileKey other = (MetaTileKey) obj; return new EqualsBuilder() .append(mapKey, other.mapKey) .append(metaTileCoords, other.metaTileCoords) .isEquals(); } public int getMetaFactor() { return 3; } public int getTileSize() { return 256; } public String toString() { return mapKey + "\nmtc:" + metaTileCoords.x + "," + metaTileCoords.y; } } /** * Gathers a tile from the cache, if available * * @param key * @param request * @return */ public synchronized RenderedImage getTile(MetaTileKey key, GetMapRequest request) { CacheElement ce = (CacheElement) tileCache.get(key); if (ce == null) { return null; } return getTile(key, request, ce.tiles); } /** * @param key * @param request * @param tiles * @return */ public RenderedImage getTile(MetaTileKey key, GetMapRequest request, RenderedImage[] tiles) { Point tileCoord = getTileCoordinates(request.getBbox(), key.mapKey.origin); Point metaCoord = key.metaTileCoords; return tiles[tileCoord.x - metaCoord.x + ((tileCoord.y - metaCoord.y) * key.getMetaFactor())]; } /** * Puts the specified tile array in the cache, and returns the tile the request was looking for * * @param key * @param request * @param tiles * @return */ public synchronized void storeTiles(MetaTileKey key, RenderedImage[] tiles) { tileCache.put(key, new CacheElement(tiles)); } class CacheElement { RenderedImage[] tiles; public CacheElement(RenderedImage[] tiles) { this.tiles = tiles; } } public void dataStoreChange(TransactionEvent event) throws WFSException { // if anything changes we just wipe out the cache. the mapkey // contains a string with part of the map request where the layer // name is included, but we would have to parse it and consider // also that the namespace may be missing in the getmap request tileCache.clear(); } }