Skip to content

Commit

Permalink
[GEOS-9034] Have SLDService raster classification work against large …
Browse files Browse the repository at this point in the history
…raster datasets
  • Loading branch information
aaime committed Nov 28, 2018
1 parent 2c21724 commit 298d071
Show file tree
Hide file tree
Showing 12 changed files with 995 additions and 135 deletions.
6 changes: 5 additions & 1 deletion doc/en/user/source/extensions/sldservice/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The parameters usable to customize the ColorMap are:
- 2
* - attribute (mandatory)
- Classification attribute
- For vector layers, one of the layer attribute names, for raster layers, a band number
- For vector layers, one of the layer attribute names, for raster layers, a band number (starting from one, like in the raster symbolizer)
- No default for vectors, "1" for rasters
* - method
- Classification method
Expand Down Expand Up @@ -201,6 +201,10 @@ The parameters usable to customize the ColorMap are:
- allows specifying a set of custom classes (client driven style); no classes calculation will happen (method, intervals, etc. are ignored)
- classes in the following format: <min>,<max>,<color>;...;<minN>,<maxN>,<colorN>)
-
* - bbox
- allows to run the classification on a specific bounding box. Recommended when the overall dataset is too big, and the classification can be performed on a smaller dataset, or to enhance the visualization of a particular subset of data
- same syntax as WMS/WFS, expected axis order is east/north unless the spatial reference system is explicitly provided, ``minx,miny,max,maxy[,srsName]``
-

Examples
~~~~~~~~~~
Expand Down
5 changes: 5 additions & 0 deletions src/extension/sldService/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,10 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/* (c) 2018 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.sldservice.rest;

import javax.xml.transform.TransformerException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,21 @@
import org.geoserver.sldservice.utils.classifier.impl.JetColorRamp;
import org.geoserver.sldservice.utils.classifier.impl.RandomColorRamp;
import org.geoserver.sldservice.utils.classifier.impl.RedColorRamp;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.data.Query;
import org.geotools.data.util.NullProgressListener;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.FeatureCollection;
import org.geotools.filter.function.RangedClassifier;
import org.geotools.image.ImageWorker;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.util.ImageUtilities;
import org.geotools.styling.ChannelSelection;
import org.geotools.styling.ColorMap;
import org.geotools.styling.ContrastEnhancement;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.NamedLayer;
import org.geotools.styling.RasterSymbolizer;
import org.geotools.styling.Rule;
import org.geotools.styling.SelectedChannelType;
import org.geotools.styling.Style;
import org.geotools.styling.StyledLayerDescriptor;
import org.geotools.util.Converters;
Expand All @@ -75,12 +77,12 @@
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridCoverageReader;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.Filter;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValue;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.spatial.BBOX;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.operation.TransformException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.CacheControl;
Expand All @@ -99,8 +101,9 @@
@ControllerAdvice
@RequestMapping(path = RestBaseController.ROOT_PATH + "/sldservice")
public class ClassifierController extends BaseSLDServiceController {
private static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
private static final Logger LOGGER = Logging.getLogger(ClassifierController.class);
public static final int NO_BAND_SELECTED = -1;
private static final int FIRST_BAND = 1;

@Autowired
public ClassifierController(@Qualifier("catalog") Catalog catalog) {
Expand Down Expand Up @@ -158,12 +161,17 @@ public Object classify(
@RequestParam(value = "cache", required = false, defaultValue = "600") long cachingTime,
@RequestParam(value = "continuous", required = false, defaultValue = "false")
boolean continuous,
@RequestParam(value = "bbox", required = false) ReferencedEnvelope bbox,
final HttpServletResponse response)
throws Exception {
LayerInfo layerInfo = catalog.getLayerByName(layerName);
if (layerInfo == null) {
throw new ResourceNotFoundException("No such layer: " + layerName);
}
// default to the layer own CRS if not provided as 5th parameter
if (bbox != null && bbox.getCoordinateReferenceSystem() == null) {
bbox = new ReferencedEnvelope(bbox, layerInfo.getResource().getCRS());
}
if (cachingTime > 0) {
response.setHeader(
"cache-control",
Expand Down Expand Up @@ -198,7 +206,8 @@ public Object classify(
stroke,
pointSize,
(FeatureTypeInfo) obj,
ramp);
ramp,
bbox);
} else if (obj instanceof CoverageInfo) {
rules =
getRasterRules(
Expand All @@ -212,7 +221,8 @@ public Object classify(
normalize,
(CoverageInfo) obj,
ramp,
continuous);
continuous,
bbox);
} else {
throw new RestException(
"The classifier can only work against vector or raster data, "
Expand Down Expand Up @@ -357,23 +367,22 @@ private List<Rule> getRasterRules(
Boolean normalize,
CoverageInfo coverageInfo,
ColorRamp ramp,
boolean continuous)
boolean continuous,
ReferencedEnvelope bbox)
throws Exception {
RasterSymbolizerBuilder builder = new RasterSymbolizerBuilder();
int selectedBand = getRequestedBand(property); // one based band name
// read the image to be classified
ImageReader imageReader =
new ImageReader(
coverageInfo,
selectedBand,
RasterSymbolizerBuilder.DEFAULT_MAX_PIXELS,
bbox)
.invoke();
boolean bandSelected = imageReader.isBandSelected();
RenderedImage image = imageReader.getImage();

// grab the raster, for the time being, read fully trying to force deferred loading where
// possible
GridCoverageReader reader = coverageInfo.getGridCoverageReader(null, null);
ParameterValue<Boolean> useImageRead = AbstractGridFormat.USE_JAI_IMAGEREAD.createValue();
useImageRead.setValue(true);
GridCoverage coverage = reader.read(new GeneralParameterValue[] {useImageRead});
RenderedImage image = coverage.getRenderedImage();
int selectedBand = getSelectedBand(property, image);
if (selectedBand != NO_BAND_SELECTED) {
ImageWorker iw = new ImageWorker(image);
iw.retainBands(selectedBand);
image = iw.getRenderedImage();
}
RasterSymbolizerBuilder builder = new RasterSymbolizerBuilder();
ColorMap colorMap;
try {
if (customClasses.isEmpty()) {
Expand All @@ -396,24 +405,40 @@ private List<Rule> getRasterRules(
colorMap = builder.createCustomColorMap(classifier, open, continuous);
}
} finally {
cleanCoverage(coverage, image);
cleanImage(image);
}

// apply the color ramp
boolean skipFirstEntry = !"uniqueInterval".equals(method) && !open && !continuous;
builder.applyColorRamp(colorMap, ramp, skipFirstEntry, reverse);

// wrap the colormap into a raster symbolizer and rule
Rule rule = SF.createRule();
RasterSymbolizer rasterSymbolizer = SF.createRasterSymbolizer();
rasterSymbolizer.setColorMap(colorMap);
if (bandSelected) {
SelectedChannelType grayChannel =
SF.createSelectedChannelType(
String.valueOf(selectedBand), (ContrastEnhancement) null);
ChannelSelection channelSelection =
SF.createChannelSelection(new SelectedChannelType[] {grayChannel});
rasterSymbolizer.setChannelSelection(channelSelection);
}

Rule rule = SF.createRule();
rule.symbolizers().add(rasterSymbolizer);
return Collections.singletonList(rule);
}

private int getSelectedBand(String property, RenderedImage image) {
/**
* Returns the selected band
*
* @param property
* @return
*/
private int getRequestedBand(String property) {
// if no selection is provided, the code picks the first band
if (property == null) {
return NO_BAND_SELECTED;
return FIRST_BAND;
}
Integer selectedBand = Converters.convert(property, Integer.class);
if (selectedBand == null) {
Expand All @@ -422,15 +447,6 @@ private int getSelectedBand(String property, RenderedImage image) {
+ property,
HttpStatus.BAD_REQUEST);
}
int numBands = image.getSampleModel().getNumBands();
if (selectedBand < 0 || selectedBand > numBands) {
throw new RestException(
"Invalid property value for raster layer, must be a valid band number, between 0 and "
+ (numBands - 1)
+ ", but was "
+ selectedBand,
HttpStatus.BAD_REQUEST);
}
return selectedBand;
}

Expand All @@ -439,10 +455,7 @@ private int getSelectedBand(String property, RenderedImage image) {
*
* @param coverage
*/
private void cleanCoverage(GridCoverage coverage, RenderedImage image) {
if (coverage instanceof GridCoverage2D) {
((GridCoverage2D) coverage).dispose(true);
}
private void cleanImage(RenderedImage image) {
if (image instanceof PlanarImage) {
ImageUtilities.disposePlanarImageChain((PlanarImage) image);
}
Expand All @@ -462,8 +475,9 @@ private List<Rule> getVectorRules(
Color strokeColor,
int pointSize,
FeatureTypeInfo obj,
ColorRamp ramp)
throws IOException {
ColorRamp ramp,
ReferencedEnvelope bbox)
throws IOException, TransformException, FactoryException {
if (property == null || property.isEmpty()) {
throw new IllegalArgumentException(
"Vector classification requires a classification property to be specified");
Expand All @@ -478,6 +492,12 @@ private List<Rule> getVectorRules(
FeatureCollection ftCollection = null;
if (customClasses.isEmpty()) {
Query query = new Query(ftType.getName().getLocalPart(), Filter.INCLUDE);
if (bbox != null) {
ReferencedEnvelope nativeBBOX =
bbox.transform(ftType.getCoordinateReferenceSystem(), true);
BBOX filter = FF.bbox(FF.property(""), nativeBBOX);
query.setFilter(filter);
}
query.setHints(getQueryHints(viewParams));
ftCollection =
obj.getFeatureSource(new NullProgressListener(), null).getFeatures(query);
Expand Down

0 comments on commit 298d071

Please sign in to comment.