/*******************************************************************************
* 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.impl.gemf;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.TreeSet;
import org.apache.log4j.Logger;
/**
* GEMF File creator class.
*
* Reference about GEMF format: https://sites.google.com/site/abudden/android-map-store
*
* @author A. S. Budden
* @author Erik Burrows
*
* This class is a stripped-down version of the GEMFFile.java class at:
* http://code.google.com/p/osmdroid/source/
* browse/trunk/osmdroid-android/src/main/java/org/osmdroid/util/GEMFFile.java
*
* (Date: Wed, 11th of April 2012)
*
* The original GEMFFile.java from above has been reduced to the functionality of writing a GEMF archive, as
* reading the archive seems not to be necessary.
*
* @author M. Reiter
*
*/
public class GEMFFileCreator {
private static final long FILE_SIZE_LIMIT = 1 * 1024 * 1024 * 1024; // 1GB
private static final int FILE_COPY_BUFFER_SIZE = 1024;
private static final int VERSION = 4;
private static final int TILE_SIZE = 256;
private static final int U32_SIZE = 4;
private static final int U64_SIZE = 8;
/*
* Constructor to create new GEMF file from directory of sources/tiles.
*
* @param pLocation String object representing path to first GEMF archive file. Additional files (if archive size
* exceeds FILE_SIZE_LIMIT will be created with numerical suffixes, eg: test.gemf-1, test.gemf-2.
*
* @param pSourceFolders Each specified folder will be imported into the GEMF archive as a seperate source. The name
* of the folder will be the name of the source in the archive.
*/
public GEMFFileCreator(final String pLocation, final List<File> pSourceFolders, Logger log) throws FileNotFoundException,
IOException {
/**
* <pre>
* 1. For each source folder
* 1. Create array of zoom levels, X rows, Y rows
* 2. Build index data structure index[source][zoom][range]
* 1. For each S-Z-X find list of Ys values
* 2. For each S-Z-X-Ys set, find complete X ranges
* 3. For each S-Z-Xr-Ys set, find complete Y ranges, create Range record
* 3. Write out index
* 1. Header
* 2. Sources
* 3. For each Range
* 1. Write Range record
* 4. For each Range record
* 1. For each Range entry
* 1. If over file size limit, start new data file
* 2. Write tile data
* </pre>
*/
// this.mLocation = pLocation;
// Create in-memory array of sources, X and Y values.
final LinkedHashMap<String, LinkedHashMap<Integer, LinkedHashMap<Integer, LinkedHashMap<Integer, File>>>> dirIndex = new LinkedHashMap<String, LinkedHashMap<Integer, LinkedHashMap<Integer, LinkedHashMap<Integer, File>>>>();
for (final File sourceDir : pSourceFolders) {
final LinkedHashMap<Integer, LinkedHashMap<Integer, LinkedHashMap<Integer, File>>> zList = new LinkedHashMap<Integer, LinkedHashMap<Integer, LinkedHashMap<Integer, File>>>();
for (final File zDir : sourceDir.listFiles()) {
// Make sure the directory name is just a number
try {
Integer.parseInt(zDir.getName());
} catch (final NumberFormatException e) {
continue;
}
final LinkedHashMap<Integer, LinkedHashMap<Integer, File>> xList = new LinkedHashMap<Integer, LinkedHashMap<Integer, File>>();
for (final File xDir : zDir.listFiles()) {
// Make sure the directory name is just a number
try {
Integer.parseInt(xDir.getName());
} catch (final NumberFormatException e) {
continue;
}
final LinkedHashMap<Integer, File> yList = new LinkedHashMap<Integer, File>();
for (final File yFile : xDir.listFiles()) {
try {
Integer.parseInt(yFile.getName().substring(0, yFile.getName().indexOf('.')));
} catch (final NumberFormatException e) {
continue;
}
yList.put(Integer.parseInt(yFile.getName().substring(0, yFile.getName().indexOf('.'))), yFile);
}
xList.put(new Integer(xDir.getName()), yList);
}
zList.put(Integer.parseInt(zDir.getName()), xList);
}
dirIndex.put(sourceDir.getName(), zList);
}
// Create a source index list
final LinkedHashMap<String, Integer> sourceIndex = new LinkedHashMap<String, Integer>();
final LinkedHashMap<Integer, String> indexSource = new LinkedHashMap<Integer, String>();
int si = 0;
for (final String source : dirIndex.keySet()) {
sourceIndex.put(source, new Integer(si));
indexSource.put(new Integer(si), source);
++si;
}
// Create the range objects
final List<GEMFRange> ranges = new ArrayList<GEMFRange>();
for (final String source : dirIndex.keySet()) {
for (final Integer zoom : dirIndex.get(source).keySet()) {
// Get non-contiguous Y sets for each Z/X
final LinkedHashMap<List<Integer>, List<Integer>> ySets = new LinkedHashMap<List<Integer>, List<Integer>>();
for (final Integer x : new TreeSet<Integer>(dirIndex.get(source).get(zoom).keySet())) {
final List<Integer> ySet = new ArrayList<Integer>();
for (final Integer y : dirIndex.get(source).get(zoom).get(x).keySet()) {
ySet.add(y);
}
if (ySet.size() == 0) {
continue;
}
Collections.sort(ySet);
if (!ySets.containsKey(ySet)) {
ySets.put(ySet, new ArrayList<Integer>());
}
ySets.get(ySet).add(x);
}
// For each Y set find contiguous X sets
final LinkedHashMap<List<Integer>, List<Integer>> xSets = new LinkedHashMap<List<Integer>, List<Integer>>();
for (final List<Integer> ySet : ySets.keySet()) {
final TreeSet<Integer> xList = new TreeSet<Integer>(ySets.get(ySet));
List<Integer> xSet = new ArrayList<Integer>();
for (int i = xList.first(); i < xList.last() + 1; ++i) {
if (xList.contains(new Integer(i))) {
xSet.add(new Integer(i));
} else {
if (xSet.size() > 0) {
xSets.put(ySet, xSet);
xSet = new ArrayList<Integer>();
}
}
}
if (xSet.size() > 0) {
xSets.put(ySet, xSet);
}
}
// For each contiguous X set, find contiguous Y sets and create GEMFRange object
for (final List<Integer> xSet : xSets.keySet()) {
final TreeSet<Integer> yList = new TreeSet<Integer>(xSet);
final TreeSet<Integer> xList = new TreeSet<Integer>(ySets.get(xSet));
GEMFRange range = new GEMFRange();
range.zoom = zoom;
range.sourceIndex = sourceIndex.get(source);
range.xMin = xList.first();
range.xMax = xList.last();
for (int i = yList.first(); i < yList.last() + 1; ++i) {
if (yList.contains(new Integer(i))) {
if (range.yMin == null) {
range.yMin = i;
}
range.yMax = i;
} else {
if (range.yMin != null) {
ranges.add(range);
range = new GEMFRange();
range.zoom = zoom;
range.sourceIndex = sourceIndex.get(source);
range.xMin = xList.first();
range.xMax = xList.last();
}
}
}
if (range.yMin != null) {
ranges.add(range);
}
}
}
}
// Calculate size of header for computation of data offsets
int source_list_size = 0;
for (final String source : sourceIndex.keySet()) {
source_list_size += (U32_SIZE + U32_SIZE + source.length());
}
long offset = U32_SIZE + // GEMF Version
U32_SIZE + // Tile size
U32_SIZE + // Number of sources
source_list_size + ranges.size() * ((U32_SIZE * 6) + U64_SIZE) + U32_SIZE; // Number of ranges
// Calculate offset for each range in the data set
for (final GEMFRange range : ranges) {
range.offset = offset;
for (int x = range.xMin; x < range.xMax + 1; ++x) {
for (int y = range.yMin; y < range.yMax + 1; ++y) {
offset += (U32_SIZE + U64_SIZE);
}
}
}
final long headerSize = offset;
RandomAccessFile gemfFile = new RandomAccessFile(pLocation, "rw");
// Write version header
gemfFile.writeInt(VERSION);
// Write file size header
gemfFile.writeInt(TILE_SIZE);
// Write number of sources
gemfFile.writeInt(sourceIndex.size());
// Write source list
for (final String source : sourceIndex.keySet()) {
gemfFile.writeInt(sourceIndex.get(source));
gemfFile.writeInt(source.length());
gemfFile.write(source.getBytes());
}
// Write number of ranges
gemfFile.writeInt(ranges.size());
// Write range objects
for (final GEMFRange range : ranges) {
gemfFile.writeInt(range.zoom);
gemfFile.writeInt(range.xMin);
gemfFile.writeInt(range.xMax);
gemfFile.writeInt(range.yMin);
gemfFile.writeInt(range.yMax);
gemfFile.writeInt(range.sourceIndex);
gemfFile.writeLong(range.offset);
}
// Write file offset list
for (final GEMFRange range : ranges) {
for (int x = range.xMin; x < range.xMax + 1; ++x) {
for (int y = range.yMin; y < range.yMax + 1; ++y) {
gemfFile.writeLong(offset);
long fileSize = 0;
try {
fileSize = dirIndex.get(indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y).length();
} catch (NullPointerException e) {
//dont' do anything here. Error will be logged later.
}
gemfFile.writeInt((int) fileSize);
offset += fileSize;
}
}
}
//
// Write tiles
//
final byte[] buf = new byte[FILE_COPY_BUFFER_SIZE];
long currentOffset = headerSize;
int fileIndex = 0;
for (final GEMFRange range : ranges) {
for (int x = range.xMin; x < range.xMax + 1; ++x) {
for (int y = range.yMin; y < range.yMax + 1; ++y) {
long fileSize = 0;
try {
fileSize = dirIndex.get(indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y).length();
} catch (NullPointerException e) {
//don't do anything here. Error will be logged later.
}
if (currentOffset + fileSize > FILE_SIZE_LIMIT) {
gemfFile.close();
++fileIndex;
gemfFile = new RandomAccessFile(pLocation + "-" + fileIndex, "rw");
currentOffset = 0;
} else {
currentOffset += fileSize;
}
try {
final FileInputStream tile = new FileInputStream(dirIndex.get(indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y));
int read = tile.read(buf, 0, FILE_COPY_BUFFER_SIZE);
while (read != -1) {
gemfFile.write(buf, 0, read);
read = tile.read(buf, 0, FILE_COPY_BUFFER_SIZE);
}
tile.close();
} catch (Exception e) {
log.warn("Please check that all required Tiles have been downloaded correctly. I am missing tile for x=" + x + ", y=" + y + ", z=" + range.zoom);
}
}
}
}
gemfFile.close();
// Complete construction of GEMFFile object
// openFiles();
// readHeader();
}
// Class to represent a range of stored tiles within the archive.
private class GEMFRange {
Integer zoom;
Integer xMin;
Integer xMax;
Integer yMin;
Integer yMax;
Integer sourceIndex;
Long offset;
@Override
public String toString() {
return String.format("GEMF Range: source=%d, zoom=%d, x=%d-%d, y=%d-%d, offset=0x%08X", sourceIndex, zoom,
xMin, xMax, yMin, yMax, offset);
}
};
}