/*******************************************************************************
* 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 java.util.ArrayList;
import java.util.Collections;
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.impl.MapTileWriter;
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.Name;
import mobac.utilities.Utilities;
/**
* <a href=
* "http://palmtopia.de/trac/GCLiveMapGen/browser/Reengineering%20of%20the%20Geocaching%20Live%20Tile%20Database.txt?format=txt"
* >File format documentation</a>
*
* <pre>
* ===
* === Reengineering of the Geocaching Live Tile Database ===
* ===
*
* index file:
* ===========
*
* 16 Bytes Header:
* ----------------
*
* 00 00 00 0A Highest index used currently for the data files. Index is incremented starting with 0.
* 00 00 4E 20 Max. number of tiles index. Current max. number is 20000.
* 00 00 01 4D Number of tile entries (16 bytes) indexed.
* 00 01 0C FF Size of the data file with the currently used highest index.
*
* 16 Bytes per tile:
* ------------------
*
* 00 00 Inverse zoom level = 17 - Z.
* 01 0E 16 Number of the X tile.
* 00 B1 0E Number of the Y tile.
* 00 00 50 92 Start offset of the PNG data in the data file.
* 00 9D 6 Size of the PNG data.
* 0 01 Index of the data file which contains the PNG data.
*
* The tiles are sorted starting by zoom level 17 (INVZ = 0).
*
* data file:
* ----------
*
* Max. 32 PNGs concated directly together.
*
*
* [0] - [x] directories:
* ----------------------
*
* Max. 32 data files per directory. The data files are named from 'data0' to 'dataN'.
* </pre>
*/
@AtlasCreatorName("Geocaching Live offline map")
@SupportedParameters(names = { Name.format })
public class GCLive extends AtlasCreator {
private static final int MAX_TILES = 65535;
private MapTileWriter mapTileWriter = null;
@Override
public boolean testMapSource(MapSource mapSource) {
return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace());
}
@Override
protected void testAtlas() throws AtlasTestException {
long tileCount = 0;
for (LayerInterface layer : atlas) {
for (MapInterface map : layer) {
// We can not use map.calculateTilesToDownload() because be need the full tile count not the sparse
int tileSize_t = 256; // Everything else is not allowed
int xMin_t = map.getMinTileCoordinate().x / tileSize_t;
int xMax_t = map.getMaxTileCoordinate().x / tileSize_t;
int yMin_t = map.getMinTileCoordinate().y / tileSize_t;
int yMax_t = map.getMaxTileCoordinate().y / tileSize_t;
tileCount += (xMax_t - xMin_t + 1) * (yMax_t - yMin_t + 1);
}
// Check for max tile count <= 65535
if (tileCount > MAX_TILES)
throw new AtlasTestException("Tile count too high in layer " + layer.getName()
+ "\n - please select smaller/fewer areas");
}
}
@Override
public void initLayerCreation(LayerInterface layer) throws IOException {
super.initLayerCreation(layer);
mapTileWriter = new GCLiveWriter(new File(atlasDir, layer.getName()));
}
@Override
public void finishLayerCreation() throws IOException {
mapTileWriter.finalizeMap();
mapTileWriter = null;
super.finishLayerCreation();
}
@Override
public void abortAtlasCreation() throws IOException {
mapTileWriter.finalizeMap();
mapTileWriter = null;
super.abortAtlasCreation();
}
@Override
public void initializeMap(MapInterface map, TileProvider mapTileProvider) {
super.initializeMap(map, mapTileProvider);
if (parameters != null) {
mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat());
}
}
@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);
//byte[] emptyTileData = Utilities.createEmptyTileData(mapSource);
for (int x = xMin; x <= xMax; x++) {
for (int y = yMin; y <= yMax; y++) {
checkUserAbort();
atlasProgress.incMapCreationProgress();
try {
byte[] sourceTileData = mapDlTileProvider.getTileData(x, y);
if (sourceTileData != null)
mapTileWriter.writeTile(x, y, null, sourceTileData);
// else
// mapTileWriter.writeTile(x, y, null, emptyTileData);
} catch (IOException e) {
throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e);
}
}
}
}
protected class GCLiveWriter implements MapTileWriter {
private File mapDir;
private int dataDirCounter = 0;
private int dataFileCounter = 0;
private int imageCounter = 0;
private RandomAccessFile currentDataFile;
private ArrayList<GCHeaderEntry> headerEntries;
public GCLiveWriter(File mapDir) throws IOException {
super();
this.mapDir = mapDir;
Utilities.mkDir(mapDir);
headerEntries = new ArrayList<GCHeaderEntry>(MAX_TILES);
prepareDataFile();
}
private void prepareDataFile() throws IOException {
if (currentDataFile != null)
Utilities.closeFile(currentDataFile);
currentDataFile = null;
File dataDir = new File(mapDir, Integer.toString(dataDirCounter));
Utilities.mkDir(dataDir);
File dataFile = new File(dataDir, "data" + Integer.toString(dataFileCounter));
currentDataFile = new RandomAccessFile(dataFile, "rw");
imageCounter = 0;
}
public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException {
imageCounter++;
if (imageCounter >= 32) {
dataFileCounter++;
if (dataFileCounter % 32 == 0) {
dataDirCounter++;
if (dataDirCounter >= 32)
throw new RuntimeException("Maximum number of tiles exceeded");
}
prepareDataFile();
}
long offset = currentDataFile.getFilePointer();
currentDataFile.write(tileData);
int len = tileData.length;
GCHeaderEntry header = new GCHeaderEntry(zoom, tilex, tiley, dataFileCounter, (int) offset, len);
headerEntries.add(header);
}
public void finalizeMap() throws IOException {
int dataPos = (int) currentDataFile.getFilePointer();
Utilities.closeFile(currentDataFile);
Collections.sort(headerEntries);
RandomAccessFile indexFile;
indexFile = new RandomAccessFile(new File(mapDir, "index"), "rw");
// Write index header (first 16 bytes)
indexFile.seek(0);
int dataFileIndex = dataDirCounter * 32 + dataFileCounter;
indexFile.writeInt(dataFileIndex); // Highest index used currently for the data files. Index is incremented
// starting with 0.
indexFile.writeInt(headerEntries.size()); // Max. number of tiles index. Current max. number is 20000.
indexFile.writeInt(headerEntries.size()); // Number of tile entries (16 bytes) indexed.
indexFile.writeInt(dataPos); // Size of the data file with the currently used highest index.
for (GCHeaderEntry entry : headerEntries) {
entry.writeHeader(indexFile);
System.out.println(entry);
}
headerEntries = null;
Utilities.closeFile(indexFile);
}
}
public static class GCHeaderEntry implements Comparable<GCHeaderEntry> {
public final int zoom;
public final int tilex;
public final int tiley;
public final int dataFileIndex;
public final int offset;
public final int len;
public GCHeaderEntry(int zoom, int tilex, int tiley, int dataFileIndex, int offset, int len) {
super();
this.zoom = zoom;
this.tilex = tilex;
this.tiley = tiley;
this.dataFileIndex = dataFileIndex;
this.offset = offset;
this.len = len;
}
public void writeHeader(RandomAccessFile file) throws IOException {
file.writeShort((short) (17 - zoom));
file.write((tilex >> 16) & 0xFF);
file.write((tilex >> 8) & 0xFF);
file.write(tilex & 0xFF);
file.write((tiley >> 16) & 0xFF);
file.write((tiley >> 8) & 0xFF);
file.write(tiley & 0xFF);
file.writeInt(offset);
int tmp = (len << 4);
tmp = tmp | ((dataFileIndex >> 8) & 0x0F);
file.write((tmp >> 16) & 0xFF);
file.write((tmp >> 8) & 0xFF);
file.write(tmp & 0xFF);
file.write(dataFileIndex & 0xFF);
}
public int compareTo(GCHeaderEntry o) {
if (zoom > o.zoom)
return -1;
if (zoom < o.zoom)
return 1;
if (tilex > o.tilex)
return 1;
if (tilex < o.tilex)
return -1;
if (tiley > o.tiley)
return 1;
if (tiley < o.tiley)
return -1;
return 0;
}
@Override
public String toString() {
return "GCHeaderEntry [zoom=" + zoom + ", tilex=" + tilex + ", tiley=" + tiley + ", dataFileIndex="
+ dataFileIndex + ", offset=" + offset + ", len=" + len + "]";
}
}
}