de.nx42.maps4cim.map.texture.osm.OsmHash.java Source code

Java tutorial

Introduction

Here is the source code for de.nx42.maps4cim.map.texture.osm.OsmHash.java

Source

/**
 * maps4cim - a real world map generator for CiM 2
 * Copyright 2013 - 2014 Sebastian Straub
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.nx42.maps4cim.map.texture.osm;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;

import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

import org.apache.commons.codec.binary.Base64;

import com.github.jinahya.io.bit.BitInput;
import com.github.jinahya.io.bit.BitOutput;

import de.nx42.maps4cim.config.texture.osm.EntityDef;
import de.nx42.maps4cim.map.Cache;
import de.nx42.maps4cim.map.ex.TextureProcessingException;
import de.nx42.maps4cim.util.Compression;
import de.nx42.maps4cim.util.gis.Area;

/**
 * A hash code for a OSM XML dataset. Consists of two individual hashes, one
 * for the bounding box (reversible) and one for the selected entities
 * (irreversible).
 */
public class OsmHash {

    protected static final HashFunction hf = Hashing.md5();
    protected static final Cache cache = new Cache();

    /** precision of a single coordinate (including 1 bit sign */
    protected static final int locationPrecision = 18;
    /** coordinates are converted to int by multiplying with this value */
    protected static final double locationMulti = 720;

    /** the hash code of the location (reversible) */
    protected final String locationHash;
    /** the hash code for the entities (irreversible, except for full query) */
    protected final String entityHash;
    /** is all data for this area downloaded, or only specific entities? */
    protected final boolean fullDataSet;
    /** the computed hash code (combination of location- and entity-hash */
    protected final String queryHash;

    public OsmHash(Area bounds, List<EntityDef> entities, boolean fullDataSet) throws TextureProcessingException {
        this.fullDataSet = fullDataSet;
        this.entityHash = fullDataSet ? "all" : getQueryHashEntities(entities);
        try {
            this.locationHash = getQueryHashLocation(bounds);
            this.queryHash = locationHash + "-" + entityHash;
        } catch (IOException e) {
            throw new TextureProcessingException("Unexpected Exception: Could not generate hash for this location",
                    e);
        }
    }

    public OsmHash(Area bounds, List<EntityDef> entities) throws TextureProcessingException {
        this(bounds, entities, false);
    }

    public OsmHash(Area bounds) throws TextureProcessingException {
        this(bounds, null, true);
    }

    // ----- getters -----

    /**
     * @return the queryHash
     */
    public String getQueryHash() {
        return queryHash;
    }

    /**
     * @return the fullDataSet
     */
    public boolean hasFullDataSet() {
        return fullDataSet;
    }

    public Area getHashedLocation() throws IOException {
        return parseLocationHash(locationHash);
    }

    // ----- actual calculations -----

    protected static String getQueryHashLocation(Area bounds) throws IOException {

        /*
         * - base64 encode, every char holds 6 bits (2^6 = 64)
         * - 3 chars per float = 18bit precision = enough for 3 sigificant digits
         *   for numbers <256 (which is the case in WGS84)
         * - 4 values -> 12 chars. 72bit or 8 byte of information
         * - use URL safe encoding, or file name failures are expected!
         */

        // calculate size:  4 values, 8 bits per byte
        int bufSize = (int) Math.ceil(4 * locationPrecision / 8.0);

        ByteBuffer byteBuf = ByteBuffer.allocate(bufSize);
        BitOutput bitOut = BitOutput.newInstance(byteBuf); // direct

        storeCoordinate(bounds.getMinLat(), bitOut);
        storeCoordinate(bounds.getMaxLat(), bitOut);
        storeCoordinate(bounds.getMinLon(), bitOut);
        storeCoordinate(bounds.getMaxLon(), bitOut);

        // get array, return as Base64 (URL safe)
        byte[] ar = byteBuf.array();
        return Base64.encodeBase64URLSafeString(ar);

    }

    protected static Area parseLocationHash(String locationHash) throws IOException {
        byte[] base64decode = Base64.decodeBase64(locationHash);
        ByteBuffer byteBuf = ByteBuffer.wrap(base64decode);
        final BitInput bitIn = BitInput.newInstance(byteBuf);

        double minLat = restoreCoordinate(bitIn);
        double maxLat = restoreCoordinate(bitIn);
        double minLon = restoreCoordinate(bitIn);
        double maxLon = restoreCoordinate(bitIn);

        return new Area(minLat, minLon, maxLat, maxLon);
    }

    protected static void storeCoordinate(double coordinate, BitOutput out) throws IOException {
        out.writeInt(locationPrecision, (int) Math.round(coordinate * locationMulti));
    }

    protected static double restoreCoordinate(BitInput in) throws IOException {
        return in.readInt(locationPrecision) / locationMulti;
    }

    protected static String getQueryHashEntities(List<EntityDef> entities) {
        Hasher h = hf.newHasher();

        // add query
        for (EntityDef def : entities) {
            h.putInt(def.hashCode());
        }

        // get hash, cap length
        byte[] hash = h.hash().asBytes();
        if (hash.length > 4) {
            hash = Arrays.copyOfRange(hash, 0, 3);
        }

        // return hash, max 4 byte (6 chars)
        return Base64.encodeBase64URLSafeString(hash);
    }

    // ----- cache -----

    public boolean isCached() {
        return cache.has(getCacheFileName(queryHash));
    }

    protected File getCached() throws IOException {
        File zipped = cache.get(getCacheFileName(queryHash));
        File unzipped = Cache.temporaray(getXmlFileName(queryHash));
        return Compression.readFirstZipEntry(zipped, unzipped);
    }

    protected String getXmlFileName() {
        return getXmlFileName(queryHash);
    }

    /**
     * Stores the file with a qualified file name and the given hash in the
     * user's cache directory, so it can be later retrieved again.
     * The file will be zipped.
     * @param xml the osm xml file to cache
     * @param hash the hash code that identifies this file
     * @throws IOException if the file cannot be moved to the cache
     */
    protected void storeInCache(File xml) throws IOException {
        File zip = cache.allocate(getCacheFileName(queryHash));
        Compression.storeAsZip(xml, zip);
        cache.moveToCache(zip, true);
    }

    protected static String getXmlFileName(String hash) {
        return "osm-" + hash + ".xml";
    }

    protected static String getCacheFileName(String hash) {
        return "osm-" + hash + ".xml.zip";
    }

    // ----- object overrides -----

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return queryHash.hashCode();
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OsmHash other = (OsmHash) obj;
        if (queryHash == null) {
            if (other.queryHash != null)
                return false;
        } else if (!queryHash.equals(other.queryHash))
            return false;
        return true;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return queryHash;
    }

}