/* Copyright (c) 2010 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.geoserver.wms;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.opengis.wfs.FeatureCollectionType;
import net.opengis.wfs.WfsFactory;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.WMSLayerInfo;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.featureinfo.FeatureCollectionDecorator;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
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.Query;
import org.geotools.data.ows.Layer;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.store.FilteringFeatureCollection;
import org.geotools.data.store.ReTypingFeatureCollection;
import org.geotools.data.wms.WebMapServer;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.NameImpl;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.filter.Filters;
import org.geotools.filter.IllegalFilterException;
import org.geotools.filter.function.EnvFunction;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.TransformedDirectPosition;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.WMSLayer;
import org.geotools.parameter.Parameter;
import org.geotools.referencing.CRS;
import org.geotools.referencing.CRS.AxisOrder;
import org.geotools.renderer.lite.MetaBufferEstimator;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.resources.geometry.XRectangle2D;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.util.NullProgressListener;
import org.geotools.util.logging.Logging;
import org.geotools.wfs.v1_0.WFSConfiguration;
import org.geotools.xml.Parser;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Or;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
/**
* WMS GetFeatureInfo operation
*
* @author Gabriel Roldan
*/
public class GetFeatureInfo {
private static final Logger LOGGER = Logging.getLogger(GetFeatureInfo.class);
private WMS wms;
public GetFeatureInfo(final WMS wms) {
this.wms = wms;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public FeatureCollectionType run(final GetFeatureInfoRequest request) throws ServiceException {
List<FeatureCollection> results;
EnvFunction.setLocalValues(request.getGetMapRequest().getEnv());
results = execute(request);
return buildResults(results);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private FeatureCollectionType buildResults(List<FeatureCollection> results) {
FeatureCollectionType result = WfsFactory.eINSTANCE.createFeatureCollectionType();
result.setTimeStamp(Calendar.getInstance());
result.getFeature().addAll(results);
return result;
}
@SuppressWarnings("rawtypes")
private List<FeatureCollection> execute(final GetFeatureInfoRequest request)
throws ServiceException {
// use the layer of the QUERY_LAYERS parameter, not the LAYERS one
List<MapLayerInfo> layers = request.getQueryLayers();
// grab the list of filters from the GetMap request, we don't want
// to return what the user explicitly excluded
List filterList = request.getGetMapRequest().getFilter();
Filter[] filters;
if (filterList != null && filterList.size() > 0) {
filters = (Filter[]) filterList.toArray(new Filter[filterList.size()]);
} else {
filters = new Filter[layers.size()];
}
// grab the list of styles for each query layer, we'll use them to
// auto-evaluate the GetFeatureInfo radius if the user did not specify one
List<Style> getMapStyles = request.getGetMapRequest().getStyles();
Style[] styles = new Style[layers.size()];
for (int i = 0; i < styles.length; i++) {
List<MapLayerInfo> getMapLayers = request.getGetMapRequest().getLayers();
final String targetLayer = layers.get(i).getName();
for (int j = 0; j < getMapLayers.size(); j++) {
if (getMapLayers.get(j).getName().equals(targetLayer)) {
if (getMapStyles != null && getMapStyles.size() > 0)
styles[i] = (Style) getMapStyles.get(j);
if (styles[i] == null)
styles[i] = getMapLayers.get(j).getDefaultStyle();
break;
}
}
}
try {
return execute(request, styles, filters);
} catch (ServiceException se) {
throw se;
} catch (Exception e) {
throw new ServiceException("Internal error occurred", e);
}
}
@SuppressWarnings("rawtypes")
private List<FeatureCollection> execute(GetFeatureInfoRequest request, Style[] styles,
Filter[] filters) throws Exception {
final List<MapLayerInfo> requestedLayers = request.getQueryLayers();
// delegate to subclasses the hard work
final int x = request.getXPixel();
final int y = request.getYPixel();
final int buffer = request.getGetMapRequest().getBuffer();
final List<Map<String, String>> viewParams = request.getGetMapRequest().getViewParams();
final GetMapRequest getMapReq = request.getGetMapRequest();
final CoordinateReferenceSystem requestedCRS = getMapReq.getCrs(); // optional, may be null
// basic information about the request
final int width = getMapReq.getWidth();
final int height = getMapReq.getHeight();
final ReferencedEnvelope bbox = new ReferencedEnvelope(getMapReq.getBbox(),
getMapReq.getCrs());
final double scaleDenominator = RendererUtilities.calculateOGCScale(bbox, width, null);
final List<Object> elevations = request.getGetMapRequest().getElevation();
final List<Object> times = request.getGetMapRequest().getTime();
final FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(GeoTools.getDefaultHints());
List<FeatureCollection> results = new ArrayList<FeatureCollection>(requestedLayers.size());
int maxFeatures = request.getFeatureCount();
for (int i = 0; i < requestedLayers.size(); i++) {
final MapLayerInfo layer = requestedLayers.get(i);
// check cascaded WMS first, it's a special case
if (layer.getType() == MapLayerInfo.TYPE_WMS) {
List<FeatureCollection> cascadedResults;
cascadedResults = handleGetFeatureInfoCascade(request, maxFeatures, layer);
if (cascadedResults != null) {
results.addAll(cascadedResults);
}
continue;
}
final Style style = styles[i];
// ok, internally rendered layer then, we check the style to see what's active
final List<Rule> rules = getActiveRules(style, scaleDenominator);
if (rules.size() == 0) {
continue;
}
FeatureCollection collection = null;
if (layer.getType() == MapLayerInfo.TYPE_VECTOR) {
final Map<String, String> viewParam = viewParams != null ? viewParams.get(i) : null;
collection = identifyVectorLayer(filters, x, y, buffer, viewParam,
requestedCRS, width, height, bbox, ff, results, i, layer, rules, maxFeatures,
times, elevations);
} else if (layer.getType() == MapLayerInfo.TYPE_RASTER) {
final CoverageInfo cinfo = requestedLayers.get(i).getCoverage();
final AbstractGridCoverage2DReader reader = (AbstractGridCoverage2DReader) cinfo
.getGridCoverageReader(new NullProgressListener(),
GeoTools.getDefaultHints());
// get the original grid geometry
final GridGeometry2D coverageGeometry = (GridGeometry2D) cinfo.getGrid();
// set the requested position in model space for this request
final Coordinate middle = pixelToWorld(x, y, bbox, width, height);
DirectPosition position = new DirectPosition2D(requestedCRS, middle.x, middle.y);
// change from request crs to coverage crs in order to compute a minimal request
// area,
// TODO this code need to be made much more robust
if (requestedCRS != null) {
final CoordinateReferenceSystem targetCRS = coverageGeometry
.getCoordinateReferenceSystem();
final TransformedDirectPosition arbitraryToInternal = new TransformedDirectPosition(
requestedCRS, targetCRS, new Hints(Hints.LENIENT_DATUM_SHIFT,
Boolean.TRUE));
try {
arbitraryToInternal.transform(position);
} catch (TransformException exception) {
throw new CannotEvaluateException("Unable to answer the geatfeatureinfo",
exception);
}
position = arbitraryToInternal;
}
// check that the provided point is inside the bbox for this coverage
if (!reader.getOriginalEnvelope().contains(position)) {
continue;
}
// read from the request
GeneralParameterValue[] parameters = wms.getWMSReadParameters(request.getGetMapRequest(),
requestedLayers.get(i), filters[i], times, elevations, reader, true);
collection = identifyRasterLayer(reader, position, parameters, cinfo, getMapReq);
} else {
LOGGER.log(Level.SEVERE,
"Can't perform feature info " + "requests on " + layer.getName()
+ ", layer type not supported");
}
if (collection != null) {
if (! (collection instanceof SimpleFeatureCollection)) {
//put wrapper around it with layer name
Name name = new NameImpl (layer.getFeature().getNamespace().getName(), layer.getFeature().getName());
collection = new FeatureCollectionDecorator(name, collection);
}
int size = collection.size();
if(size != 0) {
results.add(collection);
// don't return more than FEATURE_COUNT
maxFeatures -= size;
if(maxFeatures <= 0) {
break;
}
}
}
}
return results;
}
@SuppressWarnings("rawtypes")
private FeatureCollection identifyRasterLayer(AbstractGridCoverage2DReader reader,
DirectPosition position, GeneralParameterValue[] parameters, CoverageInfo cinfo,
GetMapRequest getMapReq) throws Exception {
// now get the position in raster space using the world to grid related to
// corner
final MathTransform worldToGrid = reader.getOriginalGridToWorld(PixelInCell.CELL_CORNER)
.inverse();
final DirectPosition rasterMid = worldToGrid.transform(position, null);
// create a 20X20 rectangle aruond the mid point and then intersect with the
// original range
final Rectangle2D.Double rasterArea = new Rectangle2D.Double();
rasterArea.setFrameFromCenter(rasterMid.getOrdinate(0), rasterMid.getOrdinate(1),
rasterMid.getOrdinate(0) + 10, rasterMid.getOrdinate(1) + 10);
final Rectangle integerRasterArea = rasterArea.getBounds();
final GridEnvelope gridEnvelope = reader.getOriginalGridRange();
final Rectangle originalArea = (gridEnvelope instanceof GridEnvelope2D) ? (GridEnvelope2D) gridEnvelope
: new Rectangle();
XRectangle2D.intersect(integerRasterArea, originalArea, integerRasterArea);
// paranoiac check, did we fall outside the coverage raster area? This should
// never really happne if the request is well formed.
if (integerRasterArea.isEmpty()) {
return null;
}
// now set the grid geometry for this request
for (int k = 0; k < parameters.length; k++) {
if (!(parameters[k] instanceof Parameter<?>))
continue;
final Parameter<?> parameter = (Parameter<?>) parameters[k];
if (parameter.getDescriptor().getName()
.equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName())) {
//
// create a suitable geometry for this request reusing the getmap (we
// could probably optimize)
//
parameter.setValue(new GridGeometry2D(new GridEnvelope2D(integerRasterArea), reader
.getOriginalGridToWorld(PixelInCell.CELL_CENTER), reader.getCrs()));
}
}
final GridCoverage2D coverage = (GridCoverage2D) reader.read(parameters);
if (coverage == null) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Unable to load raster data for this request.");
return null;
}
FeatureCollection pixel = null;
try {
final double[] pixelValues = coverage.evaluate(position, (double[]) null);
pixel = wrapPixelInFeatureCollection(coverage, pixelValues, cinfo.getQualifiedName());
} catch (PointOutsideCoverageException e) {
// it's fine, users might legitimately query point outside, we just don't
// return anything
}
return pixel;
}
@SuppressWarnings("rawtypes")
private FeatureCollection identifyVectorLayer(Filter[] filters,
final int x, final int y, final int buffer, final Map<String, String> viewParams,
final CoordinateReferenceSystem requestedCRS, final int width, final int height,
final ReferencedEnvelope bbox, final FilterFactory2 ff,
List<FeatureCollection> results, int i, final MapLayerInfo layer, final List<Rule> rules,
final int maxFeatures, List<Object> times, List<Object> elevations)
throws IOException {
CoordinateReferenceSystem dataCRS = layer.getCoordinateReferenceSystem();
// compute the request radius
double radius;
if (buffer <= 0) {
Integer layerBuffer = null;
final LayerInfo layerInfo = layer.getLayerInfo();
if (layerInfo != null) { // it is a local layer
layerBuffer = layerInfo.getMetadata().get(LayerInfo.BUFFER, Integer.class);
}
if (layerBuffer != null && layerBuffer > 0) {
radius = layerBuffer / 2.0;
} else {
// estimate the radius given the currently active rules
MetaBufferEstimator estimator = new MetaBufferEstimator();
for (Rule rule : rules) {
rule.accept(estimator);
}
if (estimator.getBuffer() < 6.0 || !estimator.isEstimateAccurate()) {
radius = 3.0;
} else {
radius = estimator.getBuffer() / 2.0;
}
}
} else {
radius = buffer;
}
// make sure we don't go overboard, the admin might have set a maximum
int maxRadius = wms.getMaxBuffer();
if (maxRadius > 0 && radius > maxRadius)
radius = maxRadius;
Polygon pixelRect = getEnvelopeFilter(x, y, width, height, bbox, radius);
if ((requestedCRS != null) && !CRS.equalsIgnoreMetadata(dataCRS, requestedCRS)) {
try {
MathTransform transform = CRS.findMathTransform(requestedCRS, dataCRS, true);
pixelRect = (Polygon) JTS.transform(pixelRect, transform); // reprojected
} catch (MismatchedDimensionException e) {
LOGGER.severe(e.getLocalizedMessage());
} catch (TransformException e) {
LOGGER.severe(e.getLocalizedMessage());
} catch (FactoryException e) {
LOGGER.severe(e.getLocalizedMessage());
}
}
final FeatureSource<? extends FeatureType, ? extends Feature> featureSource;
featureSource = layer.getFeatureSource(false);
FeatureType schema = featureSource.getSchema();
Filter getFInfoFilter = null;
try {
GeometryDescriptor geometryDescriptor = schema.getGeometryDescriptor();
String localName = geometryDescriptor.getLocalName();
getFInfoFilter = ff.intersects(ff.property(localName), ff.literal(pixelRect));
} catch (IllegalFilterException e) {
e.printStackTrace();
throw new ServiceException("Internal error : " + e.getMessage(), e);
}
// include the eventual layer definition filter
if (filters[i] != null) {
getFInfoFilter = ff.and(getFInfoFilter, filters[i]);
}
// see if we can include the rule filters as well, if too many we'll do them in
// memory
Filter postFilter = Filter.INCLUDE;
Filter rulesFilters = buildRulesFilter(ff, rules);
if (!(rulesFilters instanceof Or)
|| (rulesFilters instanceof Or && ((Or) rulesFilters).getChildren().size() <= 20)) {
getFInfoFilter = ff.and(getFInfoFilter, rulesFilters);
} else {
postFilter = rulesFilters;
}
// handle time/elevation
Filter timeElevationFilter = wms.getTimeElevationToFilter(times, elevations, layer.getFeature());
getFInfoFilter = Filters.and(ff, getFInfoFilter, timeElevationFilter);
// simplify the filter
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
getFInfoFilter = (Filter) getFInfoFilter.accept(simplifier, null);
// build the query
String typeName = schema.getName().getLocalPart();
Query q = new Query(typeName, null, getFInfoFilter, maxFeatures,
Query.ALL_NAMES, null);
// handle sql view params
if (viewParams != null && viewParams.size() > 0) {
q.setHints(new Hints(Hints.VIRTUAL_TABLE_PARAMETERS, viewParams));
}
FeatureCollection<? extends FeatureType, ? extends Feature> match;
match = featureSource.getFeatures(q);
// if we could not include the rules filter into the query, post process in
// memory
if (!Filter.INCLUDE.equals(postFilter)) {
match = DataUtilities.simple(new FilteringFeatureCollection(match, postFilter));
}
// this was crashing Gml2FeatureResponseDelegate due to not setting
// the featureresults, thus not being able of querying the SRS
// if (match.getCount() > 0) {
return match;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private List<FeatureCollection> handleGetFeatureInfoCascade(GetFeatureInfoRequest request,
int maxFeatures,
MapLayerInfo layerInfo) throws Exception {
final int x = request.getXPixel();
final int y = request.getYPixel();
WMSLayerInfo info = (WMSLayerInfo) layerInfo.getResource();
WebMapServer wms = info.getStore().getWebMapServer(null);
Layer layer = info.getWMSLayer(null);
CoordinateReferenceSystem crs = request.getGetMapRequest().getCrs();
if(crs == null) {
// use the native one
crs = info.getCRS();
}
ReferencedEnvelope bbox = new ReferencedEnvelope(request.getGetMapRequest().getBbox(), crs);
int width = request.getGetMapRequest().getWidth();
int height = request.getGetMapRequest().getHeight();
// we can cascade GetFeatureInfo on queryable layers and if the GML mime type is supported
if (!layer.isQueryable()) {
return null;
}
List<String> infoFormats;
infoFormats = wms.getCapabilities().getRequest().getGetFeatureInfo().getFormats();
if (!infoFormats.contains("application/vnd.ogc.gml")) {
return null;
}
// the wms layer does request in a CRS that's compatible with the WMS server srs
// list,
// we may need to transform
WMSLayer ml = new WMSLayer(wms, layer);
// delegate to the web map layer as there's quite a bit of reprojection magic
// code
// that we want to be consistently reproduced for GetFeatureInfo as well
final InputStream is = ml.getFeatureInfo(bbox, width, height, x, y,
"application/vnd.ogc.gml", maxFeatures);
List<FeatureCollection> results = null;
try {
Parser parser = new Parser(new WFSConfiguration());
parser.setStrict(false);
Object result = parser.parse(is);
if (result instanceof FeatureCollectionType) {
FeatureCollectionType fcList = (FeatureCollectionType) result;
results = fcList.getFeature();
List<FeatureCollection> retypedResults =
new ArrayList<FeatureCollection>(results.size());
// retyping feature collections to replace name and namespace
// from cascading server with our local WMSLayerInfo
for (Iterator it = results.iterator(); it.hasNext();) {
SimpleFeatureCollection fc = (SimpleFeatureCollection) it.next();
SimpleFeatureType ft = fc.getSchema();
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.init(ft);
builder.setName(info.getName());
builder.setNamespaceURI(info.getNamespace().getURI());
FeatureCollection rfc =
new ReTypingFeatureCollection(fc, builder.buildFeatureType());
retypedResults.add(rfc);
}
results = retypedResults;
}
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "Tried to parse GML2 response, but failed", t);
} finally {
is.close();
}
return results;
}
private Filter buildRulesFilter(org.opengis.filter.FilterFactory ff, List<Rule> rules) {
// build up a or of all the rule filters
List<Filter> filters = new ArrayList<Filter>();
for (Rule rule : rules) {
if (rule.getFilter() == null || rule.isElseFilter())
return Filter.INCLUDE;
filters.add(rule.getFilter());
}
// not or and and simplify (if there is any include/exclude we'll get
// a very simple result ;-)
Filter or = ff.or(filters);
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
return (Filter) or.accept(simplifier, null);
}
/**
* Selects the rules active at this zoom level
*
* @param style
* @param scaleDenominator
* @return
*/
private List<Rule> getActiveRules(Style style, double scaleDenominator) {
List<Rule> result = new ArrayList<Rule>();
for (FeatureTypeStyle fts : style.getFeatureTypeStyles()) {
for (Rule r : fts.rules()) {
if ((r.getMinScaleDenominator() <= scaleDenominator)
&& (r.getMaxScaleDenominator() > scaleDenominator)) {
result.add(r);
}
}
}
return result;
}
private Polygon getEnvelopeFilter(int x, int y, int width, int height, ReferencedEnvelope bbox,
double radius) {
Coordinate upperLeft = pixelToWorld(x - radius, y - radius, bbox, width, height);
Coordinate lowerRight = pixelToWorld(x + radius, y + radius, bbox, width, height);
Coordinate[] coords = new Coordinate[5];
coords[0] = upperLeft;
coords[1] = new Coordinate(lowerRight.x, upperLeft.y);
coords[2] = lowerRight;
coords[3] = new Coordinate(upperLeft.x, lowerRight.y);
coords[4] = coords[0];
GeometryFactory geomFac = new GeometryFactory();
LinearRing boundary = geomFac.createLinearRing(coords); // this needs to be done with each
// FT so it can be reprojected
Polygon pixelRect = geomFac.createPolygon(boundary, null);
return pixelRect;
}
private SimpleFeatureCollection wrapPixelInFeatureCollection(GridCoverage2D coverage,
double[] pixelValues, Name coverageName) throws SchemaException {
GridSampleDimension[] sampleDimensions = coverage.getSampleDimensions();
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName(coverageName);
final Set<String> bandNames = new HashSet<String>();
for (int i = 0; i < sampleDimensions.length; i++) {
String name = sampleDimensions[i].getDescription().toString();
// GEOS-2518
if (bandNames.contains(name))
// it might happen again that the name already exists but it pretty difficult I'd
// say
name = new StringBuilder(name).append("_Band").append(i).toString();
bandNames.add(name);
builder.add(name, Double.class);
}
SimpleFeatureType gridType = builder.buildFeatureType();
Double[] values = new Double[pixelValues.length];
for (int i = 0; i < values.length; i++) {
values[i] = new Double(pixelValues[i]);
}
return DataUtilities.collection(SimpleFeatureBuilder.build(gridType, values, ""));
}
/**
* Converts a coordinate expressed on the device space back to real world coordinates. Stolen
* from LiteRenderer but without the need of a Graphics object
*
* @param x
* horizontal coordinate on device space
* @param y
* vertical coordinate on device space
* @param map
* The map extent
* @param width
* image width
* @param height
* image height
*
* @return The correspondent real world coordinate
*
* @throws RuntimeException
*/
private Coordinate pixelToWorld(double x, double y, ReferencedEnvelope map, double width, double height) {
// set up the affine transform and calculate scale values
AffineTransform at = worldToScreenTransform(map, width, height);
Point2D result = null;
try {
result = at.inverseTransform(new java.awt.geom.Point2D.Double(x, y),
new java.awt.geom.Point2D.Double());
} catch (NoninvertibleTransformException e) {
throw new RuntimeException(e);
}
Coordinate c = new Coordinate(result.getX(), result.getY());
return c;
}
/**
* Sets up the affine transform. Stolen from liteRenderer code.
*
* @param mapExtent
* the map extent
* @param width
* the screen size
* @param height
*
* @return a transform that maps from real world coordinates to the screen
*/
private AffineTransform worldToScreenTransform(ReferencedEnvelope mapExtent, double width, double height) {
//the transformation depends on an x/y ordering, if we have a lat/lon crs swap it
CoordinateReferenceSystem crs = mapExtent.getCoordinateReferenceSystem();
boolean swap = crs != null && CRS.getAxisOrder(crs) == AxisOrder.NORTH_EAST;
if (swap) {
mapExtent = new ReferencedEnvelope(mapExtent.getMinY(), mapExtent.getMaxY(),
mapExtent.getMinX(), mapExtent.getMaxX(), null);
}
double scaleX = (double) width / mapExtent.getWidth();
double scaleY = (double) height / mapExtent.getHeight();
double tx = -mapExtent.getMinX() * scaleX;
double ty = (mapExtent.getMinY() * scaleY) + height;
AffineTransform at = new AffineTransform(scaleX, 0.0d, 0.0d, -scaleY, tx, ty);
//if we swapped concatenate a transform that swaps back
if (swap) {
at.concatenate(new AffineTransform(0, 1, 1, 0, 0, 0));
}
return at;
}
}