Skip to content

Commit

Permalink
[GEOS-8489] Add a new 'animation download' process in wps-download mo…
Browse files Browse the repository at this point in the history
…dule
  • Loading branch information
aaime committed Dec 15, 2017
1 parent 180585a commit 43a1799
Show file tree
Hide file tree
Showing 10 changed files with 499 additions and 56 deletions.
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,155 @@
/* (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.ows.kvp.TimeParser;
import org.geoserver.platform.resource.Resource;
import org.geoserver.wps.WPSException;
import org.geoserver.wps.gs.GeoServerProcess;
import org.geoserver.wps.process.RawData;
import org.geoserver.wps.process.ResourceRawData;
import org.geoserver.wps.resource.WPSResourceManager;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.ProcessException;
import org.geotools.process.factory.DescribeParameter;
import org.geotools.process.factory.DescribeProcess;
import org.geotools.process.factory.DescribeResult;
import org.geotools.util.DateRange;
import org.geotools.util.DefaultProgressListener;
import org.jcodec.api.awt.AWTSequenceEncoder;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.model.Rational;
import org.opengis.util.ProgressListener;

import javax.media.jai.PlanarImage;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.math.BigDecimal;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;

@DescribeProcess(title = "Animation Download Process", description = "Builds an animation given a set of layer " +
"definitions, " +
"area of interest, size and a series of times for animation frames.")
public class DownloadAnimationProcess implements GeoServerProcess {

public static final String VIDEO_MP4 = "video/mp4";
private static final Format MAP_FORMAT;

static {
MAP_FORMAT = new Format();
MAP_FORMAT.setName("image/png");
}

private final TimeParser timeParser;
private final DownloadMapProcess mapper;
private final WPSResourceManager resourceManager;
private final DateTimeFormatter formatter;

public DownloadAnimationProcess(DownloadMapProcess mapper, WPSResourceManager resourceManager) {
this.mapper = mapper;
this.timeParser = new TimeParser();
this.resourceManager = resourceManager;
// java 8 formatters are thread safe
this.formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX").withLocale(Locale
.ENGLISH).withZone(ZoneId.of("GMT"));

}

@DescribeResult(name = "result", description = "The animation")
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 = 1, description = "Map time specification (a range with " +
"periodicity or a list of time values)") 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 = "fps", min = 1, description = "Frames per second", minValue = 0, defaultValue =
"1") double fps,
@DescribeParameter(name = "layer", min = 1, description = "The list of layers", minValue = 1) Layer[]
layers,
@DescribeParameter(name = "format", min = 0, description = "The output format") Format format,
ProgressListener progressListener) throws Exception {

// default format if missing
if (format == null) {
format = new Format();
format.setName(VIDEO_MP4);
} else if (!VIDEO_MP4.equalsIgnoreCase(format.getName())) {
// TODO: allow more formats and codecs?
throw new WPSException("Currently the only supported format is video/mp4");
}

// avoid NPE on progress listener
if (progressListener == null) {
progressListener = new DefaultProgressListener();
}

final Resource output = resourceManager.getTemporaryResource("mp4");
Rational frameRate = getFrameRate(fps);

AWTSequenceEncoder enc = new AWTSequenceEncoder(NIOUtils.writableChannel(output.file()), frameRate);
Collection parsedTimes = timeParser.parse(time);
progressListener.started();
int count = 1;
for (Object parsedTime : parsedTimes) {
// turn parsed time into a specification and generate a "WMS" like request based on it
String mapTime = toWmsTimeSpecification(parsedTime);
RenderedImage image = mapper.buildImage(bbox, decorationName, mapTime, width, height, layers, format);
BufferedImage frame = toBufferedImage(image);
enc.encodeImage(frame);
progressListener.progress(100 * (parsedTimes.size() / count));
if (progressListener.isCanceled()) {
throw new ProcessException("Bailing out due to progress cancellation");
}
count++;
}
enc.finish();

return new ResourceRawData(output, VIDEO_MP4, "mp4");
}

private BufferedImage toBufferedImage(RenderedImage image) {
BufferedImage frame;
if (image instanceof BufferedImage) {
frame = (BufferedImage) image;
} else {
frame = PlanarImage.wrapRenderedImage(image).getAsBufferedImage();
}
return frame;
}

private String toWmsTimeSpecification(Object parsedTime) {
String mapTime;
if (parsedTime instanceof Date) {
mapTime = formatter.format(((Date) parsedTime).toInstant());
} else if (parsedTime instanceof DateRange) {
DateRange range = (DateRange) parsedTime;
mapTime = formatter.format(range.getMinValue().toInstant()) + "/" + formatter.format(range
.getMinValue().toInstant());
} else {
throw new WPSException("Unexpected parsed date type: " + parsedTime);
}
return mapTime;
}

public Rational getFrameRate(double fps) {
if (fps < 0) {
throw new WPSException("Frames per second must be greater than zero");
}
BigDecimal bigDecimal = BigDecimal.valueOf(fps);
int numerator = (int) bigDecimal.unscaledValue().longValue();
int denominator = (int) Math.pow(10L, bigDecimal.scale());
Rational frameRate = new Rational(numerator, denominator);

return frameRate;
}

}
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -79,19 +79,39 @@ public Long getMaxRenderingSize() {
*/ */
@DescribeResult(name = "result", description = "The output map") @DescribeResult(name = "result", description = "The output map")
public RawData execute( public RawData execute(
@DescribeParameter(name = "bbox", min = 1, description = "The map area and output projection") @DescribeParameter(name = "bbox", min = 1, description = "The map area and output projection")
ReferencedEnvelope bbox, ReferencedEnvelope bbox,
@DescribeParameter(name = "decoration", min = 0, description = "A WMS decoration layout name to watermark the output") String decorationName, @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 " + @DescribeParameter(name = "time", min = 0, description = "Map time specification (a single time value or " +
"a range like in WMS time parameter)") String time, "a range like in WMS time parameter)") String time,
@DescribeParameter(name = "width", min = 1, description = "Map width", minValue = 1) int width, @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 = "height", min = 1, description = "Map height", minValue = 1) int height,
@DescribeParameter(name = "layer", min = 1, description = "The list of layers", minValue = 1) Layer[] @DescribeParameter(name = "layer", min = 1, description = "The list of layers", minValue = 1) Layer[]
layers, layers,
@DescribeParameter(name = "format", min = 1, description = "The output format", minValue = 1) Format format, @DescribeParameter(name = "format", min = 1, description = "The output format", minValue = 1) Format format,
final ProgressListener progressListener) throws Exception { final ProgressListener progressListener) throws Exception {
RenderedImage result = buildImage(bbox, decorationName, time, width, height, layers, format);




// encode the output by faking a normal request
GetMapRequest request = new GetMapRequest();
request.setFormat(format.getName());
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);
}

RenderedImage buildImage(ReferencedEnvelope bbox, String decorationName, String time, int width, int height,
Layer[] layers, Format format) throws Exception {
// build GetMap template parameters // build GetMap template parameters
CaseInsensitiveMap template = new CaseInsensitiveMap(new HashMap()); CaseInsensitiveMap template = new CaseInsensitiveMap(new HashMap());
template.put("service", "WMS"); template.put("service", "WMS");
Expand Down Expand Up @@ -135,7 +155,7 @@ public RawData execute(
result = mergeImage(result, image); result = mergeImage(result, image);


} }

// past the first layer switch transparency on to allow overlaying // past the first layer switch transparency on to allow overlaying
template.put("transparent", "true"); template.put("transparent", "true");
} }
Expand All @@ -151,23 +171,10 @@ public RawData execute(
content.setTransparent(true); content.setTransparent(true);
RenderedImageMapOutputFormat renderer = new RenderedImageMapOutputFormat(wms); RenderedImageMapOutputFormat renderer = new RenderedImageMapOutputFormat(wms);
RenderedImageMap map = renderer.produceMap(content); RenderedImageMap map = renderer.produceMap(content);

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



// encode the output by faking a normal request result = mergeImage(result, map.getImage());
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());
}
} }

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


public RenderedImage mergeImage(RenderedImage result, RenderedImage image) { public RenderedImage mergeImage(RenderedImage result, RenderedImage image) {
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<constructor-arg index="0" ref="geoServer"/> <constructor-arg index="0" ref="geoServer"/>
</bean> </bean>


<bean id="downloadAnimationProcess" class="org.geoserver.wps.gs.download.DownloadAnimationProcess">
<constructor-arg index="0" ref="downloadMapProcess"/>
<constructor-arg index="1" ref="wpsResourceManager"/>
</bean>



<!-- The PPIO that need to be registered into SPI because of their dependencies --> <!-- The PPIO that need to be registered into SPI because of their dependencies -->
<bean id="archiveZipPPIO" class="org.geoserver.wps.ppio.ZipArchivePPIO"> <bean id="archiveZipPPIO" class="org.geoserver.wps.ppio.ZipArchivePPIO">
Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.geoserver.wps.gs.download;

import org.apache.commons.io.FileUtils;
import org.geoserver.catalog.DimensionPresentation;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.config.GeoServerInfo;
import org.geoserver.data.test.MockData;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.wps.WPSTestSupport;

import javax.xml.namespace.QName;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class BaseDownloadImageProcessTest extends WPSTestSupport {
protected static final String SAMPLES = "src/test/resources/org/geoserver/wps/gs/download/";
protected static final QName WATERTEMP = new QName(MockData.SF_URI, "watertemp", MockData.SF_PREFIX);
protected static final QName BMTIME = new QName(MockData.SF_URI, "bmtime", MockData.SF_PREFIX);
protected static final String UNITS = "foot";
protected static final String UNIT_SYMBOL = "ft";
protected static QName GIANT_POLYGON = new QName(MockData.CITE_URI, "giantPolygon", MockData.CITE_PREFIX);

@Override
protected void onSetUp(SystemTestData testData) throws Exception {
super.onSetUp(testData);

// disable entity resolver as it won't let the tests run in IntelliJ if also GeoTools is loaded...
GeoServerInfo global = getGeoServer().getGlobal();
global.setXmlExternalEntitiesEnabled(true);
getGeoServer().save(global);

// add water temperature
testData.addStyle("temperature", "temperature.sld", DownloadMapProcess.class, catalog);
Map propertyMap = new HashMap();
propertyMap.put(SystemTestData.LayerProperty.STYLE,"temperature");
testData.addRasterLayer(WATERTEMP, "watertemp.zip", null, propertyMap, SystemTestData.class, catalog);
setupRasterDimension(WATERTEMP, ResourceInfo.ELEVATION, DimensionPresentation.LIST, null, UNITS, UNIT_SYMBOL);
setupRasterDimension(WATERTEMP, ResourceInfo.TIME, DimensionPresentation.LIST, null, null, null);

// add a bluemarble four months mosaic
testData.addRasterLayer(BMTIME, "bm_time.zip", null, null, getClass(), catalog);
setupRasterDimension(BMTIME, ResourceInfo.TIME, DimensionPresentation.LIST, null, null, null);

// a world covering layer with no dimensions
testData.addVectorLayer(GIANT_POLYGON, Collections.EMPTY_MAP, "giantPolygon.properties",
SystemTestData.class, getCatalog());

// add a decoration layout
File layouts = getDataDirectory().findOrCreateDir("layouts");
FileUtils.copyURLToFile(getClass().getResource("watermarker.xml"), new File(layouts, "watermarker.xml"));
FileUtils.copyURLToFile(getClass().getResource("geoserver.png"), new File(layouts, "geoserver.png"));
}
}

0 comments on commit 43a1799

Please sign in to comment.