Skip to content

Commit b31f4de

Browse files
committed
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
1 parent 6889c28 commit b31f4de

4 files changed

Lines changed: 564 additions & 30 deletions

File tree

common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import javax.media.jai.InterpolationNearest;
3737
import javax.media.jai.JAI;
3838
import javax.media.jai.RenderedOp;
39+
import org.apache.sedona.common.raster.cog.CogOptions;
3940
import org.apache.sedona.common.raster.cog.CogWriter;
4041
import org.apache.sedona.common.utils.RasterUtils;
4142
import org.geotools.api.coverage.grid.GridCoverageWriter;
@@ -123,6 +124,21 @@ public static byte[] asCloudOptimizedGeoTiff(GridCoverage2D raster) {
123124
}
124125
}
125126

127+
/**
128+
* Creates a Cloud Optimized GeoTIFF (COG) byte array with the given options.
129+
*
130+
* @param raster The input raster
131+
* @param options COG generation options (compression, tileSize, resampling, overviewCount)
132+
* @return COG file as byte array
133+
*/
134+
public static byte[] asCloudOptimizedGeoTiff(GridCoverage2D raster, CogOptions options) {
135+
try {
136+
return CogWriter.write(raster, options);
137+
} catch (IOException e) {
138+
throw new RuntimeException("Failed to write Cloud Optimized GeoTIFF", e);
139+
}
140+
}
141+
126142
/**
127143
* Creates a GeoTiff file with the provided raster. Primarily used for testing.
128144
*
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.sedona.common.raster.cog;
20+
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.Locale;
24+
25+
/**
26+
* Options for Cloud Optimized GeoTIFF (COG) generation.
27+
*
28+
* <p>Use the {@link Builder} to construct instances:
29+
*
30+
* <pre>{@code
31+
* CogOptions opts = CogOptions.builder()
32+
* .compression("LZW")
33+
* .compressionQuality(0.5)
34+
* .tileSize(512)
35+
* .resampling("Bilinear")
36+
* .overviewCount(3)
37+
* .build();
38+
* }</pre>
39+
*
40+
* <p>All fields are immutable once constructed. Validation is performed in {@link Builder#build()}.
41+
*/
42+
public final class CogOptions {
43+
44+
/** Supported resampling algorithms for overview generation. */
45+
private static final List<String> VALID_RESAMPLING =
46+
Arrays.asList("Nearest", "Bilinear", "Bicubic");
47+
48+
private final String compression;
49+
private final double compressionQuality;
50+
private final int tileSize;
51+
private final String resampling;
52+
private final int overviewCount;
53+
54+
private CogOptions(Builder builder) {
55+
this.compression = builder.compression;
56+
this.compressionQuality = builder.compressionQuality;
57+
this.tileSize = builder.tileSize;
58+
this.resampling = builder.resampling;
59+
this.overviewCount = builder.overviewCount;
60+
}
61+
62+
/**
63+
* @return Compression type: "Deflate", "LZW", "JPEG", "PackBits"
64+
*/
65+
public String getCompression() {
66+
return compression;
67+
}
68+
69+
/**
70+
* @return Compression quality from 0.0 (max compression) to 1.0 (no compression)
71+
*/
72+
public double getCompressionQuality() {
73+
return compressionQuality;
74+
}
75+
76+
/**
77+
* @return Tile width and height in pixels (always a power of 2)
78+
*/
79+
public int getTileSize() {
80+
return tileSize;
81+
}
82+
83+
/**
84+
* @return Resampling algorithm for overview generation: "Nearest", "Bilinear", or "Bicubic"
85+
*/
86+
public String getResampling() {
87+
return resampling;
88+
}
89+
90+
/**
91+
* @return Number of overview levels. -1 means auto-compute based on image dimensions, 0 means no
92+
* overviews.
93+
*/
94+
public int getOverviewCount() {
95+
return overviewCount;
96+
}
97+
98+
/**
99+
* @return A new builder initialized with default values
100+
*/
101+
public static Builder builder() {
102+
return new Builder();
103+
}
104+
105+
/**
106+
* @return The default options (Deflate, quality 0.2, 256px tiles, Nearest, auto overviews)
107+
*/
108+
public static CogOptions defaults() {
109+
return new Builder().build();
110+
}
111+
112+
@Override
113+
public String toString() {
114+
return "CogOptions{"
115+
+ "compression='"
116+
+ compression
117+
+ '\''
118+
+ ", compressionQuality="
119+
+ compressionQuality
120+
+ ", tileSize="
121+
+ tileSize
122+
+ ", resampling='"
123+
+ resampling
124+
+ '\''
125+
+ ", overviewCount="
126+
+ overviewCount
127+
+ '}';
128+
}
129+
130+
/** Builder for {@link CogOptions}. */
131+
public static final class Builder {
132+
private String compression = "Deflate";
133+
private double compressionQuality = 0.2;
134+
private int tileSize = 256;
135+
private String resampling = "Nearest";
136+
private int overviewCount = -1;
137+
138+
private Builder() {}
139+
140+
/**
141+
* Set the compression type. Default: "Deflate".
142+
*
143+
* @param compression One of "Deflate", "LZW", "JPEG", "PackBits"
144+
* @return this builder
145+
*/
146+
public Builder compression(String compression) {
147+
this.compression = compression;
148+
return this;
149+
}
150+
151+
/**
152+
* Set the compression quality. Default: 0.2.
153+
*
154+
* @param compressionQuality Value from 0.0 (max compression) to 1.0 (no compression)
155+
* @return this builder
156+
*/
157+
public Builder compressionQuality(double compressionQuality) {
158+
this.compressionQuality = compressionQuality;
159+
return this;
160+
}
161+
162+
/**
163+
* Set the tile size for both width and height. Default: 256.
164+
*
165+
* @param tileSize Must be a positive power of 2 (e.g. 128, 256, 512, 1024)
166+
* @return this builder
167+
*/
168+
public Builder tileSize(int tileSize) {
169+
this.tileSize = tileSize;
170+
return this;
171+
}
172+
173+
/**
174+
* Set the resampling algorithm for overview generation. Default: "Nearest".
175+
*
176+
* @param resampling One of "Nearest", "Bilinear", "Bicubic"
177+
* @return this builder
178+
*/
179+
public Builder resampling(String resampling) {
180+
this.resampling = resampling;
181+
return this;
182+
}
183+
184+
/**
185+
* Set the number of overview levels. Default: -1 (auto-compute).
186+
*
187+
* @param overviewCount -1 for auto, 0 for no overviews, or a positive count
188+
* @return this builder
189+
*/
190+
public Builder overviewCount(int overviewCount) {
191+
this.overviewCount = overviewCount;
192+
return this;
193+
}
194+
195+
/**
196+
* Build and validate the options.
197+
*
198+
* @return A validated, immutable {@link CogOptions} instance
199+
* @throws IllegalArgumentException if any option is invalid
200+
*/
201+
public CogOptions build() {
202+
if (compression == null || compression.isEmpty()) {
203+
throw new IllegalArgumentException("compression must not be null or empty");
204+
}
205+
if (compressionQuality < 0 || compressionQuality > 1.0) {
206+
throw new IllegalArgumentException(
207+
"compressionQuality must be between 0.0 and 1.0, got: " + compressionQuality);
208+
}
209+
if (tileSize <= 0) {
210+
throw new IllegalArgumentException("tileSize must be positive, got: " + tileSize);
211+
}
212+
if ((tileSize & (tileSize - 1)) != 0) {
213+
throw new IllegalArgumentException("tileSize must be a power of 2, got: " + tileSize);
214+
}
215+
if (overviewCount < -1) {
216+
throw new IllegalArgumentException(
217+
"overviewCount must be -1 (auto), 0 (none), or positive, got: " + overviewCount);
218+
}
219+
220+
// Normalize resampling to title-case for matching
221+
String normalized = normalizeResampling(resampling);
222+
if (!VALID_RESAMPLING.contains(normalized)) {
223+
throw new IllegalArgumentException(
224+
"resampling must be one of " + VALID_RESAMPLING + ", got: '" + resampling + "'");
225+
}
226+
this.resampling = normalized;
227+
228+
return new CogOptions(this);
229+
}
230+
231+
/**
232+
* Normalize the resampling string to title-case (first letter uppercase, rest lowercase) so
233+
* callers can pass "nearest", "BILINEAR", etc.
234+
*/
235+
private static String normalizeResampling(String value) {
236+
if (value == null || value.isEmpty()) {
237+
return "Nearest";
238+
}
239+
String lower = value.toLowerCase(Locale.ROOT);
240+
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
241+
}
242+
}
243+
}

0 commit comments

Comments
 (0)