/******************************************************************************* * 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); } }; }