/******************************************************************************* * Copyright (c) MOBAC developers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.program.atlascreators.impl; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import javax.imageio.ImageIO; import mobac.exceptions.MapCreationException; import mobac.program.atlascreators.AtlasCreator; import mobac.program.atlascreators.tileprovider.TileProvider; import mobac.program.interfaces.MapInterface; import mobac.program.interfaces.MapSource; import mobac.program.interfaces.TileImageDataWriter; import mobac.program.model.TileImageParameters; import mobac.utilities.MyMath; import org.apache.log4j.Logger; public class MapTileBuilder { private static final Logger log = Logger.getLogger(MapTileBuilder.class); private final AtlasCreator atlasCreator; private final TileProvider mapDlTileProvider; private final MapInterface map; private final MapSource mapSource; private final TileImageParameters parameters; private final TileImageDataWriter tileImageDataWriter; private final int tileSize; private final int xMin; private final int xMax; private final int yMin; private final int yMax; private final boolean useRealTileSize; private int realWidth; private int realHeight; int mergedWidth; int mergedHeight; final int customTileCount; final int xStart; final int yStart; final int xEnd; final int yEnd; protected final MapTileWriter mapTileWriter; /** * @param atlasCreator * @param mapTileWriter * @param useRealTileSize * affects the tile size at the left and bottom border of the map. If <code>true</code> those tile will * have the size of the remaining image data available size (they can be smaller than the size * specified). If <code>false</code> the tile size will be as specified by the map's * {@link TileImageParameters}. */ public MapTileBuilder(AtlasCreator atlasCreator, MapTileWriter mapTileWriter, boolean useRealTileSize) { this(atlasCreator, atlasCreator.getParameters().getFormat().getDataWriter(), mapTileWriter, useRealTileSize); } /** * * @param atlasCreator * @param tileImageDataWriter * @param mapTileWriter * @param useRealTileSize * affects the tile size at the left and bottom border of the map. If <code>true</code> those tile will * have the size of the remaining image data available size (they can be smaller than the size * specified). If <code>false</code> the tile size will be as specified by the map's * {@link TileImageParameters}. */ public MapTileBuilder(AtlasCreator atlasCreator, TileImageDataWriter tileImageDataWriter, MapTileWriter mapTileWriter, boolean useRealTileSize) { this.atlasCreator = atlasCreator; this.tileImageDataWriter = tileImageDataWriter; this.mapTileWriter = mapTileWriter; this.mapDlTileProvider = atlasCreator.getMapDlTileProvider(); this.useRealTileSize = useRealTileSize; map = atlasCreator.getMap(); mapSource = map.getMapSource(); tileSize = mapSource.getMapSpace().getTileSize(); xMax = atlasCreator.getXMax(); xMin = atlasCreator.getXMin(); yMax = atlasCreator.getYMax(); yMin = atlasCreator.getYMin(); parameters = atlasCreator.getParameters(); // left upper point on the map in pixels // regarding the current zoom level xStart = xMin * tileSize; yStart = yMin * tileSize; // lower right point on the map in pixels // regarding the current zoom level xEnd = xMax * tileSize + (tileSize - 1); yEnd = yMax * tileSize + (tileSize - 1); mergedWidth = xEnd - xStart + 1; mergedHeight = yEnd - yStart + 1; realWidth = parameters.getWidth(); realHeight = parameters.getHeight(); if (useRealTileSize) { // Reduce tile size of overall map height/width // if it is smaller than one tile if (realWidth > mergedWidth) realWidth = mergedWidth; if (realHeight > mergedHeight) realHeight = mergedHeight; } customTileCount = MyMath.divCeil(mergedWidth, realWidth) * MyMath.divCeil(mergedHeight, realHeight); } public void createTiles() throws MapCreationException, InterruptedException { // Absolute positions int xAbsPos = xStart; int yAbsPos = yStart; log.trace("tile size: " + realWidth + " * " + realHeight); log.trace("X: from " + xStart + " to " + xEnd); log.trace("Y: from " + yStart + " to " + yEnd); // We don't work with large images, therefore we can disable the (file) // cache of ImageIO. This will speed up the creation process a bit ImageIO.setUseCache(false); ByteArrayOutputStream buf = new ByteArrayOutputStream(32768); tileImageDataWriter.initialize(); int currentTileHeight = realHeight; int currentTileWidth = realWidth; try { String tileType = tileImageDataWriter.getType().getFileExt(); int tiley = 0; while (yAbsPos < yEnd) { int tilex = 0; xAbsPos = xStart; if (useRealTileSize) currentTileHeight = Math.min(realHeight, yEnd - yAbsPos + 1); while (xAbsPos < xEnd) { if (useRealTileSize) currentTileWidth = Math.min(realWidth, xEnd - xAbsPos + 1); atlasCreator.checkUserAbort(); atlasCreator.getAtlasProgress().incMapCreationProgress(); BufferedImage tileImage = new BufferedImage(currentTileWidth, currentTileHeight, BufferedImage.TYPE_3BYTE_BGR); buf.reset(); try { Graphics2D graphics = tileImage.createGraphics(); prepareTile(graphics); paintCustomTile(graphics, xAbsPos, yAbsPos); graphics.dispose(); tileImageDataWriter.processImage(tileImage, buf); mapTileWriter.writeTile(tilex, tiley, tileType, buf.toByteArray()); } catch (IOException e) { throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); } tilex++; xAbsPos += realWidth; } tiley++; yAbsPos += realHeight; } } finally { tileImageDataWriter.dispose(); } } protected void prepareTile(Graphics2D graphics) { graphics.setColor(mapSource.getBackgroundColor()); graphics.fillRect(0, 0, realWidth, realHeight); } /** * Paints the graphics of the custom tile specified by the pixel coordinates <code>xAbsPos</code> and * <code>yAbsPos</code> on the currently selected map & layer. * * @param graphics * @param xAbsPos * @param yAbsPos */ private void paintCustomTile(Graphics2D graphics, int xAbsPos, int yAbsPos) { int xTile = xAbsPos / tileSize; int xTileOffset = -(xAbsPos % tileSize); for (int x = xTileOffset; x < realWidth; x += tileSize) { int yTile = yAbsPos / tileSize; int yTileOffset = -(yAbsPos % tileSize); for (int y = yTileOffset; y < realHeight; y += tileSize) { try { BufferedImage orgTileImage = loadOriginalMapTile(xTile, yTile); if (orgTileImage != null) { //int w = orgTileImage.getWidth(); //int h = orgTileImage.getHeight(); graphics.drawImage(orgTileImage, xTileOffset, yTileOffset, tileSize, tileSize, null); } } catch (Exception e) { log.error("Error while painting sub-tile", e); } yTile++; yTileOffset += tileSize; } xTile++; xTileOffset += tileSize; } } public int getCustomTileCount() { return customTileCount; } /** * A simple local cache holding the last 10 loaded original tiles. If the custom tile size is smaller than 256x256 * the efficiency of this cache is very high (~ 75% hit rate). */ private CachedTile[] cache = new CachedTile[10]; private int cachePos = 0; private BufferedImage loadOriginalMapTile(int xTile, int yTile) throws Exception { for (CachedTile ct : cache) { if (ct == null) continue; if (ct.xTile == xTile && ct.yTile == yTile) { // log.trace("cache hit"); return ct.image; } } // log.trace("cache miss"); BufferedImage image = mapDlTileProvider.getTileImage(xTile, yTile); if (image == null) return null; cache[cachePos] = new CachedTile(image, xTile, yTile); cachePos = (cachePos + 1) % cache.length; return image; } private static class CachedTile { BufferedImage image; int xTile; int yTile; public CachedTile(BufferedImage image, int tile, int tile2) { super(); this.image = image; xTile = tile; yTile = tile2; } } }