/******************************************************************************* * Copyright 2013-2015 alladin-IT GmbH * * 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 at.alladin.rmbt.mapServer; import java.awt.BasicStroke; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; import org.restlet.Request; import org.restlet.Response; import org.restlet.Restlet; import org.restlet.data.Form; import at.alladin.rmbt.mapServer.MapServerOptions.MapFilter; import at.alladin.rmbt.mapServer.MapServerOptions.MapOption; import at.alladin.rmbt.mapServer.MapServerOptions.SQLFilter; import at.alladin.rmbt.mapServer.parameters.TileParameters; import at.alladin.rmbt.mapServer.parameters.TileParameters.Path; import at.alladin.rmbt.shared.cache.CacheHelper; import at.alladin.rmbt.shared.cache.CacheHelper.ObjectWithTimestamp; public abstract class TileRestlet<Params extends TileParameters> extends Restlet { protected static final int[] TILE_SIZES = new int[] { 256, 512, 768 }; protected static final byte[][] EMPTY_IMAGES = new byte[TILE_SIZES.length][]; protected static final byte[] EMPTY_MARKER = "EMPTY".getBytes(); private static final int CACHE_STALE = 3600; private static final int CACHE_EXPIRE = 7200; private final CacheHelper cache = CacheHelper.getInstance(); static { for (int i = 0; i < TILE_SIZES.length; i++) { final BufferedImage img = new BufferedImage(TILE_SIZES[i], TILE_SIZES[i], BufferedImage.TYPE_INT_ARGB); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { ImageIO.write(img, "png", baos); } catch (final IOException e) { e.printStackTrace(); } EMPTY_IMAGES[i] = baos.toByteArray(); } } protected static class Image { protected BufferedImage bi; protected Graphics2D g; protected int width; protected int height; } static class DPoint { double x; double y; } static class DBox { double x1; double y1; double x2; double y2; double res; } @SuppressWarnings("unchecked") protected final ThreadLocal<Image>[] images = new ThreadLocal[TILE_SIZES.length]; public TileRestlet() { for (int i = 0; i < TILE_SIZES.length; i++) { final int tileSize = TILE_SIZES[i]; images[i] = new ThreadLocal<Image>() { @Override protected Image initialValue() { final Image image = new Image(); image.bi = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB); image.g = image.bi.createGraphics(); image.g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); image.g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); image.g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); image.g.setStroke(new BasicStroke(1f)); image.width = image.bi.getWidth(); image.height = image.bi.getHeight(); return image; }; }; } } protected static int valueToColor(final int[] colors, final double[] intervals, final double value) { int idx = -1; for (int i = 0; i < intervals.length; i++) if (value < intervals[i]) { idx = i; break; } if (idx == 0) return colors[0]; if (idx == -1) return colors[colors.length - 1]; final double factor = (value - intervals[idx - 1]) / (intervals[idx] - intervals[idx - 1]); final int c0 = colors[idx - 1]; final int c1 = colors[idx]; final int c0r = c0 >> 16; final int c0g = c0 >> 8 & 0xff; final int c0b = c0 & 0xff; final int r = (int) (c0r + ((c1 >> 16) - c0r) * factor); final int g = (int) (c0g + ((c1 >> 8 & 0xff) - c0g) * factor); final int b = (int) (c0b + ((c1 & 0xff) - c0b) * factor); return r << 16 | g << 8 | b; } @Override public void handle(final Request req, final Response res) { final String zoomStr = (String) req.getAttributes().get("zoom"); final String xStr = (String) req.getAttributes().get("x"); final String yStr = (String) req.getAttributes().get("y"); final Path path = new Path(zoomStr, xStr, yStr, req.getResourceRef().getQueryAsForm().getFirstValue("path", true)); final Params p = getTileParameters(path, req.getResourceRef().getQueryAsForm()); res.setEntity(new PngOutputRepresentation(getTile(p))); } protected byte[] getTile(final Params p) { boolean useCache = true; if (p.isNoCache()) useCache = false; final String cacheKey; if (useCache) { cacheKey = CacheHelper.getHash((TileParameters)p); final ObjectWithTimestamp cacheObject = cache.getWithTimestamp(cacheKey, CACHE_STALE); if (cacheObject != null) { System.out.println("cache hit for: " + cacheKey + "; is stale: " + cacheObject.stale); byte[] data = (byte[]) cacheObject.o; if (Arrays.equals(EMPTY_MARKER, data)) data = EMPTY_IMAGES[getTileSizeIdx(p)]; if (cacheObject.stale) { final Runnable refreshCacheRunnable = new Runnable() { @Override public void run() { System.out.println("adding in background: " + cacheKey); final byte[] newData = generateTile(p, getTileSizeIdx(p)); cache.set(cacheKey, CACHE_EXPIRE, newData != null ? newData : EMPTY_MARKER, true); } }; cache.getExecutor().execute(refreshCacheRunnable); } return data; } } else cacheKey = null; final int tileSizeIdx = getTileSizeIdx(p); byte[] data = generateTile(p, tileSizeIdx); // if (data == null) if (useCache) { System.out.println("adding to cache: " + cacheKey); cache.set(cacheKey, CACHE_EXPIRE, data != null ? data : EMPTY_MARKER, true); } if (data == null) data = EMPTY_IMAGES[tileSizeIdx]; return data; } private int getTileSizeIdx(final Params p) { int tileSizeIdx = 0; final int size = p.getSize(); for (int i = 0; i < TILE_SIZES.length; i++) { if (size == TILE_SIZES[i]) { tileSizeIdx = i; break; } } return tileSizeIdx; } private byte[] generateTile(final Params p, int tileSizeIdx) { final MapOption mo = MapServerOptions.getMapOptionMap().get(p.getMapOption()); if (mo == null) throw new IllegalArgumentException(); final List<SQLFilter> filters = new ArrayList<>(MapServerOptions.getDefaultMapFilters()); for (final Map.Entry<String, String> entry : p.getFilterMap().entrySet()) { final MapFilter mapFilter = MapServerOptions.getMapFilterMap().get(entry.getKey()); if (mapFilter != null) { final SQLFilter filter = mapFilter.getFilter(entry.getValue()); if (filter != null) filters.add(filter); } } final Path path = p.getPath(); final DBox box = GeoCalc.xyToMeters(TILE_SIZES[tileSizeIdx], path.getX(), path.getY(), path.getZoom()); float quantile = p.getQuantile(); if (mo.reverseScale) quantile = 1 - quantile; final byte[] data = generateTile(p, tileSizeIdx, path.getZoom(), box, mo, filters, quantile); return data; } protected abstract Params getTileParameters(TileParameters.Path path, Form params); protected abstract byte[] generateTile(Params params, int tileSizeIdx, int zoom, DBox box, MapOption mo, List<SQLFilter> filters, float quantile); }