org.osmdroid.util.GEMFFile Maven / Gradle / Ivy
Show all versions of OSMMapTilePackager Show documentation
package org.osmdroid.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* GEMF File handler class.
*
* Reference: https://sites.google.com/site/abudden/android-map-store
*
* Do not reference any android specific code in this class, it is reused in the JRE
* Tile Packager
*
* @author A. S. Budden
* @author Erik Burrows
*/
public class GEMFFile {
// ===========================================================
// Constants
// ===========================================================
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;
// ===========================================================
// Fields
// ===========================================================
// Path to first GEMF file (additional files as -1, -2, ...
private final String mLocation;
// All GEMF file parts for this archive
private final List mFiles = new ArrayList();
private final List mFileNames = new ArrayList();
// Tile ranges represented within this archive
private final List mRangeData = new ArrayList();
// File sizes for offset calculation
private final List mFileSizes = new ArrayList();
// List of tile sources within this archive
private final LinkedHashMap mSources = new LinkedHashMap();
// Fields to restrict to a single source for reading
private boolean mSourceLimited = false;
private int mCurrentSource = 0;
// ===========================================================
// Constructors
// ===========================================================
/*
* Constructor to read existing GEMF archive
*
* @param pLocation
* File object representing first GEMF archive file
*/
public GEMFFile(final File pLocation) throws FileNotFoundException, IOException {
this(pLocation.getAbsolutePath());
}
/*
* Constructor to read existing GEMF archive
*
* @param pLocation
* String object representing path to first GEMF archive file
*/
public GEMFFile(final String pLocation) throws FileNotFoundException, IOException {
mLocation = pLocation;
openFiles();
readHeader();
}
/*
* 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 GEMFFile(final String pLocation, final List pSourceFolders)
throws FileNotFoundException, IOException {
/*
* 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
*/
this.mLocation = pLocation;
// Create in-memory array of sources, X and Y values.
final LinkedHashMap>>> dirIndex =
new LinkedHashMap>>>();
for (final File sourceDir : pSourceFolders) {
final LinkedHashMap>> zList =
new LinkedHashMap>>();
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> xList =
new LinkedHashMap>();
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 yList = new LinkedHashMap();
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 sourceIndex = new LinkedHashMap();
final LinkedHashMap indexSource = new LinkedHashMap();
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 ranges = new ArrayList();
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> ySets =
new LinkedHashMap, List>();
for (final Integer x : new TreeSet(dirIndex.get(source).get(zoom).keySet())) {
final List ySet = new ArrayList();
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());
}
ySets.get(ySet).add(x);
}
// For each Y set find contiguous X sets
final LinkedHashMap, List> xSets =
new LinkedHashMap, List>();
for (final List ySet : ySets.keySet()) {
final TreeSet xList = new TreeSet(ySets.get(ySet));
List xSet = new ArrayList();
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();
}
}
}
if (xSet.size() > 0) {
xSets.put(ySet, xSet);
}
}
// For each contiguous X set, find contiguous Y sets and create GEMFRange object
for (final List xSet : xSets.keySet()) {
final TreeSet yList = new TreeSet(xSet);
final TreeSet xList = new TreeSet(ySets.get(xSet));
GEMFRange range = new GEMFFile.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 GEMFFile.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);
final long fileSize = dirIndex.get(
indexSource.get(
range.sourceIndex)).get(range.zoom).get(x).get(y).length();
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) {
final long fileSize = dirIndex.get(
indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y).length();
if (currentOffset + fileSize > FILE_SIZE_LIMIT) {
gemfFile.close();
++fileIndex;
gemfFile = new RandomAccessFile(pLocation + "-" + fileIndex, "rw");
currentOffset = 0;
} else {
currentOffset += fileSize;
}
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();
}
}
}
gemfFile.close();
// Complete construction of GEMFFile object
openFiles();
readHeader();
}
// ===========================================================
// Private Methods
// ===========================================================
/*
* Close open GEMF file handles.
*/
public void close() throws IOException {
for (final RandomAccessFile file : mFiles) {
file.close();
}
}
/*
* Find all files composing this GEMF archive, open them as RandomAccessFile
* and add to the mFiles list.
*/
private void openFiles() throws FileNotFoundException {
// Populate the mFiles array
final File base = new File(mLocation);
mFiles.add(new RandomAccessFile(base, "r"));
mFileNames.add(base.getPath());
int i = 0;
for (; ; ) {
i = i + 1;
final File nextFile = new File(mLocation + "-" + i);
if (nextFile.exists()) {
mFiles.add(new RandomAccessFile(nextFile, "r"));
mFileNames.add(nextFile.getPath());
} else {
break;
}
}
}
/*
* Read header of archive, cache Ranges.
*/
private void readHeader() throws IOException {
final RandomAccessFile baseFile = mFiles.get(0);
// Get file sizes
for (final RandomAccessFile file : mFiles) {
mFileSizes.add(file.length());
}
// Version
final int version = baseFile.readInt();
if (version != VERSION) {
throw new IOException("Bad file version: " + version);
}
// Tile Size
final int tile_size = baseFile.readInt();
if (tile_size != TILE_SIZE) {
throw new IOException("Bad tile size: " + tile_size);
}
// Read Source List
final int sourceCount = baseFile.readInt();
for (int i = 0; i < sourceCount; i++) {
final int sourceIndex = baseFile.readInt();
final int sourceNameLength = baseFile.readInt();
final byte[] nameData = new byte[sourceNameLength];
baseFile.read(nameData, 0, sourceNameLength);
final String sourceName = new String(nameData);
mSources.put(new Integer(sourceIndex), sourceName);
}
// Read Ranges
final int num_ranges = baseFile.readInt();
for (int i = 0; i < num_ranges; i++) {
final GEMFRange rs = new GEMFRange();
rs.zoom = baseFile.readInt();
rs.xMin = baseFile.readInt();
rs.xMax = baseFile.readInt();
rs.yMin = baseFile.readInt();
rs.yMax = baseFile.readInt();
rs.sourceIndex = baseFile.readInt();
rs.offset = baseFile.readLong();
mRangeData.add(rs);
}
}
// ===========================================================
// Public Methods
// ===========================================================
/*
* Returns the base name of the first file in the GEMF archive.
*/
public String getName() {
return mLocation;
}
/*
* Returns a LinkedHashMap of the sources in this archive, as names and indexes.
*/
public LinkedHashMap getSources() {
return mSources;
}
/*
* Set single source for getInputStream() to use. Otherwise, first tile found
* with specified Z/X/Y coordinates will be returned.
*/
public void selectSource(final int pSource) {
if (mSources.containsKey(new Integer(pSource))) {
mSourceLimited = true;
mCurrentSource = pSource;
}
}
/*
* Allow getInputStream() to use any source in the archive.
*/
public void acceptAnySource() {
mSourceLimited = false;
}
/*
* Return list of zoom levels contained within this archive.
*/
public Set getZoomLevels() {
final Set zoomLevels = new TreeSet();
for (final GEMFRange rs : mRangeData) {
zoomLevels.add(rs.zoom);
}
return zoomLevels;
}
/*
* Get an InputStream for the tile data specified by the Z/X/Y coordinates.
*
* @return InputStream of tile data, or null if not found.
*/
public InputStream getInputStream(final int pX, final int pY, final int pZ) {
GEMFRange range = null;
for (final GEMFRange rs : mRangeData) {
if ((pZ == rs.zoom)
&& (pX >= rs.xMin)
&& (pX <= rs.xMax)
&& (pY >= rs.yMin)
&& (pY <= rs.yMax)
&& ((!mSourceLimited) || (rs.sourceIndex == mCurrentSource))) {
range = rs;
break;
}
}
if (range == null) {
return null;
}
long dataOffset;
int dataLength;
InputStream returnValue = null;
GEMFInputStream stream = null;
ByteArrayOutputStream byteBuffer = null;
try {
// Determine offset to requested tile record in the header
final int numY = range.yMax + 1 - range.yMin;
final int xIndex = pX - range.xMin;
final int yIndex = pY - range.yMin;
long offset = (xIndex * numY) + yIndex;
offset *= (U32_SIZE + U64_SIZE);
offset += range.offset;
// Read tile record from header, get offset and size of data record
final RandomAccessFile baseFile = mFiles.get(0);
baseFile.seek(offset);
dataOffset = baseFile.readLong();
dataLength = baseFile.readInt();
// Seek to correct data file and offset.
RandomAccessFile pDataFile = mFiles.get(0);
int index = 0;
if (dataOffset > mFileSizes.get(0)) {
final int fileListCount = mFileSizes.size();
while ((index < (fileListCount - 1)) &&
(dataOffset > mFileSizes.get(index))) {
dataOffset -= mFileSizes.get(index);
index += 1;
}
pDataFile = mFiles.get(index);
}
// Read data block into a byte array
pDataFile.seek(dataOffset);
stream = new GEMFInputStream(mFileNames.get(index), dataOffset, dataLength);
// this dynamically extends to take the bytes you read
byteBuffer = new ByteArrayOutputStream();
// this is storage overwritten on each iteration with bytes
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
// we need to know how may bytes were read to write them to the byteBuffer
int len = 0;
while (stream.available() > 0) {
len = stream.read(buffer);
if (len > 0)
byteBuffer.write(buffer, 0, len);
}
// and then we can return your byte array.
byte[] bits = byteBuffer.toByteArray();
returnValue = new ByteArrayInputStream(bits);
} catch (final java.io.IOException e) {
e.printStackTrace();
} finally {
if (byteBuffer != null)
try {
byteBuffer.close();
} catch (IOException e) {
e.printStackTrace();
}
if (stream != null)
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return returnValue;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
// 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);
}
}
// InputStream class to hand to the tile loader system. It wants an InputStream, and it is more
// efficient to create a new open file handle pointed to the right place, than to buffer the file
// in memory.
class GEMFInputStream extends InputStream {
RandomAccessFile raf;
int remainingBytes;
GEMFInputStream(final String filePath, final long offset, final int length) throws IOException {
this.raf = new RandomAccessFile(filePath, "r");
raf.seek(offset);
this.remainingBytes = length;
}
@Override
public int available() {
return remainingBytes;
}
@Override
public void close() throws IOException {
raf.close();
}
@Override
public boolean markSupported() {
return false;
}
@Override
public int read(final byte[] buffer, final int offset, final int length) throws IOException {
final int read = raf.read(buffer, offset, length > remainingBytes ? remainingBytes : length);
remainingBytes -= read;
return read;
}
@Override
public int read() throws IOException {
if (remainingBytes > 0) {
remainingBytes--;
return raf.read();
} else {
throw new IOException("End of stream");
}
}
@Override
public long skip(final long byteCount) {
return 0;
}
}
}