Skip to content

Commit

Permalink
[GEOS-8488] Add a new 'large map download' process in wps-download mo…
Browse files Browse the repository at this point in the history
…dule - local layers version (one with dynamic external WMS layers will follow up)
  • Loading branch information
aaime committed Dec 14, 2017
1 parent 9fac2d1 commit b80fd94
Show file tree
Hide file tree
Showing 30 changed files with 1,188 additions and 61 deletions.
15 changes: 15 additions & 0 deletions src/community/wps-download/pom.xml
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@
<artifactId>gs-wps-core</artifactId> <artifactId>gs-wps-core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-wms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec</artifactId>
<version>0.2.2</version>
</dependency>
<dependency>
<groupId>org.jcodec</groupId>
<artifactId>jcodec-javase</artifactId>
<version>0.2.2</version>
</dependency>


<!-- test dependencies --> <!-- test dependencies -->
<dependency> <dependency>
Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,57 @@
/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wps.gs.download;

import org.apache.commons.lang.builder.ToStringBuilder;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class AbstractParametricEntity {

public static final String NAMESPACE = "http://geoserver.org/wps/download";

String name;
List<Parameter> parameters = new ArrayList<>();


@XmlElement(name="Name")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@XmlElement(name="Parameter")
public List<Parameter> getParameters() {
return parameters;
}

public void setParameters(List<Parameter> parameters) {
this.parameters = parameters;
}

public Map<String,String> getParametersMap() {
Map<String, String> result = new HashMap<>();
for (Parameter parameter : parameters) {
result.put(parameter.key, parameter.value);
}

return result;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,227 @@
/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wps.gs.download;

import org.geoserver.config.GeoServer;
import org.geoserver.ows.util.CaseInsensitiveMap;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.Operation;
import org.geoserver.platform.Service;
import org.geoserver.wms.GetMap;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContent;
import org.geoserver.wms.map.GetMapKvpRequestReader;
import org.geoserver.wms.map.RenderedImageMap;
import org.geoserver.wms.map.RenderedImageMapOutputFormat;
import org.geoserver.wms.map.RenderedImageMapResponse;
import org.geoserver.wps.WPSException;
import org.geoserver.wps.gs.GeoServerProcess;
import org.geoserver.wps.process.ByteArrayRawData;
import org.geoserver.wps.process.RawData;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.factory.DescribeParameter;
import org.geotools.process.factory.DescribeProcess;
import org.geotools.process.factory.DescribeResult;
import org.geotools.referencing.CRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.util.ProgressListener;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.media.jai.PlanarImage;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@DescribeProcess(title = "Map Download Process", description = "Builds a large map given a set of layer definitions, " +
"area of interest, size and eventual target time.")
public class DownloadMapProcess implements GeoServerProcess, ApplicationContextAware {


private final WMS wms;
private final GetMapKvpRequestReader getMapReader;
private Service service;

public DownloadMapProcess(GeoServer geoServer) {
// TODO: make these configurable
this.wms = new WMS(geoServer) {
@Override
public int getMaxRenderingTime() {
return -1;
}

@Override
public int getMaxRenderingErrors() {
return -1;
}

@Override
public Long getMaxRenderingSize() {
return null; //
}
};
this.getMapReader = new GetMapKvpRequestReader(wms);
}

/**
* This process returns a potentially large map
*/
@DescribeResult(name = "result", description = "The output map")
public RawData execute(
@DescribeParameter(name = "bbox", min = 1, description = "The map area and output projection")
ReferencedEnvelope bbox,
@DescribeParameter(name = "decoration", min = 0, description = "A WMS decoration layout name to watermark the output") String decorationName,
@DescribeParameter(name = "time", min = 0, description = "Map time specification (a single time value or " +
"a range like in WMS time parameter)") String time,
@DescribeParameter(name = "width", min = 1, description = "Map width", minValue = 1) int width,
@DescribeParameter(name = "height", min = 1, description = "Map height", minValue = 1) int height,
@DescribeParameter(name = "layer", min = 1, description = "The list of layers", minValue = 1) Layer[]
layers,
@DescribeParameter(name = "format", min = 1, description = "The output format", minValue = 1) Format format,
final ProgressListener progressListener) throws Exception {


// build GetMap template parameters
CaseInsensitiveMap template = new CaseInsensitiveMap(new HashMap());
template.put("service", "WMS");
template.put("request", "GetMap");
template.put("transparent", "false");
template.put("width", String.valueOf(width));
template.put("height", String.valueOf(height));
if (time != null) {
template.put("time", time);
}
template.put("bbox", bbox.getMinX() + "," + bbox.getMinY() + "," + bbox.getMaxX() + "," + bbox.getMaxY());
CoordinateReferenceSystem crs = bbox.getCoordinateReferenceSystem();
if (crs == null) {
throw new WPSException("The BBOX parameter must have a coordinate reference system");
} else {
// handle possible axis flipping by changing the WMS version accordingly
Integer code = CRS.lookupEpsgCode(crs, false);
if (CRS.getAxisOrder(crs) == CRS.AxisOrder.EAST_NORTH) {
template.put("version", "1.1.0");
template.put("srs", "EPSG:" + code);
} else {
template.put("version", "1.3.0");
template.put("crs", "EPSG:" + code);
}
}

// loop over layers and accumulate
RenderedImage result = null;
for (Layer layer : layers) {
RenderedImage image;
if (layer.getCapabilities() == null) {
RenderedImageMap map = renderInternalLayer(layer, template);
image = map.getImage();
} else {
throw new UnsupportedOperationException("Including cascaded layers is not yet implemented");
}

if (result == null) {
result = image;
} else {
result = mergeImage(result, image);

}

// past the first layer switch transparency on to allow overlaying
template.put("transparent", "true");
}

// Decoration handling, we'll put together a empty GetMap for it
GetMapRequest request = new GetMapRequest();
request.setFormat(format.getName());
if (decorationName != null) {
request.setFormatOptions(Collections.singletonMap("layout", decorationName));
WMSMapContent content = new WMSMapContent(request);
content.setMapWidth(width);
content.setMapHeight(height);
content.setTransparent(true);
RenderedImageMapOutputFormat renderer = new RenderedImageMapOutputFormat(wms);
RenderedImageMap map = renderer.produceMap(content);

result = mergeImage(result, map.getImage());
}


// encode the output by faking a normal request
List<RenderedImageMapResponse> encoders = GeoServerExtensions.extensions(RenderedImageMapResponse.class);
Operation op = new Operation("GetMap", service, null, new Object[]{request});
for (RenderedImageMapResponse encoder : encoders) {
if (encoder.canHandle(op)) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
encoder.formatImageOutputStream(result, bos, new WMSMapContent(request));
return new ByteArrayRawData(bos.toByteArray(), format.getName());
}
}

throw new WPSException("Could not find a map encoder for format: " + format);
}

public RenderedImage mergeImage(RenderedImage result, RenderedImage image) {
// make sure we can paint on it
if (!(result instanceof BufferedImage)) {
result = PlanarImage.wrapRenderedImage(image).getAsBufferedImage();
}

// could use mosaic here, but would require keeping all images in memory to build the op,
// this way at most two at any time are around, so uses less memory overall
BufferedImage bi = (BufferedImage) result;
Graphics2D graphics = (Graphics2D) bi.getGraphics();
graphics.drawRenderedImage(image, AffineTransform.getScaleInstance(1, 1));
graphics.dispose();
return result;
}

public RenderedImageMap renderInternalLayer(Layer layer, Map kvpTemplate) throws Exception {
WMSMapContent mapContent;
GetMapRequest request = getMapReader.createRequest();

// prepare raw and parsed KVP maps to mimick a GetMap request
CaseInsensitiveMap rawKvp = new CaseInsensitiveMap(new HashMap());
rawKvp.putAll(kvpTemplate);
rawKvp.put("format", "image/png"); // fake format, we are building a RenderedImage
rawKvp.put("layers", layer.getName());
for (Parameter parameter : layer.getParameters()) {
rawKvp.putIfAbsent(parameter.key, parameter.value);
}
// for merging layers, unless the request stated otherwise
rawKvp.putIfAbsent("transparent", "true");
CaseInsensitiveMap kvp = new CaseInsensitiveMap(new HashMap());
kvp.putAll(rawKvp);
List<Throwable> exceptions = KvpUtils.parse(kvp);
if (exceptions != null && exceptions.size() > 0) {
throw new WPSException("Failed to build map for layer: " + layer.getName(), exceptions.get(0));
}

// parse
getMapReader.read(request, kvp, rawKvp);

// render
GetMap mapBuilder = new GetMap(wms);
RenderedImageMap map = (RenderedImageMap) mapBuilder.run(request);
return map;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.wms.setApplicationContext(applicationContext);
List<Service> services = GeoServerExtensions.extensions(Service.class, applicationContext);
this.service = services.stream().filter(s -> "WMS".equalsIgnoreCase(s.getId())).findFirst().orElse(null);
if (service == null) {
throw new RuntimeException("Could not find a WMS service");
}
}
}
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,15 @@
/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wps.gs.download;

import javax.xml.bind.annotation.XmlRootElement;

/**
* The format definition in a map/animation download
*/
@XmlRootElement(name="Format")
public class Format extends AbstractParametricEntity {

}
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.geoserver.wps.gs.download;

import org.geoserver.util.EntityResolverProvider;
import org.geoserver.wps.ppio.ComplexPPIO;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXSource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class JaxbPPIO extends ComplexPPIO {

private final Class targetClass;
private JAXBContext context;
private EntityResolverProvider resolverProvider;

public JaxbPPIO(Class targetClass, EntityResolverProvider resolverProvider) throws JAXBException, TransformerException {
super(targetClass, targetClass, "text/xml");
this.targetClass = targetClass;
this.resolverProvider = resolverProvider;

// this creation is expensive, do it once and cache it
this.context = JAXBContext.newInstance((targetClass));
}

@Override
public Object decode(Object input) throws Exception {
if (input instanceof String) {
return decode(new ByteArrayInputStream(((String) input).getBytes()));
}
return super.decode(input);
}

@Override
public Object decode(InputStream input) throws Exception {
Unmarshaller unmarshaller = this.context.createUnmarshaller();

EntityResolver resolver = resolverProvider != null ? resolverProvider.getEntityResolver() : null;
if( resolver == null) {
return unmarshaller.unmarshal(input);
} else {
// setup the entity resolver
final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
final XMLReader reader = saxParserFactory.newSAXParser().getXMLReader();
reader.setEntityResolver(resolver);
final SAXSource saxSource = new SAXSource(reader, new InputSource(input));

return unmarshaller.unmarshal(saxSource);
}
}

@Override
public PPIODirection getDirection() {
return PPIODirection.DECODING;
}

@Override
public void encode(Object value, OutputStream os) throws Exception {
throw new UnsupportedOperationException();
// this is the easy implementation, but requires tests to support it
// this.context.createMarshaller().marshal(value, os);
}



}

0 comments on commit b80fd94

Please sign in to comment.