dk.dma.msinm.web.OsmStaticMap.java Source code

Java tutorial

Introduction

Here is the source code for dk.dma.msinm.web.OsmStaticMap.java

Source

/* Copyright (c) 2011 Danish Maritime Authority
 *
 * 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; either
 * version 3 of the License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.dma.msinm.web;

import dk.dma.msinm.common.repo.RepositoryService;
import dk.dma.msinm.common.util.WebUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;

import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * Currently MSI-NM use http://staticmap.openstreetmap.de/staticmap.php to generate static maps
 * via the {@code AbstractImageServlet} servlet.<br/>
 * However, this service is quite often down.
 * <p/>
 * The {@code OsmStaticMap} servlet is an attempt to re-implement the service in java.
 * It is based on the staticMapLite project ported from PHP to java, see:
 * http://sourceforge.net/p/staticmaplite/code/HEAD/tree/staticmap.php
 *
 * TODO: Fix rounding errors. Seems to be downloading too many tiles - try:
 * http://localhost:8080/static-map-lite?center=56,12&zoom=10&size=256x256
 *
 * TODO: Need to download tiles in parallel to speed it up
 * TODO: Need to clean up the tile and map caches on regular intervals
 */
@WebServlet(value = "/static-map-lite", asyncSupported = true)
public class OsmStaticMap extends HttpServlet {

    public static final String OSM_URL = "http://tile.openstreetmap.org/%d/%d/%d.png";
    public static final int RESPONSE_CACHE_TIME_SEC = 60 * 60 * 24 * 14;

    protected static final boolean useTileCache = true;
    protected static final String tileCacheBaseDir = "osm_cache/tiles";

    protected static final boolean useMapCache = true;
    protected static final String mapCacheBaseDir = "osm_cache/maps";

    @Inject
    Logger log;

    @Inject
    RepositoryService repositoryService;

    /**
     * Main GET method
     *
     * @param request  servlet request
     * @param response servlet response
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Sanity checks
        if (StringUtils.isBlank(request.getParameter("zoom")) || StringUtils.isBlank(request.getParameter("center"))
                || StringUtils.isBlank(request.getParameter("size"))) {
            throw new IllegalArgumentException("Must provide zoom, size and center parameters");
        }

        MapImageCtx ctx = new MapImageCtx();

        parseParams(ctx, request);
        if (useMapCache) {
            // use map cache, so check cache for map
            if (!checkMapCache(ctx)) {
                // map is not in cache, needs to be build
                BufferedImage image = makeMap(ctx);

                Path path = repositoryService.getRepoRoot().resolve(mapCacheIDToFilename(ctx));
                Files.createDirectories(path.getParent());
                ImageIO.write(image, "png", path.toFile());

                // And write to response
                sendHeader(response);
                ImageIO.write(image, "png", response.getOutputStream());

            } else {
                // map is in cache
                sendHeader(response);
                Path path = repositoryService.getRepoRoot().resolve(mapCacheIDToFilename(ctx));
                response.setContentLength((int) path.toFile().length());

                FileInputStream fileInputStream = new FileInputStream(path.toFile());
                OutputStream responseOutputStream = response.getOutputStream();
                int bytes;
                while ((bytes = fileInputStream.read()) != -1) {
                    responseOutputStream.write(bytes);
                }
            }

        } else {
            // no cache, make map, send headers and deliver png
            BufferedImage image = makeMap(ctx);
            sendHeader(response);
            ImageIO.write(image, "png", response.getOutputStream());
        }
    }

    public void parseParams(MapImageCtx ctx, HttpServletRequest request) {
        // Extract zoom parameter
        ctx.zoom = Integer.valueOf(request.getParameter("zoom"));
        if (ctx.zoom > 18) {
            ctx.zoom = 18;
        }

        // Extract lat-lon from the center parameter
        String[] latLon = request.getParameter("center").split(",");
        if (latLon.length != 2) {
            throw new IllegalArgumentException("Invalid center parameter");
        }
        ctx.lat = Double.valueOf(latLon[0]);
        ctx.lon = Double.valueOf(latLon[1]);

        // Extract width and height from the size parameter
        String[] size = request.getParameter("size").split("x");
        if (size.length != 2) {
            throw new IllegalArgumentException("Invalid size parameter");
        }
        ctx.width = Integer.valueOf(size[0]);
        ctx.height = Integer.valueOf(size[1]);

        log.info(String.format(
                "Received request for OSM static image with zoom=%d, center=(%f, %f), width=%d and height=%d",
                ctx.zoom, ctx.lat, ctx.lon, ctx.width, ctx.height));
    }

    private double lonToTile(double lon, int zoom) {
        return ((lon + 180.0) / 360.0) * Math.pow(2.0, zoom);
    }

    private double latToTile(double lat, int zoom) {
        return (1.0 - Math.log(Math.tan(lat * Math.PI / 180.0) + 1.0 / Math.cos(lat * Math.PI / 180.0)) / Math.PI)
                / 2.0 * Math.pow(2.0, zoom);
    }

    public void initCoords(MapImageCtx ctx) {
        ctx.centerX = lonToTile(ctx.lon, ctx.zoom);
        ctx.centerY = latToTile(ctx.lat, ctx.zoom);
        ctx.offsetX = Math.floor((Math.floor(ctx.centerX) - ctx.centerX) * ctx.tileSize);
        ctx.offsetY = Math.floor((Math.floor(ctx.centerY) - ctx.centerY) * ctx.tileSize);
    }

    public BufferedImage createBaseMap(MapImageCtx ctx) {
        BufferedImage image = new BufferedImage(ctx.width, ctx.height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = image.createGraphics();
        int startX = (int) Math.floor(ctx.centerX - (ctx.width / ctx.tileSize) / 2.0);
        int startY = (int) Math.floor(ctx.centerY - (ctx.height / ctx.tileSize) / 2.0);
        int endX = (int) Math.ceil(ctx.centerX + (ctx.width / ctx.tileSize) / 2.0);
        int endY = (int) Math.ceil(ctx.centerY + (ctx.height / ctx.tileSize) / 2.0);
        ctx.offsetX = -Math.floor((ctx.centerX - Math.floor(ctx.centerX)) * ctx.tileSize);
        ctx.offsetY = -Math.floor((ctx.centerY - Math.floor(ctx.centerY)) * ctx.tileSize);
        ctx.offsetX += Math.floor(ctx.width / 2.0);
        ctx.offsetY += Math.floor(ctx.height / 2.0);
        ctx.offsetX += Math.floor(startX - Math.floor(ctx.centerX)) * ctx.tileSize;
        ctx.offsetY += Math.floor(startY - Math.floor(ctx.centerY)) * ctx.tileSize;

        for (int x = startX; x <= endX; x++) {
            for (int y = startY; y <= endY; y++) {
                String url = String.format(OSM_URL, ctx.zoom, x, y);
                log.info("Fetching " + url);
                try {
                    BufferedImage tileImage = fetchTile(url);
                    double destX = (x - startX) * ctx.tileSize + ctx.offsetX;
                    double destY = (y - startY) * ctx.tileSize + ctx.offsetY;
                    g2.drawImage(tileImage, (int) destX, (int) destY, ctx.tileSize, ctx.tileSize, null);
                    image.flush();
                } catch (Exception e) {
                    log.warn("Failed loading image " + url);
                }
            }
        }
        return image;
    }

    private String tileUrlToFilename(String url) {
        return tileCacheBaseDir + "/" + url.replace("http://", "");
    }

    public BufferedImage checkTileCache(String url) {
        String filename = tileUrlToFilename(url);
        Path path = repositoryService.getRepoRoot().resolve(filename);
        try {
            if (Files.exists(path)) {
                return ImageIO.read(path.toFile());
            }
        } catch (IOException e) {
            log.warn("Failed to load tile cache file " + filename);
        }
        return null;
    }

    public boolean checkMapCache(MapImageCtx ctx) {
        ctx.mapCacheID = DigestUtils.md5Hex(serializeParams(ctx));
        String filename = mapCacheIDToFilename(ctx);
        Path path = repositoryService.getRepoRoot().resolve(filename);
        return Files.exists(path);
    }

    public String serializeParams(MapImageCtx ctx) {
        return Arrays.asList(ctx.zoom, ctx.lat, ctx.lon, ctx.width, ctx.height).stream().map(String::valueOf)
                .collect(Collectors.joining("&"));
    }

    public String mapCacheIDToFilename(MapImageCtx ctx) {
        if (ctx.mapCacheFile == null) {
            ctx.mapCacheFile = mapCacheBaseDir + "/" + ctx.zoom + "/cache_" + ctx.mapCacheID.substring(0, 2) + "/"
                    + ctx.mapCacheID.substring(2, 4) + "/" + ctx.mapCacheID.substring(4);
        }
        return ctx.mapCacheFile + ".png";
    }

    public void writeTileToCache(String url, BufferedImage image) {
        try {
            String filename = tileUrlToFilename(url);
            Path path = repositoryService.getRepoRoot().resolve(filename);
            Files.createDirectories(path.getParent());
            ImageIO.write(image, "png", path.toFile());
        } catch (IOException e) {
            log.warn("Failed saving cached tile for url " + url);
        }
    }

    public BufferedImage fetchTile(String url) throws Exception {
        BufferedImage image = null;
        if (useTileCache) {
            image = checkTileCache(url);
        }
        if (image == null) {
            image = ImageIO.read(new URL(url));
            if (useTileCache) {
                writeTileToCache(url, image);
            }
        }
        return image;
    }

    public void sendHeader(HttpServletResponse response) {
        WebUtils.cache(response, RESPONSE_CACHE_TIME_SEC);
        response.setContentType("image/png");
    }

    public BufferedImage makeMap(MapImageCtx ctx) {
        initCoords(ctx);
        return createBaseMap(ctx);
    }

    public static class MapImageCtx {
        int zoom;
        double lat = 0.0, lon = 0.0;
        int width = 256, height = 256;
        double centerX, centerY;
        double offsetX, offsetY;
        int tileSize = 256;
        String mapCacheID, mapCacheFile;
    }
}