package btools.codec; import btools.util.ByteDataWriter; /** * a micro-cache is a data cache for an area of some square kilometers or some * hundreds or thousands nodes * * This is the basic io-unit: always a full microcache is loaded from the * data-file if a node is requested at a position not yet covered by the caches * already loaded * * The nodes are represented in a compact way (typical 20-50 bytes per node), * but in a way that they do not depend on each other, and garbage collection is * supported to remove the nodes already consumed from the cache. * * The cache-internal data representation is different from that in the * data-files, where a cache is encoded as a whole, allowing more * redundancy-removal for a more compact encoding */ public class MicroCache extends ByteDataWriter { protected int[] faid; protected int[] fapos; protected int size = 0; private int delcount = 0; private int delbytes = 0; private int p2size; // next power of 2 of size // cache control: a virgin cache can be // put to ghost state for later recovery public boolean virgin = true; public boolean ghost = false; public static boolean debug = false; protected MicroCache( byte[] ab ) { super( ab ); } public final static MicroCache emptyNonVirgin = new MicroCache( null ); static { emptyNonVirgin.virgin = false; } public static MicroCache emptyCache() { return new MicroCache( null ); // TODO: singleton? } protected void init( int size ) { this.size = size; delcount = 0; delbytes = 0; p2size = 0x40000000; while (p2size > size) p2size >>= 1; } public final void finishNode( long id ) { fapos[size] = aboffset; faid[size] = shrinkId( id ); size++; } public final void discardNode() { aboffset = startPos( size ); } public final int getSize() { return size; } public final int getDataSize() { return ab == null ? 0 : ab.length; } /** * Set the internal reader (aboffset, aboffsetEnd) to the body data for the given id * * If a node is not found in an empty cache, this is usually an edge-effect * (data-file does not exist or neighboured data-files of differnt age), * but is can as well be a symptom of a node-identity breaking bug. * * Current implementation always returns false for not-found, however, for * regression testing, at least for the case that is most likely a bug * (node found but marked as deleted = ready for garbage collection * = already consumed) the RunException should be re-enabled * * @return true if id was found */ public final boolean getAndClear( long id64 ) { if ( size == 0 ) { return false; } int id = shrinkId( id64 ); int[] a = faid; int offset = p2size; int n = 0; while (offset > 0) { int nn = n + offset; if ( nn < size && a[nn] <= id ) { n = nn; } offset >>= 1; } if ( a[n] == id ) { if ( ( fapos[n] & 0x80000000 ) == 0 ) { aboffset = startPos( n ); aboffsetEnd = fapos[n]; fapos[n] |= 0x80000000; // mark deleted delbytes += aboffsetEnd - aboffset; delcount++; return true; } else // .. marked as deleted { // throw new RuntimeException( "MicroCache: node already consumed: id=" + id ); } } return false; } protected final int startPos( int n ) { return n > 0 ? fapos[n - 1] & 0x7fffffff : 0; } public final int collect( int threshold ) { if ( delcount <= threshold ) { return 0; } virgin = false; int nsize = size - delcount; if ( nsize == 0 ) { faid = null; fapos = null; } else { int[] nfaid = new int[nsize]; int[] nfapos = new int[nsize]; int idx = 0; byte[] nab = new byte[ab.length - delbytes]; int nab_off = 0; for ( int i = 0; i < size; i++ ) { int pos = fapos[i]; if ( ( pos & 0x80000000 ) == 0 ) { int start = startPos( i ); int end = fapos[i]; int len = end - start; System.arraycopy( ab, start, nab, nab_off, len ); nfaid[idx] = faid[i]; nab_off += len; nfapos[idx] = nab_off; idx++; } } faid = nfaid; fapos = nfapos; ab = nab; } int deleted = delbytes; init( nsize ); return deleted; } public final void unGhost() { ghost = false; delcount = 0; delbytes = 0; for ( int i = 0; i < size; i++ ) { fapos[i] &= 0x7fffffff; // clear deleted flags } } /** * @return the 64-bit global id for the given cache-position */ public final long getIdForIndex( int i ) { int id32 = faid[i]; return expandId( id32 ); } /** * expand a 32-bit micro-cache-internal id into a 64-bit (lon|lat) global-id * * @see #shrinkId */ public long expandId( int id32 ) { throw new IllegalArgumentException( "expandId for empty cache" ); } /** * shrink a 64-bit (lon|lat) global-id into a a 32-bit micro-cache-internal id * * @see #expandId */ public int shrinkId( long id64 ) { throw new IllegalArgumentException( "shrinkId for empty cache" ); } /** * @return true if the given lon/lat position is internal for that micro-cache */ public boolean isInternal( int ilon, int ilat ) { throw new IllegalArgumentException( "isInternal for empty cache" ); } /** * (stasticially) encode the micro-cache into the format used in the datafiles * * @param buffer * byte array to encode into (considered big enough) * @return the size of the encoded data */ public int encodeMicroCache( byte[] buffer ) { throw new IllegalArgumentException( "encodeMicroCache for empty cache" ); } /** * Compare the content of this microcache to another * * @return null if equals, else a diff-report */ public String compareWith( MicroCache mc ) { String msg = _compareWith( mc ); if ( msg != null ) { StringBuilder sb = new StringBuilder( msg ); sb.append( "\nencode cache:\n" ).append( summary() ); sb.append( "\ndecode cache:\n" ).append( mc.summary() ); return sb.toString(); } return null; } private String summary() { StringBuilder sb = new StringBuilder( "size=" + size + " aboffset=" + aboffset ); for ( int i = 0; i < size; i++ ) { sb.append( "\nidx=" + i + " faid=" + faid[i] + " fapos=" + fapos[i] ); } return sb.toString(); } private String _compareWith( MicroCache mc ) { if ( size != mc.size ) { return "size missmatch: " + size + "->" + mc.size; } for ( int i = 0; i < size; i++ ) { if ( faid[i] != mc.faid[i] ) { return "faid missmatch at index " + i + ":" + faid[i] + "->" + mc.faid[i]; } int start = i > 0 ? fapos[i - 1] : 0; int end = fapos[i] < mc.fapos[i] ? fapos[i] : mc.fapos[i]; int len = end - start; for ( int offset = 0; offset < len; offset++ ) { if ( mc.ab.length <= start + offset ) { return "data buffer too small"; } if ( ab[start + offset] != mc.ab[start + offset] ) { return "data missmatch at index " + i + " offset=" + offset; } } if ( fapos[i] != mc.fapos[i] ) { return "fapos missmatch at index " + i + ":" + fapos[i] + "->" + mc.fapos[i]; } } if ( aboffset != mc.aboffset ) { return "datasize missmatch: " + aboffset + "->" + mc.aboffset; } return null; } public void calcDelta( MicroCache mc1, MicroCache mc2 ) { int idx1 = 0; int idx2 = 0; while( idx1 < mc1.size || idx2 < mc2.size ) { int id1 = idx1 < mc1.size ? mc1.faid[idx1] : Integer.MAX_VALUE; int id2 = idx2 < mc2.size ? mc2.faid[idx2] : Integer.MAX_VALUE; int id; if ( id1 >= id2 ) { id = id2; int start2 = idx2 > 0 ? mc2.fapos[idx2 - 1] : 0; int len2 = mc2.fapos[idx2++] - start2; if ( id1 == id2 ) { // id exists in both caches, compare data int start1 = idx1 > 0 ? mc1.fapos[idx1 - 1] : 0; int len1 = mc1.fapos[idx1++] - start1; if ( len1 == len2 ) { int i = 0; while( i do nothing } } } write( mc2.ab, start2, len2 ); } else { idx1++; id = id1; // deleted node } fapos[size] = aboffset; faid[size] = id; size++; } } public void addDelta( MicroCache mc1, MicroCache mc2, boolean keepEmptyNodes ) { int idx1 = 0; int idx2 = 0; while( idx1 < mc1.size || idx2 < mc2.size ) { int id1 = idx1 < mc1.size ? mc1.faid[idx1] : Integer.MAX_VALUE; int id2 = idx2 < mc2.size ? mc2.faid[idx2] : Integer.MAX_VALUE; if ( id1 >= id2 ) // data from diff file wins { int start2 = idx2 > 0 ? mc2.fapos[idx2 - 1] : 0; int len2 = mc2.fapos[idx2++] - start2; if ( keepEmptyNodes || len2 > 0 ) { write( mc2.ab, start2, len2 ); fapos[size] = aboffset; faid[size++] = id2; } if ( id1 == id2 ) // // id exists in both caches { idx1++; } } else // use data from base file { int start1 = idx1 > 0 ? mc1.fapos[idx1 - 1] : 0; int len1 = mc1.fapos[idx1++] - start1; write( mc1.ab, start1, len1 ); fapos[size] = aboffset; faid[size++] = id1; } } } }