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 Original file line Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The parameters usable to customize the ColorMap are:
- 2 - 2
* - attribute (mandatory) * - attribute (mandatory)
- Classification attribute - 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 - No default for vectors, "1" for rasters
* - method * - method
- Classification 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) - 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>) - 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 Examples
~~~~~~~~~~ ~~~~~~~~~~
Expand Down
5 changes: 5 additions & 0 deletions src/extension/sldService/pom.xml
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -117,5 +117,10 @@
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>
Original file line number Original file line 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; package org.geoserver.sldservice.rest;


import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerException;
Expand Down
Original file line number Original file line 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.JetColorRamp;
import org.geoserver.sldservice.utils.classifier.impl.RandomColorRamp; import org.geoserver.sldservice.utils.classifier.impl.RandomColorRamp;
import org.geoserver.sldservice.utils.classifier.impl.RedColorRamp; 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.Query;
import org.geotools.data.util.NullProgressListener; import org.geotools.data.util.NullProgressListener;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureCollection;
import org.geotools.filter.function.RangedClassifier; 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.image.util.ImageUtilities;
import org.geotools.styling.ChannelSelection;
import org.geotools.styling.ColorMap; import org.geotools.styling.ColorMap;
import org.geotools.styling.ContrastEnhancement;
import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.NamedLayer; import org.geotools.styling.NamedLayer;
import org.geotools.styling.RasterSymbolizer; import org.geotools.styling.RasterSymbolizer;
import org.geotools.styling.Rule; import org.geotools.styling.Rule;
import org.geotools.styling.SelectedChannelType;
import org.geotools.styling.Style; import org.geotools.styling.Style;
import org.geotools.styling.StyledLayerDescriptor; import org.geotools.styling.StyledLayerDescriptor;
import org.geotools.util.Converters; import org.geotools.util.Converters;
Expand All @@ -75,12 +77,12 @@
import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon; 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.feature.type.FeatureType;
import org.opengis.filter.Filter; import org.opengis.filter.Filter;
import org.opengis.parameter.GeneralParameterValue; import org.opengis.filter.FilterFactory2;
import org.opengis.parameter.ParameterValue; 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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
Expand All @@ -99,8 +101,9 @@
@ControllerAdvice @ControllerAdvice
@RequestMapping(path = RestBaseController.ROOT_PATH + "/sldservice") @RequestMapping(path = RestBaseController.ROOT_PATH + "/sldservice")
public class ClassifierController extends BaseSLDServiceController { public class ClassifierController extends BaseSLDServiceController {
private static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
private static final Logger LOGGER = Logging.getLogger(ClassifierController.class); 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 @Autowired
public ClassifierController(@Qualifier("catalog") Catalog catalog) { 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 = "cache", required = false, defaultValue = "600") long cachingTime,
@RequestParam(value = "continuous", required = false, defaultValue = "false") @RequestParam(value = "continuous", required = false, defaultValue = "false")
boolean continuous, boolean continuous,
@RequestParam(value = "bbox", required = false) ReferencedEnvelope bbox,
final HttpServletResponse response) final HttpServletResponse response)
throws Exception { throws Exception {
LayerInfo layerInfo = catalog.getLayerByName(layerName); LayerInfo layerInfo = catalog.getLayerByName(layerName);
if (layerInfo == null) { if (layerInfo == null) {
throw new ResourceNotFoundException("No such layer: " + layerName); 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) { if (cachingTime > 0) {
response.setHeader( response.setHeader(
"cache-control", "cache-control",
Expand Down Expand Up @@ -198,7 +206,8 @@ public Object classify(
stroke, stroke,
pointSize, pointSize,
(FeatureTypeInfo) obj, (FeatureTypeInfo) obj,
ramp); ramp,
bbox);
} else if (obj instanceof CoverageInfo) { } else if (obj instanceof CoverageInfo) {
rules = rules =
getRasterRules( getRasterRules(
Expand All @@ -212,7 +221,8 @@ public Object classify(
normalize, normalize,
(CoverageInfo) obj, (CoverageInfo) obj,
ramp, ramp,
continuous); continuous,
bbox);
} else { } else {
throw new RestException( throw new RestException(
"The classifier can only work against vector or raster data, " "The classifier can only work against vector or raster data, "
Expand Down Expand Up @@ -357,23 +367,22 @@ private List<Rule> getRasterRules(
Boolean normalize, Boolean normalize,
CoverageInfo coverageInfo, CoverageInfo coverageInfo,
ColorRamp ramp, ColorRamp ramp,
boolean continuous) boolean continuous,
ReferencedEnvelope bbox)
throws Exception { 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 RasterSymbolizerBuilder builder = new RasterSymbolizerBuilder();
// 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();
}
ColorMap colorMap; ColorMap colorMap;
try { try {
if (customClasses.isEmpty()) { if (customClasses.isEmpty()) {
Expand All @@ -396,24 +405,40 @@ private List<Rule> getRasterRules(
colorMap = builder.createCustomColorMap(classifier, open, continuous); colorMap = builder.createCustomColorMap(classifier, open, continuous);
} }
} finally { } finally {
cleanCoverage(coverage, image); cleanImage(image);
} }


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


// wrap the colormap into a raster symbolizer and rule // wrap the colormap into a raster symbolizer and rule
Rule rule = SF.createRule();
RasterSymbolizer rasterSymbolizer = SF.createRasterSymbolizer(); RasterSymbolizer rasterSymbolizer = SF.createRasterSymbolizer();
rasterSymbolizer.setColorMap(colorMap); 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); rule.symbolizers().add(rasterSymbolizer);
return Collections.singletonList(rule); 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) { if (property == null) {
return NO_BAND_SELECTED; return FIRST_BAND;
} }
Integer selectedBand = Converters.convert(property, Integer.class); Integer selectedBand = Converters.convert(property, Integer.class);
if (selectedBand == null) { if (selectedBand == null) {
Expand All @@ -422,15 +447,6 @@ private int getSelectedBand(String property, RenderedImage image) {
+ property, + property,
HttpStatus.BAD_REQUEST); 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; return selectedBand;
} }


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

0 comments on commit 298d071

Please sign in to comment.