/*******************************************************************************
* 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.io.File;
import java.io.IOException;
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.LayerInterface;
import mobac.program.interfaces.MapInterface;
import mobac.program.interfaces.MapSource;
import mobac.program.model.TileImageParameters;
import mobac.program.model.TileImageParameters.Name;
import mobac.utilities.Utilities;
@AtlasCreatorName("CacheBox (PACK)")
@SupportedParameters(names = { Name.format })
public class CacheBox extends AtlasCreator {
private File packFile = null;
private RandomAccessFile packRaFile = null;
private MapInfo[] mapInfos;
private int nextMapOffsetIndex = 0;
private MapInfo activeMapInfo;
@Override
protected void testAtlas() throws AtlasTestException {
for (LayerInterface layer : atlas) {
if (layer.getMapCount() == 0)
throw new AtlasTestException("Empty layers are not allowed", layer);
Class<? extends MapSource> mapSourceClass = layer.getMap(0).getMapSource().getClass();
for (MapInterface map : layer) {
if (!mapSourceClass.equals(map.getMapSource().getClass()))
throw new AtlasTestException(
"Different map sources are not allowed within one layer", map);
}
}
}
@Override
public void finishAtlasCreation() throws IOException {
}
@Override
public void initLayerCreation(LayerInterface layer) throws IOException {
nextMapOffsetIndex = 0;
packFile = new File(atlasDir, layer.getName() + ".pack");
if (packFile.exists())
Utilities.deleteFile(packFile);
packRaFile = new RandomAccessFile(packFile, "rw");
/*
* We use the mapsource name as layer name. See feature request #2987674
* for details.
*/
writeString(layer.getMap(0).getMapSource().getName(), 32); // layer
// name
writeString(layer.getName(), 128); // layer friendly name
writeString("", 256); // layer url - unused
writeLong(0); // int64 ticks
int mapCount = layer.getMapCount();
writeInt(mapCount); // int32 number of bounding boxes / maps
long offset = 32 + 128 + 256 + 8 + 4 + 8; // = 436
offset += mapCount * 28;
mapInfos = new MapInfo[mapCount + 1];
int i = 0;
for (MapInterface map : layer) {
// For each map:
int minX = map.getMinTileCoordinate().x / 256;
int minY = map.getMinTileCoordinate().y / 256;
int maxX = map.getMaxTileCoordinate().x / 256;
int maxY = map.getMaxTileCoordinate().y / 256;
int tilesInMap = (maxX - minX + 1) * (maxY - minY + 1);
writeInt(map.getZoom()); // int32 zoom
writeInt(minX); // int32 minX
writeInt(maxX); // int32 maxX
writeInt(minY); // int32 minY
writeInt(maxY); // int32 maxY
writeLong(offset); // int64 offset to mapIndexTable
mapInfos[i++] = new MapInfo(map, offset, tilesInMap, minX, minY, maxX, maxY);
log.trace(String.format("Offset to index table [%d]: 0x%X", i, offset));
offset += tilesInMap * 8;
}
// We need to keep the offset to the last index table
// -> required for index table finalization.
mapInfos[i] = new MapInfo(null, offset, 0, 0, 0, 0, 0);
log.trace(String.format("End of bounding boxes table: 0x%X", packRaFile.getFilePointer()));
packRaFile.seek(offset);
log.trace(String.format("Start of tile data: 0x%X", packRaFile.getFilePointer()));
}
@Override
public void initializeMap(MapInterface map, TileProvider mapTileProvider) {
super.initializeMap(map, mapTileProvider);
TileImageParameters param = map.getParameters();
if (param != null)
mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, param.getFormat());
activeMapInfo = mapInfos[nextMapOffsetIndex++];
if (!activeMapInfo.map.equals(map))
throw new RuntimeException("Map does not match offset info!");
// Just to make sure we use the xy values from mapInfo
xMin = activeMapInfo.minX;
xMax = activeMapInfo.maxX;
yMin = activeMapInfo.minY;
yMax = activeMapInfo.maxY;
}
@Override
public void createMap() throws MapCreationException, InterruptedException {
createTiles();
}
protected void createTiles() throws InterruptedException, MapCreationException {
atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1));
ImageIO.setUseCache(false);
int offsetIndex = 0;
long[] offsets = new long[activeMapInfo.tileCount];
try {
for (int y = yMin; y <= yMax; y++) {
for (int x = xMin; x <= xMax; x++) {
checkUserAbort();
atlasProgress.incMapCreationProgress();
byte[] sourceTileData = mapDlTileProvider.getTileData(x, y);
offsets[offsetIndex++] = packRaFile.getFilePointer();
if (sourceTileData != null) {
packRaFile.write(sourceTileData);
}
}
}
long pos = packRaFile.getFilePointer();
// Write the offsets of all tiles in this map to the correspondent
// offset index table
// Due to a bug in CacheBox we have to subtract 8 from the offset
packRaFile.seek(activeMapInfo.indexTableOffset - 8);
for (long tileoffset : offsets)
writeLong(tileoffset);
packRaFile.seek(pos);
} catch (IOException e) {
throw new MapCreationException(map, e);
}
}
@Override
public void finishLayerCreation() throws IOException {
long tableOffset = mapInfos[mapInfos.length - 1].indexTableOffset;
long offset = packRaFile.getFilePointer();
// Due to a bug in CacheBox we have to subtract 8 from the offset
packRaFile.seek(tableOffset - 8);
// write the offset to the end of the file (after the last image)
// required by CacheBox for length calculation of the last tile
packRaFile.writeLong(swapLong(offset));
mapInfos = null;
packFile = null;
packRaFile.close();
packRaFile = null;
}
@Override
public void abortAtlasCreation() throws IOException {
mapInfos = null;
Utilities.closeFile(packRaFile);
packRaFile = null;
if (packFile != null)
Utilities.deleteFile(packFile);
packFile = null;
}
@Override
public boolean testMapSource(MapSource mapSource) {
return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace());
}
private void writeString(String text, int length) throws IOException {
byte[] buf = new byte[length];
byte[] asciiBytes = text.getBytes("ASCII");
System.arraycopy(asciiBytes, 0, buf, 0, Math.min(length, asciiBytes.length));
for (int i = asciiBytes.length; i < length; i++)
buf[i] = ' ';
packRaFile.write(buf);
}
private void writeInt(int v) throws IOException {
packRaFile.writeInt(swapInt(v));
}
private void writeLong(long v) throws IOException {
packRaFile.writeLong(swapLong(v));
}
public final static int swapInt(int v) {
return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00);
}
public final static long swapLong(long v) {
long b1 = (v >> 0) & 0xff;
long b2 = (v >> 8) & 0xff;
long b3 = (v >> 16) & 0xff;
long b4 = (v >> 24) & 0xff;
long b5 = (v >> 32) & 0xff;
long b6 = (v >> 40) & 0xff;
long b7 = (v >> 48) & 0xff;
long b8 = (v >> 56) & 0xff;
return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0;
}
private class MapInfo {
final MapInterface map;
final long indexTableOffset;
final int tileCount;
final int minX;
final int minY;
final int maxX;
final int maxY;
public MapInfo(MapInterface map, long indexOffset, int tileCount, int minX, int minY,
int maxX, int maxY) {
super();
this.map = map;
this.indexTableOffset = indexOffset;
this.tileCount = tileCount;
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
}
}
}