diff --git a/doc/en/user/source/services/wms/img/service_WMS_disableFeaturesReprojection.png b/doc/en/user/source/services/wms/img/service_WMS_disableFeaturesReprojection.png new file mode 100644 index 00000000000..3f2965dc88a Binary files /dev/null and b/doc/en/user/source/services/wms/img/service_WMS_disableFeaturesReprojection.png differ diff --git a/doc/en/user/source/services/wms/webadmin.rst b/doc/en/user/source/services/wms/webadmin.rst index e2870c7913b..74fa84690df 100644 --- a/doc/en/user/source/services/wms/webadmin.rst +++ b/doc/en/user/source/services/wms/webadmin.rst @@ -126,8 +126,12 @@ The usage of dynamic styling can be restricted on a global or per virtual servic .. figure:: img/service_WMS_disableDynamicStyling.png When the flag is checked, a GetMap/GetFeatureInfo request with a dynamic style will result in a service exception reporting the error. + +Disabling GetFeatureInfo requests results reprojection +------------------------------------------------------ + +By default GetFeatureInfo results are reproject to the map coordinate reference system. This behavior can be deactivated on a global or per virtual service basis in the **GetFeatureInfo results reprojection** section. + +.. figure:: img/service_WMS_disableFeaturesReprojection.png - - - - +When the flag is checked, GetFeatureInfo requests results will not be reprojected and will instead used the layer coordinate reference system. diff --git a/src/extension/app-schema/app-schema-test/src/test/java/org/geoserver/test/WmsGetFeatureInfoTest.java b/src/extension/app-schema/app-schema-test/src/test/java/org/geoserver/test/WmsGetFeatureInfoTest.java index 86447d0fcd4..bed66f6e4b9 100755 --- a/src/extension/app-schema/app-schema-test/src/test/java/org/geoserver/test/WmsGetFeatureInfoTest.java +++ b/src/extension/app-schema/app-schema-test/src/test/java/org/geoserver/test/WmsGetFeatureInfoTest.java @@ -7,6 +7,8 @@ import static org.junit.Assert.*; +import org.geoserver.wms.WMSInfo; +import org.junit.Before; import org.junit.Test; import org.geoserver.test.NamespaceTestData; @@ -18,6 +20,14 @@ public WmsGetFeatureInfoTest() throws Exception { super(); } + @Before + public void setupAdvancedProjectionHandling() { + // make sure GetFeatureInfo is not deactivated (this will only update the global service) + WMSInfo wms = getGeoServer().getService(WMSInfo.class); + wms.setFeaturesReprojectionDisabled(false); + getGeoServer().save(wms); + } + @Override protected WmsSupportMockData createTestData() { WmsSupportMockData mockData = new WmsSupportMockData(); @@ -69,7 +79,21 @@ public void testGetFeatureInfoGMLReprojection() throws Exception { "gu.25678", "/wfs:FeatureCollection/gml:featureMember/gsml:MappedFeature/gsml:specification/gsml:GeologicUnit/@gml:id", doc); + // check that features coordinates where reprojected to EPSG:3857 + assertXpathMatches(".*3857", + "/wfs:FeatureCollection/gml:featureMember/gsml:MappedFeature[@gml:id='mf2']/gsml:shape/gml:Polygon/@srsName", + doc); validateGet(request); + // disable features reprojection + WMSInfo wms = getGeoServer().getService(WMSInfo.class); + wms.setFeaturesReprojectionDisabled(true); + getGeoServer().save(wms); + // execute the request + doc = getAsDOM(request); + // check that features were not reprojected and still in EPSG:4326 + assertXpathMatches(".*4326", + "/wfs:FeatureCollection/gml:featureMember/gsml:MappedFeature[@gml:id='mf2']/gsml:shape/gml:Polygon/@srsName", + doc); } @Test diff --git a/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.html b/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.html index 8fbf8980a34..90cd4c15e18 100644 --- a/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.html +++ b/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.html @@ -238,6 +238,21 @@ +
  • +
    + + +
    +
  • diff --git a/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.java b/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.java index a9b82e24ab2..fd7e3e4bca9 100644 --- a/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.java +++ b/src/web/wms/src/main/java/org/geoserver/wms/web/WMSAdminPage.java @@ -236,7 +236,9 @@ public void onClick(AjaxRequestTarget target) { //dynamicStylingDisabled form.add(new CheckBox("dynamicStyling.disabled",new PropertyModel(info, WMS.DYNAMIC_STYLING_DISABLED))); - + + // disable the reprojection of GetFeatureInfo results + form.add(new CheckBox("disableFeaturesReproject", new PropertyModel<>(info, WMS.FEATURES_REPROJECTION_DISABLED))); } @Override diff --git a/src/web/wms/src/main/resources/GeoServerApplication.properties b/src/web/wms/src/main/resources/GeoServerApplication.properties index 5fe5e5f45e2..87dc76d9251 100644 --- a/src/web/wms/src/main/resources/GeoServerApplication.properties +++ b/src/web/wms/src/main/resources/GeoServerApplication.properties @@ -133,6 +133,8 @@ WMSAdminPage.aph.enabled = Enable advanced projection handling WMSAdminPage.aph.wrap = Enable continuous map wrapping WMSAdminPage.dynamicStylingDisabledTitle = Dynamic styling WMSAdminPage.dynamicStylingDisabled = Disable usage of SLD and SLD_BODY parameters in GET requests and user styles in POST requests +WMSAdminPage.disableFeaturesReprojectTitle = GetFeatureInfo results reprojection +WMSAdminPage.disableFeaturesReproject = Disable the reprojection of GetFeatureInfo results MimeTypesFormComponent.selectedHeader =Allowed MIME types MimeTypesFormComponent.availableHeader =Available MIME types diff --git a/src/web/wms/src/main/resources/GeoServerApplication_fr.properties b/src/web/wms/src/main/resources/GeoServerApplication_fr.properties index bff4eda3f79..2f4654f546b 100644 --- a/src/web/wms/src/main/resources/GeoServerApplication_fr.properties +++ b/src/web/wms/src/main/resources/GeoServerApplication_fr.properties @@ -98,6 +98,8 @@ WMSAdminPage.scalehintOptions = Scalehint (WMS 1.1.1 seulement) WMSAdminPage.scalehintUnitsPixel = Présenter le Scalehint en unités par diagonale de pixel dans la réponse GetCapabilities WMSAdminPage.getFeatureInfoMimeTypes= Types MIME autorisés pour une requête GetFeatureInfo WMSAdminPage.getMapMimeTypes = Types MIME autorisés pour une requête GetMap +WMSAdminPage.disableFeaturesReprojectTitle = Reprojection des résultats des requêtes GetFeatureInfo +WMSAdminPage.disableFeaturesReproject = Désactiver la reprojection des résultats des requêtes GetFeatureInfo MimeTypesFormComponent.selectedHeader =Types MIME autorisés MimeTypesFormComponent.availableHeader =Types MIME disponibles diff --git a/src/web/wms/src/main/resources/GeoServerApplication_pt_BR.properties b/src/web/wms/src/main/resources/GeoServerApplication_pt_BR.properties index 82e699250b8..622ada94edd 100644 --- a/src/web/wms/src/main/resources/GeoServerApplication_pt_BR.properties +++ b/src/web/wms/src/main/resources/GeoServerApplication_pt_BR.properties @@ -102,6 +102,8 @@ WMSAdminPage.scalehintUnitsPixel = Show the Scalehint as units per diagonal pixe # MimeTypesFormComponent.selectedHeader =Allowed MIME types # MimeTypesFormComponent.availableHeader =Available MIME types # MimeTypesFormComponent.mimeTypeCheckingEnabled =Enable MIME type checking +WMSAdminPage.disableFeaturesReprojectTitle = Reprojecção dos resultados de pedidos GetFeatureInfo +WMSAdminPage.disableFeaturesReproject = Desativa a reprojecção dos resultados de pedidos GetFeatureInfo WMSLayerConfig.additionalStyles = Estilos adicionais WMSLayerConfig.defaultStyle = Estilo padrão diff --git a/src/web/wms/src/test/java/org/geoserver/wms/web/data/WMSAdminPageTest.java b/src/web/wms/src/test/java/org/geoserver/wms/web/data/WMSAdminPageTest.java index 754fe2d61d3..202fcc83955 100644 --- a/src/web/wms/src/test/java/org/geoserver/wms/web/data/WMSAdminPageTest.java +++ b/src/web/wms/src/test/java/org/geoserver/wms/web/data/WMSAdminPageTest.java @@ -16,6 +16,7 @@ import org.apache.wicket.util.tester.FormTester; import org.geoserver.web.GeoServerHomePage; import org.geoserver.web.GeoServerWicketTestSupport; +import org.geoserver.wms.WMS; import org.geoserver.wms.WMSInfo; import org.geoserver.wms.web.WMSAdminPage; import org.junit.Before; @@ -89,4 +90,14 @@ public void testDynamicStylingDisabled() throws Exception { ft.submit("submit"); assertTrue(wms.isDynamicStylingDisabled()); } + + @Test + public void testFeaturesReprojectionDisabled() throws Exception { + assertFalse(wms.isFeaturesReprojectionDisabled()); + tester.startPage(WMSAdminPage.class); + FormTester ft = tester.newFormTester("form"); + ft.setValue("disableFeaturesReproject", true); + ft.submit("submit"); + assertTrue(wms.isFeaturesReprojectionDisabled()); + } } diff --git a/src/wms/src/main/java/applicationContext.xml b/src/wms/src/main/java/applicationContext.xml index ecc63dcec8c..953b2fcce6a 100644 --- a/src/wms/src/main/java/applicationContext.xml +++ b/src/wms/src/main/java/applicationContext.xml @@ -736,6 +736,7 @@ + diff --git a/src/wms/src/main/java/org/geoserver/wms/WMS.java b/src/wms/src/main/java/org/geoserver/wms/WMS.java index bbf6f65b5e4..aee178fa219 100644 --- a/src/wms/src/main/java/org/geoserver/wms/WMS.java +++ b/src/wms/src/main/java/org/geoserver/wms/WMS.java @@ -138,6 +138,8 @@ public class WMS implements ApplicationContextAware { public static final String DYNAMIC_STYLING_DISABLED = "dynamicStylingDisabled"; + public static final String FEATURES_REPROJECTION_DISABLED = "featuresReprojectionDisabled"; + static final Logger LOGGER = Logging.getLogger(WMS.class); public static final String WEB_CONTAINER_KEY = "WMS"; @@ -364,6 +366,16 @@ public boolean isDynamicStylingDisabled() { return getServiceInfo().isDynamicStylingDisabled(); } + /** + * If TRUE is returned GetFeatureInfo results should NOT be reproject + * to the map coordinate reference system. + * + * @return GetFeatureInfo results reprojection allowance + */ + public boolean isFeaturesReprojectionDisabled() { + return getServiceInfo().isFeaturesReprojectionDisabled(); + } + public JAIInfo.PngEncoderType getPNGEncoderType() { JAIInfo jaiInfo = getJaiInfo(); return jaiInfo.getPngEncoderType(); diff --git a/src/wms/src/main/java/org/geoserver/wms/WMSInfo.java b/src/wms/src/main/java/org/geoserver/wms/WMSInfo.java index 334e160b62b..4a975272afe 100644 --- a/src/wms/src/main/java/org/geoserver/wms/WMSInfo.java +++ b/src/wms/src/main/java/org/geoserver/wms/WMSInfo.java @@ -158,4 +158,24 @@ enum WMSInterpolation { * @return the status of dynamic styling (SLD and SLD_BODY params) allowance */ Boolean isDynamicStylingDisabled(); + + /** + * If set to TRUE GetFeatureInfo results will NOT be reprojected. + * + * @param featuresReprojectionDisabled features reprojection allowance + */ + default void setFeaturesReprojectionDisabled(boolean featuresReprojectionDisabled) { + // if not implemented nothing is done + } + + /** + * Flag that controls if GetFeatureInfo results should NOT be reprojected to the map + * coordinate reference system. + * + * @return GetFeatureInfo features reprojection allowance + */ + default boolean isFeaturesReprojectionDisabled() { + // deactivate features reprojection by default + return true; + } } diff --git a/src/wms/src/main/java/org/geoserver/wms/WMSInfoImpl.java b/src/wms/src/main/java/org/geoserver/wms/WMSInfoImpl.java index c2d129d7431..e53e1470e72 100644 --- a/src/wms/src/main/java/org/geoserver/wms/WMSInfoImpl.java +++ b/src/wms/src/main/java/org/geoserver/wms/WMSInfoImpl.java @@ -32,7 +32,9 @@ public class WMSInfoImpl extends ServiceInfoImpl implements WMSInfo { Set getMapMimeTypes = new HashSet(); boolean dynamicStylingDisabled; - + + // GetFeatureInfo result are reprojected by default + private boolean featuresReprojectionDisabled = false; /** * This property is transient in 2.1.x series and stored under the metadata map with key @@ -203,4 +205,14 @@ public void setDynamicStylingDisabled(Boolean dynamicStylingDisabled) { public Boolean isDynamicStylingDisabled() { return dynamicStylingDisabled; } + + @Override + public boolean isFeaturesReprojectionDisabled() { + return featuresReprojectionDisabled; + } + + @Override + public void setFeaturesReprojectionDisabled(boolean featuresReprojectionDisabled) { + this.featuresReprojectionDisabled = featuresReprojectionDisabled; + } } diff --git a/src/wms/src/main/java/org/geoserver/wms/featureinfo/LayerIdentifierUtils.java b/src/wms/src/main/java/org/geoserver/wms/featureinfo/LayerIdentifierUtils.java new file mode 100644 index 00000000000..50fd74c2b44 --- /dev/null +++ b/src/wms/src/main/java/org/geoserver/wms/featureinfo/LayerIdentifierUtils.java @@ -0,0 +1,151 @@ +/* (c) 2017 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.wms.featureinfo; + +import com.vividsolutions.jts.geom.Geometry; +import org.geotools.data.crs.ReprojectFeatureResults; +import org.geotools.data.simple.SimpleFeatureCollection; +import org.geotools.feature.FeatureCollection; +import org.geotools.feature.FeatureIterator; +import org.geotools.referencing.CRS; +import org.geotools.util.logging.Logging; +import org.opengis.feature.Feature; +import org.opengis.feature.simple.SimpleFeature; +import org.opengis.feature.type.GeometryDescriptor; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Contain helpers methods needed by layers identifiers. + */ +public final class LayerIdentifierUtils { + + private static final Logger LOGGER = Logging.getLogger(LayerIdentifierUtils.class); + + private LayerIdentifierUtils() { + } + + /** + * Helper method that tries to reproject each feature collection to the target CRS. + * Complex features collections will not be reprojected. + * + * @param featureCollections feature collections to reprojected, should NOT be NULL + * @param targetCrs reprojection target CRS, can be NULL + * @return feature collections, some may not have been reprojected + */ + @SuppressWarnings("unchecked") + public static List reproject(List featureCollections, + CoordinateReferenceSystem targetCrs) { + if (targetCrs == null) { + // nothing to do + return featureCollections; + } + // try to reproject features collections to the target CRS + return featureCollections.stream() + .map(featureCollection -> reproject(featureCollection, targetCrs)) + .collect(Collectors.toList()); + } + + /** + * Helper method that reprojects a feature collection to the target CRS. If the provided + * feature collection doesn't contain simple features or if the source CRS is equal to + * the target CRS nothing will be done. + * + * @param featureCollection feature collection to be reprojected, should NOT be NULL + * @param targetCrs reprojection target CRS, can be NULL + * @return feature collection, it may be reprojected or not + */ + @SuppressWarnings("unchecked") + public static FeatureCollection reproject(FeatureCollection featureCollection, + CoordinateReferenceSystem targetCrs) { + if (targetCrs == null) { + // nothing to do + return featureCollection; + } + if (!(featureCollection instanceof SimpleFeatureCollection)) { + // not able to reproject complex features collection + LOGGER.warning("Complex feature collection will not be reprojected."); + return featureCollection; + } + // get feature collection CRS + CoordinateReferenceSystem sourceCrs = featureCollection.getSchema().getCoordinateReferenceSystem(); + if (sourceCrs == null) { + // reprojector requires the source CRS to be defined + return featureCollection; + } + if (!CRS.equalsIgnoreMetadata(sourceCrs, targetCrs)) { + try { + // reproject to to the target CRS + return new ReprojectFeatureResults(featureCollection, targetCrs); + } catch (Exception exception) { + throw new RuntimeException(String.format( + "Error reproject feature collection from SRS '%s' to SRS '%s'.", + CRS.toSRS(sourceCrs), CRS.toSRS(targetCrs)), exception); + } + } + // the target CRS and the source CRS are the same, so nothing to do + return featureCollection; + } + + /** + * Helper method that tries to find feature collection CRS. First we try to use schema + * defined CRS then we try to find a common CRS among simple features default geometries. + * If this is not a simple feature collection or if no common CRS can be found (i.e. we + * have geometries with different CRS) NULL will be returned. + * + * @param featureCollection feature collection, should NOT be NULL + * @return the found CRS, may be NULL + */ + public static CoordinateReferenceSystem getCrs(FeatureCollection featureCollection) { + CoordinateReferenceSystem crs = featureCollection.getSchema().getCoordinateReferenceSystem(); + if (crs != null || featureCollection.isEmpty()) { + // the feature collection has a defined CRS or the feature collection is empty + return crs; + } + // try to extract the CRS from the geometry descriptor (normally it should be NULL too) + GeometryDescriptor geometryDescriptor = featureCollection.getSchema().getGeometryDescriptor(); + crs = geometryDescriptor == null ? null : geometryDescriptor.getCoordinateReferenceSystem(); + if (crs != null) { + // the geometry descriptor has a defined CRS + return crs; + } + // iterate over features and find the common CRS + try (FeatureIterator iterator = featureCollection.features()) { + while (iterator.hasNext()) { + Feature feature = iterator.next(); + if (!(feature instanceof SimpleFeature)) { + // not a simple feature, we are done + return null; + } + SimpleFeature simpleFeature = (SimpleFeature) feature; + Object object = simpleFeature.getDefaultGeometry(); + if (!(object instanceof Geometry)) { + // current feature doesn't have a geometry, move to the next one + continue; + } + // the user data may contain the coordinate reference system + Geometry geometry = (Geometry) object; + Object userData = geometry.getUserData(); + if (!(userData instanceof CoordinateReferenceSystem)) { + // no user data available or doesn't contain a coordinate reference system + return null; + } + CoordinateReferenceSystem geometryCrs = (CoordinateReferenceSystem) userData; + if (crs != null && !CRS.equalsIgnoreMetadata(crs, geometryCrs)) { + // this geometry CRS is different from the other ones, we are done + return null; + } + // store the found CRS + crs = geometryCrs; + } + } + // the found common CRS among geometries, or NULL if no geometries are defined + return crs; + } +} diff --git a/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorBasicLayerIdentifier.java b/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorBasicLayerIdentifier.java index f94adf53bfc..72f329dbb29 100644 --- a/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorBasicLayerIdentifier.java +++ b/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorBasicLayerIdentifier.java @@ -147,6 +147,11 @@ public List identify(FeatureInfoRequestParameters params, int FeatureCollection match; LOGGER.log(Level.FINE, q.toString()); + // let's see if we need to reproject + if (!wms.isFeaturesReprojectionDisabled()) { + // reproject the features to the request CRS, this way complex feature will also be reprojected + q.setCoordinateSystemReproject(requestedCRS); + } match = featureSource.getFeatures(q); // if we could not include the rules filter into the query, post process in diff --git a/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorRenderingLayerIdentifier.java b/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorRenderingLayerIdentifier.java index bd300a33b69..96d0ca5cef5 100644 --- a/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorRenderingLayerIdentifier.java +++ b/src/wms/src/main/java/org/geoserver/wms/featureinfo/VectorRenderingLayerIdentifier.java @@ -78,6 +78,7 @@ import org.opengis.filter.FilterFactory2; import org.opengis.filter.spatial.BBOX; import org.opengis.referencing.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Envelope; @@ -203,7 +204,8 @@ public List identify(FeatureInfoRequestParameters params, rim.produceMap(mc); List features = featureInfoListener.getFeatures(); - return aggregateByFeatureType(features); + + return aggregateByFeatureType(features, params.getRequestedCRS()); } finally { mc.dispose(); } @@ -287,7 +289,7 @@ private Style preprocessStyle(Style style, FeatureType schema) { return result; } - private List aggregateByFeatureType(List features) { + private List aggregateByFeatureType(List features, CoordinateReferenceSystem targetcrs) { // group by feature type (rendering transformations might cause us to get more // than one type from the original layer) Map> map = new HashMap>(); @@ -312,7 +314,14 @@ private List aggregateByFeatureType(List f result.add(new ListComplexFeatureCollection(type, list)); } } - + + // let's see if we need to reproject + if (!wms.isFeaturesReprojectionDisabled()) { + // try to reproject to target CRS + return LayerIdentifierUtils.reproject(result, targetcrs); + } + + // reprojection no allowed return result; } diff --git a/src/wms/src/main/java/org/geoserver/wms/featureinfo/WMSLayerIdentifier.java b/src/wms/src/main/java/org/geoserver/wms/featureinfo/WMSLayerIdentifier.java index 5758868557c..c7b33e080f8 100644 --- a/src/wms/src/main/java/org/geoserver/wms/featureinfo/WMSLayerIdentifier.java +++ b/src/wms/src/main/java/org/geoserver/wms/featureinfo/WMSLayerIdentifier.java @@ -15,8 +15,12 @@ import net.opengis.wfs.FeatureCollectionType; import org.geoserver.catalog.WMSLayerInfo; +import org.geoserver.platform.GeoServerExtensions; +import org.geoserver.util.EntityResolverProvider; import org.geoserver.wms.FeatureInfoRequestParameters; import org.geoserver.wms.MapLayerInfo; +import org.geoserver.wms.WMS; +import org.geotools.data.crs.ForceCoordinateSystemFeatureResults; import org.geotools.data.ows.Layer; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.store.ReTypingFeatureCollection; @@ -25,7 +29,7 @@ import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.WMSLayer; -import org.geoserver.util.EntityResolverProvider; +import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.geotools.wfs.v1_0.WFSConfiguration; import org.geotools.xml.Parser; @@ -43,8 +47,17 @@ public class WMSLayerIdentifier implements LayerIdentifier { private EntityResolverProvider resolverProvider; + // WMS service configuration facade, maybe be NULL use method getWms() + private WMS wms; + + @Deprecated public WMSLayerIdentifier(EntityResolverProvider resolverProvider) { + this(resolverProvider, null); + } + + public WMSLayerIdentifier(EntityResolverProvider resolverProvider, WMS wms) { this.resolverProvider = resolverProvider; + this.wms = wms; } public List identify(FeatureInfoRequestParameters params, int maxFeatures) throws IOException { @@ -106,8 +119,9 @@ public List identify(FeatureInfoRequestParameters params, int SimpleFeatureType targetFeatureType = builder.buildFeatureType(); FeatureCollection rfc = new ReTypingFeatureCollection(fc, targetFeatureType); - - results.add(rfc); + + // if possible force a CRS to be defined + results.add(forceCrs(rfc)); } } } catch (Throwable t) { @@ -115,6 +129,14 @@ public List identify(FeatureInfoRequestParameters params, int } finally { is.close(); } + + // let's see if we need to reproject + if (!getWms().isFeaturesReprojectionDisabled()) { + // try to reproject to target CRS + return LayerIdentifierUtils.reproject(results, params.getRequestedCRS()); + } + + // reprojection no allowed return results; } @@ -122,4 +144,42 @@ public boolean canHandle(MapLayerInfo layer) { return layer.getType() == MapLayerInfo.TYPE_WMS; } + /** + * Helper method that tries to force a CRS to be defined. If no CRS is defined + * buf if all the feature collection geometries use the same CRS that CRS will + * be forced. This only work for simple features. + */ + private FeatureCollection forceCrs(FeatureCollection featureCollection) { + if (featureCollection.getSchema().getCoordinateReferenceSystem() != null) { + // a CRS is already defined + return featureCollection; + } + // try to extract a CRS from the feature collection features + CoordinateReferenceSystem crs = LayerIdentifierUtils.getCrs(featureCollection); + if (crs == null) { + // there is nothing more we can do + return featureCollection; + } + try { + // force the CRS + return new ForceCoordinateSystemFeatureResults(featureCollection, crs); + } catch (Exception exception) { + throw new RuntimeException(String.format( + "Error forcing feature collection to use SRS '%s'.", + CRS.toSRS(crs)), exception); + } + } + + /** + * Does a lookup on the application context if needed. + * + * @return WMS service configuration facade + */ + private WMS getWms() { + if (wms == null) { + // no need for synchronization here + wms = GeoServerExtensions.bean(WMS.class); + } + return wms; + } } diff --git a/src/wms/src/test/java/org/geoserver/wms/WMSTestSupport.java b/src/wms/src/test/java/org/geoserver/wms/WMSTestSupport.java index b4712539034..52cb7a975e7 100644 --- a/src/wms/src/test/java/org/geoserver/wms/WMSTestSupport.java +++ b/src/wms/src/test/java/org/geoserver/wms/WMSTestSupport.java @@ -7,6 +7,8 @@ import static junit.framework.TestCase.fail; import static org.geoserver.data.test.MockData.WORLD; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.*; import java.awt.Color; @@ -61,6 +63,7 @@ import org.geotools.xml.Configuration; import org.geotools.xml.Parser; import org.geotools.xml.transform.TransformerBase; +import org.junit.Assert; import org.opengis.feature.Feature; import org.opengis.feature.type.FeatureType; import org.w3c.dom.Document; @@ -649,4 +652,27 @@ protected void checkWms13ValidationErrors(Document dom) throws Exception { } } + /** + * Check that a number represent by a string is similar to the expected number + * A number is considered similar to another if the difference between them is + * inferior or equal to the provided precision. + * + * @param rawValue raw value that should contain a number + * @param expected the expected numeric value + * @param precision precision that should be used to compare the two values + */ + public static void checkNumberSimilar(String rawValue, double expected, double precision) { + // try to extract a double value + assertThat(rawValue, is(notNullValue())); + assertThat(rawValue.trim().isEmpty(), is(false)); + double value = 0; + try { + value = Double.parseDouble(rawValue); + } catch (NumberFormatException exception) { + Assert.fail(String.format("Value '%s' is not a number.", rawValue)); + } + // compare the parsed double value with the expected one + double difference = Math.abs(expected - value); + assertThat(difference <= precision, is(true)); + } } diff --git a/src/wms/src/test/java/org/geoserver/wms/featureinfo/RenderingBasedFeatureInfoTest.java b/src/wms/src/test/java/org/geoserver/wms/featureinfo/RenderingBasedFeatureInfoTest.java index d29d510d1db..c4b4d2252a4 100644 --- a/src/wms/src/test/java/org/geoserver/wms/featureinfo/RenderingBasedFeatureInfoTest.java +++ b/src/wms/src/test/java/org/geoserver/wms/featureinfo/RenderingBasedFeatureInfoTest.java @@ -5,22 +5,13 @@ */ package org.geoserver.wms.featureinfo; -import static org.junit.Assert.assertEquals; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import javax.xml.namespace.QName; - +import com.vividsolutions.jts.geom.Envelope; import net.sf.json.JSONObject; - import org.apache.commons.io.FileUtils; import org.apache.commons.lang.mutable.MutableDouble; +import org.custommonkey.xmlunit.SimpleNamespaceContext; +import org.custommonkey.xmlunit.XMLUnit; +import org.custommonkey.xmlunit.XpathEngine; import org.geoserver.config.GeoServer; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; @@ -45,11 +36,43 @@ import org.junit.Test; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.TransformException; +import org.w3c.dom.Document; -import com.vividsolutions.jts.geom.Envelope; +import javax.xml.namespace.QName; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class RenderingBasedFeatureInfoTest extends WMSTestSupport { + private static final XpathEngine XPATH = XMLUnit.newXpathEngine(); + + static { + // setup XPATH engine namespaces + Map namespaces = new HashMap<>(); + namespaces.put("gml", "http://www.opengis.net/gml" ); + namespaces.put("gs", "http://geoserver.org" ); + namespaces.put("ogc", "http://www.opengis.net/ogc" ); + namespaces.put("ows", "http://www.opengis.net/ows" ); + namespaces.put("wfs", "http://www.opengis.net/wfs" ); + namespaces.put("xlink", "http://www.w3.org/1999/xlink" ); + namespaces.put("xs", "http://www.w3.org/2001/XMLSchema" ); + namespaces.put("xsi", "http://www.w3.org/2001/XMLSchema-instance"); + namespaces.put("cite", "http://www.opengis.net/cite"); + namespaces.put("sf", "http://cite.opengeospatial.org/gmlsf"); + XPATH.setNamespaceContext(new SimpleNamespaceContext(namespaces)); + } + public static QName GRID = new QName(MockData.CITE_URI, "grid", MockData.CITE_PREFIX); public static QName REPEATED = new QName(MockData.CITE_URI, "repeated", MockData.CITE_PREFIX); public static QName GIANT_POLYGON = new QName(MockData.CITE_URI, "giantPolygon", @@ -94,6 +117,10 @@ protected void onSetUp(SystemTestData testData) throws Exception { @After public void cleanup() { VectorRenderingLayerIdentifier.RENDERING_FEATUREINFO_ENABLED = true; + // make sure GetFeatureInfo is not deactivated (this will only update the global service) + WMSInfo wms = getGeoServer().getService(WMSInfo.class); + wms.setFeaturesReprojectionDisabled(false); + getGeoServer().save(wms); } /** @@ -509,4 +536,95 @@ public void testRenderingTransform() throws Exception { assertEquals(1, result.getJSONArray("features").size()); } + + @Test + public void testGetFeatureInfoReprojectionWithoutRendering() throws Exception { + // disable WMS get feature info with rendering + VectorRenderingLayerIdentifier.RENDERING_FEATUREINFO_ENABLED = false; + // request using EPSG:3857 a layer that uses EPSG:4326 + String url = "wms?REQUEST=GetFeatureInfo&BBOX=21.1507032494,76.8104486492,23.3770930655,79.0368384649&SERVICE=WMS" + + "&INFO_FORMAT=text/xml; subtype=gml/3.1.1&QUERY_LAYERS=cite%3ABridges&Layers=cite%3ABridges&WIDTH=100&HEIGHT=100" + + "&format=image%2Fpng&styles=box-offset&srs=EPSG%3A3857&version=1.1.1&x=50&y=63&feature_count=50"; + Document result = getAsDOM(url); + // check the response content, the features should have been reproject from EPSG:4326 to EPSG:3857 + String srs = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "cite:Bridges[@gml:id='Bridges.1107531599613']/cite:the_geom/gml:Point/@srsName", result); + assertThat(srs, notNullValue()); + assertThat(srs.contains("3857"), is(true)); + String rawCoordinates = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "cite:Bridges[@gml:id='Bridges.1107531599613']/cite:the_geom/gml:Point/gml:pos/text()", result); + checkCoordinates(rawCoordinates, 0.0001, 22.26389816, 77.92364356); + // disable feature reprojection + WMSInfo wms = getGeoServer().getService(WMSInfo.class); + wms.setFeaturesReprojectionDisabled(true); + getGeoServer().save(wms); + // execute the get feature info request + result = getAsDOM(url); + // check that features were not reprojected + srs = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "cite:Bridges[@gml:id='Bridges.1107531599613']/cite:the_geom/gml:Point/@srsName", result); + assertThat(srs, notNullValue()); + assertThat(srs.contains("4326"), is(true)); + rawCoordinates = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "cite:Bridges[@gml:id='Bridges.1107531599613']/cite:the_geom/gml:Point/gml:pos/text()", result); + checkCoordinates(rawCoordinates, 0.0001, 0.0002, 0.0007); + } + + @Test + public void testGetFeatureInfoReprojectionWithRendering() throws Exception { + // request using EPSG:3857 a layer that uses EPSG:4326 + String url = "wms?REQUEST=GetFeatureInfo&BBOX=-304226.149584,7404818.42511,947357.141801,10978414.0796&SERVICE=WMS" + + "&INFO_FORMAT=text/xml; subtype=gml/3.1.1&QUERY_LAYERS=GenericEntity&Layers=GenericEntity" + + "&WIDTH=397&HEIGHT=512&format=image%2Fpng&styles=line&srs=EPSG%3A3857&version=1.1.1&x=284&y=269"; + Document result = getAsDOM(url); + // check the response content, the features should have been reproject from EPSG:4326 to EPSG:3857 + String srs = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "sf:GenericEntity[@gml:id='GenericEntity.f004']/sf:attribut.geom/gml:Polygon/@srsName", result); + assertThat(srs, notNullValue()); + assertThat(srs.contains("3857"), is(true)); + String exteriorLinearRing = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "sf:GenericEntity[@gml:id='GenericEntity.f004']/sf:attribut.geom/gml:Polygon/" + + "gml:exterior/gml:LinearRing/gml:posList/text()", result); + checkCoordinates(exteriorLinearRing, 0.0001, 0, 8511908.69220489, 0, 9349764.17414691, + 695746.81745796, 9349764.17414691, 695746.81745796, 8511908.69220489, 0, 8511908.69220489); + String interiorLinearRing = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "sf:GenericEntity[@gml:id='GenericEntity.f004']/sf:attribut.geom/gml:Polygon/" + + "gml:interior/gml:LinearRing/gml:posList/text()", result); + checkCoordinates(interiorLinearRing, 0.0001, 222638.98158655, 8741545.4358357, 222638.98158655, + 8978686.31934769, 445277.96317309, 8859142.8005657, 222638.98158655, 8741545.4358357); + // disable feature reprojection + WMSInfo wms = getGeoServer().getService(WMSInfo.class); + wms.setFeaturesReprojectionDisabled(true); + getGeoServer().save(wms); + // execute the get feature info request + result = getAsDOM(url); + // check that features were not reprojected + srs = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "sf:GenericEntity[@gml:id='GenericEntity.f004']/sf:attribut.geom/gml:Polygon/@srsName", result); + assertThat(srs, notNullValue()); + assertThat(srs.contains("4326"), is(true)); + exteriorLinearRing = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "sf:GenericEntity[@gml:id='GenericEntity.f004']/sf:attribut.geom/gml:Polygon/" + + "gml:exterior/gml:LinearRing/gml:posList/text()", result); + checkCoordinates(exteriorLinearRing, 0.0001, 0, 60.5, 0, 64, 6.25, 64, 6.25, 60.5, 0, 60.5); + interiorLinearRing = XPATH.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "sf:GenericEntity[@gml:id='GenericEntity.f004']/sf:attribut.geom/gml:Polygon/" + + "gml:interior/gml:LinearRing/gml:posList/text()", result); + checkCoordinates(interiorLinearRing, 0.0001, 2, 61.5, 2, 62.5, 4, 62, 2, 61.5); + + } + + /** + * Helper method that checks if the string represented coordinates correspond to the + * expected ones. The provided precision will be used to compare the numeric values. + */ + private void checkCoordinates(String rawCoordinates, double precision, double... expected) { + assertThat(rawCoordinates, notNullValue()); + rawCoordinates = rawCoordinates.trim(); + String[] coordinates = rawCoordinates.split("\\s"); + assertThat(coordinates.length, is(expected.length)); + for (int i = 0; i < coordinates.length; i++) { + checkNumberSimilar(coordinates[i], expected[i], precision); + } + } } diff --git a/src/wms/src/test/java/org/geoserver/wms/wms_1_3/CapabilitiesIntegrationTest.java b/src/wms/src/test/java/org/geoserver/wms/wms_1_3/CapabilitiesIntegrationTest.java index 04b76564c68..76362443959 100644 --- a/src/wms/src/test/java/org/geoserver/wms/wms_1_3/CapabilitiesIntegrationTest.java +++ b/src/wms/src/test/java/org/geoserver/wms/wms_1_3/CapabilitiesIntegrationTest.java @@ -831,26 +831,6 @@ private void checkGlobalBoundingBox(ReferencedEnvelope expectedBoundingBox, Docu checkNumberSimilar(maxY, expectedBoundingBox.getMaxY(), 0.0001); } - /** - * Check that a number represent by a string is similar to the expected number - * A number is considered similar to another if the difference between them is - * inferior or equal to the provided precision. - */ - private void checkNumberSimilar(String rawValue, double expected, double precision) { - // try to extract a double value - assertThat(rawValue, is(notNullValue())); - assertThat(rawValue.trim().isEmpty(), is(false)); - double value = 0; - try { - value = Double.parseDouble(rawValue); - } catch (NumberFormatException exception) { - fail(String.format("Value '%s' is not a number.", rawValue)); - } - // compare the parsed double value with the expected one - double difference = Math.abs(expected - value); - assertThat(difference <= precision, is(true)); - } - /** * Helper method that unwraps a layer group making him suitable to be added * to the catalog. If proxyfied the proxy will also be removed. diff --git a/src/wms/src/test/java/org/geoserver/wms/wms_1_3/WMSCascadeTest.java b/src/wms/src/test/java/org/geoserver/wms/wms_1_3/WMSCascadeTest.java index 400acf54672..924d811eb1e 100644 --- a/src/wms/src/test/java/org/geoserver/wms/wms_1_3/WMSCascadeTest.java +++ b/src/wms/src/test/java/org/geoserver/wms/wms_1_3/WMSCascadeTest.java @@ -13,8 +13,6 @@ import org.custommonkey.xmlunit.NamespaceContext; import org.custommonkey.xmlunit.SimpleNamespaceContext; -import org.custommonkey.xmlunit.XMLUnit; -import org.custommonkey.xmlunit.XpathEngine; import org.geoserver.config.GeoServer; import org.geoserver.data.test.SystemTestData; import org.geoserver.test.http.MockHttpResponse; @@ -31,9 +29,14 @@ import org.springframework.mock.web.MockHttpServletResponse; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + @RunWith(Parameterized.class) public class WMSCascadeTest extends WMSCascadeTestSupport { - + private final boolean aphEnabled; @Parameters(name = "{index} APH enabled: {0}") @@ -50,6 +53,8 @@ public void setupAdvancedProjectionHandling() { GeoServer gs = getGeoServer(); WMSInfo wms = gs.getService(WMSInfo.class); wms.getMetadata().put(WMS.ADVANCED_PROJECTION_KEY, aphEnabled); + // make sure GetFeatureInfo is not deactivated (this will only update the global service) + wms.setFeaturesReprojectionDisabled(false); gs.save(wms); } @@ -65,6 +70,15 @@ protected void onSetUp(SystemTestData testData) throws Exception { + "&styles&bbox=-110.0,-200.0,110.0,200.0&crs=EPSG:4326&bgcolor=0xFFFFFF&transparent=FALSE&format=image/png&width=190&height=100"), new MockHttpResponse(pngImage, "image/png")); wms11Client.expectGet(new URL(wms11BaseURL + "?service=WMS&version=1.1.1&request=GetMap&layers=world4326" + "&styles&bbox=-200.0,-110.0,200.0,110.0&srs=EPSG:4326&bgcolor=0xFFFFFF&transparent=FALSE&format=image/png&width=190&height=100"), new MockHttpResponse(pngImage, "image/png")); + + // setup mocked get feature info (the return features use EPSG:3857) + URL featureInfo = WMSTestSupport.class.getResource("wms-features.xml"); + wms13Client.expectGet(new URL(wms13BaseURL + "?SERVICE=WMS&INFO_FORMAT=application/vnd.ogc.gml&LAYERS=world4326" + + "&CRS=EPSG:4326&FEATURE_COUNT=50&FORMAT=image%2Fpng&HEIGHT=101&TRANSPARENT=TRUE&J=-609621&REQUEST=GetFeatureInfo" + + "&I=-875268&WIDTH=101&BBOX=-103.829117187,44.3898919295,-103.804563429,44.4069939679&STYLES=&QUERY_LAYERS=world4326&VERSION=1.3.0"), + new MockHttpResponse(featureInfo, "application/vnd.ogc.gml")); + + } @Test @@ -100,7 +114,53 @@ public void testCascadeCapabilitiesClientNoGetFeatureInfo() throws Exception { xpath.evaluate("//wms:Layer[name='" + WORLD4326_110_NFI + "']", dom); } - - - + + @Test + public void testGetFeatureInfoReprojection() throws Exception { + // do the get feature request using EPSG:4326 + String url = "wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&FORMAT=image/png&TRANSPARENT=true" + + "&QUERY_LAYERS=" + WORLD4326_130 + "&STYLES&LAYERS=" + WORLD4326_130 + "&INFO_FORMAT=text/xml; subtype=gml/3.1.1" + + "&FEATURE_COUNT=50&X=50&Y=50&SRS=EPSG:4326&WIDTH=101&HEIGHT=101&BBOX=-103.829117187,44.3898919295,-103.804563429,44.4069939679"; + Document result = getAsDOM(url); + // setup XPATH engine namespaces + Map namespaces = new HashMap<>(); + namespaces.put("gml", "http://www.opengis.net/gml" ); + namespaces.put("gs", "http://geoserver.org" ); + namespaces.put("ogc", "http://www.opengis.net/ogc" ); + namespaces.put("ows", "http://www.opengis.net/ows" ); + namespaces.put("wfs", "http://www.opengis.net/wfs" ); + namespaces.put("xlink", "http://www.w3.org/1999/xlink" ); + namespaces.put("xs", "http://www.w3.org/2001/XMLSchema" ); + namespaces.put("xsi", "http://www.w3.org/2001/XMLSchema-instance"); + xpath.setNamespaceContext(new SimpleNamespaceContext(namespaces)); + // check the response content, the features should have been reproject from EPSG:3857 to EPSG:4326 + String srs = xpath.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "gs:world4326_130[@gml:id='bugsites.55']/gs:the_geom/gml:Point/@srsName", result); + assertThat(srs, notNullValue()); + assertThat(srs.contains("4326"), is(true)); + String rawCoordinates = xpath.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "gs:world4326_130[@gml:id='bugsites.55']/gs:the_geom/gml:Point/gml:pos/text()", result); + assertThat(rawCoordinates, notNullValue()); + String[] coordinates = rawCoordinates.split(" "); + assertThat(coordinates.length, is(2)); + checkNumberSimilar(coordinates[0], 44.39832008, 0.0001); + checkNumberSimilar(coordinates[1], -103.81711048, 0.0001); + // deactivate features reprojection + WMSInfo wms = getGeoServer().getService(WMSInfo.class); + wms.setFeaturesReprojectionDisabled(true); + getGeoServer().save(wms); + // execute the get feature info request + result = getAsDOM(url); + srs = xpath.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "gs:world4326_130[@gml:id='bugsites.55']/gs:the_geom/gml:Point/@srsName", result); + assertThat(srs, notNullValue()); + assertThat(srs.contains("3857"), is(true)); + rawCoordinates = xpath.evaluate("//wfs:FeatureCollection/gml:featureMembers/" + + "gs:world4326_130[@gml:id='bugsites.55']/gs:the_geom/gml:Point/gml:pos/text()", result); + assertThat(rawCoordinates, notNullValue()); + coordinates = rawCoordinates.split(" "); + assertThat(coordinates.length, is(2)); + checkNumberSimilar(coordinates[0], -11556867.874, 0.0001); + checkNumberSimilar(coordinates[1], 5527291.47718493, 0.0001); + } } diff --git a/src/wms/src/test/resources/org/geoserver/wms/wms-features.xml b/src/wms/src/test/resources/org/geoserver/wms/wms-features.xml new file mode 100644 index 00000000000..3b7c185ab74 --- /dev/null +++ b/src/wms/src/test/resources/org/geoserver/wms/wms-features.xml @@ -0,0 +1,33 @@ + + + + unknown + + + + + + -1.155686787398436E7,5527291.47718493 + + + 55 + Beetle site + + + + + + + -1.155670642380653E7,5527242.84807767 + + + 58 + Beetle site + + + \ No newline at end of file