Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package org.mapfish.print.output;

import jakarta.annotation.Nonnull;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.imageio.ImageIO;
import org.geotools.api.coverage.grid.GridCoverageWriter;
import org.geotools.api.parameter.GeneralParameterValue;
import org.geotools.api.parameter.ParameterValueGroup;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.api.referencing.operation.MathTransform;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.imageio.GeoToolsWriteParams;
import org.geotools.gce.geotiff.GeoTiffFormat;
import org.geotools.gce.geotiff.GeoTiffWriteParams;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.mapfish.print.Constants;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.config.Template;
import org.mapfish.print.processor.Processor;
import org.mapfish.print.processor.ProcessorDependencyGraph;
import org.mapfish.print.wrapper.json.PJsonArray;
import org.mapfish.print.wrapper.json.PJsonObject;

/**
* The MapCogExportOutputFormat class.
*
* @author Frank and Manuel
*/
public class MapCogExportOutputFormat extends MapExportOutputFormat {

private static final double METERS_PER_INCH = Constants.INCH_TO_MM / 1000.0;

@Override
public final Processor.ExecutionContext print(
@Nonnull final Map<String, String> mdcContext,
Comment thread
franklgln marked this conversation as resolved.
Outdated
final PJsonObject spec,
final Configuration config,
final File configDir,
final File taskDirectory,
final OutputStream outputStream)
throws Exception {
final String templateName = spec.getString(Constants.JSON_LAYOUT_KEY);

final Template template = config.getTemplate(templateName);

final Values values =
new Values(
mdcContext,
spec,
template,
taskDirectory,
this.httpRequestFactory,
null,
"tif",
httpRequestMaxNumberFetchRetry,
httpRequestFetchRetryIntervalMillis,
new AtomicBoolean(false));

final ProcessorDependencyGraph.ProcessorGraphForkJoinTask task =
template.getProcessorGraph().createTask(values);
final ForkJoinTask<Values> taskFuture = this.forkJoinPool.submit(task);

try {
taskFuture.get();
} catch (InterruptedException exc) {
// if cancel() is called on the current thread, this exception will be thrown.
// in this case, also properly cancel the task future.
taskFuture.cancel(true);
Thread.currentThread().interrupt();
throw new CancellationException();
}

String mapSubReport = values.getString(getMapSubReportVariable(template));

GridCoverageWriter writer = null;

try {

final PJsonObject mapJson = spec.getJSONObject("attributes").getJSONObject("map");
Path path =
mapSubReport.startsWith("file:")
? Paths.get(new URI(mapSubReport))
: Path.of(mapSubReport);

BufferedImage image = ImageIO.read(path.toFile());

String srs = mapJson.getString("projection");
CoordinateReferenceSystem crs = CRS.decode(srs);
GridCoverageFactory factory = new GridCoverageFactory();
GridCoverage2D coverage;

if (mapJson.has("center")) {
PJsonArray center = mapJson.getJSONArray("center");

double centerX = center.getDouble(0);
double centerY = center.getDouble(1);
double cx = image.getWidth() / 2.0;
double cy = image.getHeight() / 2.0;

double scale = mapJson.getDouble("scale");
double dpi = mapJson.getDouble("dpi");
double metersPerPixel = scale * METERS_PER_INCH / dpi;

double rotation = mapJson.has("rotation") ? mapJson.getDouble("rotation") : 0.0;

AffineTransform gridToCRS = new AffineTransform();
gridToCRS.translate(centerX, centerY);
gridToCRS.rotate(Math.toRadians(rotation));
gridToCRS.scale(metersPerPixel, -metersPerPixel);
gridToCRS.translate(-cx, -cy);

MathTransform mathTransform = new AffineTransform2D(gridToCRS);

coverage = factory.create("coverage", image, crs, mathTransform, null, null, null);
} else if (mapJson.has("bbox")) {
PJsonArray bbox = mapJson.getJSONArray("bbox");

double minX = bbox.getDouble(0);
double minY = bbox.getDouble(1);
double maxX = bbox.getDouble(2);
double maxY = bbox.getDouble(3);

ReferencedEnvelope envelope = new ReferencedEnvelope(minX, maxX, minY, maxY, crs);

coverage = factory.create("coverage", image, envelope);

} else {
throw new IOException("COG export requires either center + scale or bbox");
Comment thread
franklgln marked this conversation as resolved.
Outdated
}

final int tileWidth = 512;
final int tileHeight = 512;

// write the GridCoverage2D to a GeoTIFF file with LZW compression and tiling
// using GeoTools
final GeoTiffFormat format = new GeoTiffFormat();
final GeoTiffWriteParams wp = new GeoTiffWriteParams();

wp.setCompressionMode(GeoTiffWriteParams.MODE_EXPLICIT);
wp.setCompressionType("LZW");
wp.setCompressionQuality(0.75F);

wp.setTilingMode(GeoToolsWriteParams.MODE_EXPLICIT);
wp.setTiling(tileWidth, tileHeight);

final ParameterValueGroup params = format.getWriteParameters();
params.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString()).setValue(wp);
params.parameter(GeoTiffFormat.RETAIN_AXES_ORDER.getName().toString()).setValue(true);

final OutputStream nonClosingOutputStream =
Comment thread
franklgln marked this conversation as resolved.
new FilterOutputStream(outputStream) {
@Override
public void close() throws IOException {
flush();
}
};

writer = format.getWriter(nonClosingOutputStream);
if (writer == null) {
throw new IOException("Could not create GeoTIFF writer");
Comment thread
franklgln marked this conversation as resolved.
Outdated
}

// write the coverage to the GeoTIFF file using the specified parameters
writer.write(
coverage,
(GeneralParameterValue[]) params.values().toArray(new GeneralParameterValue[0]));

} catch (Exception e) {
throw new IOException("Error writing cog file", e);
} finally {
if (writer != null) {
try {
writer.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since your output stream is a non closing one, this dispose is not sufficient. You will need to manually call the close in the superclass of your nonClosingOutputStream.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying output stream is owned by MapFish Print and must remain open. Calling FilterOutputStream.super.close() would also close that stream, which is exactly what the wrapper is intended to prevent. GeoTools closes its internal stream during dispose(), while the wrapper only flushes the MapFish stream. Do you have a different cleanup approach in mind?

} catch (Exception e) {
/* ignore */
Comment thread
franklgln marked this conversation as resolved.
Outdated
}
}
}
return task.getExecutionContext();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,23 @@ public class MapExportOutputFormat implements OutputFormat {

private static final String MAP_SUBREPORT = "mapSubReport";

@Autowired private ForkJoinPool forkJoinPool;
/** Fork-join pool used to execute print tasks. */
@Autowired protected ForkJoinPool forkJoinPool;

@Autowired private MfClientHttpRequestFactoryImpl httpRequestFactory;
/** HTTP request factory used while processing print jobs. */
@Autowired protected MfClientHttpRequestFactoryImpl httpRequestFactory;

private String fileSuffix;

private String contentType;

/** Maximum number of retries for HTTP requests. */
@Value("${httpRequest.fetchRetry.maxNumber}")
private int httpRequestMaxNumberFetchRetry;
protected int httpRequestMaxNumberFetchRetry;

/** Interval in milliseconds between HTTP request retries. */
@Value("${httpRequest.fetchRetry.intervalMillis}")
private int httpRequestFetchRetryIntervalMillis;
protected int httpRequestFetchRetryIntervalMillis;

@Override
public final String getContentType() {
Expand All @@ -65,7 +69,7 @@ public final void setFileSuffix(final String fileSuffix) {
this.fileSuffix = fileSuffix;
}

private String getMapSubReportVariable(final Template template) {
protected String getMapSubReportVariable(final Template template) {
for (Processor<?, ?> processor : template.getProcessors()) {
if (processor instanceof CreateMapProcessor) {
String mapSubReport =
Expand All @@ -79,7 +83,7 @@ private String getMapSubReportVariable(final Template template) {
}

@Override
public final Processor.ExecutionContext print(
public Processor.ExecutionContext print(
@Nonnull final Map<String, String> mdcContext,
final PJsonObject spec,
final Configuration config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
p:fileSuffix="jpg"/>
<bean id="jpegOutputFormat" class="org.mapfish.print.output.JasperReportImageOutputFormat" scope="prototype"
p:fileSuffix="jpeg"/>
<bean id="tifOutputFormat" class="org.mapfish.print.output.JasperReportImageOutputFormat" scope="prototype"
p:fileSuffix="tif"/>
<bean id="tiffOutputFormat" class="org.mapfish.print.output.JasperReportImageOutputFormat" scope="prototype"
p:fileSuffix="tiff"/>
<bean id="gifOutputFormat" class="org.mapfish.print.output.JasperReportImageOutputFormat" scope="prototype"
Expand All @@ -33,6 +31,8 @@
p:fileSuffix="tif" p:contentType="image/tiff" />
<bean id="tiffMapOutputFormat" class="org.mapfish.print.output.MapExportOutputFormat" scope="prototype"
p:fileSuffix="tiff" p:contentType="image/tiff"/>
<bean id="cogMapOutputFormat" class="org.mapfish.print.output.MapCogExportOutputFormat" scope="prototype"
p:fileSuffix="cog" p:contentType="image/tiff"/>
<bean id="gifMapOutputFormat" class="org.mapfish.print.output.MapExportOutputFormat" scope="prototype"
p:fileSuffix="gif" p:contentType="image/gif" />
<bean id="bmpMapOutputFormat" class="org.mapfish.print.output.MapExportOutputFormat" scope="prototype"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public void testAllOutputFormats() throws Exception {
for (OutputFormat format : this.outputFormat.values()) {
if ("bmp".equals(format.getFileSuffix())
|| "jpeg".equals(format.getFileSuffix())
|| "jpg".equals(format.getFileSuffix())) {
|| "jpg".equals(format.getFileSuffix())
|| format instanceof MapCogExportOutputFormat) {
// BMP and JPEG do not support transparency
Comment thread
franklgln marked this conversation as resolved.
continue;
}
Expand Down
Loading
Loading