/******************************************************************************* * 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; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.RandomAccessFile; import javax.imageio.ImageIO; import mobac.exceptions.AtlasTestException; import mobac.exceptions.MapCreationException; import mobac.mapsources.mapspace.MercatorPower2MapSpace; import mobac.program.annotations.AtlasCreatorName; import mobac.program.annotations.SupportedParameters; import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; import mobac.program.atlascreators.tileprovider.TileProvider; import mobac.program.interfaces.AtlasInterface; import mobac.program.interfaces.MapInterface; import mobac.program.interfaces.MapSource; import mobac.program.interfaces.MapSpace; import mobac.program.interfaces.MapSpace.ProjectionCategory; import mobac.program.interfaces.TileImageDataWriter; import mobac.program.model.TileImageParameters.Name; import mobac.utilities.Utilities; import mobac.utilities.stream.ArrayOutputStream; /** * Creates maps using the MGM pack file format (.mgm). * * Each zoom level in a different directory, 64 tiles per mgm file. * * <h3>Format</h3> * <ul> * <li>2 bytes: number of tiles in this file for each tile (even for tiles that are not used, in which case the data is * left null)</li> * <li>1 byte: tile x within this file; add tileX * tilesPerFileX to get global tile number</li> * <li>1 byte: tile y within this file; add tileY * tilesPerFileY to get global tile number</li> * <li>4 bytes: offset of the end of the tile data within this file (to get the offset for the start of the tile data, * subtract the value for the previous tile, or 2 + 6 * tilesPerFile tile data 1 tile data 2 ...</li> * </ul> * * @author paour */ @AtlasCreatorName(value = "MGMaps/MyTrails (MGM)", type = "MGM") @SupportedParameters(names = { Name.format }) public class MGMaps extends AtlasCreator { private static final int TILES_PER_FILE_X = 8; private static final int TILES_PER_FILE_Y = 8; private static final int TILES_PER_FILE = TILES_PER_FILE_X * TILES_PER_FILE_Y; private double xResizeRatio = 1.0; private double yResizeRatio = 1.0; @Override public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws AtlasTestException, IOException, InterruptedException { super.startAtlasCreation(atlas, customAtlasDir); File cache_conf = new File(atlasDir, "cache.conf"); PrintWriter pw = new PrintWriter(new FileWriter(cache_conf)); try { pw.println("version=3"); pw.println("tiles_per_file=" + TILES_PER_FILE); pw.println("hash_size=1"); } finally { pw.close(); } } @Override public void initializeMap(final MapInterface map, final TileProvider mapTileProvider) { super.initializeMap(map, mapTileProvider); xResizeRatio = 1.0; yResizeRatio = 1.0; if (parameters != null) { int mapTileSize = map.getMapSource().getMapSpace().getTileSize(); if ((parameters.getWidth() != mapTileSize) || (parameters.getHeight() != mapTileSize)) { // handle image re-sampling + image re-sizing xResizeRatio = (double) parameters.getWidth() / (double) mapTileSize; yResizeRatio = (double) parameters.getHeight() / (double) mapTileSize; } else { // handle only image re-sampling mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); } } } @Override public void createMap() throws MapCreationException, InterruptedException { MGMTileWriter mgmTileWriter = null; try { if ((xResizeRatio != 1.0) || (yResizeRatio != 1.0)) mgmTileWriter = new MGMResizeTileWriter(); else mgmTileWriter = new MGMTileWriter(); String name = map.getLayer().getName(); // safe naming: replace all non-word characters: [^a-zA-Z_0-9] name = name.replaceAll("[^a-zA-Z_0-9]", "_"); // crate directory if necessary File folder = new File(atlasDir, name + "_" + map.getZoom()); Utilities.mkDirs(folder); atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); ImageIO.setUseCache(false); int pxMin = xMin / TILES_PER_FILE_X; int pxMax = xMax / TILES_PER_FILE_X; int pyMin = yMin / TILES_PER_FILE_Y; int pyMax = yMax / TILES_PER_FILE_Y; for (int px = pxMin; px <= pxMax; px++) { for (int py = pyMin; py <= pyMax; py++) { int count = 0; int pos = 2 + TILES_PER_FILE * 6; File pack = new File(folder, px + "_" + py + ".mgm"); RandomAccessFile raf = null; try { for (int i = 0; i < TILES_PER_FILE_X; i++) { int x = px * TILES_PER_FILE_X + i; if (x < xMin || x > xMax) { continue; } for (int j = 0; j < TILES_PER_FILE_Y; j++) { int y = py * TILES_PER_FILE_Y + j; if (y < yMin || y > yMax) { continue; } if (raf == null) // Only create a file when needed raf = new RandomAccessFile(pack, "rw"); checkUserAbort(); atlasProgress.incMapCreationProgress(); int res = mgmTileWriter.writeTile(x, y, i, j, raf, pos, count); if (res >= 0) { pos = res; count++; } } } if (raf != null) { // POSITION 0: number of tiles raf.seek(0); raf.writeChar(count); } } finally { Utilities.closeFile(raf); } if (count == 0) { // the file doesn't contain any tiles if (pack.exists()) Utilities.deleteFile(pack); } } } } catch (Exception e) { throw new MapCreationException(map, e); } finally { if (mgmTileWriter != null) mgmTileWriter.dispose(); } } @Override public boolean testMapSource(final MapSource mapSource) { MapSpace mapSpace = mapSource.getMapSpace(); return (mapSpace instanceof MercatorPower2MapSpace) && (ProjectionCategory.SPHERE.equals(mapSource.getMapSpace().getProjectionCategory()) || ProjectionCategory.ELLIPSOID .equals(mapSource.getMapSpace().getProjectionCategory())); } /** * Simply writes the tile to the file without resizing */ private class MGMTileWriter { protected byte[] getSourceTileData(int x, int y) throws IOException { return mapDlTileProvider.getTileData(x, y); } public int writeTile(int x, int y, int i, int j, RandomAccessFile raf, int startPos, int count) throws MapCreationException { try { byte[] sourceTileData = getSourceTileData(x, y); if (sourceTileData == null) return -1; raf.seek(startPos); raf.write(sourceTileData); // write the tile index raf.seek(2 + count * 6); raf.writeByte(i); raf.writeByte(j); int pos = startPos + sourceTileData.length; raf.writeInt(pos); return pos; } catch (IOException e) { throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); } } public void dispose() { // Nothing to do } } /** * Resizes the tile and saves it to the file */ private class MGMResizeTileWriter extends MGMTileWriter { private final BufferedImage tileImage; private final Graphics2D graphics; private final TileImageDataWriter writer; private final ArrayOutputStream buffer; public MGMResizeTileWriter() { // resize image tileImage = new BufferedImage(parameters.getWidth(), parameters.getHeight(), BufferedImage.TYPE_3BYTE_BGR); // associated graphics with affine transform graphics = tileImage.createGraphics(); graphics.setTransform(AffineTransform.getScaleInstance(xResizeRatio, yResizeRatio)); graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); // image compression writer writer = parameters.getFormat().getDataWriter(); // buffer to store compressed image buffer = new ArrayOutputStream(3 * parameters.getWidth() * parameters.getHeight()); } @Override protected byte[] getSourceTileData(int x, int y) throws IOException { // need to resize the tile final BufferedImage tile = mapDlTileProvider.getTileImage(x, y); graphics.drawImage(tile, 0, 0, null); buffer.reset(); writer.initialize(); writer.processImage(tileImage, buffer); byte[] processedTileData = buffer.toByteArray(); buffer.reset(); return processedTileData; } @Override public void dispose() { buffer.reset(); graphics.dispose(); } } }