/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library 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; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotools.map; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import org.apache.commons.lang.StringUtils; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureSource; import org.geotools.data.ows.CRSEnvelope; import org.geotools.data.ows.Layer; import org.geotools.data.wms.WebMapServer; import org.geotools.data.wms.request.GetFeatureInfoRequest; import org.geotools.data.wms.request.GetMapRequest; import org.geotools.data.wms.response.GetFeatureInfoResponse; import org.geotools.data.wms.response.GetMapResponse; import org.geotools.factory.CommonFactoryFinder; import org.geotools.geometry.DirectPosition2D; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.ows.ServiceException; import org.geotools.referencing.CRS; import org.geotools.renderer.lite.RendererUtilities; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.RasterSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.Style; import org.geotools.styling.StyleFactory; import org.opengis.coverage.grid.Format; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.geometry.Envelope; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.ParameterValue; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import com.sun.org.apache.bcel.internal.generic.ASTORE; /** * Wraps a WMS layer into a {@link MapLayer} for interactive rendering usage TODO: expose a * GetFeatureInfo that returns a feature collection TODO: expose the list of named styles and allow * choosing which style to use * * @author Andrea Aime - OpenGeo */ public class WMSMapLayer extends DefaultMapLayer { /** The logger for the map module. */ static public final Logger LOGGER = org.geotools.util.logging.Logging .getLogger("org.geotools.map"); /** * The default raster style */ static Style STYLE; static GridCoverageFactory gcf = new GridCoverageFactory(); WMSCoverageReader reader; static { StyleFactory factory = CommonFactoryFinder.getStyleFactory(null); RasterSymbolizer symbolizer = factory.createRasterSymbolizer(); Rule rule = factory.createRule(); rule.symbolizers().add(symbolizer); FeatureTypeStyle type = factory.createFeatureTypeStyle(); type.rules().add(rule); STYLE = factory.createStyle(); STYLE.featureTypeStyles().add(type); } /** * Builds a new WMS alyer * * @param wms * @param layer */ public WMSMapLayer(WebMapServer wms, Layer layer) { super((FeatureSource<SimpleFeatureType, SimpleFeature>) null, null, ""); reader = new WMSCoverageReader(wms, layer); try { this.featureSource = DataUtilities.source(FeatureUtilities.wrapGridCoverageReader( reader, null)); } catch (Throwable t) { throw new RuntimeException("Unexpected exception occurred during map layer building", t); } this.style = STYLE; } public synchronized ReferencedEnvelope getBounds() { return reader.bounds; } /** * Retrieves the feature info as text (assuming "text/plain" is a supported feature info format) * * @param pos * the position to be checked, in real world coordinates * @return * @throws IOException */ public String getFeatureInfoAsText(DirectPosition2D pos, int featureCount) throws IOException { BufferedReader br = null; try { InputStream is = reader.getFeatureInfo(pos, "text/plain", featureCount, reader.mapRequest); br = new BufferedReader(new InputStreamReader(is)); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } return sb.toString(); } catch (IOException e) { throw e; } catch (Throwable t) { throw (IOException) new IOException("Failed to grab feature info").initCause(t); } finally { if (br != null) br.close(); } } /** * Retrieves the feature info as a generic input stream, it's the duty of the caller to * interpret the contents and ensure the stream is closed feature info format) * * @param pos * the position to be checked, in real world coordinates * @param infoFormat * The INFO_FORMAT parameter in the GetFeatureInfo request * @return * @throws IOException */ public InputStream getFeatureInfo(DirectPosition2D pos, String infoFormat, int featureCount) throws IOException { return reader.getFeatureInfo(pos, infoFormat, featureCount, reader.mapRequest); } /** * Allows to run a standalone GetFeatureInfo request, without the need to have previously run a * GetMap request on this layer. Mostly useful for stateless users that rebuild the map context * for each rendering operation (e.g., GeoServer) * * @param pos * @param infoFormat * The INFO_FORMAT parameter in the GetFeatureInfo request * @return * @throws IOException */ public InputStream getFeatureInfo(ReferencedEnvelope bbox, int width, int height, int x, int y, String infoFormat, int featureCount) throws IOException { try { reader.initMapRequest(bbox, width, height); // we need to convert x/y from the screen to the original coordinates, and then to the ones // that will be used to make the request AffineTransform at = RendererUtilities.worldToScreenTransform(bbox, new Rectangle(width, height)); Point2D screenPos = new Point2D.Double(x, y); Point2D worldPos = new Point2D.Double(x, y); at.inverseTransform(screenPos, worldPos); DirectPosition2D fromPos = new DirectPosition2D(worldPos.getX(), worldPos.getY()); DirectPosition2D toPos = new DirectPosition2D(); MathTransform mt = CRS.findMathTransform(bbox.getCoordinateReferenceSystem(), reader.requestCRS, true); mt.transform(fromPos, toPos); return reader.getFeatureInfo(toPos, infoFormat, featureCount, reader.mapRequest); } catch(IOException e) { throw e; } catch(Throwable t) { throw (IOException) new IOException("Unexpected issue during GetFeatureInfo execution").initCause(t); } } /** * Returns the {@link WebMapServer} used by this layer * * @return */ public WebMapServer getWebMapServer() { return reader.wms; } /** * Returns the WMS {@link Layer} used by this layer * * @return */ public List<Layer> getWMSLayers() { return reader.layers; } /** * Returns the CRS used to make requests to the remote WMS * * @return */ public CoordinateReferenceSystem getCoordinateReferenceSystem() { return reader.getCrs(); } /** * Returns last GetMap request performed by this layer * * @return */ public GetMapRequest getLastGetMap() { return reader.mapRequest; } /** * Allows to add another WMS layer into the GetMap requests * * @param layer */ public void addLayer(Layer layer) { reader.addLayer(layer); } /** * Returns true if the specified CRS can be used directly to perform WMS requests. Natively * supported crs will provide the best rendering quality as no client side reprojection is * necessary, the image coming from the WMS server will be used as-is * * @param crs * @return */ public boolean isNativelySupported(CoordinateReferenceSystem crs) { try { String code = CRS.lookupIdentifier(crs, false); return code != null && reader.validSRS.contains(code); } catch (Throwable t) { return false; } } /** * A grid coverage readers backing onto a WMS server by issuing GetMap */ static class WMSCoverageReader extends AbstractGridCoverage2DReader { /** * The WMS server */ WebMapServer wms; /** * The layer */ List<Layer> layers = new ArrayList<Layer>(); /** * The chosen SRS name */ String srsName; /** * The format to use for requests */ String format; /** * The last GetMap request */ private GetMapRequest mapRequest; /** * The last GetMap response */ GridCoverage2D grid; /** * The set of SRS common to all layers */ Set<String> validSRS; /** * The cached layer bounds */ ReferencedEnvelope bounds; /** * The last request envelope */ ReferencedEnvelope requestedEnvelope; /** * Last request width */ int width; /** * Last request height */ int height; /** * Last request CRS (used for reprojected GetFeatureInfo) */ CoordinateReferenceSystem requestCRS; /** * Builds a new WMS coverage reader * * @param wms * @param layer */ public WMSCoverageReader(WebMapServer wms, Layer layer) { this.wms = wms; addLayer(layer); // best guess at the format with a preference for PNG (since it's normally transparent) List<String> formats = wms.getCapabilities().getRequest().getGetMap().getFormats(); this.format = formats.iterator().next(); for (String format : formats) { if ("image/png".equals(format) || "image/png24".equals(format) || "png".equals(format) || "png24".equals(format)) this.format = format; } } void addLayer(Layer layer) { this.layers.add(layer); if (srsName == null) { // initialize from first layer if (layer.getBoundingBoxes().size() > 0) { // we assume the layer is declared in its native bounding box CRSEnvelope envelope = layer.getBoundingBoxes().get( layer.getBoundingBoxes().keySet().iterator().next()); srsName = envelope.getEPSGCode(); } else if (layer.getSrs().contains("EPSG:4326")) { // otherwise we try 4326 srsName = "EPSG:4326"; } else { // if not even that works we just take the first... srsName = (String) layer.getSrs().iterator().next(); } validSRS = layer.getSrs(); } else { Set<String> intersection = new HashSet<String>(validSRS); intersection.retainAll(layer.getSrs()); // can we reuse what we have? if (!intersection.contains(srsName)) { if (intersection.size() == 0) { throw new IllegalArgumentException("The layer being appended does " + "not have any SRS in common with the ones already " + "included in the WMS request, cannot be merged"); } else if (intersection.contains("EPSG:4326")) { srsName = "EPSG:4326"; } else { // if not even that works we just take the first... srsName = (String) intersection.iterator().next(); } this.validSRS = intersection; } } CoordinateReferenceSystem crs = null; try { crs = CRS.decode(srsName); } catch (Exception e) { LOGGER.log(Level.FINE, "Bounds unavailable for layer" + layer); } this.crs = crs; // update the cached bounds and the reader original envelope updateBounds(); } /** * Issues GetFeatureInfo against a point using the params of the last GetMap request * * @param pos * @return * @throws IOException */ public InputStream getFeatureInfo(DirectPosition2D pos, String infoFormat, int featureCount, GetMapRequest getmap) throws IOException { GetFeatureInfoRequest request = wms.createGetFeatureInfoRequest(getmap); request.setFeatureCount(1); request.setQueryLayers(new LinkedHashSet<Layer>(layers)); request.setInfoFormat(infoFormat); request.setFeatureCount(featureCount); try { AffineTransform tx = RendererUtilities.worldToScreenTransform(requestedEnvelope, new Rectangle(width, height)); Point2D dest = new Point2D.Double(); Point2D src = new Point2D.Double(pos.x, pos.y); tx.transform(src, dest); request.setQueryPoint((int) dest.getX(), (int) dest.getY()); } catch (Exception e) { throw (IOException) new IOException("Failed to grab feature info").initCause(e); } try { GetFeatureInfoResponse response = wms.issueRequest(request); return response.getInputStream(); } catch (IOException e) { throw e; } catch (Throwable t) { throw (IOException) new IOException("Failed to grab feature info").initCause(t); } } @Override public GridCoverage2D read(GeneralParameterValue[] parameters) throws IllegalArgumentException, IOException { // try to get request params from the request Envelope requestedEnvelope = null; int width = -1; int height = -1; if (parameters != null) { for (GeneralParameterValue param : parameters) { final ReferenceIdentifier name = param.getDescriptor().getName(); if (name.equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName())) { final GridGeometry2D gg = (GridGeometry2D) ((ParameterValue) param) .getValue(); requestedEnvelope = gg.getEnvelope(); // the range high value is the highest pixel included in the raster, // the actual width and height is one more than that width = gg.getGridRange().getHigh(0) + 1; height = gg.getGridRange().getHigh(1) + 1; break; } } } // fill in a reasonable default if we did not manage to get the params if (requestedEnvelope == null) { requestedEnvelope = getOriginalEnvelope(); width = 640; height = (int) Math.round(requestedEnvelope.getSpan(1) / requestedEnvelope.getSpan(0) * 640); } // if the structure did not change reuse the same response if (grid != null && grid.getGridGeometry().getGridRange2D().getWidth() == width && grid.getGridGeometry().getGridRange2D().getHeight() == height && grid.getEnvelope().equals(requestedEnvelope)) return grid; grid = getMap(reference(requestedEnvelope), width, height); return grid; } /** * Execute the GetMap request */ GridCoverage2D getMap(ReferencedEnvelope requestedEnvelope, int width, int height) throws IOException { // build the request ReferencedEnvelope gridEnvelope = initMapRequest(requestedEnvelope, width, height); // issue the request and wrap response in a grid coverage InputStream is = null; try { GetMapResponse response = wms.issueRequest(mapRequest); is = response.getInputStream(); BufferedImage image = ImageIO.read(is); LOGGER.fine("GetMap completed"); return gcf.create(layers.get(0).getTitle(), image, gridEnvelope); } catch (ServiceException e) { throw (IOException) new IOException("GetMap failed").initCause(e); } } /** * Sets up a max request with the provided parameters, making sure it is compatible with * the layers own native SRS list * @param bbox * @param width * @param height * @return * @throws IOException */ ReferencedEnvelope initMapRequest(ReferencedEnvelope bbox, int width, int height) throws IOException { ReferencedEnvelope gridEnvelope = bbox; String requestSrs = srsName; try { // first see if we can cascade the request in its native SRS String code = CRS.lookupIdentifier(bbox.getCoordinateReferenceSystem(), false); if (code != null && validSRS.contains(code)) { requestSrs = code; } else { // first reproject to the map CRS gridEnvelope = bbox.transform(getCrs(), true); // then adjust the form factor if (gridEnvelope.getWidth() < gridEnvelope.getHeight()) { height = (int) Math.round(width * gridEnvelope.getHeight() / gridEnvelope.getWidth()); } else { width = (int) Math.round(height * gridEnvelope.getWidth() / gridEnvelope.getHeight()); } } } catch (Exception e) { throw (IOException) new IOException("Could not reproject the request envelope") .initCause(e); } GetMapRequest mapRequest = wms.createGetMapRequest(); // for some silly reason GetMapRequest will list the layers in the opposite order... List<Layer> reversed = new ArrayList<Layer>(layers); Collections.reverse(reversed); for (Layer layer : reversed) { mapRequest.addLayer(layer); } mapRequest.setDimensions(width, height); mapRequest.setFormat(format); mapRequest.setSRS(requestSrs); mapRequest.setBBox(gridEnvelope); mapRequest.setTransparent(true); try { this.requestCRS = CRS.decode(requestSrs); } catch(Exception e) { throw new IOException("Could not decode request SRS " + requestSrs); } this.mapRequest = mapRequest; this.requestedEnvelope = gridEnvelope; this.width = width; this.height = height; return gridEnvelope; } public Format getFormat() { // this reader has not backing format return null; } /** * Returns the layer bounds * * @return */ public void updateBounds() { ReferencedEnvelope result = reference(layers.get(0).getEnvelope(crs)); for (int i = 1; i < layers.size(); i++) { ReferencedEnvelope layerEnvelope = reference(layers.get(i).getEnvelope(crs)); result.expandToInclude(layerEnvelope); } this.bounds = result; this.originalEnvelope = new GeneralEnvelope(result); } /** * Converts a {@link Envelope} into a {@link ReferencedEnvelope} * * @param envelope * @return */ ReferencedEnvelope reference(Envelope envelope) { ReferencedEnvelope env = new ReferencedEnvelope(envelope.getCoordinateReferenceSystem()); env.expandToInclude(envelope.getMinimum(0), envelope.getMinimum(1)); env.expandToInclude(envelope.getMaximum(0), envelope.getMaximum(1)); return env; } /** * Converts a {@link GeneralEnvelope} into a {@link ReferencedEnvelope} * * @param ge * @return */ ReferencedEnvelope reference(GeneralEnvelope ge) { return new ReferencedEnvelope(ge.getMinimum(0), ge.getMinimum(1), ge.getMaximum(0), ge .getMaximum(1), ge.getCoordinateReferenceSystem()); } } }