All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.osmdroid.util.GEMFFile Maven / Gradle / Ivy

The newest version!
package org.osmdroid.util;

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
 *
 * @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 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;

		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);

			return new GEMFInputStream(mFileNames.get(index), dataOffset, dataLength);

		} catch (final java.io.IOException e) {
			return null;
		}
	}


	// ===========================================================
	// 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;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy