diff --git a/cdm/core/src/main/java/ucar/nc2/NetcdfFile.java b/cdm/core/src/main/java/ucar/nc2/NetcdfFile.java index 9c7f7e3dfb..8730fa9383 100644 --- a/cdm/core/src/main/java/ucar/nc2/NetcdfFile.java +++ b/cdm/core/src/main/java/ucar/nc2/NetcdfFile.java @@ -631,7 +631,7 @@ private static String makeUncompressed(String filename) throws IOException { try { if (suffix.equalsIgnoreCase("Z")) { - try (InputStream in = new UncompressInputStream(new FileInputStream(filename))) { + try (InputStream in = new UncompressInputStream(new BufferedInputStream(new FileInputStream(filename)))) { copy(in, fout, 100000); } if (debugCompress) @@ -639,7 +639,7 @@ private static String makeUncompressed(String filename) throws IOException { } else if (suffix.equalsIgnoreCase("zip")) { - try (ZipInputStream zin = new ZipInputStream(new FileInputStream(filename))) { + try (ZipInputStream zin = new ZipInputStream(new BufferedInputStream(new FileInputStream(filename)))) { ZipEntry ze = zin.getNextEntry(); if (ze != null) { copy(zin, fout, 100000); @@ -649,7 +649,7 @@ private static String makeUncompressed(String filename) throws IOException { } } else if (suffix.equalsIgnoreCase("bz2")) { - try (InputStream in = new CBZip2InputStream(new FileInputStream(filename), true)) { + try (InputStream in = new CBZip2InputStream(new BufferedInputStream(new FileInputStream(filename)), true)) { copy(in, fout, 100000); } if (debugCompress) @@ -657,7 +657,7 @@ private static String makeUncompressed(String filename) throws IOException { } else if (suffix.equalsIgnoreCase("gzip") || suffix.equalsIgnoreCase("gz")) { - try (InputStream in = new GZIPInputStream(new FileInputStream(filename))) { + try (InputStream in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(filename)))) { copy(in, fout, 100000); } diff --git a/cdm/core/src/main/java/ucar/nc2/NetcdfFiles.java b/cdm/core/src/main/java/ucar/nc2/NetcdfFiles.java index 79dcf51269..8409e3ef7a 100644 --- a/cdm/core/src/main/java/ucar/nc2/NetcdfFiles.java +++ b/cdm/core/src/main/java/ucar/nc2/NetcdfFiles.java @@ -575,7 +575,7 @@ private static String makeUncompressed(String filename) throws Exception { try { if (suffix.equalsIgnoreCase("Z")) { // Z file can only contain one file - copy the whole thing - try (InputStream in = new UncompressInputStream(new FileInputStream(baseFilename))) { + try (InputStream in = new UncompressInputStream(new BufferedInputStream(new FileInputStream(baseFilename)))) { copy(in, fout, 100000); } if (NetcdfFile.debugCompress) { @@ -584,7 +584,7 @@ private static String makeUncompressed(String filename) throws Exception { } else if (suffix.equalsIgnoreCase("zip")) { // find specified zip entry, if it exists - try (ZipInputStream zin = new ZipInputStream(new FileInputStream(baseFilename))) { + try (ZipInputStream zin = new ZipInputStream(new BufferedInputStream(new FileInputStream(baseFilename)))) { // If a desired zipentry ID was appended to method's filename parameter, then itempath // is of length > 1 and ID starts at itempath char offset 1. String itemName = (itempath.length() > 1) ? itempath.substring(1) : ""; @@ -606,7 +606,8 @@ private static String makeUncompressed(String filename) throws Exception { } else if (suffix.equalsIgnoreCase("bz2")) { // bz2 can only contain one file - copy the whole thing - try (InputStream in = new CBZip2InputStream(new FileInputStream(baseFilename), true)) { + try (InputStream in = + new CBZip2InputStream(new BufferedInputStream(new FileInputStream(baseFilename)), true)) { copy(in, fout, 100000); } if (NetcdfFile.debugCompress) @@ -614,7 +615,7 @@ private static String makeUncompressed(String filename) throws Exception { } else if (suffix.equalsIgnoreCase("gzip") || suffix.equalsIgnoreCase("gz")) { // gzip/gz concatenates streams - copy the whole thing - try (InputStream in = new GZIPInputStream(new FileInputStream(baseFilename))) { + try (InputStream in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(baseFilename)))) { copy(in, fout, 100000); } diff --git a/cdm/core/src/main/java/ucar/unidata/geoloc/projection/UnstructuredProjection.java b/cdm/core/src/main/java/ucar/unidata/geoloc/projection/UnstructuredProjection.java new file mode 100644 index 0000000000..50fa856c4c --- /dev/null +++ b/cdm/core/src/main/java/ucar/unidata/geoloc/projection/UnstructuredProjection.java @@ -0,0 +1,99 @@ +package ucar.unidata.geoloc.projection; + +import ucar.unidata.geoloc.*; + +import java.util.Objects; + +/** + * A dummy ProjectionImpl subclass for unstructured grids. + * Since unstructured grids do not follow a single mathematical projection, + * this class does not implement real forward/inverse transformations. + * Instead, it serves as a placeholder so netCDF-Java can represent the + * grid in a coordinate system, while actual lat/lon positions are handled + * separately (e.g., by per-cell coordinate arrays). + */ +public class UnstructuredProjection extends ProjectionImpl { + + public static final String EARTH_SHAPE = "earth_shape"; + public static final String NUMBER_OF_GRID_USED = "number_of_grid_used"; + public static final String NUMBER_OF_GRID_IN_REFERENCE = "number_of_grid_in_reference"; + public static final String UUID = "uuid"; + + int earthShape, numberOfGridUsed, numberOfGridInReference; + String uuid; + + /** + * Create a new UnstructuredProjection with a given name. + */ + public UnstructuredProjection(int earthShape, int numberOfGridUsed, int numberOfGridInReference, String uuid) { + super("UnstructuredProjection", false); // false => not lat/lon + + this.earthShape = earthShape; + this.numberOfGridUsed = numberOfGridUsed; + this.numberOfGridInReference = numberOfGridInReference; + this.uuid = uuid; + + addParameter(EARTH_SHAPE, earthShape); + addParameter(NUMBER_OF_GRID_USED, numberOfGridUsed); + addParameter(NUMBER_OF_GRID_IN_REFERENCE, numberOfGridInReference); + addParameter(UUID, uuid); + } + + /** + * Copy constructor for UnstructuredProjection. + */ + private UnstructuredProjection(UnstructuredProjection that) { + this(that.earthShape, that.numberOfGridUsed, that.numberOfGridInReference, that.uuid); + } + + @Override + public ProjectionImpl constructCopy() { + // Return a new instance with the same name, etc. + return new UnstructuredProjection(this); + } + + /** + * Since we do not have a formula for an unstructured grid, + * throw an exception if someone tries to do forward transform. + */ + @Override + public ProjectionPoint latLonToProj(LatLonPoint latlon, ProjectionPointImpl dest) { + throw new UnsupportedOperationException("UnstructuredProjection: no formula-based transform available"); + } + + /** + * Similarly, inverse transform is not defined for unstructured grids. + */ + @Override + public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl dest) { + throw new UnsupportedOperationException("UnstructuredProjection: no formula-based transform available"); + } + + /** + * For unstructured grids, the notion of crossing a seam is irrelevant, + * so we return false (or you could throw an exception). + */ + @Override + public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) { + return false; + } + + @Override + public String paramsToString() { + return "UnstructuredProjection: no transform parameters"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof UnstructuredProjection)) + return false; + UnstructuredProjection that = (UnstructuredProjection) o; + return earthShape == that.earthShape && numberOfGridUsed == that.numberOfGridUsed + && numberOfGridInReference == that.numberOfGridInReference && Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(earthShape, numberOfGridUsed, numberOfGridInReference, uuid); + } +} diff --git a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Gds.java b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Gds.java index 4cbd1226e5..27117448fd 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Gds.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Gds.java @@ -14,10 +14,13 @@ import ucar.unidata.geoloc.projection.LatLonProjection; import ucar.unidata.geoloc.projection.RotatedPole; import ucar.unidata.geoloc.projection.Stereographic; +import ucar.unidata.geoloc.projection.UnstructuredProjection; import ucar.unidata.geoloc.projection.sat.MSGnavigation; + import javax.annotation.concurrent.Immutable; import java.util.Arrays; import java.util.Formatter; +import java.util.UUID; /** * Template-specific fields for Grib2SectionGridDefinition @@ -62,6 +65,11 @@ public static Grib2Gds factory(int template, byte[] data) { result = new SpaceViewPerspective(data); break; + // General Unstructured Grid + case 101: + result = new GdsUnstructured(data, template); + break; + // LOOK NCEP specific case 204: result = new CurvilinearOrthogonal(data); @@ -85,6 +93,9 @@ public static Grib2Gds factory(int template, byte[] data) { protected final byte[] data; public int template; + + public int numberOfDataPoints; + public int center; public float earthRadius, majorAxis, minorAxis; // in meters protected int scanMode; @@ -102,13 +113,20 @@ protected Grib2Gds(byte[] data, int template) { this.data = data; this.template = template; + this.numberOfDataPoints = getOctet4(7); + earthShape = getOctet(15); - earthRadius = getScaledValue(16); - majorAxis = getScaledValue(21); - minorAxis = getScaledValue(26); - nx = getOctet4(31); - ny = getOctet4(35); + // the following does not apply to template 101 (unstructured data) + // TODO most probably also for other templates + if (template != 101) { + earthRadius = getScaledValue(16); + majorAxis = getScaledValue(21); + minorAxis = getScaledValue(26); + + nx = getOctet4(31); + ny = getOctet4(35); + } } protected void finish() { @@ -503,8 +521,8 @@ public GdsHorizCoordSys makeHorizCoordSys() { // ProjectionPoint startP = proj.latLonToProj(LatLonPoint.create(la1, lo1)); double startx = lo1; // startP.getX(); double starty = la1; // startP.getY(); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, startx, deltaLon, starty, - deltaLat, getNxRaw(), getNyRaw(), getNptsInLine()); + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, startx, deltaLon, + starty, deltaLat, getNxRaw(), getNyRaw(), getNptsInLine()); } public void testHorizCoordSys(Formatter f) { @@ -625,8 +643,8 @@ public GdsHorizCoordSys makeHorizCoordSys() { // LatLonPoint startLL = proj.projToLatLon(ProjectionPoint.create(lo1, la1)); // double startx = startLL.getLongitude(); // double starty = startLL.getLatitude(); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, lo1, deltaLon, la1, deltaLat, - getNxRaw(), getNyRaw(), getNptsInLine()); + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, lo1, deltaLon, la1, + deltaLat, getNxRaw(), getNyRaw(), getNptsInLine()); } public void testHorizCoordSys(Formatter f) { @@ -838,7 +856,7 @@ public GdsHorizCoordSys makeHorizCoordSys() { double startx = startP.getX(); double starty = startP.getY(); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, startx, dX, starty, dY, + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, startx, dX, starty, dY, getNxRaw(), getNyRaw(), getNptsInLine()); } @@ -1010,7 +1028,7 @@ public GdsHorizCoordSys makeHorizCoordSys() { } ProjectionPointImpl start = (ProjectionPointImpl) proj.latLonToProj(LatLonPoint.create(la1, lo1)); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, start.getX(), dX, + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, start.getX(), dX, start.getY(), dY, getNxRaw(), getNyRaw(), getNptsInLine()); } @@ -1185,7 +1203,7 @@ public GdsHorizCoordSys makeHorizCoordSys() { LatLonPoint startLL = LatLonPoint.create(la1, lo1); ProjectionPointImpl start = (ProjectionPointImpl) proj.latLonToProj(startLL); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, start.getX(), dX, + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, start.getX(), dX, start.getY(), dY, getNxRaw(), getNyRaw(), getNptsInLine()); } @@ -1272,7 +1290,7 @@ public GdsHorizCoordSys makeHorizCoordSys() { LatLonPoint startLL = LatLonPoint.create(la1, lo1); ProjectionPointImpl start = (ProjectionPointImpl) proj.latLonToProj(startLL); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, start.getX(), dX, + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, start.getX(), dX, start.getY(), dY, getNxRaw(), getNyRaw(), getNptsInLine()); } @@ -1438,7 +1456,7 @@ public GdsHorizCoordSys makeHorizCoordSys() { * } */ - GdsHorizCoordSys coordSys = new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, + GdsHorizCoordSys coordSys = new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, new LatLonProjection(), lo1, deltaLon, 0, 0, getNxRaw(), getNyRaw(), getNptsInLine()); coordSys.setGaussianLats(Nparellels, la1, la2); @@ -1680,8 +1698,8 @@ public GdsHorizCoordSys makeHorizCoordSys() { } MSGnavigation proj = new MSGnavigation(LaP, LoP, majorAxis, minorAxis, Nr * majorAxis, scale_x, scale_y); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, startx, incrx, starty, incry, - getNxRaw(), getNyRaw(), getNptsInLine()); + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, startx, incrx, starty, + incry, getNxRaw(), getNyRaw(), getNptsInLine()); } public void testHorizCoordSys(Formatter f) { @@ -1703,6 +1721,124 @@ public void testHorizCoordSys(Formatter f) { } + /** + * GRIB2 Grid Definition Template 3.101: General Unstructured Grid. + * Parses and stores metadata for unstructured grids (e.g., ICON model grids), + * including grid identifiers and UUID. + * + * https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_temp3-101.shtml + */ + public static class GdsUnstructured extends Grib2Gds { + // Template 3.101 specific fields + protected final int numberOfGridUsed; + protected final int numberOfGridInReference; + protected final UUID horizontalGridUUID; + + /** + * Construct a GdsUnstructured from a GRIB2 GDS byte array. + * + * @param data The full GDS section bytes. + * @param template Template number (should be 101 for this class). + */ + protected GdsUnstructured(byte[] data, int template) { + super(data, template); // let base class parse common fields (Earth shape, etc.) + + // Octets are 1-indexed in spec, but data[] is 0-indexed. + // Octet 16-18: 3-byte unsigned integer for numberOfGridUsed + this.numberOfGridUsed = GribNumbers.int3(getOctet(16), getOctet(17), getOctet(18)); + // Octet 19: 1-byte unsigned integer for numberOfGridInReference + this.numberOfGridInReference = data[19] & 0xFF; + // Octets 20-35: 16-byte UUID for horizontal grid + long msb = 0, lsb = 0; + for (int i = 20; i <= 27; i++) { // first 8 bytes -> most significant bits + msb = (msb << 8) | (getOctet(i) & 0xff); + } + for (int i = 28; i <= 35; i++) { // next 8 bytes -> least significant bits + lsb = (lsb << 8) | (getOctet(i) & 0xff); + } + this.horizontalGridUUID = new UUID(msb, lsb); + + // Set lastOctet position to 35 + 1 = 36 (if needed by base class logic) + this.lastOctet = 36; // indicates we've read through octet 35 + } + + /** Unstructured grids are not regular lat/lon grids. */ + @Override + public boolean isLatLon() { + return false; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof GdsUnstructured)) + return false; + if (!super.equals(obj)) + return false; + GdsUnstructured other = (GdsUnstructured) obj; + return this.numberOfGridUsed == other.numberOfGridUsed + && this.numberOfGridInReference == other.numberOfGridInReference + && this.horizontalGridUUID.equals(other.horizontalGridUUID); + } + + @Override + public int hashCode() { + // Combine base class hash with hash of new fields + int result = super.hashCode(); + result = 31 * result + numberOfGridUsed; + result = 31 * result + numberOfGridInReference; + result = 31 * result + horizontalGridUUID.hashCode(); + return result; + } + + @Override + public GdsHorizCoordSys makeHorizCoordSys() { + // Use a standard LatLonProjection (an identity projection) + ProjectionImpl proj = new UnstructuredProjection(earthShape, numberOfGridUsed, numberOfGridInReference, + horizontalGridUUID.toString()); + // Define a global bounding box: latitudes -90 to 90, longitudes -180 to 180. + double startx = -180.0; // lon + double starty = -90.0; // lat + double dx = 360.0; + double dy = 180.0; + // Set minimal grid dimensions; these are dummy values since the grid is unstructured. + int nx = numberOfDataPoints; + int ny = 1; + int[] nptsInLine = null; + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, startx, dx, starty, dy, + nx, ny, nptsInLine) { + @Override + public ucar.unidata.geoloc.LatLonRect getLatLonBB() { + return new ucar.unidata.geoloc.LatLonRect(ucar.unidata.geoloc.LatLonPoint.create(starty, startx), dy, dx); + } + + @Override + public String makeId() { + return getNameShort() + "_" + earthShape + "-" + numberOfGridUsed + "-" + numberOfGridInReference + "_" + + horizontalGridUUID.toString(); + } + + @Override + public String makeDescription() { + return getNameShort() + ": earthShape " + earthShape + ", numberOfGridUsed " + numberOfGridUsed + + ", numberOfGridInReference " + numberOfGridInReference + ", UUID " + horizontalGridUUID.toString(); + } + }; + } + + @Override + public void testHorizCoordSys(Formatter f) { + GdsHorizCoordSys cs = makeHorizCoordSys(); + f.format("%s testHorizCoordSys%n", getClass().getName()); + f.format(" Projection: %s%n", cs.proj.getClass().getName()); + f.format(" Start (proj): (%f, %f)%n", cs.startx, cs.starty); + f.format(" dx = %f, dy = %f%n", cs.dx, cs.dy); + f.format(" Grid dimensions: nx = %d, ny = %d%n", cs.nx, cs.ny); + f.format(" LatLon bounding box: %s%n", cs.getLatLonBB()); + } + } + /* * Curvilinear Orthogonal Grids (NCEP grid 204) * see http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-1.shtml @@ -1760,7 +1896,7 @@ public int hashCode() { public GdsHorizCoordSys makeHorizCoordSys() { LatLonProjection proj = new LatLonProjection(); - return new GdsHorizCoordSys(getNameShort(), template, getOctet4(7), scanMode, proj, 0, 1, 0, 1, getNxRaw(), + return new GdsHorizCoordSys(getNameShort(), template, numberOfDataPoints, scanMode, proj, 0, 1, 0, 1, getNxRaw(), getNyRaw(), getNptsInLine()); } diff --git a/grib/src/test/java/ucar/nc2/grib/grib2/TestGdsUnstructured.java b/grib/src/test/java/ucar/nc2/grib/grib2/TestGdsUnstructured.java new file mode 100644 index 0000000000..8bbc9c2fd5 --- /dev/null +++ b/grib/src/test/java/ucar/nc2/grib/grib2/TestGdsUnstructured.java @@ -0,0 +1,148 @@ +package ucar.nc2.grib.grib2; + +import org.junit.Test; +import org.junit.experimental.categories.Category; +import ucar.ma2.Array; +import ucar.nc2.AttributeContainer; +import ucar.nc2.Group; +import ucar.nc2.Variable; +import ucar.nc2.dataset.NetcdfDataset; +import ucar.nc2.dataset.NetcdfDatasets; +import ucar.unidata.util.test.TestDir; +import ucar.unidata.util.test.category.NeedsCdmUnitTest; + +import java.io.IOException; +import java.util.UUID; + +import static org.junit.Assert.*; + +public class TestGdsUnstructured { + + @Test + public void testParseFromBytes() { + // Construct a raw GDS (Section 3) byte array for an icosahedral grid. + // We'll use a very coarse grid (e.g., n2=1, n3=0 => Ni = 2^1 * 3^0 = 2 intervals). + byte[] gdsBytes = new byte[35]; + // Set Section 3 length (bytes 0-3) and section number (byte 4) + int length = gdsBytes.length; + gdsBytes[0] = 0; + gdsBytes[1] = 0; + gdsBytes[2] = (byte) ((length >> 8) & 0xFF); + gdsBytes[3] = (byte) (length & 0xFF); + gdsBytes[4] = 3; // Section number = 3 + + // Source of grid definition = 0 (specified in Section 3) + gdsBytes[5] = 0; + int totalPoints = 20; + // Fill totalPoints in bytes 6-9 (32-bit int) + gdsBytes[6] = (byte) ((totalPoints >> 24) & 0xFF); + gdsBytes[7] = (byte) ((totalPoints >> 16) & 0xFF); + gdsBytes[8] = (byte) ((totalPoints >> 8) & 0xFF); + gdsBytes[9] = (byte) (totalPoints & 0xFF); + // No optional list (byte 10 = 0 length, byte 11 = 0) + gdsBytes[10] = 0; + gdsBytes[11] = 0; + // Template number 3.101 (bytes 12-13) + gdsBytes[12] = 0; + gdsBytes[13] = 101; + + int earthShape = 6; + gdsBytes[14] = (byte) earthShape; // shape of the earth + + int numberOfGridUsed = 26; + gdsBytes[15] = (byte) ((numberOfGridUsed >> 16) & 0xFF); + gdsBytes[16] = (byte) ((numberOfGridUsed >> 8) & 0xFF); + gdsBytes[17] = (byte) (numberOfGridUsed & 0xFF); + + int numberOfGridInReference = 162; + gdsBytes[18] = (byte) numberOfGridInReference; // number of grid in reference + + UUID uuid = UUID.fromString("a27b8de6-18c4-11e4-820a-b5b098c6a5c0"); + long msb = uuid.getMostSignificantBits(); + long lsb = uuid.getLeastSignificantBits(); + + // Fill UUID bytes 19-34 + // first msb (8 byte) + gdsBytes[19] = (byte) ((msb >> 56) & 0xFF); + gdsBytes[20] = (byte) ((msb >> 48) & 0xFF); + gdsBytes[21] = (byte) ((msb >> 40) & 0xFF); + gdsBytes[22] = (byte) ((msb >> 32) & 0xFF); + gdsBytes[23] = (byte) ((msb >> 24) & 0xFF); + gdsBytes[24] = (byte) ((msb >> 16) & 0xFF); + gdsBytes[25] = (byte) ((msb >> 8) & 0xFF); + gdsBytes[26] = (byte) (msb & 0xFF); + // then lsb (8 byte) + gdsBytes[27] = (byte) ((lsb >> 56) & 0xFF); + gdsBytes[28] = (byte) ((lsb >> 48) & 0xFF); + gdsBytes[29] = (byte) ((lsb >> 40) & 0xFF); + gdsBytes[30] = (byte) ((lsb >> 32) & 0xFF); + gdsBytes[31] = (byte) ((lsb >> 24) & 0xFF); + gdsBytes[32] = (byte) ((lsb >> 16) & 0xFF); + gdsBytes[33] = (byte) ((lsb >> 8) & 0xFF); + gdsBytes[34] = (byte) (lsb & 0xFF); + + + // Parse the byte array into a Grib2Gds (should produce a Grib2Gds.GdsUnstructured instance) + Grib2Gds gds = Grib2Gds.factory(101, gdsBytes); + assertNotNull("Parsed GDS should not be null", gds); + assertTrue("Factory should return Grib2Gds.GdsUnstructured instance", gds instanceof Grib2Gds.GdsUnstructured); + Grib2Gds.GdsUnstructured usGds = (Grib2Gds.GdsUnstructured) gds; + + // Verify the parsed fields + assertEquals("Template number", 101, usGds.template); + + assertEquals("numberOfDataPoints", totalPoints, usGds.numberOfDataPoints); + assertEquals("earthShape", earthShape, usGds.earthShape); + + assertEquals("numberOfGridUsed", numberOfGridUsed, usGds.numberOfGridUsed); + assertEquals("numberOfGridInReference", numberOfGridInReference, usGds.numberOfGridInReference); + assertEquals("horizontalGridUUID", uuid, usGds.horizontalGridUUID); + assertEquals("isLatLon", false, usGds.isLatLon()); + } + + @Test + @Category(NeedsCdmUnitTest.class) + public void testIconGrib2FileRead() throws IOException { + String iconFile = + TestDir.cdmUnitTestDir + "/formats/grib2/ugrid/icon_global_icosahedral_single-level_2025031912_004_T_2M.grib2"; + // Open the GRIB2 file as a NetcdfDataset (netCDF-Java will handle the GRIB indexing and reading) + try (NetcdfDataset ds = NetcdfDatasets.openDataset(iconFile)) { + // Ensure the dataset opened + assertNotNull("NetcdfDataset should be opened", ds); + + Group rootGroup = ds.getRootGroup(); + assertNotNull("Root group should not be null", rootGroup); + + int length = 2949120; + assertEquals("x dimension", length, rootGroup.findDimension("x").getLength()); + assertEquals("y dimension", 1, rootGroup.findDimension("y").getLength()); + assertEquals("time dimension", 1, rootGroup.findDimension("time").getLength()); + assertEquals("height_above_ground dimension", 1, rootGroup.findDimension("height_above_ground").getLength()); + + Variable projection = rootGroup.findVariableLocal("GdsUnstructured_Projection"); + assertNotNull("projection should not be null", projection); + AttributeContainer projectionAttributes = projection.attributes(); + assertEquals("earth_shape", 6d, projectionAttributes.findAttributeDouble("earth_shape", Double.NaN), 0); + assertEquals("number_of_grid_used", 26d, + projectionAttributes.findAttributeDouble("number_of_grid_used", Double.NaN), 0); + assertEquals("number_of_grid_in_reference", 162d, + projectionAttributes.findAttributeDouble("number_of_grid_in_reference", Double.NaN), 0); + assertEquals("uuid", "a27b8de6-18c4-11e4-820a-b5b098c6a5c0", + projectionAttributes.findAttributeString("uuid", null)); + + float[] heightAboveGround = + (float[]) rootGroup.findVariableLocal("height_above_ground").read().copyToNDJavaArray(); + assertEquals("heightAboveGround length", 1, heightAboveGround.length); + assertEquals("heightAboveGround", 2f, heightAboveGround[0], 0); + + Variable ddata1 = rootGroup.findVariableLocal("Temperature_height_above_ground"); + Array imageDataA = ddata1.read(); + float[][][][] thag = (float[][][][]) imageDataA.copyToNDJavaArray(); + assertEquals("data dimension 0", 1, thag.length); + assertEquals("data dimension 1", 1, thag[0].length); + assertEquals("data dimension 2", 1, thag[0][0].length); + assertEquals("data dimension 3", length, thag[0][0][0].length); + } + } + +}