-
Notifications
You must be signed in to change notification settings - Fork 419
Add new outputformat geotiff-cog #4166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 11 commits
638718d
e043737
89a1cb6
d98e55f
aa9e583
bfd24a5
66092e5
f802d87
bd2072c
35d95b4
016893c
9a3fe46
4468170
6419b63
7cc89d0
be9529f
99d7a14
859b536
770ad64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| 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"); | ||
|
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 = | ||
|
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"); | ||
|
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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 */ | ||
|
franklgln marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| } | ||
| return task.getExecutionContext(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.