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

org.geotools.tpk.TPKZoomLevelV2 Maven / Gradle / Ivy

The newest version!
/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2019, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library 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
 *    Lesser General Public License for more details.
 *
 */

package org.geotools.tpk;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * TPKZoomLevel interface implementation for ESRI Compact Cache V2 files
 *
 * 

TPK is a ZIP archive * *

In V2 the bundle index is a incorporated into the same file as the bundle data. The index * entries are each 8-bytes long and have both the tile data length and tile data offset. * *

Also note that the index is stored in row-major order INSTEAD of column-major order! */ public class TPKZoomLevelV2 implements TPKZoomLevel { // pattern isolates the "base" row and column number for each bundle (in hexidecimal) static Pattern PARSE_BUNDLE_NAME = Pattern.compile("^.*/R([0-9a-f]+)C([0-9a-f]+)\\.bundle$"); static int BUNDLE_DIMENSION = 128; // bundle is 128 columns by 128 rows static int INDEX_ENTRY_LENGTH = 8; // each index entry is 8 bytes long in a V2 cache static int DATA_HEADER_LENGTH = 64; // first 64 bytes of V2 bundle data file is header static int HEXADECIMAL = 16; // row and column numbers in bundle name are base-16 encoded // injected via constructor or via "setTPKandEntryMap" private ZipFile theTPK; private Map zipEntryMap; // there are getters for these 5 members private final long zoomLevel; // zoom level number (0 to a max of 24) private long minRow; // minimum row number found in TPK for this zoom level private long maxRow; // maximum row number found in TPK for this zoom level private long minColumn; // minimum col number found in TPK for this zoom level private long maxColumn; // maximum col number found in TPK for this zoom level private long max_row_column; // max value for row and col (2** zoomLevel) -1 private List bundles; // list of bundles for this zoom level // constructor public TPKZoomLevelV2( ZipFile theTPK, // a TPK file is a ZIP file! Map zipEntryMap, // maps file path/name to ZipEntry List bundleNames, // path/names for all bundles in this zoom level long zoomLevel) { this.theTPK = theTPK; this.zipEntryMap = zipEntryMap; this.zoomLevel = zoomLevel; minColumn = Long.MAX_VALUE; maxColumn = Long.MIN_VALUE; minRow = Long.MAX_VALUE; maxRow = Long.MIN_VALUE; // maximum value for row and column at this zoom level max_row_column = (long) (Math.pow(2, zoomLevel) - 1); bundles = new ArrayList<>(); init(bundleNames); } // *********************************************************************** // ** ** // ** Public Methods ** // ** ** // *********************************************************************** /** * On subsequent uses of a TPK file we must use this method to set current references!! * * @param theTPK -- reference to the TPK ZipFile object * @param zipEntryMap -- reference to the ZipEntry mapping object */ @Override public void setTPKandEntryMap(ZipFile theTPK, Map zipEntryMap) { this.theTPK = theTPK; this.zipEntryMap = zipEntryMap; } /** * Read the tile data for the given coverage and return a list of tile objects to the caller. * Note the read order we used is optimized for this version of the content cache (ie we try to * always read forward as much as possible!!) * * @param top -- topmost row of coverage * @param bottom -- bottommost row of coverage * @param left -- leftmost column of coverage * @param right -- rightmost column of coverage * @param format -- format of tile data (PNG, JPEG) * @return -- list of TPKTile objects */ @Override public List getTiles(long top, long bottom, long left, long right, String format) { // first find the tile coverage in the bundle indexes List tiles = makeTileSet(top, bottom, left, right, format); // then sort the tiles into the optimal read order and go read them!! tiles.stream().sorted(new TPKTile.TPKTileSorter()).forEach(this::readTileData); return tiles; } /** * When done with TPK file call this method for each Zoom Level to free up held resources and * remove object references that would prevent garbage collection */ @Override public void releaseResources() { bundles.forEach(TPKBundle::releaseResources); // close open input streams zipEntryMap = null; // don't keep references alive!! theTPK = null; } @Override public long getZoomLevel() { return zoomLevel; } @Override public long getMinRow() { return minRow; } @Override public long getMaxRow() { return maxRow; } @Override public long getMinColumn() { return minColumn; } @Override public long getMaxColumn() { return maxColumn; } // *********************************************************************** // ** ** // ** Private Methods ** // ** ** // *********************************************************************** /** * Given the list of bundle data files for this zoom level, parse them to create Bundle objects * * @param bundleNames -- names of each bundle for zoom level */ private void init(List bundleNames) { for (String bundleName : bundleNames) { Matcher m = PARSE_BUNDLE_NAME.matcher(bundleName); if (m.matches()) { String row_start = m.group(1); String col_start = m.group(2); parseBundle(bundleName, row_start, col_start); } else { throw new RuntimeException("Unable to parse bundle name??"); } } } /** * Do an initial scan of a bundle index to determine the row and column coverage contained in * the bundle and create a bundle object for subsequent access * * @param bundleName -- path/name of the bundle file * @param rowStart -- hex string yields the "base" row number for this bundle * @param colStart -- hex string yields the "base" column number for this bundle */ private void parseBundle(String bundleName, String rowStart, String colStart) { // get the starting row and column number for this bundle/index pair long baseRow = Long.parseLong(rowStart, HEXADECIMAL); long baseColumn = Long.parseLong(colStart, HEXADECIMAL); // as long as row and column are in allowable range ... if (baseRow <= max_row_column && baseColumn <= max_row_column) { TPKBundle bundle = new TPKBundle( bundleName, null, baseColumn, baseRow, this::TPKSupplier, this::zipEntryMapSupplier); long indexReadOffset = DATA_HEADER_LENGTH; // skip first 64 bytes of index for (int row = 0; row < BUNDLE_DIMENSION; row++) { // 128 columns of 128 rows for (int col = 0; col < BUNDLE_DIMENSION; col++) { // calculate row and column this index entry is for long thisRow = baseRow + row; long thisColumn = baseColumn + col; // read the index entry and convert it to a data file offset TPKTile.TileInfo tileInfo = getTileInfo(bundle, indexReadOffset); // make sure the row and column are "in bounds" for this zoom level if (thisColumn <= max_row_column && thisRow <= max_row_column) { thisRow = max_row_column - thisRow; // if the tile is zero then the tile does not exist if (tileInfo.tileLength > 0) { // update min/max values for this bundle bundle.minRow = Math.min(bundle.minRow, thisRow); bundle.maxRow = Math.max(bundle.maxRow, thisRow); bundle.minColumn = Math.min(bundle.minColumn, thisColumn); bundle.maxColumn = Math.max(bundle.maxColumn, thisColumn); } } // quit early?? if (thisRow == max_row_column && thisColumn > max_row_column) { col = BUNDLE_DIMENSION; row = BUNDLE_DIMENSION; } indexReadOffset += INDEX_ENTRY_LENGTH; // next slot in bundle index } } // update min/max values for the zoom level this.minRow = Math.min(this.minRow, bundle.minRow); this.maxRow = Math.max(this.maxRow, bundle.maxRow); this.minColumn = Math.min(this.minColumn, bundle.minColumn); this.maxColumn = Math.max(this.maxColumn, bundle.maxColumn); bundles.add(bundle); } } /** * Read the bundle index for the given set of tiles and return a corresponding list of TPKTile * objects. Note that we read the index in the optimal order for this cache format * * @param top -- topmost row of coverage * @param bottom -- bottommost row of coverage * @param left -- leftmost column of coverage * @param right -- rightmost column of coverage * @param format -- format tile data is in (JPEG, PNG) * @return -- list of TPKTile objects */ private List makeTileSet(long top, long bottom, long left, long right, String format) { TPKBundle bundle = bundles.get(0); int bundleIndex = 0; List tiles = new ArrayList<>(); for (long row = top; row >= bottom; row--) { for (long col = left; col <= right; col++) { // find bundle for tile if (!bundle.inBundle(col, row)) { final long c = col; final long r = row; TPKBundle saveBundle = bundle; bundle = bundles.stream().filter(b -> b.inBundle(c, r)).findFirst().orElse(null); if (bundle == null) { bundle = saveBundle; continue; } bundleIndex = bundles.indexOf(bundle); } // calculate position of the tile index long bundleRow = (max_row_column - row) - bundle.baseRow; long indexReadOffset = DATA_HEADER_LENGTH + ((bundleRow * BUNDLE_DIMENSION) + (col - bundle.baseColumn)) * INDEX_ENTRY_LENGTH; // read the tile index and get the offset to the tile data TPKTile.TileInfo ti = getTileInfo(bundle, indexReadOffset); TPKTile tile = new TPKTile(zoomLevel, col, row, format, ti, bundleIndex); tiles.add(tile); } } return tiles; } /** * For a given TPKTile object read the actual tile data * * @param tile -- input (and output!) TPKTile instance */ private void readTileData(TPKTile tile) { // get the tile's bundle TPKBundle bundle = bundles.get(tile.bundleNum); // read tile data if present byte[] tileData = null; TPKTile.TileInfo ti = tile.tileInfo; if (ti.tileLength > 0) { tileData = bundle.bundleData.read(ti.tileDataOffset, ti.tileLength); } // and set the reference in our TPKTile object tile.setTileData(tileData); } /** * Given a bundle and an bundle index offset, read the 5-byte data pointer in the index and * convert that to a byte-offset in the bundle data file * * @param bundle -- bundle in question * @param indexReadOffset -- offset within the bundle tile index * @return -- the converted data offset "pointer" */ private TPKTile.TileInfo getTileInfo(TPKBundle bundle, long indexReadOffset) { byte[] tileIndex = bundle.bundleData.read(indexReadOffset, INDEX_ENTRY_LENGTH); // convert 5-byte little-endian value into a long long dataOffset = (long) (tileIndex[0] & 0xff) + ((long) (tileIndex[1] & 0xff) << 8) + ((long) (tileIndex[2] & 0xff) << 16) + ((long) (tileIndex[3] & 0xff) << 24) + ((long) (tileIndex[4] & 0xff) << 32); // convert 3-byte little-endian value into an int int dataLength = (tileIndex[5] & 0xff) + ((tileIndex[6] & 0xff) << 8) + ((tileIndex[7] & 0xff) << 16); return new TPKTile.TileInfo(dataLength, dataOffset); } /** * Supplier function injected into TPKBundle objects at construction * * @return -- ref to the current ZipFile object */ private ZipFile TPKSupplier() { return theTPK; } /** * Supplier function injected into TPKBundle objects at construction * * @return -- ref to the current zipEntryMap */ private Map zipEntryMapSupplier() { return zipEntryMap; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy