001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.vfny.geoserver.wms.responses.map.metatile;
006:
007: import java.awt.Point;
008: import java.awt.geom.Point2D;
009: import java.awt.image.RenderedImage;
010: import java.util.Enumeration;
011: import java.util.HashSet;
012: import java.util.Set;
013:
014: import javax.servlet.http.HttpServletRequest;
015:
016: import org.apache.commons.lang.builder.EqualsBuilder;
017: import org.apache.commons.lang.builder.HashCodeBuilder;
018: import org.geoserver.wfs.TransactionEvent;
019: import org.geoserver.wfs.TransactionListener;
020: import org.geoserver.wfs.WFSException;
021: import org.geotools.util.SoftValueHashMap;
022: import org.geotools.util.WeakHashSet;
023: import org.vfny.geoserver.global.GeoServer;
024: import org.vfny.geoserver.wms.requests.GetMapRequest;
025:
026: import com.vividsolutions.jts.geom.Envelope;
027:
028: public class QuickTileCache implements TransactionListener {
029: /**
030: * Set of parameters that we can ignore, since they do not define a map, are
031: * either unrelated, or define the tiling instead
032: */
033: private static final Set ignoredParameters;
034:
035: static {
036: ignoredParameters = new HashSet();
037: ignoredParameters.add("REQUEST");
038: ignoredParameters.add("TILED");
039: ignoredParameters.add("BBOX");
040: ignoredParameters.add("WIDTH");
041: ignoredParameters.add("HEIGHT");
042: ignoredParameters.add("SERVICE");
043: ignoredParameters.add("VERSION");
044: ignoredParameters.add("EXCEPTIONS");
045: }
046:
047: /**
048: * Canonicalizer used to return the same object when two threads ask for the
049: * same meta-tile
050: */
051: private WeakHashSet metaTileKeys = new WeakHashSet();
052:
053: private SoftValueHashMap tileCache = new SoftValueHashMap(0);
054:
055: public QuickTileCache(GeoServer geoServer) {
056: geoServer.addListener(new GeoServer.Listener() {
057:
058: public void changed() {
059: tileCache.clear();
060: }
061:
062: });
063: }
064:
065: /**
066: * For testing only
067: */
068: QuickTileCache() {
069: }
070:
071: /**
072: * Given a tiled request, builds a key that can be used to access the cache
073: * looking for a specific meta-tile, and also as a synchronization tool to
074: * avoid multiple requests to trigger parallel computation of the same
075: * meta-tile
076: *
077: * @param request
078: * @return
079: */
080: public MetaTileKey getMetaTileKey(GetMapRequest request) {
081: String mapDefinition = buildMapDefinition(request
082: .getHttpServletRequest());
083: Envelope bbox = request.getBbox();
084: Point2D origin = request.getTilesOrigin();
085: MapKey mapKey = new MapKey(mapDefinition, normalize(bbox
086: .getWidth()
087: / request.getWidth()), origin);
088: Point tileCoords = getTileCoordinates(bbox, origin);
089: Point metaTileCoords = getMetaTileCoordinates(tileCoords);
090: MetaTileKey key = new MetaTileKey(mapKey, metaTileCoords);
091:
092: // since this will be used for thread synchronization, we have to make
093: // sure two thread asking for the same meta tile will get the same key
094: // object
095: return (MetaTileKey) metaTileKeys.canonicalize(key);
096: }
097:
098: /**
099: * Given a tile, returns the coordinates of the meta-tile that contains it
100: * (where the meta-tile coordinate is the coordinate of its lower left
101: * subtile)
102: *
103: * @param tileCoords
104: * @return
105: */
106: Point getMetaTileCoordinates(Point tileCoords) {
107: int x = tileCoords.x;
108: int y = tileCoords.y;
109: int rx = x % 3;
110: int ry = y % 3;
111: int mtx = (rx == 0) ? x : ((x >= 0) ? (x - rx) : (x - 3 - rx));
112: int mty = (ry == 0) ? y : ((y >= 0) ? (y - ry) : (y - 3 - ry));
113:
114: return new Point(mtx, mty);
115: }
116:
117: /**
118: * Given an envelope and origin, find the tile coordinate (row,col)
119: *
120: * @param env
121: * @param origin
122: * @return
123: */
124: Point getTileCoordinates(Envelope env, Point2D origin) {
125: // this was using the low left corner and Math.round, but turned
126: // out to be fragile when fairly zoomed in. Using the tile center
127: // and then flooring the division seems to work much more reliably.
128: double centerx = env.getMinX() + env.getWidth() / 2;
129: double centery = env.getMinY() + env.getHeight() / 2;
130: int x = (int) Math.floor((centerx - origin.getX())
131: / env.getWidth());
132: int y = (int) Math.floor((centery - origin.getY())
133: / env.getWidth());
134:
135: return new Point(x, y);
136: }
137:
138: /**
139: * This is tricky. We need to have doubles that can be compared by equality
140: * because resolution and origin are doubles, and are part of a hashmap key,
141: * so we have to normalize them somehow, in order to make the little
142: * differences disappear. Here we take the mantissa, which is made of 52
143: * bits, and throw away the 20 more significant ones, which means we're
144: * dealing with 12 significant decimal digits (2^40 -> more or less one
145: * billion million). See also <a
146: * href="http://en.wikipedia.org/wiki/IEEE_754">IEEE 754</a> on Wikipedia.
147: *
148: * @param d
149: * @return
150: */
151: static double normalize(double d) {
152: if (Double.isInfinite(d) || Double.isNaN(d)) {
153: return d;
154: }
155:
156: return Math.round(d * 10e6) / 10e6;
157: }
158:
159: /**
160: * Turns the request back into a sort of GET request (not url-encoded) for
161: * fast comparison
162: *
163: * @param request
164: * @return
165: */
166: private String buildMapDefinition(HttpServletRequest request) {
167: StringBuffer sb = new StringBuffer();
168: Enumeration en = request.getParameterNames();
169:
170: while (en.hasMoreElements()) {
171: String paramName = (String) en.nextElement();
172:
173: if (ignoredParameters.contains(paramName.toUpperCase())) {
174: continue;
175: }
176:
177: // we don't have multi-valued parameters afaik, otherwise we would
178: // have to use getParameterValues and deal with the returned array
179: sb.append(paramName).append('=').append(
180: request.getParameter(paramName));
181:
182: if (en.hasMoreElements()) {
183: sb.append('&');
184: }
185: }
186:
187: return sb.toString();
188: }
189:
190: /**
191: * Key defining a tiling layer in a map
192: */
193: static class MapKey {
194: String mapDefinition;
195:
196: double resolution;
197:
198: Point2D origin;
199:
200: public MapKey(String mapDefinition, double resolution,
201: Point2D origin) {
202: super ();
203: this .mapDefinition = mapDefinition;
204: this .resolution = resolution;
205: this .origin = origin;
206: }
207:
208: public int hashCode() {
209: return new HashCodeBuilder().append(mapDefinition).append(
210: resolution).append(resolution).append(origin)
211: .toHashCode();
212: }
213:
214: public boolean equals(Object obj) {
215: if (!(obj instanceof MapKey)) {
216: return false;
217: }
218:
219: MapKey other = (MapKey) obj;
220:
221: return new EqualsBuilder().append(mapDefinition,
222: other.mapDefinition).append(resolution,
223: other.resolution).append(origin, other.origin)
224: .isEquals();
225: }
226:
227: public String toString() {
228: return mapDefinition + "\nw:" + "\nresolution:"
229: + resolution + "\norig:" + origin.getX() + ","
230: + origin.getY();
231: }
232: }
233:
234: /**
235: * Key that identifies a certain meta-tile in a tiled map layer
236: */
237: static class MetaTileKey {
238: MapKey mapKey;
239:
240: Point metaTileCoords;
241:
242: public MetaTileKey(MapKey mapKey, Point metaTileCoords) {
243: super ();
244: this .mapKey = mapKey;
245: this .metaTileCoords = metaTileCoords;
246: }
247:
248: public Envelope getMetaTileEnvelope() {
249: double minx = mapKey.origin.getX()
250: + (mapKey.resolution * 256 * metaTileCoords.x);
251: double miny = mapKey.origin.getY()
252: + (mapKey.resolution * 256 * metaTileCoords.y);
253:
254: return new Envelope(minx, minx
255: + (mapKey.resolution * 256 * 3), miny, miny
256: + (mapKey.resolution * 256 * 3));
257: }
258:
259: public int hashCode() {
260: return new HashCodeBuilder().append(mapKey).append(
261: metaTileCoords).toHashCode();
262: }
263:
264: public boolean equals(Object obj) {
265: if (!(obj instanceof MetaTileKey)) {
266: return false;
267: }
268:
269: MetaTileKey other = (MetaTileKey) obj;
270:
271: return new EqualsBuilder().append(mapKey, other.mapKey)
272: .append(metaTileCoords, other.metaTileCoords)
273: .isEquals();
274: }
275:
276: public int getMetaFactor() {
277: return 3;
278: }
279:
280: public int getTileSize() {
281: return 256;
282: }
283:
284: public String toString() {
285: return mapKey + "\nmtc:" + metaTileCoords.x + ","
286: + metaTileCoords.y;
287: }
288: }
289:
290: /**
291: * Gathers a tile from the cache, if available
292: *
293: * @param key
294: * @param request
295: * @return
296: */
297: public synchronized RenderedImage getTile(MetaTileKey key,
298: GetMapRequest request) {
299: CacheElement ce = (CacheElement) tileCache.get(key);
300:
301: if (ce == null) {
302: return null;
303: }
304:
305: return getTile(key, request, ce.tiles);
306: }
307:
308: /**
309: *
310: * @param key
311: * @param request
312: * @param tiles
313: * @return
314: */
315: public RenderedImage getTile(MetaTileKey key,
316: GetMapRequest request, RenderedImage[] tiles) {
317: Point tileCoord = getTileCoordinates(request.getBbox(),
318: key.mapKey.origin);
319: Point metaCoord = key.metaTileCoords;
320:
321: return tiles[tileCoord.x - metaCoord.x
322: + ((tileCoord.y - metaCoord.y) * key.getMetaFactor())];
323: }
324:
325: /**
326: * Puts the specified tile array in the cache, and returns the tile the
327: * request was looking for
328: *
329: * @param key
330: * @param request
331: * @param tiles
332: * @return
333: */
334: public synchronized void storeTiles(MetaTileKey key,
335: RenderedImage[] tiles) {
336: tileCache.put(key, new CacheElement(tiles));
337: }
338:
339: class CacheElement {
340: RenderedImage[] tiles;
341:
342: public CacheElement(RenderedImage[] tiles) {
343: this .tiles = tiles;
344: }
345: }
346:
347: public void dataStoreChange(TransactionEvent event)
348: throws WFSException {
349: // if anything changes we just wipe out the cache. the mapkey
350: // contains a string with part of the map request where the layer
351: // name is included, but we would have to parse it and consider
352: // also that the namespace may be missing in the getmap request
353: tileCache.clear();
354: }
355: }
|