/* The MIT License (MIT) * * Copyright (c) 2015 Reinventing Geospatial, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.rgi.geopackage.features; import com.rgi.geopackage.features.geometry.Geometry; import com.rgi.geopackage.features.geometry.m.EnvelopeM; import com.rgi.geopackage.features.geometry.xy.Envelope; import com.rgi.geopackage.features.geometry.z.EnvelopeZ; import com.rgi.geopackage.features.geometry.zm.EnvelopeZM; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; /** * Java implementation of the <a * href="http://www.geopackage.org/spec/#gpb_spec">GeoPackage Binary * Header</a> * * @author Luke Lambert */ public class BinaryHeader { /** * Constructor * * @param bytes * Bytes that should comprise the GeoPackage Binary Header */ protected BinaryHeader(final byte[] bytes) { if(bytes == null) { throw new IllegalArgumentException("Byte buffer may not be null"); } if(bytes.length < 8) { throw new IllegalArgumentException("Byte buffer must be at least 8 bytes to contain a valid GeoPackage geometry binary header"); } if(bytes[0] != magic[0] || bytes[1] != magic[1]) { throw new IllegalArgumentException("The first two bytes of a GeoPackage geometry binary header must be 'G', 'P'"); } this.version = bytes[2]; this.flags = bytes[3]; // read flags this.binaryType = BinaryType.type(bytes[3]); this.contents = (bytes[3] & Contents.Empty.getBitMask()) > 0 ? Contents.Empty : Contents.NotEmpty; // TODO add a 'from bitmask' method in Contents? this.byteOrder = ((this.flags & 1) == 0) ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; final ByteBuffer srsIdByteBuffer = ByteBuffer.wrap(bytes, 4, 4); // Bytes 5->9 are int32 srs_id srsIdByteBuffer.order(this.byteOrder); this.spatialReferenceSystemIdentifier = srsIdByteBuffer.getInt(); this.envelopeContentsIndicator = EnvelopeContentsIndicator.fromCode((this.flags & 0b00001110) >> 1); this.byteSize = 2 + // 2 bytes for the 'magic' header 1 + // 1 byte for version 1 + // 1 byte for flags 4 + // 4 bytes (int32) for the srs id (8 * this.envelopeContentsIndicator.getArraySize()); // 8 bytes per double, array size number of doubles if(bytes.length < this.byteSize) { throw new IllegalArgumentException("Byte array length is shorter than the envelope array size would indicate"); } this.envelopeArray = getHeaderEnvelopeDoubles(bytes, this.byteOrder, this.envelopeContentsIndicator.getArraySize()); } /** * Constructor * * @param version * 8-bit unsigned integer, 0 = version 1 * @param binaryType * Standard or Extended * @param contents * Whether or not the geometry is "empty" * @param byteOrder * Order of the header's bytes * @param spatialReferenceSystemIdentifier * Spatial reference system identifier for the geometry. Should * match the identifier in the contents table. * @param envelopeContentsIndicator * Indicator of the envelope array's contents (empty, not * empty, and dimensionality) * @param envelopeArray * Geometry envelopeArray. Size depends on the envelopeArray * contents indicator. Elements are listed min, max, and xyzm * order. */ protected BinaryHeader(final byte version, final BinaryType binaryType, final Contents contents, final ByteOrder byteOrder, final int spatialReferenceSystemIdentifier, final EnvelopeContentsIndicator envelopeContentsIndicator, final double[] envelopeArray) { if(binaryType == null) { throw new IllegalArgumentException("Binary type may not be null"); } if(contents == null) { throw new IllegalArgumentException("Contents enumeration may not be null"); } if(byteOrder == null) { throw new IllegalArgumentException("Byte order may not be null"); } if(envelopeContentsIndicator == null) { throw new IllegalArgumentException("Envelope contents indicator may not be null"); } if(envelopeArray == null) { throw new IllegalArgumentException("Envelope may not be null. Use Envelope.Empty to represent an empty envelope array."); } if(envelopeArray.length != envelopeContentsIndicator.getArraySize()) { throw new IllegalArgumentException("Envelope content indicator does not agree with the length of the envelope rray"); } this.version = version; this.binaryType = binaryType; this.contents = contents; this.byteOrder = byteOrder; this.spatialReferenceSystemIdentifier = spatialReferenceSystemIdentifier; this.envelopeContentsIndicator = envelopeContentsIndicator; this.envelopeArray = envelopeArray.clone(); @SuppressWarnings("NumericCastThatLosesPrecision") final int envelopeContentsMask = (byte)(this.envelopeContentsIndicator.getCode() << 1); //noinspection NumericCastThatLosesPrecision this.flags = (byte)(this.binaryType.getBitMask() | this.contents.getBitMask() | envelopeContentsMask | (byteOrder.equals(ByteOrder.BIG_ENDIAN) ? 0 : 1)); this.byteSize = 2 + // 2 bytes for the 'magic' header 1 + // 1 byte for version 1 + // 1 byte for flags 4 + // 4 bytes (int32) for the srs id (8 * this.envelopeContentsIndicator.getArraySize()); // 8 bytes per double, array size number of doubles } @Override public boolean equals(final Object obj) { if(this == obj) { return true; } if(obj == null || this.getClass() != obj.getClass()) { return false; } final BinaryHeader other = (BinaryHeader)obj; return this.version == other.version && this.spatialReferenceSystemIdentifier == other.spatialReferenceSystemIdentifier && this.flags == other.flags && this.byteSize == other.byteSize && this.binaryType == other.binaryType && this.contents == other.contents && this.byteOrder .equals(other.byteOrder) && this.envelopeContentsIndicator == other.envelopeContentsIndicator && Arrays.equals(this.getEnvelopeArray(), other.getEnvelopeArray()); } @Override public int hashCode() { int result = (int) this.version; result = 31 * result + this.binaryType.hashCode(); result = 31 * result + this.contents.hashCode(); result = 31 * result + this.byteOrder.hashCode(); result = 31 * result + this.spatialReferenceSystemIdentifier; result = 31 * result + this.envelopeContentsIndicator.hashCode(); result = 31 * result + Arrays.hashCode(this.getEnvelopeArray()); result = 31 * result + (int)this.flags; result = 31 * result + this.byteSize; return result; } public byte getVersion() { return this.version; } public BinaryType getBinaryType() { return this.binaryType; } public Contents getContents() { return this.contents; } public ByteOrder getByteOrder() { return this.byteOrder; } public int getSpatialReferenceSystemIdentifier() { return this.spatialReferenceSystemIdentifier; } public EnvelopeContentsIndicator getEnvelopeContentsIndicator() { return this.envelopeContentsIndicator; } public double[] getEnvelopeArray() { return this.envelopeArray.clone(); } public Envelope getEnvelope() { switch(this.envelopeContentsIndicator) { case NoEnvelope: return null; case Xy: return new Envelope(this.envelopeArray[0], // min x this.envelopeArray[2], // min y this.envelopeArray[1], // max x this.envelopeArray[3]); // max y case Xyz: return new EnvelopeZ(this.envelopeArray[0], // min x this.envelopeArray[2], // min y this.envelopeArray[4], // min z this.envelopeArray[1], // max y this.envelopeArray[3], // max y this.envelopeArray[5]); // max z case Xym: return new EnvelopeM(this.envelopeArray[0], // min x this.envelopeArray[2], // min y this.envelopeArray[4], // min m this.envelopeArray[1], // max y this.envelopeArray[3], // max y this.envelopeArray[5]); // max m case Xyzm: return new EnvelopeZM(this.envelopeArray[0], // min x this.envelopeArray[2], // min y this.envelopeArray[4], // min z this.envelopeArray[6], // min m this.envelopeArray[1], // max y this.envelopeArray[3], // max z this.envelopeArray[5], // max z this.envelopeArray[7]); // max m } return null; } public byte getFlags() { return this.flags; } public int getByteSize() { return this.byteSize; } /** * Writes the binary header to the supplied byte buffer * * @param byteOutputStream * Destination for the bytes of the header */ public void writeBytes(final ByteOutputStream byteOutputStream) { if(byteOutputStream == null) { throw new IllegalArgumentException("Byte buffer may not be null or read only"); } // http://www.geopackage.org/spec/#gpb_spec byteOutputStream.setByteOrder(this.byteOrder); byteOutputStream.write(BinaryHeader.magic); byteOutputStream.write(this.version); byteOutputStream.write(this.flags); byteOutputStream.write(this.spatialReferenceSystemIdentifier); for(final double envelopeBound : this.envelopeArray) { byteOutputStream.write(envelopeBound); } } /** * Constructs a header from the supplied arguments, and writes it to a * byte buffer. Currently there's no way to skip writing the envelopeArray. * * @param byteOutputStream * Destination for the bytes of the header * @param geometry * Used to determine the envelopeArray, contents (empty or not), and * the geometry's binary type * @param spatialReferenceSystemIdentifier * Spatial reference system identifier for the geometry */ public static void writeBytes(final ByteOutputStream byteOutputStream, final Geometry geometry, final int spatialReferenceSystemIdentifier) { if(geometry == null) { throw new IllegalArgumentException("Geometry may not be null"); } final Envelope envelope = geometry.createEnvelope(); new BinaryHeader(defaultVersion, BinaryType.fromGeometryTypeName(geometry.getGeometryTypeName()), geometry.getContents(), defaultByteOrder, spatialReferenceSystemIdentifier, envelope.getContentsIndicator(), envelope.toArray()).writeBytes(byteOutputStream); } private static double[] getHeaderEnvelopeDoubles(final byte[] header, final ByteOrder byteOrder, final int numberOfDoubles) { final ByteBuffer byteBuffer = ByteBuffer.wrap(header, 8, // Envelope starts after the first 8 bytes 8*numberOfDoubles); // 8 bytes per double byteBuffer.order(byteOrder); final double[] envelope = new double[numberOfDoubles]; for(int x = 0; x < numberOfDoubles; ++x) { envelope[x] = byteBuffer.getDouble(); } return envelope; } private static final byte defaultVersion = (byte)0; // Confusingly, 0 = "version 1", see: http://www.geopackage.org/spec/#gpb_spec private static final ByteOrder defaultByteOrder = ByteOrder.BIG_ENDIAN; // Java default (?), also the network byte order private static final byte[] magic = {(byte)71, // 'G' (byte)80 // 'P' }; private final byte version; // This is an *unsigned* value, regardless of Java's interpretation private final BinaryType binaryType; private final Contents contents; private final ByteOrder byteOrder; // NOTE: this applies *only* to the bytes of the GeoPackage binary header. The byte order of the well known binary that follows the header is specified by the first byte of its contents: 0 - big endian, 1 - little endian private final int spatialReferenceSystemIdentifier; private final EnvelopeContentsIndicator envelopeContentsIndicator; private final double[] envelopeArray; private final byte flags; private final int byteSize; }