diff --git a/documentation/en/user/source/services/wms.rst b/documentation/en/user/source/services/wms.rst index e50c6a4fac..39aeea4b8f 100644 --- a/documentation/en/user/source/services/wms.rst +++ b/documentation/en/user/source/services/wms.rst @@ -39,6 +39,31 @@ Note that to use WMS you should not have two grid sets with the same SRS defined Support for Regular WMS Clients ------------------------------- -GeoWebCache can recombine and resample tiles to answer arbitrary WMS requests. To enable this feature, open ``geowebcache-wmsservice-context.xml``, find ``FALSE`` and change to ``TRUE``. Another way to enable this feature is to add the following string to the ``geowebcache.xml`` file: ``TRUE``. All layers that are to support this feature must currently be configured to support a PNG format. Inside the WMS request the user can add a new WMS parameter called **hints** which can be set to one of the following configurations: *speed*, *default*, *quality*. Going from *speed* to *quality* the image quality is increased but also the computation time. - +GeoWebCache can recombine and resample tiles to answer arbitrary WMS requests. To enable this feature, open ``geowebcache-wmsservice-context.xml``, find ``FALSE`` and change to ``TRUE``. Another way to enable this feature is to add the following string to the ``geowebcache.xml`` file: ``TRUE``. All layers that are to support this feature must currently be configured to support a PNG format. Inside the WMS request the user can add a new WMS parameter called **hints** which can be set to one of the following configurations: *speed*, *default*, *quality*. Going from *speed* to *quality* the image quality is increased but also the computation time. Note that this requires GeoWebCache to decompress many tiles and recompress the resulting canvas; also for PNG8 and GIF output formats an optimal palette is calculated. Response times will therefore be on the order of seconds, depending on the size of the requested image and the tile sizes. You may have to increase the heap size of the Java process (``-Xmx256M``) to use this functionality. +The resolution used for tile recomposition is selected as the closest available match from the underlying grid set to the requested resolution. If an exact match is not available, the nearest resolution level is chosen based on proximity, and the resulting image is rescaled to the requested output size. + + +Configuring interpolation for WMS Layer via HintsLevel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When GeoWebCache recombines tiles into a single image, the resulting raster is rescaled to match the requested output size. +The interpolation method used during this rescaling step can be controlled by setting an optional configuration parameter ``hintsLevel``. +It can be specified on WMS layers in ``geowebcache.xml``: + +:: + + + ... + QUALITY + ... + + +Valid values are: + +- ``SPEED`` — uses nearest-neighbor interpolation (fastest, lowest quality) +- ``DEFAULT`` — uses bilinear interpolation (balanced) +- ``QUALITY`` — uses bicubic interpolation (highest quality, slowest) + +If not specified, the default behavior is equivalent to ``DEFAULT``. + +This setting affects the final scaling step when GeoWebCache assembles tiles into a single image for non-tiled WMS requests. diff --git a/geowebcache/core/src/main/java/org/geowebcache/config/HintsLevel.java b/geowebcache/core/src/main/java/org/geowebcache/config/HintsLevel.java new file mode 100644 index 0000000000..84ecdac4df --- /dev/null +++ b/geowebcache/core/src/main/java/org/geowebcache/config/HintsLevel.java @@ -0,0 +1,112 @@ +/** + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + *

You should have received a copy of the GNU Lesser General Public License along with this program. If not, see + * . + * + *

Copyright 2026 + */ +package org.geowebcache.config; + +import java.awt.RenderingHints; +import java.util.Map; + +/** Enum storing the Hints associated to one of the 3 configurations(SPEED, QUALITY, DEFAULT) */ +public enum HintsLevel { + QUALITY(0, "quality"), + DEFAULT(1, "default"), + SPEED(2, "speed"); + + @SuppressWarnings("ImmutableEnumChecker") // RenderingHints is mutable + private final RenderingHints hints; + + private final String mode; + + HintsLevel(int numHint, String mode) { + this.mode = mode; + switch (numHint) { + // QUALITY HINTS + case 0: + hints = new RenderingHints( + RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)); + hints.add(new RenderingHints( + RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON)); + hints.add(new RenderingHints( + RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)); + hints.add(new RenderingHints( + RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)); + hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); + hints.add(new RenderingHints( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)); + hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE)); + break; + // DEFAULT HINTS + case 1: + hints = new RenderingHints( + RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_DEFAULT); + hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT)); + hints.add(new RenderingHints( + RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT)); + hints.add(new RenderingHints( + RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT)); + hints.add(new RenderingHints( + RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)); + hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT)); + hints.add(new RenderingHints( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT)); + hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT)); + break; + // SPEED HINTS + case 2: + hints = new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); + hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)); + hints.add(new RenderingHints( + RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF)); + hints.add(new RenderingHints( + RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED)); + hints.add(new RenderingHints( + RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)); + hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)); + hints.add(new RenderingHints( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)); + hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)); + break; + default: + hints = null; + } + } + + public RenderingHints getRenderingHints() { + if (hints == null) { + return null; + } + @SuppressWarnings("unchecked") + RenderingHints copy = new RenderingHints((Map) hints); + return copy; + } + + public String getModeName() { + return mode; + } + + public static HintsLevel getHintsForMode(String mode) { + + if (mode != null) { + if (mode.equalsIgnoreCase(QUALITY.getModeName())) { + return QUALITY; + } else if (mode.equalsIgnoreCase(SPEED.getModeName())) { + return SPEED; + } else { + return DEFAULT; + } + } else { + return DEFAULT; + } + } +} diff --git a/geowebcache/core/src/main/java/org/geowebcache/layer/TileLayer.java b/geowebcache/core/src/main/java/org/geowebcache/layer/TileLayer.java index 03bddb9a5c..6d436eda0d 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/layer/TileLayer.java +++ b/geowebcache/core/src/main/java/org/geowebcache/layer/TileLayer.java @@ -29,6 +29,7 @@ import javax.annotation.Nullable; import org.geotools.util.logging.Logging; import org.geowebcache.GeoWebCacheException; +import org.geowebcache.config.HintsLevel; import org.geowebcache.config.Info; import org.geowebcache.conveyor.ConveyorTile; import org.geowebcache.filter.parameters.ParameterFilter; @@ -187,6 +188,12 @@ public Map getLayerLegendsInf public abstract boolean isQueryable(); + /** @return the hints level to use when rendering this layer */ + public HintsLevel getHintsLevel() { + // default implementation, subclasses can override to provide a different hints level + return HintsLevel.DEFAULT; + } + /** * The timeout used when querying the backend server. The same value is used for both the connection and the data * timeout, so in theory the timeout could be twice this value. diff --git a/geowebcache/core/src/main/java/org/geowebcache/layer/wms/WMSLayer.java b/geowebcache/core/src/main/java/org/geowebcache/layer/wms/WMSLayer.java index 32cb857dbd..d97d79b059 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/layer/wms/WMSLayer.java +++ b/geowebcache/core/src/main/java/org/geowebcache/layer/wms/WMSLayer.java @@ -28,6 +28,7 @@ import org.apache.hc.core5.http.HttpEntity; import org.geotools.util.logging.Logging; import org.geowebcache.GeoWebCacheException; +import org.geowebcache.config.HintsLevel; import org.geowebcache.config.XMLGridSubset; import org.geowebcache.config.legends.LegendsRawInfo; import org.geowebcache.conveyor.Conveyor.CacheResult; @@ -123,6 +124,8 @@ public enum HttpRequestMode { private HttpRequestMode httpRequestMode = HttpRequestMode.Get; + private HintsLevel hintsLevel; + WMSLayer() { // default constructor for XStream } @@ -679,6 +682,15 @@ public String getProxyUrl() { return proxyUrl; } + @Override + public HintsLevel getHintsLevel() { + return hintsLevel != null ? hintsLevel : HintsLevel.DEFAULT; + } + + public void setHintsLevel(HintsLevel hintsLevel) { + this.hintsLevel = hintsLevel; + } + /** Mandatory */ public void setSourceHelper(WMSSourceHelper source) { log.fine("Setting sourceHelper on " + this.name); diff --git a/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd b/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd index ab9866d6f0..bd9009b067 100644 --- a/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd +++ b/geowebcache/core/src/main/resources/org/geowebcache/config/geowebcache.xsd @@ -568,7 +568,13 @@ - + + + + + + + @@ -606,6 +612,15 @@ + + + + Rendering hints level used when recombining tiles for full WMS responses. + SPEED uses nearest-neighbor interpolation, DEFAULT uses bilinear interpolation, + and QUALITY uses bicubic interpolation. + + + diff --git a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/BufferedImageWrapper.java b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/BufferedImageWrapper.java index 03e6aeeebc..3e4e46654f 100644 --- a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/BufferedImageWrapper.java +++ b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/BufferedImageWrapper.java @@ -17,6 +17,7 @@ import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import org.geowebcache.config.HintsLevel; class BufferedImageWrapper { /** Mosaic image */ @@ -57,7 +58,7 @@ public BufferedImage getCanvas() { } // Hints settings - RenderingHints hintsTemp = WMSTileFuser.HintsLevel.DEFAULT.getRenderingHints(); + RenderingHints hintsTemp = HintsLevel.DEFAULT.getRenderingHints(); if (hints != null) { hintsTemp = hints; diff --git a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSService.java b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSService.java index de57ed06e8..c72af677b2 100644 --- a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSService.java +++ b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSService.java @@ -34,6 +34,7 @@ import org.geowebcache.GeoWebCacheException; import org.geowebcache.GeoWebCacheExtensions; import org.geowebcache.config.BaseConfiguration; +import org.geowebcache.config.HintsLevel; import org.geowebcache.config.ServerConfiguration; import org.geowebcache.config.TileLayerConfiguration; import org.geowebcache.conveyor.Conveyor; @@ -290,6 +291,13 @@ public void handleRequest(Conveyor conv) throws GeoWebCacheException { } else if (tile.getHint().equalsIgnoreCase("getmap")) { getSecurityDispatcher().checkSecurity(tile); WMSTileFuser wmsFuser = getFuser(tile.servletReq); + TileLayer tileLayer = tile.getLayer(); + if (tileLayer != null) { + HintsLevel hintsLevel = tileLayer.getHintsLevel(); + if (hintsLevel != null) { + wmsFuser.setHintsConfiguration(hintsLevel.getModeName()); + } + } try { wmsFuser.writeResponse(tile.servletResp, stats); } catch (SecurityException e) { diff --git a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSTileFuser.java b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSTileFuser.java index 223f650550..5e7176ea77 100644 --- a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSTileFuser.java +++ b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSTileFuser.java @@ -34,6 +34,7 @@ import org.geotools.image.util.ImageUtilities; import org.geotools.util.logging.Logging; import org.geowebcache.GeoWebCacheException; +import org.geowebcache.config.HintsLevel; import org.geowebcache.conveyor.Conveyor.CacheResult; import org.geowebcache.conveyor.ConveyorTile; import org.geowebcache.filter.request.RequestFilterException; @@ -149,100 +150,6 @@ static class PixelOffsets { private SecurityDispatcher securityDispatcher; - /** Enum storing the Hints associated to one of the 3 configurations(SPEED, QUALITY, DEFAULT) */ - public enum HintsLevel { - QUALITY(0, "quality"), - DEFAULT(1, "default"), - SPEED(2, "speed"); - - @SuppressWarnings("ImmutableEnumChecker") // RenderingHints is mutable - private final RenderingHints hints; - - private final String mode; - - HintsLevel(int numHint, String mode) { - this.mode = mode; - switch (numHint) { - // QUALITY HINTS - case 0: - hints = new RenderingHints( - RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); - hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)); - hints.add(new RenderingHints( - RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON)); - hints.add(new RenderingHints( - RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)); - hints.add(new RenderingHints( - RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)); - hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); - hints.add(new RenderingHints( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)); - hints.add(new RenderingHints( - RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE)); - break; - // DEFAULT HINTS - case 1: - hints = new RenderingHints( - RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_DEFAULT); - hints.add(new RenderingHints( - RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT)); - hints.add(new RenderingHints( - RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT)); - hints.add(new RenderingHints( - RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT)); - hints.add(new RenderingHints( - RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)); - hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT)); - hints.add(new RenderingHints( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT)); - hints.add( - new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT)); - break; - // SPEED HINTS - case 2: - hints = new RenderingHints( - RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); - hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF)); - hints.add(new RenderingHints( - RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF)); - hints.add(new RenderingHints( - RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED)); - hints.add(new RenderingHints( - RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)); - hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)); - hints.add(new RenderingHints( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF)); - hints.add(new RenderingHints(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)); - break; - default: - hints = null; - } - } - - public RenderingHints getRenderingHints() { - return hints; - } - - public String getModeName() { - return mode; - } - - public static HintsLevel getHintsForMode(String mode) { - - if (mode != null) { - if (mode.equalsIgnoreCase(QUALITY.getModeName())) { - return QUALITY; - } else if (mode.equalsIgnoreCase(SPEED.getModeName())) { - return SPEED; - } else { - return DEFAULT; - } - } else { - return DEFAULT; - } - } - } - protected WMSTileFuser(TileLayerDispatcher tld, StorageBroker sb, HttpServletRequest servReq) throws GeoWebCacheException { this.sb = sb; @@ -256,7 +163,6 @@ protected WMSTileFuser(TileLayerDispatcher tld, StorageBroker sb, HttpServletReq String layerName = values.get("layers"); layer = tld.getTileLayer(layerName); - gridSubset = layer.getGridSubsetForSRS(SRS.getSRS(values.get("srs"))); outputFormat = (ImageMime) ImageMime.createFromFormat(values.get("format")); @@ -294,6 +200,8 @@ protected WMSTileFuser(TileLayerDispatcher tld, StorageBroker sb, HttpServletReq reqHeight = Integer.valueOf(values.get("height")); fullParameters = layer.getModifiableParameters(servReq.getParameterMap(), servReq.getCharacterEncoding()); + this.hints = layer.getHintsLevel().getRenderingHints(); + if (values.get("hints") != null) { hints = HintsLevel.getHintsForMode(values.get("hints")).getRenderingHints(); } @@ -340,35 +248,33 @@ protected void determineSourceResolution() { xResolution = reqBounds.getWidth() / reqWidth; yResolution = reqBounds.getHeight() / reqHeight; - double tmpResolution; - // We use the smallest one - if (yResolution < xResolution) { - tmpResolution = yResolution; - } else { - tmpResolution = xResolution; - } + double tmpResolution = Math.min(xResolution, yResolution); log.fine("x res: " + xResolution + " y res: " + yResolution + " tmpResolution: " + tmpResolution); // Cut ourselves 0.5% slack - double compResolution = 1.005 * tmpResolution; - double[] resArray = gridSubset.getResolutions(); for (srcIdx = 0; srcIdx < resArray.length; srcIdx++) { - srcResolution = resArray[srcIdx]; - if (srcResolution < compResolution) { + if (resArray[srcIdx] < tmpResolution) { break; } } if (srcIdx >= resArray.length) { + // Clamp to last valid index srcIdx = resArray.length - 1; - } + } else if (srcIdx > 0) { + // Compare neighbors only if we have two valid ones + double res1 = resArray[srcIdx - 1]; + double res2 = resArray[srcIdx]; + if (Math.abs(res1 - tmpResolution) <= Math.abs(res2 - tmpResolution)) { + srcIdx = srcIdx - 1; + } + } + srcResolution = resArray[srcIdx]; log.fine("z: " + srcIdx + " , resolution: " + srcResolution + " (" + tmpResolution + ")"); - - // At worst, we have the best resolution possible } protected void determineCanvasLayout() { @@ -679,4 +585,8 @@ public void setHintsConfiguration(String hintsConfig) { public void setSecurityDispatcher(SecurityDispatcher securityDispatcher) { this.securityDispatcher = securityDispatcher; } + + RenderingHints getHints() { + return hints; + } } diff --git a/geowebcache/wms/src/test/java/org/geowebcache/service/wms/WMSTileFuserTest.java b/geowebcache/wms/src/test/java/org/geowebcache/service/wms/WMSTileFuserTest.java index 53a6016fda..04907b06a5 100644 --- a/geowebcache/wms/src/test/java/org/geowebcache/service/wms/WMSTileFuserTest.java +++ b/geowebcache/wms/src/test/java/org/geowebcache/service/wms/WMSTileFuserTest.java @@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.awt.Color; import java.awt.Graphics; +import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; @@ -44,6 +45,7 @@ import javax.imageio.ImageIO; import org.geowebcache.GeoWebCacheException; import org.geowebcache.config.DefaultGridsets; +import org.geowebcache.config.HintsLevel; import org.geowebcache.conveyor.ConveyorTile; import org.geowebcache.filter.security.SecurityDispatcher; import org.geowebcache.grid.BoundingBox; @@ -474,13 +476,59 @@ public boolean get(TileObject stObj) throws StorageException { assertEquals(3, image.getSampleModel().getNumBands()); // the output image has two red stripes, the transparent pixels became white - // in particular, it's white, red, white, red // red is not 255 because of JPEG compression WritableRaster raster = image.getRaster(); assertArrayEquals(new int[] {255, 255, 255}, raster.getPixel(30, 125, new int[3])); assertArrayEquals(new int[] {254, 0, 0}, raster.getPixel(90, 125, new int[3])); - assertArrayEquals(new int[] {255, 255, 255}, raster.getPixel(150, 125, new int[3])); - assertArrayEquals(new int[] {254, 0, 0}, raster.getPixel(210, 125, new int[3])); + assertArrayEquals(new int[] {254, 0, 0}, raster.getPixel(150, 125, new int[3])); + assertArrayEquals(new int[] {255, 255, 255}, raster.getPixel(210, 125, new int[3])); } } + + @Test + public void testHintsLevelFromLayerConfig() throws Exception { + WMSLayer layer = createWMSLayer(); + layer.setHintsLevel(HintsLevel.QUALITY); // new field added to WMSLayer + + BoundingBox bounds = new BoundingBox(-25.0, 17.0, 40.0, 22); + int width = (int) bounds.getWidth() * 10; + int height = (int) bounds.getHeight() * 10; + GridSubset gridSubset = + layer.getGridSubset(layer.getGridSubsets().iterator().next()); + + TileLayerDispatcher tld = mock(TileLayerDispatcher.class); + StorageBroker sb = mock(StorageBroker.class); + Mockito.when(tld.getTileLayer("test:layer")).thenReturn(layer); + + WMSTileFuser tileFuser = new WMSTileFuser(tld, sb, fuserRequest(layer, gridSubset, bounds, width, height)); + + assertEquals( + RenderingHints.VALUE_INTERPOLATION_BICUBIC, tileFuser.getHints().get(RenderingHints.KEY_INTERPOLATION)); + assertEquals(RenderingHints.VALUE_RENDER_QUALITY, tileFuser.getHints().get(RenderingHints.KEY_RENDERING)); + } + + @Test + public void testRequestHintsOverridesLayerConfig() throws Exception { + WMSLayer layer = createWMSLayer(); + layer.setHintsLevel(HintsLevel.QUALITY); + + BoundingBox bounds = new BoundingBox(-25.0, 17.0, 40.0, 22.0); + int width = (int) bounds.getWidth() * 10; + int height = (int) bounds.getHeight() * 10; + GridSubset gridSubset = + layer.getGridSubset(layer.getGridSubsets().iterator().next()); + + TileLayerDispatcher tld = mock(TileLayerDispatcher.class); + StorageBroker sb = mock(StorageBroker.class); + Mockito.when(tld.getTileLayer("test:layer")).thenReturn(layer); + + MockHttpServletRequest req = (MockHttpServletRequest) fuserRequest(layer, gridSubset, bounds, width, height); + req.addParameter("hints", "speed"); + + WMSTileFuser tileFuser = new WMSTileFuser(tld, sb, req); + + RenderingHints hints = tileFuser.getHints(); + assertEquals(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR, hints.get(RenderingHints.KEY_INTERPOLATION)); + assertEquals(RenderingHints.VALUE_RENDER_SPEED, hints.get(RenderingHints.KEY_RENDERING)); + } }