Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GEOS-11277] Have MapML TCRS instances work as actual coordinate reference systems. #357

Closed
wants to merge 11 commits into from
Closed
22 changes: 18 additions & 4 deletions doc/en/user/source/extensions/mapml/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,24 @@ MapML resources will be available for any published WMS layers by making a GetMa

Note that the WMS SRS or CRS must be one of the projections supported by MapML:

- EPSG:3857
- EPSG:3978
- EPSG:5936
- EPSG:4326
- MapML:WGS84 (or EPSG:4326)
- MapML:OSMTILE (or EPSG:3857)
- MapML:CBMTILE (or EPSG:3978)
- MapML:APSTILE (or EPSG:5936)

The equivalent EPSG codes are provided for reference, but the MapML names are recommended, as they
imply not only a coordinate refefence system, but also a tile grid and a set of zoom levels (Tiled CRS),
that the MapML client will use when operating in tiled mode. When using tiles, it's also recommended
to set up tile caching for the same-named gridsets.

If the native SRS of a layer is not a match for the MapML ones, remember to configure the projection
policy to "reproject native to declare". You might have to save and reload the layer configuration
in order to re-compute the native bounds correctly.

If the SRS or CRS is not one of the above, the GetMap request will fail with an ``InvalidParameterValue`` exception.
The main "MapML" link in the preview page generates a HTML client able to consume MapML resources.
The link is generated so that it always work, if the CRS configured for the layer is not supported, it will automatically fall back on MapML:WGS84.


**MapML Output Format**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public void testTwoCRSSetByFindThenApply() {
// The second (4) should still be set
tester.assertModelValue(
"taskTable:listContainer:items:4:itemProperties:2:component:form:crs:srs",
"EPSG:4269");
"CRS:83");
}

void fill(String formPath, String fieldPath, String value) {
Expand Down
7 changes: 6 additions & 1 deletion src/extension/mapml/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,15 @@
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-wms</artifactId>

<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-iau-wkt</artifactId>
<version>${gt.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-wfs</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -58,6 +59,7 @@
import org.geoserver.mapml.xml.RelType;
import org.geoserver.mapml.xml.Select;
import org.geoserver.mapml.xml.UnitType;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.URLMangler;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.ServiceException;
Expand All @@ -66,10 +68,13 @@
import org.geoserver.wms.WMSInfo;
import org.geoserver.wms.WMSMapContent;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.api.referencing.operation.TransformException;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.renderer.crs.ProjectionHandler;
import org.geotools.renderer.crs.ProjectionHandlerFinder;
import org.geotools.util.logging.Logging;
import org.geowebcache.grid.GridSubset;
import org.locationtech.jts.geom.Envelope;
Expand Down Expand Up @@ -103,9 +108,8 @@ public class MapMLDocumentBuilder {
private final String layersCommaDelimited;
private final String layerTitlesCommaDelimited;
private final String stylesCommaDelimited;

private final GetMapRequest getMapRequest;
private String defaultStyle;

private String layerTitle;
private String imageFormat;
private String baseUrl;
Expand Down Expand Up @@ -149,7 +153,7 @@ public MapMLDocumentBuilder(
this.geoServer = geoServer;
this.request = request;
// this.layers = mapContent.layers();
GetMapRequest getMapRequest = mapContent.getRequest();
this.getMapRequest = mapContent.getRequest();
String rawLayersCommaDL = getMapRequest.getRawKvp().get("layers");
this.layers = toRawLayers(rawLayersCommaDL);
this.stylesCommaDelimited =
Expand Down Expand Up @@ -238,12 +242,7 @@ private Optional<Object> getFormat(GetMapRequest getMapRequest) {
* @throws ServiceException In the event of a service error.
*/
public Mapml getMapMLDocument() throws ServiceException {
try {
initialize();
} catch (RuntimeException re) {
LOGGER.log(Level.INFO, re.getMessage());
return null;
}
initialize();
prepareDocument();
return this.mapml;
}
Expand Down Expand Up @@ -353,12 +352,7 @@ private MapMLLayerMetadata layersToOneMapMLLayerMetadata(List<RawLayer> layers)
mapMLLayerMetadata.setTimeEnabled(false);
mapMLLayerMetadata.setElevationEnabled(false);
mapMLLayerMetadata.setTransparent(transparent.orElse(false));
ProjType projType = null;
try {
projType = ProjType.fromValue(proj.toUpperCase());
} catch (IllegalArgumentException | FactoryException iae) {
throw new ServiceException("Invalid TCRS name");
}
ProjType projType = parseProjType();
mapMLLayerMetadata.setBbbox(layersToBBBox(layers, projType));
mapMLLayerMetadata.setQueryable(layersToQueryable(layers));
mapMLLayerMetadata.setLayerLabel(layersToLabel(layers));
Expand All @@ -367,6 +361,31 @@ private MapMLLayerMetadata layersToOneMapMLLayerMetadata(List<RawLayer> layers)
return mapMLLayerMetadata;
}

/**
* Parses the projection into a ProjType, or throws a proper service exception indicating the
* unsupported CRS
*/
private ProjType parseProjType() {
try {
return ProjType.fromValue(proj.toUpperCase());
} catch (IllegalArgumentException | FactoryException iae) {
// figure out the parameter name (version dependent) and the actual original
// string value for the srs/crs parameter
String parameterName =
Optional.ofNullable(getMapRequest.getVersion())
.filter(v -> v.equals("1.3.0"))
.map(v -> "crs")
.orElse("srs");
Map<String, Object> rawKvp = Dispatcher.REQUEST.get().getRawKvp();
String value = (String) rawKvp.get("srs");
if (value == null) value = (String) rawKvp.get("crs");
throw new ServiceException(
"This projection is not supported by MapML: " + value,
ServiceException.INVALID_PARAMETER_VALUE,
parameterName);
}
}

/**
* Generate a merged queryable flag for a collection of raw layers
*
Expand Down Expand Up @@ -475,7 +494,6 @@ private MapMLLayerMetadata layerToMapMLLayerMetadata(RawLayer layer, String styl
String layerTitle = null;
ResourceInfo resourceInfo = null;
boolean isTransparent = true;
ProjType projType = null;
String styleName = null;
boolean tileLayerExists = false;
if (isLayerGroup) {
Expand Down Expand Up @@ -510,11 +528,7 @@ private MapMLLayerMetadata layerToMapMLLayerMetadata(RawLayer layer, String styl
layerName = layerInfo.getName().isEmpty() ? layer.getTitle() : layerInfo.getName();
layerTitle = getTitle(layerInfo, layerName);
}
try {
projType = ProjType.fromValue(proj.toUpperCase());
} catch (IllegalArgumentException | FactoryException iae) {
throw new ServiceException("Invalid TCRS name");
}
ProjType projType = parseProjType();
styleName = style != null ? style : "";
tileLayerExists =
gwc.hasTileLayer(isLayerGroup ? layerGroupInfo : layerInfo)
Expand Down Expand Up @@ -636,7 +650,7 @@ private HeadContent prepareHead() {
wmsParams.put(
"format_options", MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION + ":" + imageFormat);
wmsParams.put("layers", layersCommaDelimited);
wmsParams.put("crs", projType.getEpsgCode());
wmsParams.put("crs", projType.getCRSCode());
wmsParams.put("version", "1.3.0");
wmsParams.put("service", "WMS");
wmsParams.put("request", "GetMap");
Expand Down Expand Up @@ -757,23 +771,62 @@ private HeadContent prepareHead() {
for (ProjType pt : ProjType.values()) {
// skip the current proj
if (pt.equals(projType)) continue;
Link projectionLink = new Link();
projectionLink.setRel(RelType.ALTERNATE);
projectionLink.setProjection(pt);
// Copy the base params to create one for self style
Map<String, String> projParams = new HashMap<>(wmsParams);
projParams.put("crs", pt.getEpsgCode());
projParams.put("width", Integer.toString(width));
projParams.put("height", Integer.toString(height));
projParams.put("bbox", bbox);
String projURL =
ResponseUtils.buildURL(baseUrl, "wms", projParams, URLMangler.URLType.SERVICE);
projectionLink.setHref(projURL);
links.add(projectionLink);
try {
Link projectionLink = new Link();
projectionLink.setRel(RelType.ALTERNATE);
projectionLink.setProjection(pt);
// reproject the bounds
ReferencedEnvelope reprojectedBounds = reproject(projectedBox, pt);
// Copy the base params to create one for self style
Map<String, String> projParams = new HashMap<>(wmsParams);
projParams.put("crs", pt.getCRSCode());
projParams.put("width", Integer.toString(width));
projParams.put("height", Integer.toString(height));
projParams.put("bbox", toCommaDelimitedBbox(reprojectedBounds));
String projURL =
ResponseUtils.buildURL(
baseUrl, "wms", projParams, URLMangler.URLType.SERVICE);
projectionLink.setHref(projURL);
links.add(projectionLink);
} catch (Exception e) {
// we gave it our best try but reprojection failed anyways, log and skip this link
LOGGER.log(Level.INFO, "Unable to reproject bounds for " + pt.value(), e);
}
}
return head;
}

/**
* Reproject the bounds to the target CRS
*
* @param bounds ReferencedEnvelope object
* @param pt ProjType object
* @return ReferencedEnvelope object
* @throws FactoryException In the event of a factory error.
* @throws TransformException In the event of a transform error.
*/
private ReferencedEnvelope reproject(ReferencedEnvelope bounds, ProjType pt)
throws FactoryException, TransformException {
CoordinateReferenceSystem targetCRS = PREVIEW_TCRS_MAP.get(pt.value()).getCRS();
// leverage the rendering ProjectionHandlers to build a set of envelopes
// inside the valid area of the target CRS, and fuse them
ProjectionHandler ph = ProjectionHandlerFinder.getHandler(bounds, targetCRS, true);
ReferencedEnvelope targetBounds = null;
if (ph != null) {
List<ReferencedEnvelope> queryEnvelopes = ph.getQueryEnvelopes();
for (ReferencedEnvelope envelope : queryEnvelopes) {
if (targetBounds == null) {
targetBounds = envelope;
} else {
targetBounds.expandToInclude(envelope);
}
}
} else {
targetBounds = bounds.transform(targetCRS, true);
}
return targetBounds;
}

/**
* Create and return MapML BodyContent JAXB object
*
Expand Down Expand Up @@ -1102,7 +1155,7 @@ private void generateTiledWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata)
params.put("version", "1.3.0");
params.put("service", "WMS");
params.put("request", "GetMap");
params.put("crs", PREVIEW_TCRS_MAP.get(projType.value()).getCode());
params.put("crs", projType.getCRSCode());
params.put("layers", mapMLLayerMetadata.getLayerName());
params.put("language", this.request.getLocale().getLanguage());
params.put("styles", mapMLLayerMetadata.getStyleName());
Expand Down Expand Up @@ -1247,7 +1300,7 @@ public void generateWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) {
params.put("version", "1.3.0");
params.put("service", "WMS");
params.put("request", "GetMap");
params.put("crs", PREVIEW_TCRS_MAP.get(projType.value()).getCode());
params.put("crs", projType.getCRSCode());
params.put("layers", mapMLLayerMetadata.getLayerName());
params.put("styles", mapMLLayerMetadata.getStyleName());
if (mapMLLayerMetadata.isTimeEnabled()) {
Expand Down Expand Up @@ -1358,7 +1411,7 @@ private void generateWMSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata)
params.put("service", "WMS");
params.put("request", "GetFeatureInfo");
params.put("feature_count", "50");
params.put("crs", PREVIEW_TCRS_MAP.get(projType.value()).getCode());
params.put("crs", projType.getCRSCode());
params.put("language", this.request.getLocale().getLanguage());
params.put("layers", mapMLLayerMetadata.getLayerName());
params.put("query_layers", mapMLLayerMetadata.getLayerName());
Expand Down Expand Up @@ -1401,11 +1454,7 @@ private void generateWMSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata)
* @return String
*/
public String getMapMLHTMLDocument() {
try {
initialize();
} catch (ServiceException se) {
throw se;
}
initialize();
String layerLabel = "";
String layer = "";
String styleName = "";
Expand Down Expand Up @@ -1511,37 +1560,50 @@ public String getMapMLHTMLDocument() {
.append(escapeHtml4(layerLabel))
.append("\" ")
.append("src=\"")
.append(request.getContextPath())
.append("/wms?")
.append("&LAYERS=")
.append(escapeHtml4(layer))
.append("&BBOX=")
.append(String.valueOf(projectedBbox.getMinX()) + ",")
.append(String.valueOf(projectedBbox.getMinY()) + ",")
.append(String.valueOf(projectedBbox.getMaxX()) + ",")
.append(String.valueOf(projectedBbox.getMaxY()))
.append("&HEIGHT=")
.append(height)
.append("&WIDTH=")
.append(width)
.append("&SRS=")
.append(escapeHtml4(proj))
.append("&STYLES=")
.append(escapeHtml4(styleName))
.append("&FORMAT=")
.append(MAPML_MIME_TYPE)
.append("&format_options=")
.append(MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION)
.append(":")
.append(escapeHtml4((String) format.orElse("image/png")))
.append("&SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0")
.append(
buildGetMap(
layer,
projectedBbox,
width,
height,
escapeHtml4(proj),
styleName,
format))
.append("\" checked></layer->\n")
.append("</mapml-viewer>\n")
.append("</body>\n")
.append("</html>");
return sb.toString();
}

/** Builds the GetMap backlink to get MapML */
private String buildGetMap(
String layer,
ReferencedEnvelope projectedBbox,
int height,
int width,
String proj,
String styleName,
Optional<Object> format) {
Map<String, String> kvp = new LinkedHashMap<>();
kvp.put("LAYERS", escapeHtml4(layer));
kvp.put("BBOX", toCommaDelimitedBbox(projectedBbox));
kvp.put("HEIGHT", String.valueOf(height));
kvp.put("WIDTH", String.valueOf(width));
kvp.put("SRS", escapeHtml4(proj));
kvp.put("STYLES", escapeHtml4(styleName));
kvp.put("FORMAT", MAPML_MIME_TYPE);
kvp.put(
"format_options",
MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION
+ ":"
+ escapeHtml4((String) format.orElse("image/png")));
kvp.put("SERVICE", "WMS");
kvp.put("REQUEST", "GetMap");
kvp.put("VERSION", "1.3.0");
return ResponseUtils.buildURL(baseUrl, "wms", kvp, URLMangler.URLType.SERVICE);
}

/**
* Get the potentially localized label string for a layer or layer group
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ private List<Link> alternateProjections() {
throw new ServiceException("Invalid TCRS name");
}
l.setRel(RelType.ALTERNATE);
this.query.put("srsName", projection.getCode());
this.query.put("srsName", "MapML:" + projection.getName());
HashMap<String, String> kvp = new HashMap<>(this.query.size());
this.query
.keySet()
Expand Down
Loading
Loading