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

org.opengis.cite.gpkg12.features.FeaturesTests Maven / Gradle / Ivy

The newest version!
package org.opengis.cite.gpkg12.features;

import static org.testng.Assert.assertTrue;
import static org.testng.AssertJUnit.fail;

import java.math.BigInteger;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.opengis.cite.gpkg12.ColumnDefinition;
import org.opengis.cite.gpkg12.ErrorMessage;
import org.opengis.cite.gpkg12.ErrorMessageKeys;
import org.opengis.cite.gpkg12.FeaturesFixture;
import org.opengis.cite.gpkg12.TableVerifier;
import org.opengis.cite.gpkg12.util.GeoPackageVersion;
import org.opengis.cite.gpkg12.util.DatabaseUtility;
import org.testng.Assert;
import org.testng.SkipException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Defines test methods that apply to descriptive information about a GeoPackage's content
 * as it pertains to features.
 *
 * 

* Sources *

* * * @author Jeff Yutzler */ public class FeaturesTests extends FeaturesFixture { // --------------------- Private Class Variables ----------------- private final Boolean NativeOrderIsBE = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN); private static final Map FeatureTableExpectedColumns; private static final Map> GeometrySubtypesAllowed; // These flags can be set once and then used to quickly skip tests that depend on // these two things private boolean hasGeometryColumnsTable; private boolean hasGPKGExtensionsTable; private final Collection possibleFeatureTableNames = new ArrayList<>(); private final Collection featureTableNames = new ArrayList<>(); // For tests 19, 20, 32, 33, 19b, 67, 78 - they will all be run based on an iterator // and we need the error reporting to be capped, // hence these counters and limits are defined to cap those error reports. int errorDetected19 = 0; int errorDetected32 = 0; int errorDetected33 = 0; int errorDetectedNSG19b = 0; int errorDetected66 = 0; int errorDetected67 = 0; int errorDetected78 = 0; int errorDetected20 = 0; // Since a feature typically will have all instances reporting the same errors, this // maximum count reduces the reporting per feature. private static final int maxErrorsToReport19 = 15; // This is a per feature maximum // report. This test has several // different things going on. // and it should be split into sub // items to better report these // issues. private static final int maxErrorsToReport32 = 5; // Per feature maximum, this test // reports one issue - but it'll // list all instances found private static final int maxErrorsToReport33 = 5; // Per feature maximum, this test // reports one issue - but it'll // list all instances found // private static final int maxErrorsToReportNSG19b = 5; private static final int maxErrorsToReport66 = 5; private static final int maxErrorsToReport67 = 5; private static final int maxErrorsToReport78 = 5; private static final int maxErrorsToReport20 = 5; // Masks and starting byte index for Geometry BLOB Contents // private static final byte magicB0 = 0x47; // private static final byte magicB1 = 0x50; private static final byte maskFlagBinaryType = 0x20; private static final int shiftFlagBinaryType = 5; private static final byte maskFlagEmptyGeometry = 0x10; private static final int shiftFlagEmptyGeometry = 4; private static final byte maskFlagEnvelope = 0x0E; private static final int shiftFlagEnvelope = 1; private static final byte maskFlagHeaderEndian = 0x01; private static final int startOfVersion = 2; private static final int startOfFlags = 3; private static final int startOfSRIDIndex = 4; private static final int startOfEnvelopeIndex = 8; private static final int startOfGeometryType = 1; // private static final int startOfEnvelopeCodeIndex = 3; private static final int maximumEnvelopeSize = 8 * Double.BYTES; // As per the OGC // spec, no more // than 8 doubles // should be in // the envelope private static final String myminx = "minx"; private static final String myminy = "miny"; private static final String mymaxx = "maxx"; private static final String mymaxy = "maxy"; private static final String myminz = "minz"; private static final String mymaxz = "maxz"; private static final String myminm = "minm"; private static final String mymaxm = "maxm"; private static final String geomCIRCULARSTRING = "CIRCULARSTRING"; private static final String geomCOMPOUNDCURVE = "COMPOUNDCURVE"; private static final String geomCURVEPOLYGON = "CURVEPOLYGON"; private static final String geomMULTICURVE = "MULTICURVE"; private static final String geomMULTISURFACE = "MULTISURFACE"; private static final String geomCURVE = "CURVE"; private static final String geomSURFACE = "SURFACE"; private static final String geomUNSUPPORTED = "UNSUPPORTED"; // From Annex G: These are in a "Note" under table 28 // GEOMETRY subtypes are POINT, CURVE, SURFACE and GEOMCOLLECTION <-- assume they must // mean GEOMETRYCOLLECTION because GEOMCOLLECTION is not a geometry type // CURVE subtypes are LINESTRING, CIRCULARSTRING and COMPOUNDCURVE // SURFACE subtype is CURVEPOLYGON // CURVEPOLYGON subtype is POLYGON // GEOMETRYCOLLECTION subtypes are MULTIPOINT, MULTICURVE and MULTISURFACE // MULTICURVE subtype is MULTILINESTRING <- contradicted in 2.1.1 // MULTISURFACE subtype is MULTIPOLYGON <- contradicted in 2.1.1 // Assumption that subtypes of subtypes are also allowed // This static hasmap defines for each supertype, all of the allowed subtype // geometries. // TODO this section needs work. There are inconsistencies in the spec and the test // requirement isn't very specific static { GeometrySubtypesAllowed = new HashMap<>(); GeometrySubtypesAllowed.put(geomCURVE, Arrays.asList(geomLINESTRING, geomCIRCULARSTRING, geomCOMPOUNDCURVE // Subtypes )); GeometrySubtypesAllowed.put(geomSURFACE, Arrays.asList(geomCURVEPOLYGON, // Subtypes geomPOLYGON)); // Polygon is a subtype of a subtype GeometrySubtypesAllowed.put(geomCURVEPOLYGON, Arrays.asList(geomPOLYGON, // Subtypes geomCURVE, geomLINESTRING, geomCIRCULARSTRING, geomCOMPOUNDCURVE)); // 2.1.1 Polygon: A restricted form of CurvePolygon where each ring is defined as // a simple, closed LineString. GeometrySubtypesAllowed.put(geomPOLYGON, Arrays.asList(geomLINESTRING)); GeometrySubtypesAllowed.put(geomGEOMETRYCOLLECTION, Arrays.asList(geomMULTIPOINT, geomMULTICURVE, geomMULTISURFACE, // Subtypes geomMULTIPOLYGON, geomMULTILINESTRING)); // Multipolygon is a // subtype of a // subtype and // Multilinestring is // subtype of a // subtype // 2.1.1 MultiSurface: A restricted form of GeometryCollection where each Geometry // in the collection must be of type Surface. GeometrySubtypesAllowed.put(geomMULTISURFACE, Arrays.asList(geomSURFACE)); // 2.1.1 MultiPolygon: A restricted form of MultiSurface where each Surface in the // collection must be of type Polygon. GeometrySubtypesAllowed.put(geomMULTIPOLYGON, Arrays.asList(geomPOLYGON)); // 2.1.1 MultiCurve: A restricted form of GeometryCollection where each Geometry // in the collection must be of type Curve GeometrySubtypesAllowed.put(geomMULTICURVE, Arrays.asList(geomCURVE)); // 2.1.1 MultiLineString: A restricted form of MultiCurve where each Curve in the // collection must be of type LineString. GeometrySubtypesAllowed.put(geomMULTILINESTRING, Arrays.asList(geomLINESTRING)); // 2.1.1 ??? GeometrySubtypesAllowed.put(geomMULTIPOINT, Arrays.asList(geomPOINT)); GeometrySubtypesAllowed.put(geomGEOMETRY, Arrays.asList(geomPOINT, geomCURVE, geomSURFACE, geomGEOMETRYCOLLECTION, // Subtypes geomLINESTRING, geomCIRCULARSTRING, geomCOMPOUNDCURVE, geomCURVEPOLYGON, geomPOLYGON, geomMULTIPOINT, geomMULTICURVE, geomMULTISURFACE, geomMULTIPOLYGON, geomMULTILINESTRING)); } // Define the required columns of the feature table static { FeatureTableExpectedColumns = new HashMap<>(); FeatureTableExpectedColumns.put("id", new ColumnDefinition("INTEGER", false, true, true, null)); FeatureTableExpectedColumns.put("geometry", new ColumnDefinition("GEOMETRY", true, false, false, null)); } // End Private Class Information /** * Sets up variables used across methods * @throws SQLException if there is a database error */ @BeforeClass public void setUp() throws SQLException { // Set internal flag denoting whether the geopackage has geometry columns table or // not. If it does not, then many tests will be skipped. this.hasGeometryColumnsTable = DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_geometry_columns"); this.hasGPKGExtensionsTable = DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_extensions"); // Capture any **potential** feature table names that are **NOT** listed within // the gpkg_contents // Save these names as we will compare them later on to the names specified in the // gpkg_contents. try (Statement statement = this.databaseConnection.createStatement(); ResultSet resultSet = statement.executeQuery( "SELECT tbl_name FROM sqlite_master WHERE tbl_name NOT LIKE \'gpkg_%\' AND (type = \'tile\' OR type = \'view\');")) { while (resultSet.next()) { // Examine each of the potential feature tables in the geopackage to see // if they are missing from the // contents specification. This test is not specifically identified in the // standard. try { final String tableName = resultSet.getString("tbl_name"); // If we think we have a feature table, make sure it has the expected // columns. // This throws if the table definition doesn't match, and won't be // added to the collection TableVerifier.verifyTable(this.databaseConnection, tableName, FeatureTableExpectedColumns, null, null); // Save the feature table on a list so that we may use it later on. this.possibleFeatureTableNames.add(tableName); } catch (final Throwable ignore) { // If verification fails- it's not a features table and we don't care // about it so ignore } } } try (final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("SELECT table_name FROM gpkg_contents WHERE data_type = \'features\';");) { while (resultSet.next()) { this.featureTableNames.add(resultSet.getString(1)); } } Assert.assertTrue(!this.featureTableNames.isEmpty(), ErrorMessage.format(ErrorMessageKeys.CONFORMANCE_CLASS_DISABLED, getTestName())); } /** * A DataProvider that supplies feature instance test methods with content from * gpkg_geometry_columns such that feature instance test methods may verify feature * internal content is consistent with gpkg_geometry_columns information. * @return An Iterator over an array containing a set of objects representing the * information needed to process tests against geometry BLOB information */ @DataProvider(name = "feature-geometry-information") public Iterator getFeaturesWithGeometryInfo() { List data = new ArrayList(); /* * 1. SELECT table_name AS tn, column_name AS cn FROM gpkg_geometry_columns WHERE * table_name IN (SELECT table_name FROM gpkg_contents WHERE data_type = * ‘features’) added srs_id, geometry_type_name, z, m in order to capture the * remaining testable values in gpkg_geometry_columns for which we will visit * every feature instance. */ try (final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement.executeQuery( "SELECT table_name AS tn, column_name AS cn, srs_id, geometry_type_name AS gt_name, z as z_flag, m as m_flag FROM gpkg_geometry_columns WHERE table_name IN (SELECT table_name FROM gpkg_contents WHERE data_type = \'features\');")) { while (resultSet.next()) { /* * Package the parameters: String TableName String GeometryColumnName * gpkg_geometry_columns String GeometryType from gpkg_geometry_columns * Integer SRS id from gpkg_geometry_columns byte Z flag from * gpkg_geometry_columns byte M flag from gpkg_geometry_columns */ Object[] tuple = { resultSet.getString("tn"), resultSet.getString("cn"), resultSet.getString("gt_name").toUpperCase(), resultSet.getInt("srs_id"), (byte) resultSet.getInt("z_flag"), (byte) resultSet.getInt("m_flag") }; data.add(tuple); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return data.iterator(); } /** * Verify that the gpkg_contents table_name value table exists, and is apparently a * feature table for every row with a data_type column value of 'features' * * Test Case {@code /opt/features/contents/data/features_row} * @see Vector * Features - Requirement 18 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r14: Requirement 18") public void features_contents_data_features_row() throws SQLException { /* * TEST METHOD 1. Execute * test
/opt/features/vector_features/data/feature_table_integer_primary_key */ final Collection missingFeatureTableNames = this.possibleFeatureTableNames.stream() .filter(tableName -> !this.featureTableNames.contains(tableName)) .collect(Collectors.toList()); final String reportOut = String.join(", ", missingFeatureTableNames); Assert.assertTrue((missingFeatureTableNames == null || missingFeatureTableNames.isEmpty()), ErrorMessage.format(ErrorMessageKeys.FEATURE_TABLE_NAMES_MISSING, reportOut)); } /** * A GeoPackage MAY contain tables or updateable views containing vector features. * Every such feature table or view in a GeoPackage SHALL have a column with column * type INTEGER and PRIMARY KEY AUTOINCREMENT column constraints per EXAMPLE : Sample * Feature Table or View Definition and sample_feature_table Table Definition SQL * (Informative). * * Test case * {@code /opt/features/vector_features/data/feature_table_integer_primary_key} * * @see Vector Features User * Data Tables - Requirement 29 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 29") public void featureTableIntegerPrimaryKey() throws SQLException { // 1 for (final String tableName : this.featureTableNames) { try (final Statement statement = this.databaseConnection.createStatement(); // 3a final ResultSet resultSet = statement .executeQuery(String.format("PRAGMA table_info(\'%s\');", tableName));) { // 3b assertTrue(resultSet.next(), ErrorMessage.format(ErrorMessageKeys.MISSING_TABLE, tableName)); } // 3c/3d checkPrimaryKey(tableName, getPrimaryKeyColumn(tableName, false), false); } } /** * Verify that geometries stored in feature table geometry columns are encoded in the * StandardGeoPackageBinary format. and are consistent with the information specified * in the gpkg_geometry_columns table for the feature type. Also verify that the * geometry information is consistent with other elements of the geopackage as per * specified test requirements. In order to optimize processing as much as possible, * all tests involving the feature instance geometry element are performed within this * block. The entry to this test is via an iterator operating on each feature table - * hence the test reports will be specific to each feature table processed. * * Test Case for tests 19, 20, 32, 33, 66 (partial) 67, 78 * {@code /opt/features/geometry_encoding/data/blob * /opt/features/geometry_encoding/data/core_types_existing_sparse_data * /opt/features/vector_features/data/data_values_geometry_type * /opt/features/vector_features/data/data_value_geometry_srs_id * /extensions/geometry_types/extension_name * /reg_ext/features/spatial_indexes/implementation/sql_functions} * * * @see Geometry Encoding - Requirements 19, 20, 32, 33, 66 (partial), 67, 78 and * Geometry Extents check (NSG 19B) * @param thisTableName The feature table name being processed * @param thisColumnName The feature column name (the geometry column) being * processed; as specified in the gpkg_geometry_columns entry table associated with * this feature * @param geomType The geometry type as specified in the gpkg_geometry_columns entry * table associated with this feature * @param srs_id The srs identifier as specified in the gpkg_geometry_columns entry * table associated with this feature * @param z_flag The z flag as specified in the gpkg_geometry_columns entry table * associated with this feature * @param m_flag The m flag as specified in the gpkg_geometry_columns entry table * associated with this feature * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r14: Requirements 19, 20, 32, 33, 66 (partial), 67, 78; and NSG Requirement 19B", dataProvider = "feature-geometry-information") public void featureGeometryEncodingTesting(String thisTableName, String thisColumnName, String geomType, Integer srs_id, byte z_flag, byte m_flag) throws SQLException { try { /* * 1. SELECT table_name AS tn, column_name AS cn FROM gpkg_geometry_columns * WHERE table_name IN (SELECT table_name FROM gpkg_contents WHERE data_type = * ‘features’) 2. Not testable if returns an empty result set 3. For each row * from step 1 */ try (final Statement statementInternal = this.databaseConnection.createStatement(); final ResultSet resultSetInternal = statementInternal.executeQuery( String.format("SELECT rowid, %s as geom FROM \'%s\';", thisColumnName, thisTableName))) { /* * a. SELECT cn FROM tn b. Not testable if none found */ // quick fix for https://github.com/opengeospatial/ets-gpkg12/issues/74 // We may wish to replace this with something externally configurable final int MAX_COUNT = 100; int counter = 0; while (resultSetInternal.next() && (counter < MAX_COUNT)) { counter++; // The SQL should give us a numeric identifier and a geometry blob. // All of the tests in this series operate off // of these two values and the parameters passed in by the iterator. final long rowID = (long) resultSetInternal.getLong(1); final byte[] bytes = resultSetInternal.getBytes("geom"); // We must allow for null geometries. if (bytes == null) { continue; } // From the geometry blob, populate a few of the values that we can // easily extract from the geometry final byte envelopeCode = (byte) ((bytes[startOfFlags] & maskFlagEnvelope) >> shiftFlagEnvelope); final byte binaryTypeFlag = (byte) ((bytes[startOfFlags] & maskFlagBinaryType) >> shiftFlagBinaryType); final byte emptyGeometryFlag = (byte) ((bytes[startOfFlags] & maskFlagEmptyGeometry) >> shiftFlagEmptyGeometry); final byte headerLE = (byte) (bytes[startOfFlags] & maskFlagHeaderEndian); // variables needed by this series of tests that will be used by more // than one test Map envelopeVals = new HashMap<>(); // We will put the // envelope in // here in a bit final int envelopeSize = mygetEnvelopeByteSize(envelopeCode); final boolean swapHeaderBytes = (this.NativeOrderIsBE && headerLE == 1) || (!this.NativeOrderIsBE && headerLE == 0); boolean nanDetected = false; // Tests begin now // ** START ************** 19 ************************ 19 // ************************** 19 ************************** /* * Requirement 19: c. For each cn value from step a [ cn is * essentially a BLOB - layout partly shown below ] i. Fail if the * first two bytes of each gc are not 'GP' ii. Fail if * gc.version_number is not 0 iii. Fail if gc.flags.GeopackageBinary * type != 0 iv. (Fail if cn.flags.E is 5-7) Previously in 128r12 was: * Fail if ST_IsEmpty(cn value) = 1 v. *Fail if the geometry is empty * but the envelope is not empty (gc.flags.envelope != 0 and envelope * values are not NaN) 4. Pass if no fails */ // GeoPackageBinaryHeader { // byte[2] magic = 0x4750; // "GP" in ASCII // byte version; // 0 = version 1 // byte flags; // bit layout for flags below, note flags includes // endianness for the rest of this header // int32 srs_id; // double[] envelope; // size of this is implied by the envelope // indicator code in the flags // } // // flags bit layout: // 7 6 5 4 3 2 1 0 // R R X Y E E E B // // 7: R: Reserved set to 0 // 6: R: Reserved set to 0 // 5: X: GeoPackage Binary Type 0=Standard, 1=Extended // 4: Y: Empty Geometry Flag 0= non-empty geometry, 1=Empty Geometry // (so envelope should be empty or NaN too, test v.) // 3-1: E: Envelope Indicator Code (3-bit unsigned) // 0 = no envelope 0 byte envelope // 1 = envelope is [minx, maxx, miny, maxy] 32 bytes envelope // 2 = envelope is [minx, maxx, miny, maxy, minz, maxz] 48 bytes // envelope // 3 = envelope is [minx, maxx, miny, maxy, minz, maxz, minm, maxm] 64 // bytes envelope // 5-7 = invalid value for envelope (test iv.) // 0: B: Byte order for header values 0 = Big Endian, 1 = Little // Endian // // The GeoPackageBinaryHeader is followed by WKB try { // i. Fail if the first two bytes of each gc are not "GP" final byte[] GP_HEADER = new String("GP").getBytes(StandardCharsets.US_ASCII); if ((bytes[0] != GP_HEADER[0]) || (bytes[1] != GP_HEADER[1])) { errorDetected19++; if (errorDetected19 < maxErrorsToReport19) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_INVALID_MAGIC_NUMBER, thisTableName, rowID, thisColumnName, String.format("0x%02x%02x", bytes[0], bytes[1]))); } // ii. Fail if gc.version_number is not 0 final byte version = (byte) bytes[startOfVersion]; if (version != 0) { errorDetected19++; if (errorDetected19 < maxErrorsToReport19) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_INVALID_VERSION, thisTableName, rowID, thisColumnName, version)); } // iii. Fail if gc.flags.GeopackageBinary type != 0 if (binaryTypeFlag != 0) { errorDetected19++; if (errorDetected19 < maxErrorsToReport19) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_INVALID_BINARY_CODE, thisTableName, rowID, thisColumnName, binaryTypeFlag)); } // iv. (Fail if cn.flags.E is 5-7) if (envelopeCode > 4 || envelopeCode < 0) { errorDetected19++; if (errorDetected19 < maxErrorsToReport19) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_INVALID_ENVELOPE_CODE, thisTableName, rowID, thisColumnName, envelopeCode)); } } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, String.format("Failure testing requirement 19i-iv on feature {0}", thisTableName), th.getMessage())); } // ** END ************** 19 i-iv ************************ 19 // ************************** 19 ************************** // ** START ************** 33 ************************ ************ // {@code // /opt/features/vector_features/data/data_value_geometry_srs_id} // description = "See OGC 12-128r14: Requirement 33", dataProvider = // "feature-geometry-information" /* * REQUIREMENT 33 a. SELECT DISTINCT st_srid(cn) FROM tn Note in this * code, we process each row and we already performed the SELECT above * to get each row b. For each row from step a i. Fail if returnvalue * not equal to gc_srs_id 4. Pass if no fails */ // Note: dependency on the value of currentSRID that we used for a // different test is within this one. // the SRSID extracted from the geometry BLOB will be compared to the // srsContents (srs_id) retrieved from gpkg_contents // Get a byte array at the correct offset for the srs_id and set up // the byte array size to be the size of an integer byte[] srspartID = byteArraySubset(bytes, startOfSRIDIndex, Integer.BYTES); // 4 // bytes // Using our helper function, get the integer from the byte array, and // signify whether there is byte swapping needed // We will SAVE this SRID as it is needed for a couple of tests int currentSRID = getIntegerFromBytesWithPossibleSwap(srspartID, swapHeaderBytes); // this // value // is // needed // for // this // test // and // a // test // in // #19 // Check for possible byte swap error on this SRS ID content vs the // gpkg_geometry_columns value (which was a parameter to this test). // This will tell us if there is a problem with the detection // of the endianness either within the geometry blob, or of the // machine hardware upon which the test is being run. // Since this is the ONLY place we are going to try to test the // endianness, it needs to be done prior to messing // with the envelope values. Hence, this is why this test is put prior // to 19v. try { if (currentSRID != srs_id) { // So far, the test failed. Perform an extra test - swap the // bytes and see if we get the SRID now - // to see if the issue can be identified as a byte swap issue. // Note, this may not work with large or negative values // due to sign extension on the integer, but we will try it // anyway. int tempSRID = Integer.reverseBytes(currentSRID); if (tempSRID == srs_id) { // It appears this may then be a byte swap issue, so // report the SR ID problem and the fact it may be a byte // swap issue // meaning the header endianness may be incorrect or this // code base is incorrect. errorDetected33++; if (errorDetected33 < maxErrorsToReport33) Assert.assertTrue(false, ErrorMessage.format( ErrorMessageKeys.FEATURE_GEOMETRY_POSSIBLE_BYTE_SWAP_ERROR_SRS_MATCH, rowID, thisColumnName, currentSRID, srs_id, thisTableName)); } else { // Not able to determine if the issue is a byte swap // issue, so report it as an SRS ID issue errorDetected33++; if (errorDetected33 < maxErrorsToReport33) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_SRS_MISMATCH, rowID, thisColumnName, currentSRID, srs_id, thisTableName)); } } } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, String.format("Failure testing requirement 33 part b on feature {0}", thisTableName), th.getMessage())); } // ** END *** 33 ************************ 33 ************************* // ** START ************** 19v ************************ 19 // ************************** 19 ************************** try { // v. *Fail if the geometry is empty but the envelope is not empty // (gc.flags.envelope != 0 and envelope values are not NaN) if (envelopeSize > 0 && envelopeSize < maximumEnvelopeSize) { final byte bytesEnvelope[] = byteArraySubset(bytes, startOfEnvelopeIndex, envelopeSize); try { // Ignoring the return value; we are just looking for the // exception processing at this time mygetEnvelope(envelopeSize, swapHeaderBytes, bytesEnvelope, envelopeVals); } catch (IllegalArgumentException ee) // this should catch the // indication that we a // nan values in the // envelope { // final String errMsg = ee.getMessage(); nanDetected = true; } if (!nanDetected && emptyGeometryFlag != 0) { errorDetected19++; if (errorDetected19 < maxErrorsToReport19) Assert.assertTrue(false, ErrorMessage.format( ErrorMessageKeys.FEATURE_GEOMETRY_INVALID_DETECTED_EMPTY_GEOMETRY_FLAG_BUT_ENVELOPE_HAS_CONTENT, rowID, thisColumnName, thisTableName)); } } // end if envelopeSize > 0 ... Currently it is valid to have an // envelope that is 0 size. In the future, that may change if // there is geometry. // also we should never get an envelopeSize over the max size // (as it comes from the code and a private method here) so we // are not checking for that. } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, String.format("Failure testing requirement 19v on feature {0}", thisTableName), th.getMessage())); } // ** END ************** 19v ************************ 19 // ************************** 19 ************************** // WKB Header // byte byteOrder 0 = Big Endian; 1 = Little Endian // uint32 wkbGeometryType // GEOMETRY String actualGeometryType = geomUNSUPPORTED; // These next tests require that there be geometry if (emptyGeometryFlag == 0) { final int geometryStart = startOfEnvelopeIndex + envelopeSize; // Find // the // start // of // the // WKB // Geometry // Blob // within // the // SHAPE // Blob // The geometry WKB has yet another structure with yet another // possible big / little endian setting final byte geometryByteOrderLE = bytes[geometryStart]; // The // Geometry // Blob // may // have a // different // byte // order // than // the // header // or the // SQLite final boolean swapGeometryBytes = (geometryByteOrderLE == 1 && this.NativeOrderIsBE) || (geometryByteOrderLE == 0 && !this.NativeOrderIsBE); // Get the subset of bytes representing the geometry type, // then get the geometry type integer from those bytes, // then get the geometry type string from the integer byte[] geomtypeBytes = byteArraySubset(bytes, geometryStart + startOfGeometryType, Integer.BYTES); final int currentGeomType = getIntegerFromBytesWithPossibleSwap(geomtypeBytes, swapGeometryBytes); actualGeometryType = getGeomTypeFromNum(currentGeomType).toUpperCase(); // ** START ****** 20 ************************ 20 // ************************** 20 ************************** /* * REQUIREMENT 20 1. b. For each row from step a, if bytes 2-5 of * cn.wkb as uint32 in endianness of gc.wkb * * ***** NOTE THIS TEST SHOULD NOT be performed if there is NO * geometry * * byte 1 of cn from #1 are a geometry type value from Annex G * Table 42, then i. Log cn.header values, wkb endianness and * geometry type * * TEST IS NOT including full decomposition of WKB, only the * geometry type ii. *If cn.wkb is not correctly encoded per ISO * 13249-3 clause 5.1.46 then log fail iii. Otherwise log pass 6. * Pass if log contains pass and no fails */ try { // Verify that the actual geometry type is something valid, // our getGeomTypeFromNum will assign "UNSUPPORTED" if it is // invalid if (actualGeometryType.equals(geomUNSUPPORTED)) { // So the geometry type is not recognized. Try a byte swap // on the value and see if we get any // supported value now. Report a different error if we get // a value. final int altGeomType = getIntegerFromBytesWithPossibleSwap(geomtypeBytes, !swapGeometryBytes); // test if the byte swap flag // might be wrong final String testGeomType = getGeomTypeFromNum(altGeomType).toUpperCase(); if (testGeomType.equals(geomUNSUPPORTED)) { errorDetected20++; if (errorDetected20 < maxErrorsToReport20) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_TYPE_INVALID, rowID, thisColumnName, geomType, (int) currentGeomType, thisTableName)); } else { // Report a possible byte swap problem in the WKB // portion of this BLOB errorDetected20++; if (errorDetected20 < maxErrorsToReport20) Assert.assertTrue(false, ErrorMessage.format( ErrorMessageKeys.FEATURE_GEOMETRY_TYPE_INVALID_POSSIBLE_BYTE_SWAP, rowID, thisColumnName, geomType, (int) currentGeomType, thisTableName, (int) altGeomType, testGeomType)); } } } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, String.format("Failure testing requirement 20 on feature {0}", thisTableName), th.getMessage())); } // ** END ****** 20 ************************ 20 // ************************** 20 ***************************/ // ** START ****** 32 ************************ 32 // ************************** 32 ***************************/ // @ID // (/opt/features/vector_features/data/data_value_geometry_srs_id) // @Test( description = "See OGC 12-128r14: Requirement 32 all // items", dataProvider = "feature-geometry-information") /* * REQUIREMENT 32 Test Method 3. For each row from step 1 a. * *Select the set of geometry types in use for the values in cn * (geometry column) WAS: SELECT DISTINCT ST_GeometryType(cn) FROM * tn b. For each row actual_type_name from step a i. Determine if * each geometry type is assignable to the actual_type_name ii. * Fail if any are not assignable 4. Pass if no fails * */ // Guess what - we have already retrieved both the current WKB // geometry type and the geometry specified for this feature type // so it is just a matter of determining whether the // geom_type representing the geometry_column_table type vs. the // GeometryType in the current WKB record are compatible. try { // Verify that the actual geometry type is 'assignable' i.e. // equal to or a subset of the specified geometry from the // geometry table // The call to IsAssignable is expecting a return of 1 if // assignable, 0 if not. // the variable actualGeometryType is coming from the feature // WKB while the geomType is from the gpkg_geometry_columns // If the value actualGeometryType is equal to or is defined a // s a subtype to the geomType, this will return the value of // 1 if (this.IsAssignable(geomType, actualGeometryType) == 0) { errorDetected32++; if (errorDetected32 < maxErrorsToReport32) Assert.assertTrue(false, ErrorMessage.format( ErrorMessageKeys.FEATURE_GEOMETRY_NOT_ASSIGNABLE_TO_SUPERTYPE, rowID, thisColumnName, geomType, actualGeometryType, thisTableName)); } } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, String.format("Failure testing requirement 32 on feature {0}", thisTableName), th.getMessage())); } // ** END ****** 32 ************************ 32 // ************************** 32 ************************** // ************** PARTIAL TEST for Requirement 66 here ********** // 66************************66 *********************66 // *************** // int geometryItemCount = 1; if (actualGeometryType != geomPOINT) { byte[] wkbGeometryCount = byteArraySubset(bytes, geometryStart + startOfGeometryType + Integer.BYTES, Integer.BYTES); geometryItemCount = getIntegerFromBytesWithPossibleSwap(wkbGeometryCount, swapGeometryBytes); } if (geometryItemCount < 0) { errorDetected66++; if (errorDetected66 < maxErrorsToReport66) Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_WKB_ITEM_COUNT_ILLEGAL, thisTableName, rowID, thisColumnName, actualGeometryType, geometryItemCount)); } // ** END ****** PARTIAL 66 ************************ 66 // ************************** 66 ************************** // ** START ****** 67 ************************ 67 // ************************** 67 ************************** /* * REQUIREMENT 67 C. SELECT ST_GeometryType(geometry column value) * AS ; D. SELECT extension_name FROM gpkg_extensions WERE * table_name = result_set_table_name AND column_name = * result_set_column_name AND extension_name = \'gpkg_geom_' || * I. Fail if result set is empty II. Log pass otherwise * 4. Pass if logged pass and no fails */ if (this.hasGPKGExtensionsTable == true) { try { // Tests have shown it is possible to NOT have these in // the extensions table at all. Since they are // 'extensions' that would // not be an error! Therefore, first check to be sure the // gpkg_geom_* extensions are present at all in the // gpkg_extensions // table. If, at some time, it is determine that these are // requirements and not just extensions, we can remove // this // first sub-test. boolean testForGeomExtensions = false; try (final Statement statementST = this.databaseConnection.createStatement(); final ResultSet resultSetST = statementST.executeQuery(String.format( "SELECT extension_name FROM gpkg_extensions WHERE (extension_name LIKE \'gpkg_geom_\');"))) { if (resultSetST.next() == true) { // false if the // result is empty testForGeomExtensions = true; } } // If we have verified the geometry extensions are present // in the extensions table, proceed if (testForGeomExtensions) { try (final Statement statementST = this.databaseConnection.createStatement(); final ResultSet resultSetST = statementST.executeQuery(String.format( "SELECT extension_name FROM gpkg_extensions WHERE (table_name = \'%s\' AND column_name = \'%s\' AND extension_name = \'gpkg_geom_%s\');", thisTableName, thisColumnName, actualGeometryType))) { if (resultSetST.next() == false) { // returns // false if // the result // set is // empty errorDetected67++; if (errorDetected67 < maxErrorsToReport67) Assert.assertTrue(false, ErrorMessage.format( ErrorMessageKeys.FEATURE_GEOMETRY_TYPE_NOT_PRESENT_AS_EXTENSION, rowID, thisColumnName, actualGeometryType, actualGeometryType, thisTableName)); } } } } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, String.format("Failure testing requirement 67 on feature {0}", thisTableName), th.getMessage())); } } // ** END ****** 67 ************************ 67 // ************************** 67 ************************** } // End tests of WKB Geometry requiring non empty geometry flag // ** START ****** NSG 19B ************************ NSG 19B // ************************** NSG 19B ************************** // @Test( description = "See NSG Requirement 19b: Requirement NSG 19b // features portion", dataProvider = "feature-geometry-information") // This next set looks at the gpkg_contents as compared to the values // in the feature // instance geometry BLOB. This is for NSG requirement 19 B // NOTE: This test does not test profiles but we're going to keep it // (commented out) for now // because a requirement could be added as part of GPKG 1.3.0. /* * try { if (!envelopeVals.isEmpty()) { try(final Statement * statementST = this.databaseConnection.createStatement(); final * ResultSet resultSetST = statementST.executeQuery( String.format( * "SELECT srs_id as srsContents, min_x, min_y, max_x, max_y FROM gpkg_contents WHERE (data_type = \'features\' and table_name = \'%s\' and srs_id IN (%s) );" * , thisTableName, srs_id))) { * * while (resultSetST.next()) { // Get the srs extents and save for * comparison final Map extentsforsrs = new * HashMap<>(); extentsforsrs.put(myminx, * resultSetST.getDouble("min_x")); extentsforsrs.put(mymaxx, * resultSetST.getDouble("max_x")); extentsforsrs.put(myminy, * resultSetST.getDouble("min_y")); extentsforsrs.put(mymaxy, * resultSetST.getDouble("max_y")); * * * // If they are null, all values will be 0 (getDouble) will put them * to 0 if they are null. // Perform the test if any values are set * and test only if we have anything of value here. * * if * (!checkIfValueWithinToleranceOfTargetValue(extentsforsrs.get(myminx * ),0.0D, 1.0e-10) || * !checkIfValueWithinToleranceOfTargetValue(extentsforsrs.get(myminy) * ,0.0D, 1.0e-10) || * !checkIfValueWithinToleranceOfTargetValue(extentsforsrs.get(mymaxx) * ,0.0D, 1.0e-10) || * !checkIfValueWithinToleranceOfTargetValue(extentsforsrs.get(mymaxy) * ,0.0D, 1.0e-10)) { final String enveloperesult = * geometryEnvelopeWithinExtents(envelopeVals, extentsforsrs); if * (enveloperesult != "") { errorDetectedNSG19b ++; if * (errorDetectedNSG19b < maxErrorsToReportNSG19b) { * Assert.assertTrue(false, ErrorMessage.format(ErrorMessageKeys. * FEATURE_GEOMETRY_ENVELOPE_OUTSIDE_BOUNDS_OF_GEOPACKAGE, rowID, * thisColumnName, srs_id, thisTableName, envelopeVals.get(myminx), * envelopeVals.get(mymaxx), envelopeVals.get(myminy), * envelopeVals.get(mymaxy), enveloperesult )); } * * } // end if geometryEvelopeWithinExtents == false } // end if check * of min max values show they are not 0 } } } } catch(final Exception * th) { * * fail( ErrorMessage.format( * ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, * String.format("Failure testing requirement NSG 19B on feature {0}", * thisTableName), th.getMessage())); } */ // ** END ****** NSG 19B ************************ NSG 19B // ************************** NSG 19B ************************** // ** START ****** 78 ************************ 78 // ************************** 78 ************************** // @Test( description = "See OGC 12-128r14: Requirement 78", // dataProvider = "feature-geometry-information") // Tests on gpkg_extensions and rtree. We have to flip the stated test // around because we are doing this per // feature instance. // Stated test #78 from Spec 128r14 // // Using the rtree_FeatureTableName_GeometryColumnName // 1. Open Geometry Test Data Set GeoPackage with GeoPackage SQLite // Extension // 2. For each Geometry Test Data Set data table row for // each geometry type in Annex G, for an assortment of srs_ids, // for an assortment of coordinate values including empty geometries, // without and with z and / or m values, in both big and little endian // encodings: // a. SELECT 'Fail' FROM WHERE ST_IsEmpty(geom.) != empty // <---- note we are not checking this. This test is not going to // happen if the geometry is empty // b. SELECT 'Fail' FROM WHERE ST_MinX(geom) != minx // c. SELECT 'Fail' FROM WHERE ST_MaxX(geom) != maxx // d. SELECT 'Fail' FROM WHERE ST_MinY(geom) != miny // e. SELECT 'Fail' FROM WHERE ST_MaxY(geom) != maxy // 3. Pass if no 'Fail' selected from step 2 // Dependent on the feature instance min and max geometry values and // whether we have envelope values try { // If we have no envelope, we cannot perform this test // Commented as deprecated in OGC 12-128r14 (Refer issue #99) /* * if (!envelopeVals.isEmpty()) { * * final String rtreeTable = * String.format("rtree_%s_%s",thisTableName, thisColumnName); * * try(final Statement statementST = * this.databaseConnection.createStatement(); final ResultSet * resultSetST = statementST.executeQuery(String. * format("SELECT minx, maxx, miny, maxy FROM \'%s\' WHERE (rowid = %s);" * , rtreeTable, rowID))) { final Double localtolerance = 1.0e-4; * // 1.0e-5 is not good enough for most geopackages to pass! * * while(resultSetST.next() ) { final Map * rtreeminmaxVals = new HashMap<>(); rtreeminmaxVals.put(myminx, * resultSetST.getDouble(myminx)); rtreeminmaxVals.put(mymaxx, * resultSetST.getDouble(mymaxx)); rtreeminmaxVals.put(myminy, * resultSetST.getDouble(myminy)); rtreeminmaxVals.put(mymaxy, * resultSetST.getDouble(mymaxy)); * * // Verify the geometry envelope falls within the rtree extents * for this instance // final String enveloperesult = * geometryEnvelopeWithinExtents(envelopeVals, rtreeminmaxVals); * // this test would test for the envelope within the rtree, not * equal to if * (!checkIfValueWithinToleranceOfTargetValue(rtreeminmaxVals.get( * myminx), envelopeVals.get(myminx), localtolerance) || * !checkIfValueWithinToleranceOfTargetValue(rtreeminmaxVals.get( * mymaxx), envelopeVals.get(mymaxx), localtolerance) || * !checkIfValueWithinToleranceOfTargetValue(rtreeminmaxVals.get( * myminy), envelopeVals.get(myminy), localtolerance) || * !checkIfValueWithinToleranceOfTargetValue(rtreeminmaxVals.get( * mymaxy), envelopeVals.get(mymaxy), localtolerance) ) { * errorDetected78 ++; if (errorDetected78 < maxErrorsToReport78) * { Assert.assertTrue(false, * ErrorMessage.format(ErrorMessageKeys. * FEATURE_GEOMETRY_ENVELOPE_OUTSIDE_TOLERANCE_OF_RTREE_TRIGGER_MIN_MAX, * rowID, thisColumnName, thisTableName, envelopeVals.get(myminx), * envelopeVals.get(mymaxx), envelopeVals.get(myminy), * envelopeVals.get(mymaxy))); } * * } } } } */ } catch (final Exception th) { // At this time, do not report an error here. If there is no rtree // table, I guess it is not worth reporting an error // ets-gpkg12 tests including the features-0.gpkg sample will fail // right here. // errorDetected78 ++; // if (errorDetected78 < maxErrorsToReport78) { // Assert.assertTrue(false, // ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_ENVELOPE_RTREE_TABLE_MISSING_OR_IN_ERROR, // thisTableName, thisColumnName)); // } } // ** END ****** 78 ************************ 78 // ************************** 78 ************************** } // End while result set } } catch (final Exception th) { fail(ErrorMessage.format(ErrorMessageKeys.FEATURE_GEOMETRY_BLOB_PROCESSING_TEST_FAILURE, "main loop", th.getMessage())); } } /** * Test case {@code /opt/features/geometry_columns/data/table_def} * * @see Vector Features Geometry Columns * Table - Requirement 21 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 21") public void featureGeometryColumnsTableDef() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement.executeQuery("PRAGMA table_info(\'gpkg_geometry_columns\');");) { // 2 int passFlag = 0; final int flagMask = 0b00111111; while (resultSet.next()) { // 3 final String name = resultSet.getString("name"); if ("geometry_type_name".equals(name)) { assertTrue("TEXT".equals(resultSet.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("notnull") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("pk") == 0, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); passFlag |= 1; } else if ("table_name".equals(name)) { assertTrue("TEXT".equals(resultSet.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("notnull") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("pk") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); passFlag |= (1 << 1); } else if ("m".equals(name)) { assertTrue("TINYINT".equals(resultSet.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("notnull") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("pk") == 0, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); passFlag |= (1 << 2); } else if ("z".equals(name)) { assertTrue("TINYINT".equals(resultSet.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("notnull") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("pk") == 0, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); passFlag |= (1 << 3); } else if ("srs_id".equals(name)) { assertTrue("INTEGER".equals(resultSet.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("notnull") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("pk") == 0, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); passFlag |= (1 << 4); } else if ("column_name".equals(name)) { assertTrue("TEXT".equals(resultSet.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("notnull") == 1, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); assertTrue(resultSet.getInt("pk") == 2, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); passFlag |= (1 << 5); } } assertTrue((passFlag & flagMask) == flagMask, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID); } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_geometry_columns} * * @see Vector Features Geometry Columns * Table - Requirement 22 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 22") public void featureGeometryColumnsDataValues() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("SELECT table_name FROM gpkg_contents WHERE data_type = \'features\';");) { // 2 if (resultSet.next()) { try ( // 3 final Statement statement2 = this.databaseConnection.createStatement(); final ResultSet resultSet2 = statement2.executeQuery( "SELECT table_name FROM gpkg_contents WHERE data_type = \'features\' AND table_name NOT IN (SELECT table_name FROM gpkg_geometry_columns);");) { assertTrue(!resultSet2.next(), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_MISMATCH); } } } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_table_name} and * {@code /opt/features/geometry_columns/data/data_values_srs_id} * * @see Vector Features Geometry Columns * Table - Requirement 23 and Vector * Features Geometry Columns SRS ID - Requirement 26 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 23, 26") public void featureGeometryColumnsDataValuesTableName() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("PRAGMA foreign_key_list(\'gpkg_geometry_columns\');");) { boolean foundContents = false; boolean foundSpatialRefSys = false; // 2 while (resultSet.next()) { // 3 final String table = resultSet.getString("table"); if ("gpkg_spatial_ref_sys".equals(table)) { if ("srs_id".equals(resultSet.getString("from")) && "srs_id".equals(resultSet.getString("to"))) { foundSpatialRefSys = true; } } else if ("gpkg_contents".equals(table)) { if ("table_name".equals(resultSet.getString("from")) && "table_name".equals(resultSet.getString("to"))) { foundContents = true; } } } assertTrue(foundContents && foundSpatialRefSys, ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_NO_FK); } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_table_name} * * Verify that the table_name column values in the gpkg_geometry_columns table are * valid. * * @see Data Values Table Name - Requirement 23 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r14: Requirement 23") public void featureGeometryColumnsDataValuesTableNameNEW() throws SQLException { /* * Test Method Test as per 12-128r14 1. PRAGMA * foreign_key_list(gpkg_geometry_columns); 2. Fail if there is no row * designating table_name as a foreign key to table_name in gpkg_contents */ if (this.hasGeometryColumnsTable) { final Collection reportFKIssues = new ArrayList<>(); int countResults = 0; try (final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("PRAGMA foreign_key_list(\'gpkg_geometry_columns\');")) { while (resultSet.next()) { Boolean testFailedForFKItem = true; final String thisTableName = resultSet.getString("table"); // maybe // not the // right // column? // final String thisColumnFrom = resultSet.getString("from"); final String thisColumnTo = resultSet.getString("to"); countResults++; try (final Statement preparedStatement = this.databaseConnection.createStatement(); final ResultSet pragmaTableInfo = preparedStatement .executeQuery(String.format("PRAGMA table_info(\'%s\');", thisTableName))) { while (pragmaTableInfo.next() && testFailedForFKItem) { final String columnName = pragmaTableInfo.getString("name"); if (thisColumnTo.equals(columnName)) { testFailedForFKItem = false; } } } // failure if EITHER no table exists that matches the specified FK, // the specified FK column in the table does not exist if (testFailedForFKItem) { reportFKIssues.add(thisTableName); } } // end while resultSet.next() } // end try to get foreign key list // Check for the possible initial failure indicating there were no foreign // keys specified at all if (countResults == 0) { reportFKIssues.add("No foreign key specified in gpkg_geometry_columns"); } final String reportOut = String.join(", ", reportFKIssues); Assert.assertTrue((reportFKIssues == null || reportFKIssues.isEmpty()), ErrorMessage.format(ErrorMessageKeys.FEATURE_FOREIGN_KEY_NOT_SPECIFIED_CORRECTLY, reportOut)); } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_column_name} * * @see Vector Features Geometry Columns * Column - Requirement 24 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 24") public void featureGeometryColumnsDataValuesColumnName() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("SELECT table_name, column_name FROM gpkg_geometry_columns;");) { // 2 while (resultSet.next()) { final String tableName = resultSet.getString("table_name"); final String columnName = resultSet.getString("column_name"); try (final Statement statement2 = this.databaseConnection.createStatement(); final ResultSet resultSet2 = statement2 .executeQuery(String.format("PRAGMA table_info(\'%s\');", tableName));) { boolean foundMatch = false; while (resultSet2.next()) { if (resultSet2.getString("name").equals(columnName)) { foundMatch = true; break; } } assertTrue(foundMatch, ErrorMessage.format(ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID_COL, tableName, columnName)); } } } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_srs_id_match} * * @see Vector Features Geometry Columns * Column - Requirement 146 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 146") public void featureGeometryColumnsDataValuesSrsId() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement.executeQuery( "SELECT a.srs_id srs_id, a.table_name tn FROM gpkg_geometry_columns a, gpkg_contents b WHERE a.table_name = b.table_name and a.srs_id != b.srs_id");) { // 2 if (resultSet.next()) { fail(ErrorMessage.format(ErrorMessageKeys.SRS_MISMATCH, "gpkg_geometry_columns", resultSet.getInt("srs_id"), resultSet.getString("tn"))); } } } /** * Test case * {@code /opt/features/geometry_columns/data/data_values_geometry_type_name} * * @see Vector Features Geometry Columns * Geometry Type - Requirement 25 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 25") public void featureGeometryColumnsDataValuesGeometryType() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("SELECT table_name, column_name, geometry_type_name FROM gpkg_geometry_columns");) { // 2 while (resultSet.next()) { // 3 final String geometryTypeName = resultSet.getString("geometry_type_name"); final String tableName = resultSet.getString("table_name"); final String columnName = resultSet.getString("column_name"); boolean pass = false; if (geopackageVersion.equals(GeoPackageVersion.V120)) { pass = ALLOWED_GEOMETRY_TYPES.contains(geometryTypeName); } else { final Iterator iterator = ALLOWED_GEOMETRY_TYPES.iterator(); while (iterator.hasNext()) { if (geometryTypeName.equalsIgnoreCase(iterator.next())) { pass = true; break; } } } if (!pass) { pass = isExtendedType(tableName, columnName); } assertTrue(pass, ErrorMessage.format(ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID_GEOM, geometryTypeName, tableName)); } } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_z} * * @see Vector Features Geometry Columns Z * - Requirement 27 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 27") public void featureGeometryColumnsDataValuesZ() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement.executeQuery("SELECT z FROM gpkg_geometry_columns");) { // 2 if (resultSet.next()) { try ( // 3 final Statement statement2 = this.databaseConnection.createStatement(); final ResultSet resultSet2 = statement2 .executeQuery("SELECT z FROM gpkg_geometry_columns WHERE z NOT IN (0,1,2)");) { if (resultSet2.next()) { assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID_Z, resultSet2.getInt("z"))); } } } } } /** * Test case {@code /opt/features/geometry_columns/data/data_values_m} * * @see Vector Features Geometry Columns M * - Requirement 28 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 28") public void featureGeometryColumnsDataValuesM() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement.executeQuery("SELECT m FROM gpkg_geometry_columns");) { // 2 if (resultSet.next()) { try ( // 3 final Statement statement2 = this.databaseConnection.createStatement(); final ResultSet resultSet2 = statement2 .executeQuery("SELECT m FROM gpkg_geometry_columns WHERE m NOT IN (0,1,2)");) { if (resultSet2.next()) { assertTrue(false, ErrorMessage.format(ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID_M, resultSet2.getInt("m"))); } } } } } /** * Test case * {@code /opt/features/vector_features/data/feature_table_one_geometry_column} * * @see Vector Features One Geometry Column * - Requirement 30 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 30") public void featureTableOneGeometryColumn() throws SQLException { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement .executeQuery("SELECT table_name FROM gpkg_contents WHERE data_type=\'features\'");) { // 2 while (resultSet.next()) { // 3 final String tableName = resultSet.getString("table_name"); try (final Statement statement2 = this.databaseConnection.createStatement(); final ResultSet resultSet2 = statement2.executeQuery(String.format( "SELECT count(*) FROM gpkg_geometry_columns WHERE table_name = \'%s\'", tableName));) { resultSet2.next(); assertTrue(resultSet2.getInt(1) == 1, ErrorMessageKeys.FEATURES_ONE_GEOMETRY_COLUMN); } } } } /** * Test case * {@code /opt/features/vector_features/data/feature_table_geometry_column_type} * * @see Vector Features Geometry Column * Type - Requirement 31 * @throws SQLException If an SQL query causes an error */ @Test(description = "See OGC 12-128r13: Requirement 31") public void featureTableGeometryColumnType() throws SQLException { // We're just going to skip this test on older GeoPackages and hope for the best. if (geopackageVersion.equals(GeoPackageVersion.V120)) { try ( // 1 final Statement statement = this.databaseConnection.createStatement(); final ResultSet resultSet = statement.executeQuery( "SELECT table_name, column_name, geometry_type_name FROM gpkg_geometry_columns WHERE table_name IN (SELECT table_name FROM gpkg_contents WHERE data_type = \'features\')");) { // 2 while (resultSet.next()) { // 2a final String geometryTypeName = resultSet.getString("geometry_type_name"); // This assertion being removed as per // https://github.com/opengeospatial/geopackage/issues/347 // assertTrue(allowedGeometryTypes.contains(geometryTypeName), // ErrorMessage.format(ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_INVALID_GEOM, // geometryTypeName)); // 2b final String tableName = resultSet.getString("table_name"); final String columnName = resultSet.getString("column_name"); try (final Statement statement2 = this.databaseConnection.createStatement(); final ResultSet resultSet2 = statement2 .executeQuery(String.format("PRAGMA table_info(\'%s\')", tableName));) { while (resultSet2.next()) { if (columnName.equals(resultSet2.getString("name"))) { assertTrue(geometryTypeName.equals(resultSet2.getString("type")), ErrorMessageKeys.FEATURES_GEOMETRY_COLUMNS_MISMATCH); break; } } } } } } } // *************************SUPPORTING METHODS ***************** SUPPORTING METHODS // *********************** /** * Returns an integer from a byte array and provides for byte swap if necessary. * @param bytesIn The integer bytes as an array; must be number of bytes in an integer * @param swapFlag Boolean indicator for whether the bytes are to be swapped or not * @return Integer value of the bytes * @throws IllegalArgumentException if the byte array length is not equivalent to the * number of bytes in an integer */ private int getIntegerFromBytesWithPossibleSwap(byte[] bytesIn, boolean swapFlag) { int returnValue = 0; // Verify length of byte array is correct, throw and error on failure if (bytesIn.length == Integer.BYTES) { // Based on endianness of this platform and the swapFlag, and our intends use // of BigInteger, which assumes BigEndian data, // determine whether the bytes will be swapped or not. Note: We could do this // the old-fashioned way with shifts as an alternative. if ((!swapFlag && this.NativeOrderIsBE) || (swapFlag && !this.NativeOrderIsBE)) { returnValue = new BigInteger(bytesIn).intValue(); } else { returnValue = Integer.reverseBytes(new BigInteger(bytesIn).intValue()); } } else { throw new IllegalArgumentException("Invalid byte array length"); } return returnValue; } /** * Returns an long integer from a byte array and provides for byte swap if necessary. * @param bytesIn The long bytes as an array of bytes; must be number of bytes in an * long integer * @param swapFlag Boolean indicator for whether the bytes are to be swapped or not * @return Long value of the bytes * @throws IllegalArgumentException if the byte array length is not equivalent to the * number of bytes in a Long integer */ private long getLongFromBytesWithPossibleSwap(byte[] bytesIn, boolean swapFlag) { long returnValue = 0; // Verify length of byte array is correct, throw and error on failure if (bytesIn.length == Long.BYTES) { // Based on endianness of this platform and the swapFlag, and our intends use // of BigInteger, which assumes BigEndian data, // determine whether the bytes will be swapped or not. Note: We could do this // the with bit shifts as an alternative; may be faster. // alternative method: (long)(bytesIn[ii+0] << 56 | (bytesIn[ii+1] & 0xFF) << // 48 | (bytesIn[ii+2] & 0xFF) << 40 | (bytesIn[ii+3] & 0xFF) << 32 | // (bytesIn[ii+4] & 0xFF) << 24 | (bytesIn[ii+5] & 0xFF) << 16 | // (bytesIn[ii+6] & 0xFF) << 8 | (bytesIn[ii+7] & 0xFF)); // alternative method: (long)(bytesIn[ii+7] << 56 | (bytesIn[ii+6] & 0xFF) << // 48 | (bytesIn[ii+5] & 0xFF) << 40 | (bytesIn[ii+4] & 0xFF) << 32 | // (bytesIn[ii+3] & 0xFF) << 24 | (bytesIn[ii+2] & 0xFF) << 16 | // (bytesIn[ii+1] & 0xFF) << 8 | (bytesIn[ii+0] & 0xFF)); if ((!swapFlag && this.NativeOrderIsBE) || (swapFlag && !this.NativeOrderIsBE)) { returnValue = new BigInteger(bytesIn).longValue(); } else { returnValue = Long.reverseBytes(new BigInteger(bytesIn).longValue()); } } else { throw new IllegalArgumentException("Invalid byte array length"); } return returnValue; } /** * Returns an double from a byte array and provides for byte swap if necessary. Upon * detection of NaN * @param bytesIn The integer bytes as an array; must be number of bytes in an integer * @param swapFlag Boolean indicator for whether the bytes are to be swapped or not * @return Double value of the bytes * @throws IllegalArgumentException if the byte array length is not equivalent to the * number of bytes in a Double * @throws IllegalArgumentException if any value is NaN */ private double getDoubleFromBytesWithPossibleSwap(byte[] bytesIn, boolean swapFlag) { double returnValue = 0; // Verify length of byte array is correct, throw and error on failure if (bytesIn.length == Double.BYTES) { // Cannot simply convert the bytes into a double as the floating point // processor may change the // bits on us during the conversion. First get the bytes into a Long and then // use the LongBitsToDouble // to move the bits into a double. long tempLong = getLongFromBytesWithPossibleSwap(bytesIn, swapFlag); // If the tempLong is NaN, throw an error that the double is not valid // Need to test for NaN here as conversion to Double may change the bits. Java // encodes NaN a infinity if (isLongRepresentationNaN(tempLong)) { throw new IllegalArgumentException("NaN"); } returnValue = Double.longBitsToDouble(tempLong); } else { throw new IllegalArgumentException("Invalid byte array length"); } return returnValue; } /** * Detects a numeric representation as NaN if provided a long integer representation * of a double. This tests for the NaN as specified in 12-128r14 Table 6. bit layout * of GeoPackageBinary flags byte NaN * @param inValue The long integer representation of the bits of a double * @return Boolean true if NaN, false if not NaN */ private boolean isLongRepresentationNaN(long inValue) { return ((this.NativeOrderIsBE && (inValue == 0x7ff8000000000000L)) || (!this.NativeOrderIsBE && (inValue == 0x000000000000f87fL))) ? true : false; } /** * Test for Java representation of Double NaN or Infinity which may be the Java * representation of the NaN as specified in OGC 12-128r14 Table 6. * @param inValue The double value that is to be tested * @return Boolean true if NaN, false if not NaN */ private boolean isDoubleRepresentationNaNorInfinity(Double inValue) { return (Double.isNaN(inValue) || Double.isInfinite(inValue)) ? true : false; } /** * Get the geometry type string given the geometry type integer from WKB. of the NaN * as specified in OGC 12-128r14 Table 28. * @param geomTypeIn The integer geometry type from WKB * @return String geometry type. If the geometry type is not found, returns * "UNSUPPORTED" */ private String getGeomTypeFromNum(int geomTypeIn) { String result = ""; switch (geomTypeIn) { // Table 28. Geometry Type Codes case 0: case 1000: case 2000: case 3000: result = geomGEOMETRY; break; case 1: case 1001: case 2001: case 3001: result = geomPOINT; break; case 2: case 1002: case 2002: case 3002: result = geomLINESTRING; break; case 3: case 1003: case 2003: case 3003: result = geomPOLYGON; break; case 4: case 1004: case 2004: case 3004: result = geomMULTIPOINT; break; case 5: case 1005: case 2005: case 3005: result = geomMULTILINESTRING; break; case 6: case 1006: case 2006: case 3006: result = geomMULTIPOLYGON; break; case 7: case 1007: case 2007: case 3007: result = geomGEOMETRYCOLLECTION; break; // Table 28. Geometry Type Codes (Extension) case 8: case 1008: case 2008: case 3008: result = geomCIRCULARSTRING; break; case 9: case 1009: case 2009: case 3009: result = geomCOMPOUNDCURVE; break; case 10: case 1010: case 2010: case 3010: result = geomCURVEPOLYGON; break; case 11: case 1011: case 2011: case 3011: result = geomMULTICURVE; break; case 12: case 1012: case 2012: case 3012: result = geomMULTISURFACE; break; case 13: case 1013: case 2013: case 3013: result = geomCURVE; break; case 14: case 1014: case 2014: case 3014: result = geomSURFACE; break; default: result = geomUNSUPPORTED; // Specific value is returned to enable tests // that detect this rather than look for // nothing break; } return result; } /** * Test if the geometry is assignable. A geometry may be more specific within a * feature subtype than the superclass. * @param supertypeGeometry The geometry type expected * @param subtypeGeometry the actual geometry that has been specified * @return Integer 1 if assignable, 0 if not assignable */ private int IsAssignable(String supertypeGeometry, String subtypeGeometry) { int returnValue = 0; // Verify the string input contains values if (!supertypeGeometry.isEmpty() && !subtypeGeometry.isEmpty()) { // Return 1 if the stings are the same or if the supertype and subtype are // valid together if (supertypeGeometry.equals(subtypeGeometry) || (GeometrySubtypesAllowed.containsKey(supertypeGeometry) && (GeometrySubtypesAllowed.get(supertypeGeometry)).contains(subtypeGeometry))) { returnValue = 1; } } return returnValue; } /** * Return a subset byte array from a larger starting byte array * @param bytesIn The source byte array * @param startIndex The starting index from which the subset shall begin * @param numberOfBytes The number of bytes the subset shall contain * @return Byte[] subsetArray subset byte array * @throws IllegalArgumentException if the number of bytes is invalid or the start * index + number of bytes desired exceeds the length of the source array */ private byte[] byteArraySubset(byte[] bytesIn, int startIndex, int numberOfBytes) { byte[] subsetArray = new byte[numberOfBytes]; if ((startIndex + numberOfBytes) <= bytesIn.length) { System.arraycopy(bytesIn, startIndex, subsetArray, 0, numberOfBytes); } else { throw new IllegalArgumentException(String.format( "Invalid numberOfBytes value: %d or startIndex %d. The startIndex + numberOfBytes exceed the length of the byte array.", numberOfBytes, startIndex)); } return subsetArray; } /** * Get the set of envelope double values from the byte array * @param bytesExpected A count of the number of bytes expected. This value must be a * multiple of 8 (the size of a double) * @param swapFlag Boolean indicator for whether the bytes are to be swapped or not * @param bytesIn The integer bytes as an array; must be number of bytes in an integer * @param envelopeValues Hash map in which the envelope values will be placed * @return boolean true if values are returned, false if all values are 0 * @throws IllegalArgumentException if the byte array length is invalid * @throws IllegalArgumentException if any value is NaN */ private boolean mygetEnvelope(int bytesExpected, boolean swapFlag, byte[] bytesIn, Map envelopeValues) { boolean allzerovalues = true; int envelopeIndex = 0; Assert.assertTrue(envelopeValues.isEmpty(), "Attempt to get envelope when there are already values"); if (bytesExpected > 0 && (bytesExpected % Double.BYTES == 0) && bytesIn.length == bytesExpected) { for (int ii = 0; ii < bytesExpected; ii += Double.BYTES) { final byte[] envibtem = byteArraySubset(bytesIn, ii, Double.BYTES); // 8 // bytes try { final Double tempDouble = this.getDoubleFromBytesWithPossibleSwap(envibtem, swapFlag); if (this.isDoubleRepresentationNaNorInfinity(tempDouble)) { throw new IllegalArgumentException(String.format("NaN value detected.")); } // System.out.println(String.format("Envelope item %d with value %f", // ii, tempDouble)); // 1 = envelope is [minx, maxx, miny, maxy] 32 bytes envelope // 2 = envelope is [minx, maxx, miny, maxy, minz, maxz] 48 bytes // envelope // 3 = envelope is [minx, maxx, miny, maxy, minz, maxz, minm, maxm] 64 // bytes envelope switch (envelopeIndex) { case 0: envelopeValues.put(myminx, tempDouble); break; case 1: envelopeValues.put(mymaxx, tempDouble); break; case 2: envelopeValues.put(myminy, tempDouble); break; case 3: envelopeValues.put(mymaxy, tempDouble); break; case 4: envelopeValues.put(myminz, tempDouble); break; case 5: envelopeValues.put(mymaxz, tempDouble); break; case 6: envelopeValues.put(myminm, tempDouble); break; case 7: envelopeValues.put(mymaxm, tempDouble); break; } // Extra check here because if the entire envelope is all zero values, // we basically need to clear the envelope out // to save from later excessive processing. This will set a flag if // there // are ANY non-zero envelope values seen that that we do not // accidentally clear // it. if (!checkIfValueWithinToleranceOfTargetValue(tempDouble, 0.0D, 1.0e-10)) allzerovalues = false; } catch (IllegalArgumentException ee) // This is supposed to catch a NaN { throw new IllegalArgumentException(ee.getMessage()); } envelopeIndex++; } } else { // we either got 0 bytes length or the size of the byte array is inconsistent // for a set of doubles throw new IllegalArgumentException(String.format( "Invalid bytesExpected value: %d. Is 0 or is not divisible by the size of a double or not the size of the byte array parameter.", bytesExpected)); } // If we found the entire envelope to be all zero, clear all the elements out of // it if (allzerovalues == true) envelopeValues.clear(); return !allzerovalues; } /** * Compare a envelope A defined by minx, maxx, miny, maxy values against an envelope B * to determine if A falls inside or is equal to envelope B * @param envelopein A hash map of 4 (or more with z and m) values representing an * envelope * @param extentin A hash map of 4 values representing the min and max extents * @return String Empty string, if no issue. Otherwise it reports the first issue * found * */ /* * This test is only used as part of the NSG 19B test, but could be reinstated later. * private String geometryEnvelopeWithinExtents(Map envelopein, * Map extentin) { String fallswithin = ""; if (envelopein.get(myminx) * < extentin.get(myminx)) fallswithin = "Envelope minx less than extent minx"; else * if (envelopein.get(myminx) > extentin.get(mymaxx)) fallswithin = * "Envelope minx greater than extent maxx"; else if (envelopein.get(mymaxx) < * extentin.get(myminx)) fallswithin = "Envelope maxx less than extent minx"; else if * (envelopein.get(mymaxx) > extentin.get(mymaxx)) fallswithin = * "Envelope maxx greater than extent maxx"; * * else if (envelopein.get(myminy) < extentin.get(myminy)) fallswithin = * "Envelope miny less than extent miny"; else if (envelopein.get(myminy) > * extentin.get(mymaxy)) fallswithin = "Envelope miny greater than extent maxy"; else * if (envelopein.get(mymaxy) < extentin.get(myminy)) fallswithin = * "Envelope maxy less than extent miny"; else if (envelopein.get(mymaxy) > * extentin.get(mymaxy)) fallswithin = "Envelope maxy greater than extent maxy"; * * return fallswithin; } */ /** * Return the expected size of the envelope based on the envelope code * @param envelopeCode The byte code for the envelope * @return int bytesExpected Number of bytes expected to be present in this envelope */ private int mygetEnvelopeByteSize(byte envelopeCode) { int bytesExpected = 0; // Note, Caution! Java sign extends each byte for this test, // but these all should be okay because we are dealing with a small value switch (envelopeCode) { case 0: bytesExpected = 0; break; case 1: // 4 values 32 bytes bytesExpected = 32; break; case 2: case 3: // 48 bytes total (another 16 bytes) bytesExpected = 48; break; case 4: // 64 bytes bytesExpected = 64; break; default: // invalid bytesExpected = 0; break; } // end switch return bytesExpected; } /** * Compares two double values to determine if the are close enough to be called equal * within a specified level of tolerance * @param valueIn The input value (double) * @param targetValue The target value (double) * @param tolerance The tolerance value (double) * @return true if the value is within tolerance of the target value, false if not */ private boolean checkIfValueWithinToleranceOfTargetValue(double valueIn, double targetValue, double tolerance) { if (Math.abs(valueIn - targetValue) > tolerance) return false; else return true; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy