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-11461: Support MapML embedded viewer on HTML for WFS GetFeature #369

Merged
merged 1 commit into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions doc/en/user/source/extensions/mapml/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,8 @@ MapML visualization is supported by the Web-Map-Custom-Element project. The MapM
</html>

In the above example, the place-holders ``topp:states``, ``localhost:8080``, ``osmtile``, and ``population`` would need to be replaced with the appropriate values, and/or the ``style`` parameter could be removed entirely from the URL if not needed. You may also like to "View Source" on the preview page to see what the markup looks like for any layer. This code can be copied and pasted without harm, and you should try it and see what works and what the limitations are. For further information about MapML, and the Maps for HTML Community Group, please visit http://maps4html.org.

In addition the MapML viewer is also available as output of a WFS GetFeature request. Select the "text/html; subtype=mapml" from the dropdown as shown below:

.. figure:: images/mapml_wfs_format_dropdown.png

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import org.geoserver.mapml.tcrs.Bounds;
import org.geoserver.mapml.tcrs.Point;
import org.springframework.http.MediaType;

/**
Expand All @@ -31,6 +33,9 @@ public final class MapMLConstants {
/** format name */
public static final String FORMAT_NAME = "MAPML";

/** format name needed to have a parseable format in WFS 1.0.0 capabilities */
public static final String HTML_FORMAT_NAME = "MAPML-HTML";

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WFS 1.0.0 requires a simple format name to be reported in the capabilities document.
This will reported in 1.1.0 and 2.0.0 too, together with the text/html; subtype=mapml

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about text/html; subtype=mapml? This was used in the WMS preview for example. The important thing is to serve content (when asked for mapml) as text/mapml

Copy link
Member Author

@dromagnoli dromagnoli Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. We are indeed using "text/html; subtype=mapml" to serve WFS as HTML (doing the same thing that WMS does).
The problem is in WFS 1.0.0 that is not supporting that as element name for the capabilities due to this check.

https://github.com/geoserver/geoserver/blob/main/src/wfs/src/main/java/org/geoserver/wfs/WFSGetFeatureOutputFormat.java#L125

So we ended up exposing it as MAPML-HTML as well as text/html; subtype=mapml (where the first one makes WFS 1.0.0 capabilities happy).
image

The WFS KML output format does a similar thing, using KML for WFS 1.0.0 (see above screenshot)
This will result in having variations of the same output listed in WFS 1.1.0 and 2.0.0.

Check the below 1.1.0 WFS capabilities document where KML output is listed with the 3 variations (marked in red) and MapML-HTML in 2 variations (marked in yellow)
image

Long story short:
When requesting WFS for MAPML, it will indeed serve content as text/mapml
When requesting WFS 1.0.0 for MAPML-HTML or text/html; subtype=mapml it will indeed serve the Mapml client as text/html; subtype=mapml.

Does it clarify?

Copy link
Collaborator

@prushforth prushforth Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does, thank you very much! So the MAPML value gets sent under the text/mapml Content-type: text/mapml. Curious why text/csv didn't get caught by that rule.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's just that the CSV rule is implemented but the MAPML rule not quite yet, perhaps.

Copy link
Member Author

@dromagnoli dromagnoli Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does, thank you very much! So the MAPML value gets sent under the text/mapml Content-type: text/mapml. Curious why text/csv didn't get caught by that rule.

Actually it gets caught too.
WFS 1.0.0 only reports CSV whilst WFS 1.1.0 reports both csv and text/csv

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I understand now but in no case do we see text/mapml in the list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed you don't, it's already the case in an un-modified GeoServer, e.g.:

https://gs-main.geosolutionsgroup.com/geoserver/ows?service=WFS&version=1.1.0&request=GetCapabilities

/** MapML format option enabling features */
public static final String MAPML_FEATURE_FO = "mapmlfeatures";

Expand Down Expand Up @@ -113,6 +118,8 @@ public final class MapMLConstants {
public static final String REL_LICENSE = "license";

public static final List<String> ZOOM_RELS = Arrays.asList(REL_ZOOMIN, REL_ZOOMOUT);
public static final Bounds DISPLAY_BOUNDS_DESKTOP_LANDSCAPE =
new Bounds(new Point(0, 0), new Point(768, 1024));

public static int PAGESIZE = 100;
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
import org.geoserver.gwc.GWC;
import org.geoserver.gwc.layer.GeoServerTileLayer;
import org.geoserver.mapml.tcrs.Bounds;
import org.geoserver.mapml.tcrs.Point;
import org.geoserver.mapml.tcrs.TiledCRS;
import org.geoserver.mapml.xml.AxisType;
import org.geoserver.mapml.xml.Base;
Expand Down Expand Up @@ -92,11 +91,8 @@
/** Builds a MapML document from a WMSMapContent object */
public class MapMLDocumentBuilder {
private static final Logger LOGGER = Logging.getLogger(MapMLDocumentBuilder.class);
private static final Bounds DISPLAY_BOUNDS_DESKTOP_LANDSCAPE =
new Bounds(new Point(0, 0), new Point(768, 1024));

private static final Pattern ALL_COMMAS = Pattern.compile("^,+$");
public static final HashMap<String, TiledCRS> PREVIEW_TCRS_MAP = new HashMap<>();

/**
* The key for the metadata entry that controls whether a multi-layer request is rendered as a
Expand Down Expand Up @@ -153,13 +149,6 @@ public class MapMLDocumentBuilder {

private Boolean isMultiExtent = MAPML_MULTILAYER_AS_MULTIEXTENT_DEFAULT;

static {
PREVIEW_TCRS_MAP.put("OSMTILE", new TiledCRS("OSMTILE"));
PREVIEW_TCRS_MAP.put("CBMTILE", new TiledCRS("CBMTILE"));
PREVIEW_TCRS_MAP.put("APSTILE", new TiledCRS("APSTILE"));
PREVIEW_TCRS_MAP.put("WGS84", new TiledCRS("WGS84"));
}

/**
* Constructor
*
Expand Down Expand Up @@ -482,7 +471,9 @@ private String layersToLabel(List<RawLayer> layers) {
private ReferencedEnvelope layersToBBBox(List<RawLayer> layers, ProjType projType) {

ReferencedEnvelope bbbox;
bbbox = new ReferencedEnvelope(PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
bbbox =
new ReferencedEnvelope(
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
for (int i = 0; i < layers.size(); i++) {
RawLayer layer = layers.get(i);
try {
Expand All @@ -495,15 +486,18 @@ private ReferencedEnvelope layersToBBBox(List<RawLayer> layers, ProjType projTyp
if (i == 0) {
bbbox =
layerBbbox.transform(
PREVIEW_TCRS_MAP.get(projType.value()).getCRS(), true);
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS(),
true);
} else {
bbbox.expandToInclude(
layerBbbox.transform(
PREVIEW_TCRS_MAP.get(projType.value()).getCRS(), true));
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS(),
true));
}
} catch (Exception e) {
// get the default max/min of the pcrs from the TCRS
Bounds defaultBounds = PREVIEW_TCRS_MAP.get(projType.value()).getBounds();
Bounds defaultBounds =
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getBounds();
double x1, x2, y1, y2;
x1 = defaultBounds.getMin().x;
x2 = defaultBounds.getMax().x;
Expand All @@ -512,7 +506,11 @@ private ReferencedEnvelope layersToBBBox(List<RawLayer> layers, ProjType projTyp
// use the bounds of the TCRS as the default bounds for this layer
bbbox =
new ReferencedEnvelope(
x1, x2, y1, y2, PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
x1,
x2,
y1,
y2,
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
}
}

Expand Down Expand Up @@ -911,7 +909,8 @@ private String buildStyles() throws IOException {
*/
private ReferencedEnvelope reproject(ReferencedEnvelope bounds, ProjType pt)
throws FactoryException, TransformException {
CoordinateReferenceSystem targetCRS = PREVIEW_TCRS_MAP.get(pt.value()).getCRS();
CoordinateReferenceSystem targetCRS =
MapMLHTMLOutput.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);
Expand Down Expand Up @@ -977,7 +976,7 @@ private List<Extent> prepareExtents() throws IOException {
}

Input extentZoomInput = new Input();
TiledCRS tiledCRS = PREVIEW_TCRS_MAP.get(projType.value());
TiledCRS tiledCRS = MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value());
extentZoomInput.setName("z");
extentZoomInput.setType(InputType.ZOOM);
// passing in max sld denominator to get min zoom
Expand All @@ -987,7 +986,7 @@ private List<Extent> prepareExtents() throws IOException {
tiledCRS.getMinZoomForDenominator(
scaleDenominators.getMaxValue().intValue()))
: "0");
int mxz = PREVIEW_TCRS_MAP.get(projType.value()).getScales().length - 1;
int mxz = MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getScales().length - 1;
// passing in min sld denominator to get max zoom
String maxZoom =
scaleDenominators != null
Expand Down Expand Up @@ -1229,20 +1228,25 @@ private void generateTiledWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata)
// of WGS84 is a cartesian cs per the table on this page:
// https://docs.geotools.org/stable/javadocs/org/opengis/referencing/cs/package-summary.html#AxisNames
// input.setAxis(previewTcrsMap.get(projType.value()).getCRS(UnitType.PCRS).getAxisByDirection(AxisDirection.DISPLAY_RIGHT));
bbbox = new ReferencedEnvelope(PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
bbbox =
new ReferencedEnvelope(
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
LayerInfo layerInfo = mapMLLayerMetadata.getLayerInfo();

try {
bbbox =
mapMLLayerMetadata.isLayerGroup()
? mapMLLayerMetadata.getLayerGroupInfo().getBounds()
: layerInfo.getResource().boundingBox();
bbbox = bbbox.transform(PREVIEW_TCRS_MAP.get(projType.value()).getCRS(), true);
bbbox =
bbbox.transform(
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS(),
true);
} catch (Exception e) {
// sometimes, when the geographicBox is right to 90N or 90S, in epsg:3857,
// the transform method will throw. In that case, use the
// bounds of the TCRS to define the geographicBox for the layer
TiledCRS t = PREVIEW_TCRS_MAP.get(projType.value());
TiledCRS t = MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value());
double x1 = t.getBounds().getMax().x;
double y1 = t.getBounds().getMax().y;
double x2 = t.getBounds().getMin().x;
Expand Down Expand Up @@ -1365,7 +1369,9 @@ public void generateWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) {
try {
// initialization is necessary so as to set the PCRS to which
// the resource's geographicBox will be transformed, below.
bbbox = new ReferencedEnvelope(PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
bbbox =
new ReferencedEnvelope(
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
bbbox =
mapMLLayerMetadata.isLayerGroup
? mapMLLayerMetadata.getLayerGroupInfo().getBounds()
Expand All @@ -1378,10 +1384,14 @@ public void generateWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) {
// the projectedBox.transform will leave the CRS set to that of whatever
// was returned by layerInfo.getResource().boundingBox() or
// layerGroupInfo.getBounds(), above.
bbbox = bbbox.transform(PREVIEW_TCRS_MAP.get(projType.value()).getCRS(), true);
bbbox =
bbbox.transform(
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS(),
true);
} catch (Exception e) {
// get the default max/min of the pcrs from the TCRS
Bounds defaultBounds = PREVIEW_TCRS_MAP.get(projType.value()).getBounds();
Bounds defaultBounds =
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getBounds();
double x1, x2, y1, y2;
x1 = defaultBounds.getMin().x;
x2 = defaultBounds.getMax().x;
Expand All @@ -1390,7 +1400,11 @@ public void generateWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) {
// use the bounds of the TCRS as the default bounds for this layer
bbbox =
new ReferencedEnvelope(
x1, x2, y1, y2, PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
x1,
x2,
y1,
y2,
MapMLHTMLOutput.PREVIEW_TCRS_MAP.get(projType.value()).getCRS());
}
}

Expand Down Expand Up @@ -1657,12 +1671,10 @@ public String getMapMLHTMLDocument() {
String layer = "";
String styleName = "";
String cqlFilter = "";
int zoom = 0;
Double latitude = 0.0;
Double longitude = 0.0;
ReferencedEnvelope projectedBbox = this.projectedBox;
ReferencedEnvelope geographicBox = new ReferencedEnvelope(DefaultGeographicCRS.WGS84);
TiledCRS tcrs = PREVIEW_TCRS_MAP.get(projType.value());
for (MapMLLayerMetadata mapMLLayerMetadata : mapMLLayerMetadataList) {
layer += mapMLLayerMetadata.getLayerName() + ",";
styleName += mapMLLayerMetadata.getStyleName() + ",";
Expand Down Expand Up @@ -1708,77 +1720,26 @@ public String getMapMLHTMLDocument() {
if (ALL_COMMAS.matcher(cqlFilter).matches()) {
cqlFilter = "";
}
final Bounds pb =
new Bounds(
new Point(projectedBbox.getMinX(), projectedBbox.getMinY()),
new Point(projectedBbox.getMaxX(), projectedBbox.getMaxY()));
// allowing for the data to be displayed at 1024x768 pixels, figure out
// the zoom level at which the projected bounds fits into 1024x768
// in both dimensions
zoom = tcrs.fitProjectedBoundsToDisplay(pb, DISPLAY_BOUNDS_DESKTOP_LANDSCAPE);
String base = ResponseUtils.baseURL(request);
String viewerPath =
ResponseUtils.buildURL(
base,
"/mapml/viewer/widget/mapml-viewer.js",
null,
URLMangler.URLType.RESOURCE);
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n")
.append("<html>\n")
.append("<head>\n")
.append("<title>")
.append(escapeHtml4(layerLabel))
.append("</title>\n")
.append("<meta charset='utf-8'>\n")
.append("<script type=\"module\" src=\"")
.append(viewerPath)
.append("\"></script>\n")
.append("<style>\n")
.append("html, body { height: 100%; }\n")
.append("* { margin: 0; padding: 0; }\n")
.append(
"mapml-viewer:defined { max-width: 100%; width: 100%; height: 100%; border: none; vertical-align: middle }\n")
.append("mapml-viewer:not(:defined) > * { display: none; } n")
.append("layer- { display: none; }\n")
.append("</style>\n")
.append("<noscript>\n")
.append("<style>\n")
.append("mapml-viewer:not(:defined) > :not(layer-) { display: initial; }\n")
.append("</style>\n")
.append("</noscript>\n")
.append("</head>\n")
.append("<body>\n")
.append("<mapml-viewer projection=\"")
.append(projType.value())
.append("\" ")
.append("zoom=\"")
.append(zoom)
.append("\" lat=\"")
.append(latitude)
.append("\" ")
.append("lon=\"")
.append(longitude)
.append("\" controls controlslist=\"geolocation\">\n")
.append("<layer- label=\"")
.append(escapeHtml4(layerLabel))
.append("\" ")
.append("src=\"")
.append(
buildGetMap(
layer,
projectedBbox,
width,
height,
escapeHtml4(proj),
styleName,
format,
cqlFilter))
.append("\" checked></layer->\n")
.append("</mapml-viewer>\n")
.append("</body>\n")
.append("</html>");
return sb.toString();
MapMLHTMLOutput htmlOutput =
new MapMLHTMLOutput.HTMLOutputBuilder()
.setSourceUrL(
buildGetMap(
layer,
projectedBbox,
width,
height,
escapeHtml4(proj),
styleName,
format,
cqlFilter))
.setProjType(projType)
.setLatitude(latitude)
.setLongitude(longitude)
.setRequest(request)
.setProjectedBbox(projectedBbox)
.setLayerLabel(layerLabel)
.build();
return htmlOutput.toHTML();
}

/** Builds the GetMap backlink to get MapML */
Expand Down
Loading
Loading