/******************************************************************************* * 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.Dimension; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.Date; import javax.imageio.ImageIO; import mobac.exceptions.MapCreationException; import mobac.mapsources.mapspace.MercatorPower2MapSpace; import mobac.program.ProgramInfo; import mobac.program.annotations.AtlasCreatorName; import mobac.program.annotations.SupportedParameters; import mobac.program.atlascreators.impl.aqm.FlatPackCreator; import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; import mobac.program.atlascreators.tileprovider.TileProvider; import mobac.program.interfaces.LayerInterface; 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.I18nUtils; import mobac.utilities.Utilities; import mobac.utilities.stream.ArrayOutputStream; /** * Creates maps using the AlpineQuestMap atlas format (AQM v2 complient). * * AQM format pack tiles in a unique file using the FlatPack format. Supports multi-layers, tile resizing. * * @author Camille */ @AtlasCreatorName("AlpineQuestMap (AQM)") @SupportedParameters(names = { Name.format, Name.height, Name.width }) public class AlpineQuestMap extends AtlasCreator { public static final String AQM_VERSION = "2"; public static final String AQM_HEADER = "V" + AQM_VERSION + "HEADER"; public static final String AQM_LEVEL = "V" + AQM_VERSION + "LEVEL"; public static final String AQM_LEVEL_DELIMITER = "@LEVEL"; public static final String AQM_END_DELIMITER = "#END"; private static final String[] SCALES = new String[] { "1:512 000 000", // 00 "1:256 000 000", // 01 "1:128 000 000", // 02 "1:64 000 000", // 03 "1:32 000 000", // 04 "1:16 000 000", // 05 "1:8 000 000", // 06 "1:4 000 000", // 07 "1:2 000 000", // 08 "1:1 000 000", // 09 "1:512 000", // 10 "1:256 000", // 11 "1:128 000", // 12 "1:64 000", // 13 "1:32 000", // 14 "1:16 000", // 15 "1:8 000", // 16 "1:4 000", // 17 "1:2 000", // 18 "1:1 000", // 19 "1:512", // 20 "1:128", // 21 "1:64", // 22 "1:32", // 23 "1:16", // 24 "1:8", // 25 "1:4", // 26 "1:2", // 27 "1:1" // 28 }; private FlatPackCreator packCreator = null; private File filePack = null; private double xResizeRatio = 1.0; private double yResizeRatio = 1.0; private int lastZoomLevel = 0; @Override public void initLayerCreation(final LayerInterface layer) throws IOException { super.initLayerCreation(layer); if (layer.getMapCount() > 0) { // create the file this.filePack = new File(atlasDir + "/" + layer.getName() + ".AQM"); this.packCreator = new FlatPackCreator(filePack); this.lastZoomLevel = -1; // add map header this.addMapHeader(layer.getMap(0).getMapSource().toString(), layer.getName()); // add level headers for (int i = 0; i < layer.getMapCount(); i++) { // needed to merge splitted maps due to map size (map split not needed by AQM format) Insets bounds = new Insets(layer.getMap(i).getMinTileCoordinate().y, layer.getMap(i) .getMinTileCoordinate().x, layer.getMap(i).getMaxTileCoordinate().y, layer.getMap(i) .getMaxTileCoordinate().x); // loops over all maps with the same level and add the bounds while (((i + 1) < layer.getMapCount()) && (layer.getMap(i).getZoom() == layer.getMap(i + 1).getZoom())) { i++; bounds.top = Math.min(bounds.top, layer.getMap(i).getMinTileCoordinate().y); bounds.left = Math.min(bounds.left, layer.getMap(i).getMinTileCoordinate().x); bounds.bottom = Math.max(bounds.bottom, layer.getMap(i).getMaxTileCoordinate().y); bounds.right = Math.max(bounds.right, layer.getMap(i).getMaxTileCoordinate().x); } this.addLevelHeader(layer.getMap(i), bounds); } } } @Override public void finishLayerCreation() throws IOException { // add end of file delimiter packCreator.add(new byte[0], AQM_END_DELIMITER); packCreator.close(); packCreator = null; super.finishLayerCreation(); } @Override public void abortAtlasCreation() throws IOException { if (packCreator != null) packCreator.close(); packCreator = null; if (filePack != null) Utilities.deleteFile(filePack); filePack = null; super.abortAtlasCreation(); } private final void addMapHeader(final String strID, final String strName) throws IOException { // version of the AQM format (internal use) final String strVersion = AQM_VERSION; // software used to create the map (displayed to user) final String strSoftware = ProgramInfo.getCompleteTitle(); // date of creation (displayed to user) final String strDate = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); // name of the person that created the map (displayed to user) final String strCreator = ""; StringWriter w = new StringWriter(); w.write("[map]\n"); w.write("id = " + strID + "\n"); w.write("name = " + strName + "\n"); w.write("version = " + strVersion + "\n"); w.write("date = " + strDate + "\n"); w.write("creator = " + strCreator + "\n"); w.write("software = " + strSoftware + "\n"); w.write("\n"); w.flush(); w.close(); // add the metadata file into map packCreator.add(w.getBuffer().toString().getBytes(), AQM_HEADER); } private final void addLevelHeader(final MapInterface map, final Insets bounds) throws IOException { final int tileSize = map.getMapSource().getMapSpace().getTileSize(); final int xMin = bounds.left / tileSize; final int xMax = bounds.right / tileSize; final int yMin = bounds.top / tileSize; final int yMax = bounds.bottom / tileSize; // unique identifier for a data source / zoom (internal use) final String strID = new DecimalFormat("00").format(map.getZoom()); // name of this specific map (displayed to user) String strName = map.getLayer().getName(); if (strName == null || strName.length() == 0) strName = I18nUtils.localizedStringForKey("Unnamed"); // scale of the map (displayed to user) String strScale = ""; if (map.getZoom() >= 0 && map.getZoom() < SCALES.length) strScale = SCALES[map.getZoom()]; // source of the map data (displayed to user) final String strDataSource = map.getMapSource().toString(); // copyright of map data (displayed to user) final String strCopyright = map.getMapSource().toString(); // projection of tiles (internal use) final String strProjection = "mercator"; String strGeoid = ""; if (ProjectionCategory.SPHERE.equals(map.getMapSource().getMapSpace().getProjectionCategory())) strGeoid = "sphere"; else if (ProjectionCategory.ELLIPSOID.equals(map.getMapSource().getMapSpace().getProjectionCategory())) strGeoid = "wgs84"; // number of tiles (internal use) final long nbTotalTiles = (256 * Math.round(Math.pow(2, map.getZoom()))) / tileSize; // check resize or resample parameters String strImageFormat = null; Dimension tilesSize = null; if (map.getParameters() != null) { strImageFormat = map.getParameters().getFormat().getFileExt(); tilesSize = map.getParameters().getDimension(); } else { strImageFormat = map.getMapSource().getTileImageType().getFileExt(); tilesSize = map.getTileSize(); } if (strImageFormat != null) strImageFormat = strImageFormat.toUpperCase(); // write metadata StringWriter w = new StringWriter(); w.write("[level]\n"); w.write("id = " + strID + "\n"); w.write("name = " + strName + "\n"); w.write("scale = " + strScale + "\n"); w.write("datasource = " + strDataSource + "\n"); w.write("copyright = " + strCopyright + "\n"); w.write("projection = " + strProjection + "\n"); w.write("geoid = " + strGeoid + "\n"); w.write("xtsize = " + (int) tilesSize.getWidth() + "\n"); w.write("ytsize = " + (int) tilesSize.getHeight() + "\n"); w.write("xtratio = " + (nbTotalTiles / 360.0) + "\n"); w.write("ytratio = " + (nbTotalTiles / 360.0) + "\n"); w.write("xtoffset = " + (nbTotalTiles / 2.0) + "\n"); w.write("ytoffset = " + (nbTotalTiles / 2.0) + "\n"); w.write("xtmin = " + xMin + "\n"); w.write("xtmax = " + xMax + "\n"); w.write("ytmin = " + (nbTotalTiles - yMax) + "\n"); w.write("ytmax = " + (nbTotalTiles - yMin) + "\n"); w.write("background = " + "#FFFFFF" + "\n"); w.write("imgformat = " + strImageFormat + "\n"); w.write("\n"); w.flush(); w.close(); // add the metadata file into map packCreator.add(w.getBuffer().toString().getBytes(), AQM_LEVEL); } @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())); } @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 { try { if (map.getZoom() > lastZoomLevel) { // metadata information at the beginning this.addLevelDelimiter(); this.lastZoomLevel = map.getZoom(); } // add tiles this.addLevelTiles(); } catch (InterruptedException e) { throw e; } catch (Exception e) { throw new MapCreationException(map, e); } } private final void addLevelDelimiter() throws IOException { // add empty level delimiter file packCreator.add(new byte[0], AQM_LEVEL_DELIMITER); } private final void addLevelTiles() throws InterruptedException, MapCreationException { atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); // number of tiles for this zoom level final long nbTotalTiles = (256 * Math.round(Math.pow(2, map.getZoom()))) / tileSize; // tile resizing BufferedImage tileImage = null; Graphics2D graphics = null; ArrayOutputStream buffer = null; TileImageDataWriter writer = null; if ((parameters != null) || (xResizeRatio != 1.0) || (yResizeRatio != 1.0)) { // 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()); ImageIO.setUseCache(false); writer.initialize(); } for (int x = xMin; x <= xMax; x++) { for (int y = yMin; y <= yMax; y++) { checkUserAbort(); atlasProgress.incMapCreationProgress(); try { // retrieve the tile data (already re-sampled if needed) byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); if (sourceTileData != null) { // there is some data if ((graphics != null) && (buffer != null) && (writer != null)) { // need to resize the tile final BufferedImage tile = ImageIO.read(new ByteArrayInputStream(sourceTileData)); graphics.drawImage(tile, 0, 0, null); buffer.reset(); writer.processImage(tileImage, buffer); sourceTileData = buffer.toByteArray(); if (sourceTileData == null) throw new MapCreationException("Image resizing failed.", map); } packCreator.add(sourceTileData, "" + x + "_" + (nbTotalTiles - y)); // y tiles count began by // bottom in AQM } } catch (IOException e) { throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); } } } } }