/******************************************************************************* * 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/>. ******************************************************************************/ /* * add to mobac.program.model.AtlasOutputFormat.java * * import mobac.program.atlascreators.TwoNavRmap; * TwoNavRMAP("TwoNav (RMAP)", TwoNavRmap.class), // * */ package mobac.program.atlascreators; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Locale; 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.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.TileImageFormat; import mobac.program.model.TileImageParameters.Name; import mobac.program.model.TileImageType; import mobac.program.tiledatawriter.TileImageJpegDataWriter; import mobac.utilities.Utilities; import org.apache.log4j.Level; /** * * Creates one RMAP file per layer. * * @author Luka Logar * @author r_x * */ @AtlasCreatorName(value = "TwoNav (RMAP)") @SupportedParameters(names = { Name.format }) public class TwoNavRMAP extends AtlasCreator { private RmapFile rmapFile = null; private class ZoomLevel { private int index = 0; private long offset = 0; private int width = 0; private int height = 0; private int xTiles = 0; private int yTiles = 0; private long jpegOffsets[][] = null; private int zoom = 0; private boolean dl = false; private void writeHeader() throws IOException { if (offset == 0) { offset = rmapFile.getFilePointer(); } else { rmapFile.seek(offset); } log.trace(String.format("Writing ZoomLevel %d (%dx%d pixels, %dx%d tiles) header at offset %d", index, width, height, xTiles, yTiles, offset)); rmapFile.writeIntI(width); rmapFile.writeIntI(-height); rmapFile.writeIntI(xTiles); rmapFile.writeIntI(yTiles); if (jpegOffsets == null) { jpegOffsets = new long[xTiles][yTiles]; } for (int y = 0; y < yTiles; y++) { for (int x = 0; x < xTiles; x++) { rmapFile.writeLongI(jpegOffsets[x][y]); } } } private BufferedImage loadJpegAtOffset(long offset) throws IOException { if (offset == 0) { throw new IOException("offset == 0"); } rmapFile.seek(offset); int TagId = rmapFile.readIntI(); if (TagId != 7) { throw new IOException("TagId != 7"); } int TagLen = rmapFile.readIntI(); byte[] jpegImageBuf = new byte[TagLen]; rmapFile.readFully(jpegImageBuf); ByteArrayInputStream input = new ByteArrayInputStream(jpegImageBuf); return ImageIO.read(input); } private byte[] getTileData(TileImageDataWriter writer, ZoomLevel source, int x, int y) throws IOException { log.trace(String.format("Shrinking jpegs (%d,%d,%d - %d,%d,%d)", source.index, x, y, source.index, (x + 1 < source.xTiles) ? x + 1 : x, (y + 1 < source.yTiles) ? y + 1 : y)); BufferedImage bi11 = loadJpegAtOffset(source.jpegOffsets[x][y]); BufferedImage bi21 = (x + 1 < source.xTiles) ? loadJpegAtOffset(source.jpegOffsets[x + 1][y]) : null; BufferedImage bi12 = (y + 1 < source.yTiles) ? loadJpegAtOffset(source.jpegOffsets[x][y + 1]) : null; BufferedImage bi22 = (x + 1 < source.xTiles) && (y + 1 < source.yTiles) ? loadJpegAtOffset(source.jpegOffsets[x + 1][y + 1]) : null; int biWidth = bi11.getWidth() + (bi21 != null ? bi21.getWidth() : 0); int biHeight = bi11.getHeight() + (bi12 != null ? bi12.getHeight() : 0); BufferedImage bi = new BufferedImage(biWidth, biHeight, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g = bi.createGraphics(); g.drawImage(bi11, 0, 0, null); if (bi21 != null) { g.drawImage(bi21, bi11.getWidth(), 0, null); } if (bi12 != null) { g.drawImage(bi12, 0, bi11.getHeight(), null); } if (bi22 != null) { g.drawImage(bi22, bi11.getWidth(), bi11.getHeight(), null); } AffineTransformOp op = new AffineTransformOp(new AffineTransform(0.5, 0, 0, 0.5, 0, 0), AffineTransformOp.TYPE_BILINEAR); BufferedImage biOut = new BufferedImage(biWidth / 2, biHeight / 2, BufferedImage.TYPE_3BYTE_BGR); op.filter(bi, biOut); ByteArrayOutputStream buffer = new ByteArrayOutputStream(biOut.getWidth() * biOut.getHeight() * 4); writer.processImage(biOut, buffer); g.dispose(); return buffer.toByteArray(); } private void shrinkFrom(ZoomLevel source) { try { TileImageDataWriter writer = new TileImageJpegDataWriter(0.9); writer.initialize(); writeHeader(); atlasProgress.initMapCreation(xTiles * yTiles); for (int x = 0; x < xTiles; x++) { for (int y = 0; y < yTiles; y++) { checkUserAbort(); atlasProgress.incMapCreationProgress(); jpegOffsets[x][y] = rmapFile.getFilePointer(); byte[] tileData = getTileData(writer, source, 2 * x, 2 * y); rmapFile.seek(jpegOffsets[x][y]); log.trace(String.format("Writing shrunken jpeg (%d,%d,%d) at offset %d", index, x, y, jpegOffsets[x][y])); rmapFile.writeIntI(7); rmapFile.writeIntI(tileData.length); rmapFile.write(tileData); tileData = null; } } } catch (Exception e) { log.error("Failed generating ZoomLevel " + index + ": " + e.getMessage()); } } } private class RmapFile extends RandomAccessFile { private String name = ""; private int width = 0; private int height = 0; private int tileWidth = 0; private int tileHeight = 0; private double longitudeMin = 0; private double longitudeMax = 0; private double latitudeMin = 0; private double latitudeMax = 0; private long mapDataOffset = 0; private ZoomLevel zoomLevels[] = null; private RmapFile(File file) throws FileNotFoundException { super(file, "rw"); this.name = file.getName(); } private int readIntI() throws IOException { int ch1 = this.read(); int ch2 = this.read(); int ch3 = this.read(); int ch4 = this.read(); if ((ch1 | ch2 | ch3 | ch4) < 0) { throw new IOException(); } return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0)); } private void writeIntI(int i) throws IOException { write((i >>> 0) & 0xFF); write((i >>> 8) & 0xFF); write((i >>> 16) & 0xFF); write((i >>> 24) & 0xFF); } private void writeLongI(long l) throws IOException { write((int) (l >>> 0) & 0xFF); write((int) (l >>> 8) & 0xFF); write((int) (l >>> 16) & 0xFF); write((int) (l >>> 24) & 0xFF); write((int) (l >>> 32) & 0xFF); write((int) (l >>> 40) & 0xFF); write((int) (l >>> 48) & 0xFF); write((int) (l >>> 56) & 0xFF); } private void writeHeader() throws IOException { log.trace("Writing rmap header"); if (zoomLevels == null) { throw new IOException("zoomLevels == null"); } seek(0); write("CompeGPSRasterImage".getBytes()); writeIntI(10); writeIntI(7); writeIntI(0); writeIntI(width); writeIntI(-height); writeIntI(24); writeIntI(1); writeIntI(tileWidth); writeIntI(tileHeight); writeLongI(mapDataOffset); writeIntI(0); writeIntI(zoomLevels.length); for (int n = 0; n < zoomLevels.length; n++) { writeLongI(zoomLevels[n].offset); } } private void writeMapInfo() throws IOException { if (mapDataOffset == 0) { mapDataOffset = getFilePointer(); } else { seek(mapDataOffset); } log.trace("Writing MAP data at offset %d" + mapDataOffset); StringBuffer sbMap = new StringBuffer(); sbMap.append("CompeGPS MAP File\r\n"); sbMap.append("<Header>\r\n"); sbMap.append("Version=2\r\n"); sbMap.append("VerCompeGPS=MOBAC\r\n"); sbMap.append("Projection=2,Mercator,\r\n"); sbMap.append("Coordinates=1\r\n"); sbMap.append("Datum=WGS 84\r\n"); sbMap.append("</Header>\r\n"); sbMap.append("<Map>\r\n"); sbMap.append("Bitmap=" + name + "\r\n"); sbMap.append("BitsPerPixel=0\r\n"); sbMap.append(String.format("BitmapWidth=%d\r\n", width)); sbMap.append(String.format("BitmapHeight=%d\r\n", height)); sbMap.append("Type=10\r\n"); sbMap.append("</Map>\r\n"); sbMap.append("<Calibration>\r\n"); String pointLine = "P%d=%d,%d,A,%s,%s\r\n"; DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); df.applyPattern("#0.00000000"); sbMap.append(String.format(pointLine, 0, 0, 0, df.format(longitudeMin), df.format(latitudeMax))); sbMap.append(String.format(pointLine, 1, width - 1, 0, df.format(longitudeMax), df.format(latitudeMax))); sbMap.append(String.format(pointLine, 2, width - 1, height - 1, df.format(longitudeMax), df.format(latitudeMin))); sbMap.append(String.format(pointLine, 3, 0, height - 1, df.format(longitudeMin), df.format(latitudeMin))); sbMap.append("</Calibration>\r\n"); sbMap.append("<MainPolygonBitmap>\r\n"); String polyLine = "M%d=%d,%d\r\n"; sbMap.append(String.format(polyLine, 0, 0, 0)); sbMap.append(String.format(polyLine, 1, width, 0)); sbMap.append(String.format(polyLine, 2, width, height)); sbMap.append(String.format(polyLine, 3, 0, height)); sbMap.append("</MainPolygonBitmap>\r\n"); writeIntI(1); writeIntI(sbMap.length()); write(sbMap.toString().getBytes()); } } // ************************************************************ public TwoNavRMAP() { super(); log.setLevel(Level.TRACE); } @Override protected void testAtlas() throws AtlasTestException { for (LayerInterface layer : atlas) { MapInterface map0 = layer.getMap(0); MapSpace mapSpace0 = map0.getMapSource().getMapSpace(); //double longitudeMin = mapSpace0.cXToLon(map0.getMinTileCoordinate().x, map0.getZoom()); //double longitudeMax = mapSpace0.cXToLon(map0.getMaxTileCoordinate().x + 1, map0.getZoom()); //double latitudeMin = mapSpace0.cYToLat(map0.getMaxTileCoordinate().y + 1, map0.getZoom()); //double latitudeMax = mapSpace0.cYToLat(map0.getMinTileCoordinate().y, map0.getZoom()); Point2D.Double p1 = mapSpace0.cXYToLonLat(map0.getMinTileCoordinate().x, map0.getMinTileCoordinate().y, map0.getZoom()); Point2D.Double p2 = mapSpace0.cXYToLonLat(map0.getMaxTileCoordinate().x + 1, map0.getMaxTileCoordinate().y + 1, map0.getZoom()); double longitudeMin = p1.x; double longitudeMax = p2.x; double latitudeMin = p2.y; double latitudeMax = p1.y; for (int n = 1; n < layer.getMapCount(); n++) { MapInterface mapN = layer.getMap(n); MapSpace mapSpaceN = mapN.getMapSource().getMapSpace(); //double longitudeMinN = mapSpaceN.cXToLon(mapN.getMinTileCoordinate().x, mapN.getZoom()); //double longitudeMaxN = mapSpaceN.cXToLon(mapN.getMaxTileCoordinate().x + 1, mapN.getZoom()); //double latitudeMinN = mapSpaceN.cYToLat(mapN.getMaxTileCoordinate().y + 1, mapN.getZoom()); //double latitudeMaxN = mapSpaceN.cYToLat(mapN.getMinTileCoordinate().y, mapN.getZoom()); p1 = mapSpaceN.cXYToLonLat(mapN.getMinTileCoordinate().x, mapN.getMinTileCoordinate().y, mapN.getZoom()); p2 = mapSpaceN.cXYToLonLat(mapN.getMaxTileCoordinate().x + 1, mapN.getMaxTileCoordinate().y + 1, mapN.getZoom()); double longitudeMinN = p1.x; double longitudeMaxN = p2.x; double latitudeMinN = p2.y; double latitudeMaxN = p1.y; if ((longitudeMin != longitudeMinN) || (longitudeMax != longitudeMaxN) || (latitudeMin != latitudeMinN) || (latitudeMax != latitudeMaxN)) { throw new AtlasTestException("All maps in one layer have to cover the same area!\n" + "Use grid zoom on the lowest zoom level to get an acceptable result."); } for (int m = 0; m < layer.getMapCount(); m++) { if ((mapN.getZoom() == layer.getMap(m).getZoom()) && (m != n)) { throw new AtlasTestException("Several maps with the same zoom level within the same layer " + "are not supported!"); } } } } } public boolean testMapSource(MapSource mapSource) { MapSpace mapSpace = mapSource.getMapSpace(); return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace .getProjectionCategory())); } @Override public void initLayerCreation(LayerInterface layer) throws IOException { if (rmapFile != null) throw new RuntimeException("Layer mismatch - last layer has not been finished correctly!"); super.initLayerCreation(layer); // Logging.configureConsoleLogging(org.apache.log4j.Level.ALL, new SimpleLayout()); rmapFile = new RmapFile(new File(atlasDir, layer.getName() + ".rmap")); int DefaultMap = 0; rmapFile.width = 0; rmapFile.height = 0; for (int n = 0; n < layer.getMapCount(); n++) { int width = layer.getMap(n).getMaxTileCoordinate().x - layer.getMap(n).getMinTileCoordinate().x + 1; int height = layer.getMap(n).getMaxTileCoordinate().y - layer.getMap(n).getMinTileCoordinate().y + 1; if ((width > rmapFile.width) || (height > rmapFile.height)) { rmapFile.width = width; rmapFile.height = height; DefaultMap = n; } } log.trace("rmap width = " + rmapFile.width); log.trace("rmap height = " + rmapFile.height); rmapFile.tileWidth = layer.getMap(DefaultMap).getTileSize().width; rmapFile.tileHeight = layer.getMap(DefaultMap).getTileSize().height; log.trace("rmap tileWidth = " + rmapFile.tileWidth); log.trace("rmap tileHeight = " + rmapFile.tileHeight); MapSpace mapSpace = layer.getMap(DefaultMap).getMapSource().getMapSpace(); //rmapFile.longitudeMin = mapSpace.cXToLon(layer.getMap(DefaultMap).getMinTileCoordinate().x, // layer.getMap(DefaultMap).getZoom()); //rmapFile.longitudeMax = mapSpace.cXToLon(layer.getMap(DefaultMap).getMaxTileCoordinate().x, // layer.getMap(DefaultMap).getZoom()); //rmapFile.latitudeMin = mapSpace.cYToLat(layer.getMap(DefaultMap).getMaxTileCoordinate().y, // layer.getMap(DefaultMap).getZoom()); //rmapFile.latitudeMax = mapSpace.cYToLat(layer.getMap(DefaultMap).getMinTileCoordinate().y, // layer.getMap(DefaultMap).getZoom()); MapInterface map = layer.getMap(DefaultMap); Point2D.Double p1 = mapSpace.cXYToLonLat(map.getMinTileCoordinate().x, map.getMinTileCoordinate().y, map.getZoom()); Point2D.Double p2 = mapSpace.cXYToLonLat(map.getMaxTileCoordinate().x, map.getMaxTileCoordinate().y, map.getZoom()); rmapFile.longitudeMin = p1.x; rmapFile.longitudeMax = p2.x; rmapFile.latitudeMin = p2.y; rmapFile.latitudeMax = p1.y; log.trace("rmap longitudeMin = " + rmapFile.longitudeMin); log.trace("rmap longitudeMax = " + rmapFile.longitudeMax); log.trace("rmap latitudeMin = " + rmapFile.latitudeMin); log.trace("rmap latitudeMax = " + rmapFile.latitudeMax); double width = rmapFile.width; double height = rmapFile.height; int count = 1; while ((width >= 256.0) || (height >= 256.0)) { width = Math.ceil(width / 2.0); height = Math.ceil(height / 2.0); count++; } log.trace("rmap zoomLevels count = " + count); rmapFile.zoomLevels = new ZoomLevel[count]; width = rmapFile.width; height = rmapFile.height; for (int n = 0; n < rmapFile.zoomLevels.length; n++) { rmapFile.zoomLevels[n] = new ZoomLevel(); rmapFile.zoomLevels[n].index = n; rmapFile.zoomLevels[n].width = (int) Math.round(width); rmapFile.zoomLevels[n].height = (int) Math.round(height); rmapFile.zoomLevels[n].xTiles = (int) Math.ceil((double) rmapFile.zoomLevels[n].width / (double) rmapFile.tileWidth); rmapFile.zoomLevels[n].yTiles = (int) Math.ceil((double) rmapFile.zoomLevels[n].height / (double) rmapFile.tileHeight); rmapFile.zoomLevels[n].jpegOffsets = new long[rmapFile.zoomLevels[n].xTiles][rmapFile.zoomLevels[n].yTiles]; rmapFile.zoomLevels[n].zoom = layer.getMap(DefaultMap).getZoom() - n; rmapFile.zoomLevels[n].dl = false; for (int m = 0; m < layer.getMapCount(); m++) { if ((rmapFile.zoomLevels[n].zoom == layer.getMap(m).getZoom())) { rmapFile.zoomLevels[n].dl = true; } } width = Math.ceil(width / 2.0); height = Math.ceil(height / 2.0); } for (int n = 0; n < rmapFile.zoomLevels.length; n++) { log.trace(String.format("zoomLevels[%d] zoom=%d %dx%d pixels, %dx%d tiles %s", rmapFile.zoomLevels[n].index, rmapFile.zoomLevels[n].zoom, rmapFile.zoomLevels[n].width, rmapFile.zoomLevels[n].height, rmapFile.zoomLevels[n].xTiles, rmapFile.zoomLevels[n].yTiles, rmapFile.zoomLevels[n].dl == false ? "calc" : "dl")); } rmapFile.writeHeader(); } public void createMap() throws MapCreationException, InterruptedException { try { int index; for (index = 0; index < rmapFile.zoomLevels.length; index++) { if (rmapFile.zoomLevels[index].zoom == map.getZoom()) { break; } } if (index == rmapFile.zoomLevels.length) { throw new MapCreationException("Map not found in the zoomLevels list", map); } try { rmapFile.zoomLevels[index].writeHeader(); } catch (IOException e) { throw new MapCreationException("rmapFile.zoomLevels[Index].writeHeader() failed: " + e.getMessage(), map, e); } int tilex = 0; int tiley = 0; atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); if ((map.getMapSource().getTileImageType() != TileImageType.JPG) || (map.getParameters() != null)) { // Tiles have to be converted to jpeg format TileImageFormat imageFormat = TileImageFormat.JPEG90; if (map.getParameters() != null) imageFormat = map.getParameters().getFormat(); mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, imageFormat); } ImageIO.setUseCache(false); byte[] emptyTileData = Utilities.createEmptyTileData(mapSource); for (int x = xMin; x <= xMax; x++) { tiley = 0; for (int y = yMin; y <= yMax; y++) { checkUserAbort(); atlasProgress.incMapCreationProgress(); try { // Remember offset to tile rmapFile.zoomLevels[index].jpegOffsets[tilex][tiley] = rmapFile.getFilePointer(); byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); if (sourceTileData != null) { rmapFile.writeIntI(7); rmapFile.writeIntI(sourceTileData.length); rmapFile.write(sourceTileData); } else { log.trace(String.format("Tile x=%d y=%d not found in tile archive - creating default", tilex, tiley)); rmapFile.writeIntI(7); rmapFile.writeIntI(emptyTileData.length); rmapFile.write(emptyTileData); } } catch (IOException e) { throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); } tiley++; } tilex++; } } catch (MapCreationException e) { throw e; } catch (Exception e) { throw new MapCreationException(map, e); } } @Override public void abortAtlasCreation() throws IOException { try { rmapFile.setLength(0); } finally { Utilities.closeFile(rmapFile); } super.abortAtlasCreation(); } @Override public void finishLayerCreation() throws IOException { try { for (int n = 0; n < rmapFile.zoomLevels.length; n++) { if (rmapFile.zoomLevels[n].offset == 0) { if (n == 0) { throw new IOException("Missing top level map"); } rmapFile.zoomLevels[n].shrinkFrom(rmapFile.zoomLevels[n - 1]); } } rmapFile.writeMapInfo(); rmapFile.writeHeader(); for (int n = 0; n < rmapFile.zoomLevels.length; n++) { rmapFile.zoomLevels[n].writeHeader(); } rmapFile.close(); } catch (IOException e) { log.error("Failed writing rmap file \"" + rmapFile.name + "\": " + e.getMessage(), e); abortAtlasCreation(); throw e; } rmapFile = null; super.finishLayerCreation(); } }