This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch feature/cog-writer
in repository https://gitbox.apache.org/repos/asf/sedona.git

commit b31f4de563f996e6c1e7c0d06188aa56c2382fef
Author: Jia Yu <[email protected]>
AuthorDate: Thu Feb 19 02:11:46 2026 -0800

    Add CogOptions builder for configurable COG generation
    
    - CogOptions: immutable builder with compression, compressionQuality,
      tileSize, resampling (Nearest/Bilinear/Bicubic), and overviewCount
    - CogWriter: new write(raster, CogOptions) primary method; existing
      overloads delegate to it; generateOverview accepts Interpolation param
    - RasterOutputs: new asCloudOptimizedGeoTiff(raster, CogOptions) overload
    - 13 new tests (25 total): builder validation, resampling modes,
      overviewCount=0/1, tileSize=512, RasterOutputs CogOptions path
---
 .../apache/sedona/common/raster/RasterOutputs.java |  16 ++
 .../sedona/common/raster/cog/CogOptions.java       | 243 +++++++++++++++++++++
 .../apache/sedona/common/raster/cog/CogWriter.java | 106 ++++++---
 .../sedona/common/raster/cog/CogWriterTest.java    | 229 +++++++++++++++++++
 4 files changed, 564 insertions(+), 30 deletions(-)

diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java 
b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
index 791e2d27e7..5a74fea38d 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
@@ -36,6 +36,7 @@ import javax.imageio.ImageWriteParam;
 import javax.media.jai.InterpolationNearest;
 import javax.media.jai.JAI;
 import javax.media.jai.RenderedOp;
+import org.apache.sedona.common.raster.cog.CogOptions;
 import org.apache.sedona.common.raster.cog.CogWriter;
 import org.apache.sedona.common.utils.RasterUtils;
 import org.geotools.api.coverage.grid.GridCoverageWriter;
@@ -123,6 +124,21 @@ public class RasterOutputs {
     }
   }
 
+  /**
+   * Creates a Cloud Optimized GeoTIFF (COG) byte array with the given options.
+   *
+   * @param raster The input raster
+   * @param options COG generation options (compression, tileSize, resampling, 
overviewCount)
+   * @return COG file as byte array
+   */
+  public static byte[] asCloudOptimizedGeoTiff(GridCoverage2D raster, 
CogOptions options) {
+    try {
+      return CogWriter.write(raster, options);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to write Cloud Optimized GeoTIFF", e);
+    }
+  }
+
   /**
    * Creates a GeoTiff file with the provided raster. Primarily used for 
testing.
    *
diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/cog/CogOptions.java 
b/common/src/main/java/org/apache/sedona/common/raster/cog/CogOptions.java
new file mode 100644
index 0000000000..d253be7428
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/raster/cog/CogOptions.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sedona.common.raster.cog;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Options for Cloud Optimized GeoTIFF (COG) generation.
+ *
+ * <p>Use the {@link Builder} to construct instances:
+ *
+ * <pre>{@code
+ * CogOptions opts = CogOptions.builder()
+ *     .compression("LZW")
+ *     .compressionQuality(0.5)
+ *     .tileSize(512)
+ *     .resampling("Bilinear")
+ *     .overviewCount(3)
+ *     .build();
+ * }</pre>
+ *
+ * <p>All fields are immutable once constructed. Validation is performed in 
{@link Builder#build()}.
+ */
+public final class CogOptions {
+
+  /** Supported resampling algorithms for overview generation. */
+  private static final List<String> VALID_RESAMPLING =
+      Arrays.asList("Nearest", "Bilinear", "Bicubic");
+
+  private final String compression;
+  private final double compressionQuality;
+  private final int tileSize;
+  private final String resampling;
+  private final int overviewCount;
+
+  private CogOptions(Builder builder) {
+    this.compression = builder.compression;
+    this.compressionQuality = builder.compressionQuality;
+    this.tileSize = builder.tileSize;
+    this.resampling = builder.resampling;
+    this.overviewCount = builder.overviewCount;
+  }
+
+  /**
+   * @return Compression type: "Deflate", "LZW", "JPEG", "PackBits"
+   */
+  public String getCompression() {
+    return compression;
+  }
+
+  /**
+   * @return Compression quality from 0.0 (max compression) to 1.0 (no 
compression)
+   */
+  public double getCompressionQuality() {
+    return compressionQuality;
+  }
+
+  /**
+   * @return Tile width and height in pixels (always a power of 2)
+   */
+  public int getTileSize() {
+    return tileSize;
+  }
+
+  /**
+   * @return Resampling algorithm for overview generation: "Nearest", 
"Bilinear", or "Bicubic"
+   */
+  public String getResampling() {
+    return resampling;
+  }
+
+  /**
+   * @return Number of overview levels. -1 means auto-compute based on image 
dimensions, 0 means no
+   *     overviews.
+   */
+  public int getOverviewCount() {
+    return overviewCount;
+  }
+
+  /**
+   * @return A new builder initialized with default values
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * @return The default options (Deflate, quality 0.2, 256px tiles, Nearest, 
auto overviews)
+   */
+  public static CogOptions defaults() {
+    return new Builder().build();
+  }
+
+  @Override
+  public String toString() {
+    return "CogOptions{"
+        + "compression='"
+        + compression
+        + '\''
+        + ", compressionQuality="
+        + compressionQuality
+        + ", tileSize="
+        + tileSize
+        + ", resampling='"
+        + resampling
+        + '\''
+        + ", overviewCount="
+        + overviewCount
+        + '}';
+  }
+
+  /** Builder for {@link CogOptions}. */
+  public static final class Builder {
+    private String compression = "Deflate";
+    private double compressionQuality = 0.2;
+    private int tileSize = 256;
+    private String resampling = "Nearest";
+    private int overviewCount = -1;
+
+    private Builder() {}
+
+    /**
+     * Set the compression type. Default: "Deflate".
+     *
+     * @param compression One of "Deflate", "LZW", "JPEG", "PackBits"
+     * @return this builder
+     */
+    public Builder compression(String compression) {
+      this.compression = compression;
+      return this;
+    }
+
+    /**
+     * Set the compression quality. Default: 0.2.
+     *
+     * @param compressionQuality Value from 0.0 (max compression) to 1.0 (no 
compression)
+     * @return this builder
+     */
+    public Builder compressionQuality(double compressionQuality) {
+      this.compressionQuality = compressionQuality;
+      return this;
+    }
+
+    /**
+     * Set the tile size for both width and height. Default: 256.
+     *
+     * @param tileSize Must be a positive power of 2 (e.g. 128, 256, 512, 1024)
+     * @return this builder
+     */
+    public Builder tileSize(int tileSize) {
+      this.tileSize = tileSize;
+      return this;
+    }
+
+    /**
+     * Set the resampling algorithm for overview generation. Default: 
"Nearest".
+     *
+     * @param resampling One of "Nearest", "Bilinear", "Bicubic"
+     * @return this builder
+     */
+    public Builder resampling(String resampling) {
+      this.resampling = resampling;
+      return this;
+    }
+
+    /**
+     * Set the number of overview levels. Default: -1 (auto-compute).
+     *
+     * @param overviewCount -1 for auto, 0 for no overviews, or a positive 
count
+     * @return this builder
+     */
+    public Builder overviewCount(int overviewCount) {
+      this.overviewCount = overviewCount;
+      return this;
+    }
+
+    /**
+     * Build and validate the options.
+     *
+     * @return A validated, immutable {@link CogOptions} instance
+     * @throws IllegalArgumentException if any option is invalid
+     */
+    public CogOptions build() {
+      if (compression == null || compression.isEmpty()) {
+        throw new IllegalArgumentException("compression must not be null or 
empty");
+      }
+      if (compressionQuality < 0 || compressionQuality > 1.0) {
+        throw new IllegalArgumentException(
+            "compressionQuality must be between 0.0 and 1.0, got: " + 
compressionQuality);
+      }
+      if (tileSize <= 0) {
+        throw new IllegalArgumentException("tileSize must be positive, got: " 
+ tileSize);
+      }
+      if ((tileSize & (tileSize - 1)) != 0) {
+        throw new IllegalArgumentException("tileSize must be a power of 2, 
got: " + tileSize);
+      }
+      if (overviewCount < -1) {
+        throw new IllegalArgumentException(
+            "overviewCount must be -1 (auto), 0 (none), or positive, got: " + 
overviewCount);
+      }
+
+      // Normalize resampling to title-case for matching
+      String normalized = normalizeResampling(resampling);
+      if (!VALID_RESAMPLING.contains(normalized)) {
+        throw new IllegalArgumentException(
+            "resampling must be one of " + VALID_RESAMPLING + ", got: '" + 
resampling + "'");
+      }
+      this.resampling = normalized;
+
+      return new CogOptions(this);
+    }
+
+    /**
+     * Normalize the resampling string to title-case (first letter uppercase, 
rest lowercase) so
+     * callers can pass "nearest", "BILINEAR", etc.
+     */
+    private static String normalizeResampling(String value) {
+      if (value == null || value.isEmpty()) {
+        return "Nearest";
+      }
+      String lower = value.toLowerCase(Locale.ROOT);
+      return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
+    }
+  }
+}
diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java 
b/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java
index 4adadedd43..4cbf19ea67 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/cog/CogWriter.java
@@ -25,6 +25,8 @@ import java.util.ArrayList;
 import java.util.List;
 import javax.imageio.ImageWriteParam;
 import javax.media.jai.Interpolation;
+import javax.media.jai.InterpolationBicubic;
+import javax.media.jai.InterpolationBilinear;
 import javax.media.jai.InterpolationNearest;
 import org.geotools.api.coverage.grid.GridCoverageWriter;
 import org.geotools.api.parameter.GeneralParameterValue;
@@ -65,49 +67,40 @@ public class CogWriter {
   private static final int MIN_OVERVIEW_SIZE = 2;
 
   /**
-   * Write a GridCoverage2D as a Cloud Optimized GeoTIFF byte array.
+   * Write a GridCoverage2D as a Cloud Optimized GeoTIFF byte array using the 
given options.
    *
    * @param raster The input raster
-   * @param compressionType Compression type: "Deflate", "LZW", "JPEG", 
"PackBits", or null for
-   *     default (Deflate)
-   * @param compressionQuality Quality 0.0 (max compression) to 1.0 (no 
compression), or -1 for
-   *     default
-   * @param tileSize Tile width and height in pixels
+   * @param options COG generation options (compression, tileSize, resampling, 
overviewCount)
    * @return COG file as byte array
    * @throws IOException if writing fails
    */
-  public static byte[] write(
-      GridCoverage2D raster, String compressionType, double 
compressionQuality, int tileSize)
-      throws IOException {
-
-    if (compressionType == null) {
-      compressionType = "Deflate";
-    }
-    if (compressionQuality < 0) {
-      compressionQuality = 0.2;
-    }
-    if (compressionQuality > 1.0) {
-      throw new IllegalArgumentException(
-          "compressionQuality must be between 0.0 and 1.0, got: " + 
compressionQuality);
-    }
-    if (tileSize <= 0) {
-      throw new IllegalArgumentException("tileSize must be positive, got: " + 
tileSize);
-    }
-    if ((tileSize & (tileSize - 1)) != 0) {
-      throw new IllegalArgumentException("tileSize must be a power of 2, got: 
" + tileSize);
-    }
+  public static byte[] write(GridCoverage2D raster, CogOptions options) throws 
IOException {
+    String compressionType = options.getCompression();
+    double compressionQuality = options.getCompressionQuality();
+    int tileSize = options.getTileSize();
+    String resampling = options.getResampling();
+    int requestedOverviewCount = options.getOverviewCount();
 
     RenderedImage image = raster.getRenderedImage();
     int cols = image.getWidth();
     int rows = image.getHeight();
 
     // Step 1: Compute overview decimation factors
-    List<Integer> decimations = computeOverviewDecimations(cols, rows, 
tileSize);
+    List<Integer> decimations;
+    if (requestedOverviewCount == 0) {
+      decimations = new ArrayList<>();
+    } else {
+      decimations = computeOverviewDecimations(cols, rows, tileSize);
+      if (requestedOverviewCount > 0 && requestedOverviewCount < 
decimations.size()) {
+        decimations = decimations.subList(0, requestedOverviewCount);
+      }
+    }
 
     // Step 2: Generate overview coverages
+    Interpolation interpolation = getInterpolation(resampling);
     List<GridCoverage2D> overviews = new ArrayList<>();
     for (int decimation : decimations) {
-      GridCoverage2D overview = generateOverview(raster, decimation);
+      GridCoverage2D overview = generateOverview(raster, decimation, 
interpolation);
       overviews.add(overview);
     }
 
@@ -128,6 +121,32 @@ public class CogWriter {
     return CogAssembler.assemble(parsedTiffs);
   }
 
+  /**
+   * Write a GridCoverage2D as a Cloud Optimized GeoTIFF byte array.
+   *
+   * @param raster The input raster
+   * @param compressionType Compression type: "Deflate", "LZW", "JPEG", 
"PackBits", or null for
+   *     default (Deflate)
+   * @param compressionQuality Quality 0.0 (max compression) to 1.0 (no 
compression), or -1 for
+   *     default
+   * @param tileSize Tile width and height in pixels
+   * @return COG file as byte array
+   * @throws IOException if writing fails
+   */
+  public static byte[] write(
+      GridCoverage2D raster, String compressionType, double 
compressionQuality, int tileSize)
+      throws IOException {
+
+    CogOptions.Builder builder = CogOptions.builder().tileSize(tileSize);
+    if (compressionType != null) {
+      builder.compression(compressionType);
+    }
+    if (compressionQuality >= 0) {
+      builder.compressionQuality(compressionQuality);
+    }
+    return write(raster, builder.build());
+  }
+
   /**
    * Write a GridCoverage2D as COG with default settings (Deflate compression, 
256x256 tiles).
    *
@@ -186,9 +205,11 @@ public class CogWriter {
    *
    * @param raster The full resolution raster
    * @param decimationFactor Factor to reduce by (2 = half size, 4 = quarter, 
etc.)
+   * @param interpolation The interpolation method to use for resampling
    * @return A new GridCoverage2D at reduced resolution
    */
-  static GridCoverage2D generateOverview(GridCoverage2D raster, int 
decimationFactor) {
+  static GridCoverage2D generateOverview(
+      GridCoverage2D raster, int decimationFactor, Interpolation 
interpolation) {
     RenderedImage image = raster.getRenderedImage();
     int newWidth = (int) Math.ceil((double) image.getWidth() / 
decimationFactor);
     int newHeight = (int) Math.ceil((double) image.getHeight() / 
decimationFactor);
@@ -218,10 +239,35 @@ public class CogWriter {
             crs,
             null);
 
-    Interpolation interpolation = new InterpolationNearest();
     return (GridCoverage2D) Operations.DEFAULT.resample(raster, null, 
gridGeometry, interpolation);
   }
 
+  /**
+   * Generate an overview using default nearest-neighbor interpolation. Kept 
for backward
+   * compatibility with tests.
+   */
+  static GridCoverage2D generateOverview(GridCoverage2D raster, int 
decimationFactor) {
+    return generateOverview(raster, decimationFactor, new 
InterpolationNearest());
+  }
+
+  /**
+   * Map a resampling algorithm name to a JAI Interpolation instance.
+   *
+   * @param resampling One of "Nearest", "Bilinear", "Bicubic"
+   * @return The corresponding JAI Interpolation
+   */
+  private static Interpolation getInterpolation(String resampling) {
+    switch (resampling) {
+      case "Bilinear":
+        return new InterpolationBilinear();
+      case "Bicubic":
+        return new InterpolationBicubic(8);
+      case "Nearest":
+      default:
+        return new InterpolationNearest();
+    }
+  }
+
   /**
    * Write a GridCoverage2D as a tiled GeoTIFF byte array using GeoTools.
    *
diff --git 
a/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java 
b/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java
index dfbbd07d0e..7989456e6b 100644
--- 
a/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java
+++ 
b/common/src/test/java/org/apache/sedona/common/raster/cog/CogWriterTest.java
@@ -419,4 +419,233 @@ public class CogWriterTest {
     // The last IFD end should be well before the end of the file
     assertTrue("IFD region should be at start of file", lastIfdEnd < 
cogBytes.length / 2);
   }
+
+  // --- CogOptions tests ---
+
+  @Test
+  public void testCogOptionsDefaults() {
+    CogOptions opts = CogOptions.defaults();
+    assertEquals("Deflate", opts.getCompression());
+    assertEquals(0.2, opts.getCompressionQuality(), 0.001);
+    assertEquals(256, opts.getTileSize());
+    assertEquals("Nearest", opts.getResampling());
+    assertEquals(-1, opts.getOverviewCount());
+  }
+
+  @Test
+  public void testCogOptionsBuilder() {
+    CogOptions opts =
+        CogOptions.builder()
+            .compression("LZW")
+            .compressionQuality(0.8)
+            .tileSize(512)
+            .resampling("Bilinear")
+            .overviewCount(3)
+            .build();
+
+    assertEquals("LZW", opts.getCompression());
+    assertEquals(0.8, opts.getCompressionQuality(), 0.001);
+    assertEquals(512, opts.getTileSize());
+    assertEquals("Bilinear", opts.getResampling());
+    assertEquals(3, opts.getOverviewCount());
+  }
+
+  @Test
+  public void testCogOptionsResamplingNormalization() {
+    // Case-insensitive resampling names
+    assertEquals("Nearest", 
CogOptions.builder().resampling("nearest").build().getResampling());
+    assertEquals("Bilinear", 
CogOptions.builder().resampling("BILINEAR").build().getResampling());
+    assertEquals("Bicubic", 
CogOptions.builder().resampling("bicubic").build().getResampling());
+  }
+
+  @Test
+  public void testCogOptionsInvalidResampling() {
+    try {
+      CogOptions.builder().resampling("Lanczos").build();
+      fail("Expected IllegalArgumentException for invalid resampling");
+    } catch (IllegalArgumentException e) {
+      assertTrue(e.getMessage().contains("resampling"));
+    }
+  }
+
+  @Test
+  public void testCogOptionsInvalidTileSize() {
+    try {
+      CogOptions.builder().tileSize(300).build();
+      fail("Expected IllegalArgumentException for non-power-of-2 tileSize");
+    } catch (IllegalArgumentException e) {
+      assertTrue(e.getMessage().contains("power of 2"));
+    }
+  }
+
+  @Test
+  public void testCogOptionsInvalidOverviewCount() {
+    try {
+      CogOptions.builder().overviewCount(-2).build();
+      fail("Expected IllegalArgumentException for negative overviewCount");
+    } catch (IllegalArgumentException e) {
+      assertTrue(e.getMessage().contains("overviewCount"));
+    }
+  }
+
+  @Test
+  public void testWriteWithCogOptions() throws IOException {
+    double[] bandValues = new double[512 * 512];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = (i * 7) % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    CogOptions opts =
+        
CogOptions.builder().compression("LZW").compressionQuality(0.5).tileSize(256).build();
+
+    byte[] cogBytes = CogWriter.write(raster, opts);
+    assertNotNull(cogBytes);
+    assertTrue(cogBytes.length > 0);
+
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(512, readBack.getRenderedImage().getWidth());
+    assertEquals(512, readBack.getRenderedImage().getHeight());
+
+    double[] originalValues = MapAlgebra.bandAsArray(raster, 1);
+    double[] readBackValues = MapAlgebra.bandAsArray(readBack, 1);
+    assertArrayEquals(originalValues, readBackValues, 0.01);
+  }
+
+  @Test
+  public void testWriteWithBilinearResampling() throws IOException {
+    double[] bandValues = new double[512 * 512];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = (i * 3) % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    CogOptions opts = CogOptions.builder().resampling("Bilinear").build();
+    byte[] cogBytes = CogWriter.write(raster, opts);
+    assertNotNull(cogBytes);
+    assertTrue(cogBytes.length > 0);
+
+    // Must be valid TIFF and readable
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(512, readBack.getRenderedImage().getWidth());
+  }
+
+  @Test
+  public void testWriteWithBicubicResampling() throws IOException {
+    double[] bandValues = new double[512 * 512];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = (i * 5) % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    CogOptions opts = CogOptions.builder().resampling("Bicubic").build();
+    byte[] cogBytes = CogWriter.write(raster, opts);
+    assertNotNull(cogBytes);
+
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(512, readBack.getRenderedImage().getWidth());
+  }
+
+  @Test
+  public void testWriteWithOverviewCountZero() throws IOException {
+    double[] bandValues = new double[512 * 512];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = (i * 11) % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 512, 512, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    CogOptions opts = CogOptions.builder().overviewCount(0).build();
+    byte[] cogBytes = CogWriter.write(raster, opts);
+    assertNotNull(cogBytes);
+
+    // With overviewCount=0, there should be exactly 1 IFD (no overviews)
+    ByteOrder byteOrder = (cogBytes[0] == 'I') ? ByteOrder.LITTLE_ENDIAN : 
ByteOrder.BIG_ENDIAN;
+    ByteBuffer buf = ByteBuffer.wrap(cogBytes).order(byteOrder);
+    int firstIfdOffset = buf.getInt(4);
+    int tagCount = buf.getShort(firstIfdOffset) & 0xFFFF;
+    int nextIfdOffset = buf.getInt(firstIfdOffset + 2 + tagCount * 12);
+    assertEquals("Should have no overview IFD when overviewCount=0", 0, 
nextIfdOffset);
+
+    // Should still be readable
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(512, readBack.getRenderedImage().getWidth());
+  }
+
+  @Test
+  public void testWriteWithSpecificOverviewCount() throws IOException {
+    // 1024x1024 with tileSize=256 would normally produce 2 overviews (2, 4)
+    double[] bandValues = new double[1024 * 1024];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = (i * 13) % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 1024, 1024, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    // Request only 1 overview
+    CogOptions opts = CogOptions.builder().overviewCount(1).build();
+    byte[] cogBytes = CogWriter.write(raster, opts);
+    assertNotNull(cogBytes);
+
+    // Count IFDs: should have exactly 2 (full-res + 1 overview)
+    ByteOrder byteOrder = (cogBytes[0] == 'I') ? ByteOrder.LITTLE_ENDIAN : 
ByteOrder.BIG_ENDIAN;
+    ByteBuffer buf = ByteBuffer.wrap(cogBytes).order(byteOrder);
+
+    int ifdCount = 0;
+    int ifdOffset = buf.getInt(4);
+    while (ifdOffset != 0) {
+      ifdCount++;
+      int tagCount = buf.getShort(ifdOffset) & 0xFFFF;
+      ifdOffset = buf.getInt(ifdOffset + 2 + tagCount * 12);
+    }
+    assertEquals("Should have exactly 2 IFDs (full-res + 1 overview)", 2, 
ifdCount);
+
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(1024, readBack.getRenderedImage().getWidth());
+  }
+
+  @Test
+  public void testWriteWithTileSize512() throws IOException {
+    double[] bandValues = new double[1024 * 1024];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = (i * 17) % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 1024, 1024, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    CogOptions opts = CogOptions.builder().tileSize(512).build();
+    byte[] cogBytes = CogWriter.write(raster, opts);
+    assertNotNull(cogBytes);
+    assertTrue(cogBytes.length > 0);
+
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(1024, readBack.getRenderedImage().getWidth());
+  }
+
+  @Test
+  public void testRasterOutputsWithCogOptions() throws IOException {
+    double[] bandValues = new double[256 * 256];
+    for (int i = 0; i < bandValues.length; i++) {
+      bandValues[i] = i % 256;
+    }
+    GridCoverage2D raster =
+        RasterConstructors.makeNonEmptyRaster(
+            1, "d", 256, 256, 0, 0, 1, -1, 0, 0, 4326, new double[][] 
{bandValues});
+
+    CogOptions opts = 
CogOptions.builder().compression("LZW").overviewCount(0).build();
+    byte[] cogBytes = RasterOutputs.asCloudOptimizedGeoTiff(raster, opts);
+    assertNotNull(cogBytes);
+
+    GridCoverage2D readBack = RasterConstructors.fromGeoTiff(cogBytes);
+    assertEquals(256, readBack.getRenderedImage().getWidth());
+  }
 }

Reply via email to