package org.mapfish.print.map.image;
import com.codahale.metrics.MetricRegistry;
import com.google.common.collect.Maps;
import com.google.common.io.Closer;
import com.vividsolutions.jts.util.Assert;
import jsr166y.ForkJoinPool;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.GridCoverageLayer;
import org.geotools.map.MapContent;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.styling.Style;
import org.mapfish.print.Constants;
import org.mapfish.print.attribute.map.MapBounds;
import org.mapfish.print.attribute.map.MapfishMapContext;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.config.Template;
import org.mapfish.print.http.MfClientHttpRequestFactory;
import org.mapfish.print.map.AbstractLayerParams;
import org.mapfish.print.map.MapLayerFactoryPlugin;
import org.mapfish.print.map.geotools.AbstractGridCoverageLayerPlugin;
import org.mapfish.print.map.geotools.StyleSupplier;
import org.mapfish.print.parser.HasDefaultValue;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nonnull;
import javax.imageio.ImageIO;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE;
/**
* <p>Reads a image file from an URL.</p>
*
* @author MaxComse on 11/08/16.
*/
public final class ImageLayer extends AbstractSingleImageLayer {
private final ImageParam params;
private final StyleSupplier<GridCoverage2D> styleSupplier;
private final ExecutorService executorService;
private final Configuration configuration;
/**
* Constructor.
*
* @param executorService the thread pool for doing the rendering.
* @param styleSupplier the style to use when drawing the constructed grid coverage on the map.
* @param params the params from the request data.
* @param configuration the configuration.
*/
protected ImageLayer(
@Nonnull final ExecutorService executorService,
@Nonnull final StyleSupplier<GridCoverage2D> styleSupplier,
@Nonnull final ImageParam params,
@Nonnull final Configuration configuration) {
super(executorService, styleSupplier, params);
this.params = params;
this.styleSupplier = styleSupplier;
this.executorService = executorService;
this.configuration = configuration;
}
/**
* <p>Renders an image as layer.</p>
* <p>Type: <code>image</code></p>
*/
public static final class ImageLayerPlugin extends AbstractGridCoverageLayerPlugin
implements MapLayerFactoryPlugin<ImageParam> {
private static final String TYPE = "image";
@Autowired
private ForkJoinPool forkJoinPool;
@Autowired
private MetricRegistry metricRegistry;
@Override
public Set<String> getTypeNames() {
return Collections.singleton(TYPE);
}
@Override
public ImageParam createParameter() {
return new ImageParam();
}
@Nonnull
@Override
public ImageLayer parse(
@Nonnull final Template template,
@Nonnull final ImageParam layerData) {
String styleRef = layerData.style;
return new ImageLayer(this.forkJoinPool,
super.<GridCoverage2D>createStyleSupplier(template, styleRef),
layerData, template.getConfiguration());
}
}
/**
* The parameters for reading an image file, either from the server or from a URL.
*/
public static final class ImageParam extends AbstractLayerParams {
private static final int NUMBER_OF_EXTENT_COORDS = 4;
/**
* The base URL for the image file. Used for making request.
*/
public String baseURL;
/**
* The extent of the image. Used for placing image on map.
*/
public double[] extent;
/**
* The styles to apply to the image.
*/
@HasDefaultValue
public String style = Constants.Style.Raster.NAME;
/**
* Validate the properties have the correct values.
* @throws URISyntaxException
*/
public void postConstruct() throws URISyntaxException {
Assert.equals(NUMBER_OF_EXTENT_COORDS, this.extent.length,
"maxExtent must have exactly 4 elements to the array. Was: " +
Arrays.toString(this.extent));
}
public String getBaseUrl() {
return this.baseURL;
}
}
@Override
protected BufferedImage loadImage(final MfClientHttpRequestFactory requestFactory,
final MapfishMapContext transformer) throws Throwable {
final ImageParam layerParam = this.params;
final URI commonUri = new URI(layerParam.getBaseUrl());
final Double extentMinX = layerParam.extent[0];
final Double extentMinY = layerParam.extent[1];
final Double extentMaxX = layerParam.extent[2];
final Double extentMaxY = layerParam.extent[3];
final Rectangle paintArea = transformer.getPaintArea();
final ReferencedEnvelope envelope = transformer.getBounds().toReferencedEnvelope(paintArea);
final CoordinateReferenceSystem mapProjection = envelope.getCoordinateReferenceSystem();
Closer closer = Closer.create();
final BufferedImage bufferedImage = new BufferedImage(paintArea.width, paintArea.height,
TYPE_INT_ARGB_PRE);
final Graphics2D graphics = bufferedImage.createGraphics();
MapBounds bounds = transformer.getBounds();
MapContent content = new MapContent();
try {
final ClientHttpRequest request = requestFactory.createRequest(commonUri, HttpMethod.GET);
final ClientHttpResponse httpResponse = closer.register(request.execute());
final BufferedImage image = ImageIO.read(httpResponse.getBody());
if (image == null) {
return createErrorImage(paintArea);
}
GridCoverageFactory factory = CoverageFactoryFinder.getGridCoverageFactory(null);
GeneralEnvelope gridEnvelope = new GeneralEnvelope(mapProjection);
gridEnvelope.setEnvelope(extentMinX, extentMinY, extentMaxX, extentMaxY);
GridCoverage2D coverage = factory.create(layerParam.getBaseUrl(), image, gridEnvelope,
null, null, null);
Style style = this.styleSupplier.load(requestFactory, coverage);
GridCoverageLayer l = new GridCoverageLayer(coverage, style);
content.addLayers(Collections.singletonList(l));
StreamingRenderer renderer = new StreamingRenderer();
RenderingHints hints = new RenderingHints(Collections.<RenderingHints.Key, Object>emptyMap());
hints.add(new RenderingHints(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON));
hints.add(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE));
hints.add(new RenderingHints(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON));
hints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC));
hints.add(new RenderingHints(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY));
hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE));
hints.add(new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON));
graphics.addRenderingHints(hints);
renderer.setJava2DHints(hints);
Map<String, Object> renderHints = Maps.newHashMap();
if (transformer.isForceLongitudeFirst() != null) {
renderHints.put(StreamingRenderer.FORCE_EPSG_AXIS_ORDER_KEY,
transformer.isForceLongitudeFirst());
}
renderer.setRendererHints(renderHints);
renderer.setMapContent(content);
renderer.setThreadPool(this.executorService);
final ReferencedEnvelope mapArea = bounds.toReferencedEnvelope(paintArea);
renderer.paint(graphics, paintArea, mapArea);
return bufferedImage;
} finally {
graphics.dispose();
closer.close();
}
}
private BufferedImage createErrorImage(final Rectangle area) {
final BufferedImage bufferedImage = new BufferedImage(area.width, area.height, TYPE_INT_ARGB_PRE);
final Graphics2D graphics = bufferedImage.createGraphics();
try {
graphics.setBackground(this.configuration.getTransparentTileErrorColor());
graphics.clearRect(0, 0, area.width, area.height);
return bufferedImage;
} finally {
graphics.dispose();
}
}
@Override
public RenderType getRenderType() {
return RenderType.UNKNOWN;
}
}