package org.mapfish.print.processor.map;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.mapfish.print.attribute.Attribute;
import org.mapfish.print.attribute.DataSourceAttribute;
import org.mapfish.print.attribute.DataSourceAttribute.DataSourceAttributeValue;
import org.mapfish.print.attribute.map.AreaOfInterest;
import org.mapfish.print.attribute.map.MapAttribute;
import org.mapfish.print.attribute.map.MapAttribute.MapAttributeValues;
import org.mapfish.print.attribute.map.PagingAttribute;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.map.DistanceUnit;
import org.mapfish.print.processor.AbstractProcessor;
import org.mapfish.print.processor.ProvideAttributes;
import org.mapfish.print.processor.RequireAttributes;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Rectangle;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.mapfish.print.Constants.PDF_DPI;
/**
* <p>Processor used to display a map on multiple pages.</p>
* <p>
* This processor will take the defined <a href="attributes.html#!map">map attribute</a> and using the geometry defined in the
* <a href="attributes.html#!map">map attribute's</a> area of interest, will create an Iterable<Values> each of which
* contains:
* </p>
* <ul>
* <li>a new definition of a <a href="attributes.html#!map">map attribute</a></li>
* <li>name value which is a string that roughly describes which part of the main map this sub-map is</li>
* <li>left value which is the name of the sub-map to the left of the current map</li>
* <li>right value which is the name of the sub-map to the right of the current map</li>
* <li>top value which is the name of the sub-map to the top of the current map</li>
* <li>bottom value which is the name of the sub-map to the bottom of the current map</li>
* </ul>
* <p>
* The iterable of values can be consumed by a <a href="processors.html#!createDataSource">!createDataSource</a> processor
* and as a result be put in the report (or one of the sub-reports) table. One must be careful as
* this can result in truly giant reports.
* </p>
* <p>See also: <a href="attributes.html#!paging">!paging</a> attribute</p>
* [[examples=paging]]
*/
public class CreateMapPagesProcessor
extends AbstractProcessor<CreateMapPagesProcessor.Input, CreateMapPagesProcessor.Output>
implements ProvideAttributes, RequireAttributes {
private static final Logger LOGGER = LoggerFactory.getLogger(CreateMapPagesProcessor.class);
private static final int DO_NOT_RENDER_BBOX_INDEX = -1;
private static final String MAP_KEY = "map";
private final GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
private MapAttribute mapAttribute;
/**
* Constructor.
*/
protected CreateMapPagesProcessor() {
super(Output.class);
}
@Override
protected void extraValidation(final List<Throwable> validationErrors, final Configuration configuration) {
}
@Override
public final Input createInputParameter() {
return new Input();
}
@SuppressWarnings("unchecked")
@Override
public final Output execute(final Input values, final ExecutionContext context) throws Exception {
final MapAttributeValues map = values.map;
final PagingAttribute.PagingProcessorValues paging = values.paging;
CoordinateReferenceSystem projection = map.getMapBounds().getProjection();
final Rectangle paintArea = new Rectangle(map.getMapSize());
final DistanceUnit projectionUnit = DistanceUnit.fromProjection(projection);
AreaOfInterest areaOfInterest = map.areaOfInterest;
if (areaOfInterest == null) {
areaOfInterest = new AreaOfInterest();
areaOfInterest.display = AreaOfInterest.AoiDisplay.NONE;
ReferencedEnvelope mapBBox = map.getMapBounds().toReferencedEnvelope(paintArea);
areaOfInterest.setPolygon(this.geometryFactory.toGeometry(mapBBox));
}
Envelope aoiBBox = areaOfInterest.getArea().getEnvelopeInternal();
final double paintAreaWidthIn = paintArea.getWidth() * paging.scale / PDF_DPI;
final double paintAreaHeightIn = paintArea.getHeight() * paging.scale / PDF_DPI;
final double paintAreaWidth = DistanceUnit.IN.convertTo(paintAreaWidthIn, projectionUnit);
final double paintAreaHeight = DistanceUnit.IN.convertTo(paintAreaHeightIn, projectionUnit);
final double overlapProj = DistanceUnit.IN.convertTo(paging.overlap * paging.scale / PDF_DPI, projectionUnit);
final int nbWidth = (int) Math.ceil((aoiBBox.getWidth() + overlapProj) / (paintAreaWidth - overlapProj));
final int nbHeight = (int) Math.ceil((aoiBBox.getHeight() + overlapProj) / (paintAreaHeight - overlapProj));
final double marginWidth = (paintAreaWidth * nbWidth - (nbWidth - 1) * overlapProj - aoiBBox.getWidth()) / 2;
final double marginHeight = (paintAreaHeight * nbHeight - (nbHeight - 1) * overlapProj - aoiBBox.getHeight()) / 2;
final double minX = aoiBBox.getMinX() - marginWidth - overlapProj / 2;
final double minY = aoiBBox.getMinY() - marginHeight - overlapProj / 2;
LOGGER.info("Paging generate a grid of " + nbWidth + "x" + nbHeight + " potential maps.");
final int[][] mapIndexes = new int[nbWidth][nbHeight];
final Envelope[][] mapsBounds = new Envelope[nbWidth][nbHeight];
int mapIndex = 0;
for (int j = 0; j < nbHeight; j++) {
for (int i = 0; i < nbWidth; i++) {
final double x1 = minX + i * (paintAreaWidth - overlapProj);
final double x2 = x1 + paintAreaWidth;
final double y1 = minY + j * (paintAreaHeight - overlapProj);
final double y2 = y1 + paintAreaHeight;
Coordinate[] coords = new Coordinate[] {
new Coordinate(x1, y1),
new Coordinate(x1, y2),
new Coordinate(x2, y2),
new Coordinate(x2, y1),
new Coordinate(x1, y1)
};
LinearRing ring = this.geometryFactory.createLinearRing(coords);
final Polygon bbox = this.geometryFactory.createPolygon(ring);
if (areaOfInterest.getArea().intersects(bbox)) {
mapsBounds[i][j] = bbox.getEnvelopeInternal();
mapIndexes[i][j] = mapIndex;
mapIndex++;
} else {
mapIndexes[i][j] = DO_NOT_RENDER_BBOX_INDEX;
}
}
}
final List<Map<String, Object>> mapList = Lists.newArrayList();
for (int j = 0; j < nbHeight; j++) {
for (int i = 0; i < nbWidth; i++) {
if (mapIndexes[i][j] != DO_NOT_RENDER_BBOX_INDEX) {
Map<String, Object> mapValues = new HashMap<String, Object>();
mapValues.put("name", mapIndexes[i][j]);
mapValues.put("left", i != 0 ? mapIndexes[i - 1][j] : DO_NOT_RENDER_BBOX_INDEX);
mapValues.put("bottom", j != 0 ? mapIndexes[i][j - 1] : DO_NOT_RENDER_BBOX_INDEX);
mapValues.put("right", i != nbWidth - 1 ? mapIndexes[i + 1][j] : DO_NOT_RENDER_BBOX_INDEX);
mapValues.put("top", j != nbHeight - 1 ? mapIndexes[i][j + 1] : DO_NOT_RENDER_BBOX_INDEX);
final Envelope mapsBound = mapsBounds[i][j];
MapAttributeValues theMap = map.copy(map.getWidth(), map.getHeight(),
new Function<MapAttributeValues, Void>() {
@Nullable
@Override
public Void apply(@Nonnull final MapAttributeValues input) {
input.center = null;
input.bbox = new double[]{
mapsBound.getMinX(),
mapsBound.getMinY(),
mapsBound.getMaxX(),
mapsBound.getMaxY()
};
input.dpi = PDF_DPI;
if (paging.aoiDisplay != null) {
input.areaOfInterest.display = paging.aoiDisplay;
}
if (paging.aoiStyle != null) {
input.areaOfInterest.style = paging.aoiStyle;
}
return null;
}
});
mapValues.put(MAP_KEY, theMap);
mapList.add(mapValues);
}
}
}
LOGGER.info("Paging generate " + mapList.size() + " maps definitions.");
DataSourceAttributeValue datasourceAttributes = new DataSourceAttributeValue();
datasourceAttributes.attributesValues = mapList.toArray(new Map[mapList.size()]);
return new Output(datasourceAttributes);
}
/**
* Set the map attribute.
*
* @param name the attribute name
* @param attribute the attribute
*/
public void setAttribute(final String name, final Attribute attribute) {
if (name.equals(MAP_KEY)) {
this.mapAttribute = (MapAttribute) attribute;
}
}
/**
* Gets the attributes provided by the processor.
*
* @return the attributes
*/
public Map<String, Attribute> getAttributes() {
Map<String, Attribute> result = new HashMap<String, Attribute>();
DataSourceAttribute datasourceAttribute = new DataSourceAttribute();
Map<String, Attribute> dsResult = new HashMap<String, Attribute>();
dsResult.put(MAP_KEY, this.mapAttribute);
datasourceAttribute.setAttributes(dsResult);
result.put("datasource", datasourceAttribute);
return result;
}
/**
* The Input object for processor.
*/
public static class Input {
/**
* The required parameters for the map.
*/
public MapAttribute.MapAttributeValues map;
/**
* Attributes that define how each page/sub-map will be generated. It defines the scale and how to render the area of interest,
* etc...
*/
public PagingAttribute.PagingProcessorValues paging;
}
/**
* Output of processor.
*/
public static final class Output {
/**
* Resulting list of values for the maps.
*/
public final DataSourceAttributeValue datasource;
private Output(final DataSourceAttributeValue tableList) {
this.datasource = tableList;
}
}
}