From ef6a006295ea5feb64e13199524aa1c3b1ea2e65 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Sun, 7 Jul 2024 16:38:48 +0200 Subject: [PATCH 1/6] Attempt updating to latest marlin --- src/pom.xml | 2 +- src/release/bin/startup.bat | 2 +- src/release/bin/startup.sh | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pom.xml b/src/pom.xml index 909827a0b95..582b2df3f16 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -140,7 +140,7 @@ 2.17.1 ${jackson2.version} 1.0.3 - 0.9.3 + 0.9.4.8 42.7.3 1.3.3 19.18.0.0 diff --git a/src/release/bin/startup.bat b/src/release/bin/startup.bat index 46faf0ba918..bde21acdb71 100755 --- a/src/release/bin/startup.bat +++ b/src/release/bin/startup.bat @@ -135,7 +135,7 @@ goto setMarlinRenderer echo Marlin renderer jar not found goto run ) - set MARLIN_ENABLER=-Xbootclasspath/a:"%MARLIN_JAR%" -Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine + set MARLIN_ENABLER=--patch-module java.desktop="%MARLIN_JAR%" set JAVA_OPTS=%JAVA_OPTS% %MARLIN_ENABLER% goto run diff --git a/src/release/bin/startup.sh b/src/release/bin/startup.sh index ce0f42b3de0..e9d3f58f027 100755 --- a/src/release/bin/startup.sh +++ b/src/release/bin/startup.sh @@ -79,9 +79,8 @@ cd "${GEOSERVER_HOME}" || exit 1 if [ -z "${MARLIN_JAR:-}" ]; then MARLIN_JAR=$(find "$(pwd)/webapps" -name "marlin*.jar" | head -1) if [ "${MARLIN_JAR:-}" != "" ]; then - MARLIN_ENABLER="-Xbootclasspath/a:${MARLIN_JAR}" - RENDERER="-Dsun.java2d.renderer=org.marlin.pisces.MarlinRenderingEngine" - export MARLIN_JAR MARLIN_ENABLER RENDERER + MARLIN_ENABLER="--patch-module java.desktop=${MARLIN_JAR}" + export MARLIN_ENABLER fi fi @@ -89,4 +88,4 @@ echo "GEOSERVER DATA DIR is ${GEOSERVER_DATA_DIR}" #added headless to true by default, if this messes anyone up let the list #know and we can change it back, but it seems like it won't hurt -ch IFS=$(printf '\n\t ') -exec "${_RUNJAVA}" ${JAVA_OPTS:--DNoJavaOpts} "${MARLIN_ENABLER:--DMarlinDisabled}" "${RENDERER:--DDefaultrenderer}" "-Djetty.base=${GEOSERVER_HOME}" "-DGEOSERVER_DATA_DIR=${GEOSERVER_DATA_DIR}" -Djava.awt.headless=true -DSTOP.PORT=8079 -DSTOP.KEY=geoserver -jar "${GEOSERVER_HOME}/start.jar" +exec "${_RUNJAVA}" ${JAVA_OPTS:--DNoJavaOpts} ${MARLIN_ENABLER:--DMarlinDisabled} "-Djetty.base=${GEOSERVER_HOME}" "-DGEOSERVER_DATA_DIR=${GEOSERVER_DATA_DIR}" -Djava.awt.headless=true -DSTOP.PORT=8079 -DSTOP.KEY=geoserver -jar "${GEOSERVER_HOME}/start.jar" From d8e38814dff0475073e7073b9f73aae81ed8bb3c Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Sat, 13 Jul 2024 14:16:50 +0200 Subject: [PATCH 2/6] Applying review feedback --- doc/en/user/source/production/java.rst | 2 +- src/release/bin/startup.bat | 2 ++ src/release/bin/startup.sh | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/en/user/source/production/java.rst b/doc/en/user/source/production/java.rst index 8b12b55ad9e..d60314ce2c2 100644 --- a/doc/en/user/source/production/java.rst +++ b/doc/en/user/source/production/java.rst @@ -38,7 +38,7 @@ GeoServer code depends on a variety of libraries trying to access the JDK intern it does not seem to matter when running as a web application. However, in case of need, here is the full list of opens used by the build process:: - --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED + --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED Running on Java 11 ------------------ diff --git a/src/release/bin/startup.bat b/src/release/bin/startup.bat index bde21acdb71..3c0e5b4cc71 100755 --- a/src/release/bin/startup.bat +++ b/src/release/bin/startup.bat @@ -139,6 +139,8 @@ goto setMarlinRenderer set JAVA_OPTS=%JAVA_OPTS% %MARLIN_ENABLER% goto run +set JAVA_OPTS=%JAVA_OPTS% --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED + :run cd "%GEOSERVER_HOME%" echo Please wait while loading GeoServer... diff --git a/src/release/bin/startup.sh b/src/release/bin/startup.sh index e9d3f58f027..4f0eba3c944 100755 --- a/src/release/bin/startup.sh +++ b/src/release/bin/startup.sh @@ -84,6 +84,9 @@ if [ -z "${MARLIN_JAR:-}" ]; then fi fi +JAVA_OPTS="${JAVA_OPTS:+"$JAVA_OPTS"} --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED" + + echo "GEOSERVER DATA DIR is ${GEOSERVER_DATA_DIR}" #added headless to true by default, if this messes anyone up let the list #know and we can change it back, but it seems like it won't hurt -ch From e629d0c8f1bfc721c7b831478025458eff78fa51 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Sat, 13 Jul 2024 14:41:35 +0200 Subject: [PATCH 3/6] One more doc clarification, fiddling with the startup scripts no longer needed --- doc/en/user/source/production/java.rst | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/doc/en/user/source/production/java.rst b/doc/en/user/source/production/java.rst index d60314ce2c2..582af128af2 100644 --- a/doc/en/user/source/production/java.rst +++ b/doc/en/user/source/production/java.rst @@ -28,22 +28,16 @@ GeoServer is compatible with Java 17, but requires extra care for running in som Deployment on Tomcat 9.0.55 has been tested with success. -The "bin" packaging can work too, but requires turning off the Marlin rasterizer integration. -This can be done by modifying the scripts, or by simply removing the Marlin jars:: - - rm webapps/geoserver/WEB-INF/lib/marlin-0.9.3.jar - - -GeoServer code depends on a variety of libraries trying to access the JDK internals. As reported above, -it does not seem to matter when running as a web application. However, in case of need, here is -the full list of opens used by the build process:: +GeoServer code depends on a variety of libraries trying to access the JDK internals. +It does not seem to matter when running as a web application. However, in case of need, +here is the full list of opens used by the build process:: --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED Running on Java 11 ------------------ -GeoServer 2.15 will run under Java 11 with no additional configuration on **Tomcat 9** or newer and **Jetty 9.4.12** or newer. +GeoServer 2.15 and above will run under Java 11 with no additional configuration on **Tomcat 9** or newer and **Jetty 9.4.12** or newer. Running GeoServer under Java 11 on other Application Servers may require some additional configuration. Some Application Servers do not support Java 11 yet. From 1cfc346258cdbeee9aea8fb89db1459045935825 Mon Sep 17 00:00:00 2001 From: Peter Smythe Date: Thu, 1 Aug 2024 17:23:24 +0200 Subject: [PATCH 4/6] Move set JAVA_OPTS to the right place, no quoting necessary --- src/release/bin/startup.bat | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/release/bin/startup.bat b/src/release/bin/startup.bat index 3c0e5b4cc71..d5c39bcde63 100755 --- a/src/release/bin/startup.bat +++ b/src/release/bin/startup.bat @@ -139,9 +139,10 @@ goto setMarlinRenderer set JAVA_OPTS=%JAVA_OPTS% %MARLIN_ENABLER% goto run -set JAVA_OPTS=%JAVA_OPTS% --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED - :run + + set JAVA_OPTS=%JAVA_OPTS% --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED --add-opens=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.desktop/sun.java2d.pipe=ALL-UNNAMED + cd "%GEOSERVER_HOME%" echo Please wait while loading GeoServer... echo. From b613e24f1b729a181e96e7ba0b7388709a456c07 Mon Sep 17 00:00:00 2001 From: Joe <31628530+turingtestfail@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:43:01 -0700 Subject: [PATCH 5/6] [GEOS-11368] Allow Freemarker templates to update MapML responses (#7804) * [GEOS-11368] Allow Freemarker templates to update MapML responses started on template page ftl file names correction fixed xml code block and added map-property Created preview header integration test and started extracting template swapped in better method for empty feature finished first integration test switched to dedicated template head template style registering servicelink fixed servicelink changed serviceLink to base,path,kvp cleanup dollar signs breaking code block started on subfeature span insertion with integration test Got point xml interpolation working progressing on polygon Got polygon unmarshal working got multipolygon to unmarshal changed strings to coords and linestring parsing removed interpolated, created head styles using basic mapml attributes and point test working and phantom space removed polygon and multipolygon tests added feature id check to multipolygon test tests with space replacement version that uses lists of coordinates instead of strings removed some other remainder stuff from the space thing cleanup added check for tagged geom doc update PR changes, mainly map-span tests updated without xml escape updated documentation removing cdata escaping added template attributes to mapml feature fixed attribute replacement and added documentation examples more doc update line test fix fixed issue with geometrycollection and updated documentation with example better output if error format coordinates, including number of decimals fixed pmd PR doc updates and started on a wrapper fixed underline in template doc preview header change restored test remoe ipr iws simplified optional doc updates * Small doc edits * Make MapMLMapTemplate thread safe * Make MapMLMapTemplate thread safe --------- Co-authored-by: Andrea Aime --- doc/en/user/source/extensions/mapml/index.rst | 307 +----------- .../source/extensions/mapml/installation.rst | 319 +++++++++++++ .../user/source/extensions/mapml/template.rst | 442 ++++++++++++++++++ .../geoserver/mapml/MapMLDocumentBuilder.java | 282 +++++++++++ .../org/geoserver/mapml/MapMLEncoder.java | 2 + .../org/geoserver/mapml/MapMLFeatureUtil.java | 101 +++- .../org/geoserver/mapml/MapMLGenerator.java | 218 ++++++++- .../MapMLGetFeatureInfoOutputFormat.java | 3 +- .../org/geoserver/mapml/MapMLHTMLOutput.java | 9 + .../mapml/template/CharArrayWriterPool.java | 45 ++ .../mapml/template/MapMLMapTemplate.java | 288 ++++++++++++ .../main/java/org/geoserver/mapml/xml/A.java | 59 +++ .../mapml/xml/GeometryCollection.java | 8 +- .../org/geoserver/mapml/xml/LineString.java | 14 +- .../geoserver/mapml/xml/MultiLineString.java | 8 +- .../org/geoserver/mapml/xml/MultiPoint.java | 14 +- .../geoserver/mapml/xml/ObjectFactory.java | 15 +- .../java/org/geoserver/mapml/xml/Point.java | 14 +- .../src/main/resources/applicationContext.xml | 1 + .../geoserver/mapml/MapMLWMSFeatureTest.java | 334 ++++++++++++- .../org/geoserver/mapml/MapMLWMSTest.java | 95 ++++ 21 files changed, 2215 insertions(+), 363 deletions(-) create mode 100644 doc/en/user/source/extensions/mapml/installation.rst create mode 100644 doc/en/user/source/extensions/mapml/template.rst create mode 100644 src/extension/mapml/src/main/java/org/geoserver/mapml/template/CharArrayWriterPool.java create mode 100644 src/extension/mapml/src/main/java/org/geoserver/mapml/template/MapMLMapTemplate.java create mode 100644 src/extension/mapml/src/main/java/org/geoserver/mapml/xml/A.java diff --git a/doc/en/user/source/extensions/mapml/index.rst b/doc/en/user/source/extensions/mapml/index.rst index 191625a8e4e..f110f4137d7 100644 --- a/doc/en/user/source/extensions/mapml/index.rst +++ b/doc/en/user/source/extensions/mapml/index.rst @@ -12,308 +12,9 @@ The MapML module for GeoServer adds new MapML resources to access WMS, WMTS and .. warning:: MapML is an experimental proposed extension of HTML for the Web. The objective of the project is to standardize map, layer and feature semantics in HTML. As the format progresses towards a Web standard, it may change slightly. Always use the latest version of this extension, and follow or join in the project's progress at https://maps4html.org. -Installation --------------------- - -#. Visit the :website:`website download ` page, locate your release, and download: :download_extension:`mapml` +.. toctree:: + :maxdepth: 2 - .. warning:: Make sure to match the version of the extension (for example |release| above) to the version of the GeoServer instance! - -#. Extract the contents of the archive into the :file:`WEB-INF/lib` directory of the GeoServer installation. - -#. Restart GeoServer. - -Configuration -------------- - -Configuration can be done using the Geoserver administrator GUI. The MapML configuration is accessible in the *MapML Settings* section under the *Publishing* tab of the Layer or Layer Group Configuration page for the layer or layer group being configured. Here is the MapML Settings section, with some example values filled in: - -.. figure:: images/mapml_config_ui.png - -There is also a MapML-specific global WMS setting in the *MapML Extension* section of the ``WMS`` Services Settings Page. This setting is used to control the handling of multi-layer requests. - -.. figure:: images/mapml_config_wms.png - -If the ``Represent multi-layer requests as multiple elements`` is checked (and the configuration is saved), an individually accessible element will be generated for each requested layer. The default is to represent the layers as a single (hidden) . - -.. figure:: images/mapml_wms_multi_extent.png - -Styles ------- - -Like any WMS layer or layer group available from GeoServer, a comma-separated list of styles may be supplied in the WMS GetMap `styles` parameter. If no style name is requested, the default style will be used for that layer. For single-layer or single-layer group requests, the set of alternate styles is presented as an option list in the layer preview map's layer control, with the currently requested style indicated. - -.. figure:: images/mapml_preview_multiple_styles_menu.png - -Note that in order to ensure that the default layer style is properly available to the preview map's option list, make sure that the style is moved to the ``Available Styles`` list in the ``Publishing`` tab of the Layer Configuration page. If the style is set to ``Default`` but not explicitly made ``Available``, the style will not be available to MapML. Similarly but a with a slight variation in requirement, for Layer Groups, the 'default' layer group style must be copied and given a name matching `default-style-` plus the layer group name. - -License Info -^^^^^^^^^^^^ - -Together these two attributes allow the administrator to define the contents of the ```` element in the MapML header. Here is an example of the resulting XML: - - - -**License Title** - The License Title will be included as the value of ``title`` attribute of the ```` element in the MapML header. - -**License Link** - The License Link will be included as the value of ``href`` attribute of the ```` element in the MapML header, and should be a valid URL referencing the license document. - - -Tile Settings -^^^^^^^^^^^^^ - -Using tiles to access the layer can increase the performance of your web map. This is especially true if there is a tile cache mechanism in use between GeoServer and the browser client. - -**Use Tiles** - If the "Use Tiles" checkbox is checked, by default the output MapML will define a tile-based reference to the WMS server. Otherwise, an image-based reference will be used. If one or more of the MapML-defined GridSets is referenced by the layer or layer group in its "Tile Caching" profile, GeoServer will generate tile references instead of generating WMS GetMap URLs in the MapML document body. - -Vector Settings -^^^^^^^^^^^^^^^ - -MapML supports the serving of vector feature representations of the data. This results in a smoother user navigation experience, smaller bandwidth requirements, and more options for dynamic styling on the client-side. - -**Use Features** - If the "Use Features" checkbox is checked, by default the output MapML will define a feature-based reference to the WMS server. Otherwise, an image-based reference will be used. Note that this option is only available for vector source data. MapML element with a feature link: - -.. code-block:: html - - - - - - - - - - - - -When both "Use Tiles" and "Use Features" are checked, the MapML extension will request tiled maps in ``text/mapml`` format. -The contents of the tiles will be clipped to the requested area, and feature attributes will be skiipped, as the MapML client cannot leverage them for the moment. - - -**Feature Styling** - Basic styling of vector features is supported by the MapML extension. The style is defined in the WMS GetMap request, and the MapML extension will convert the rules and style attributes defined in the SLD into CSS classes and apply those classes to the appropriate features. Note that this conversion is currently limited to basic styling and does not include transformation functions, external graphics, or styling dependent on individual feature attributes (non-static style values). See below for a more detailed compatibility table: - -+------------------+-------------------+-----------+ -| Symbolizer | Style Attribute | Supported | -+==================+===================+===========+ -| PointSymbolizer | Opacity | yes | -| +-------------------+-----------+ -| | Default Radius | yes | -| +-------------------+-----------+ -| | Radius | yes | -| +-------------------+-----------+ -| | Rotation | no | -| +-------------------+-----------+ -| | Displacement | no | -| +-------------------+-----------+ -| | Anchor Point | no | -| +-------------------+-----------+ -| | Gap | no | -| +-------------------+-----------+ -| | Initial Gap | no | -| +-------------------+-----------+ -| | Well Known Name | yes | -| +-------------------+-----------+ -| | External Mark | no | -| +-------------------+-----------+ -| | Graphic Fill | no | -| +-------------------+-----------+ -| | Fill Color | yes | -| +-------------------+-----------+ -| | Fill Opacity | yes | -| +-------------------+-----------+ -| | Stroke Color | yes | -| +-------------------+-----------+ -| | Stroke Opacity | yes | -| +-------------------+-----------+ -| | Stroke Width | yes | -| +-------------------+-----------+ -| | Stroke Linecap | yes | -| +-------------------+-----------+ -| | Stroke Dash Array | yes | -| +-------------------+-----------+ -| | Stroke Dash Offset| yes | -| +-------------------+-----------+ -| | Stroke Line Join | no | -+------------------+-------------------+-----------+ -| LineSymbolizer | Stroke Linecap | yes | -| +-------------------+-----------+ -| | Stroke Dash Array | yes | -| +-------------------+-----------+ -| | Stroke Dash Offset| yes | -| +-------------------+-----------+ -| | Stroke Line Join | no | -+------------------+-------------------+-----------+ -| PolygonSymbolizer| Displacement | no | -| +-------------------+-----------+ -| | Perpendicular Offs| no | -| +-------------------+-----------+ -| | Graphic Fill | no | -| +-------------------+-----------+ -| | Fill Color | yes | -| +-------------------+-----------+ -| | Fill Opacity | yes | -| +-------------------+-----------+ -| | Stroke Color | yes | -| +-------------------+-----------+ -| | Stroke Opacity | yes | -| +-------------------+-----------+ -| | Stroke Width | yes | -| +-------------------+-----------+ -| | Stroke Linecap | yes | -| +-------------------+-----------+ -| | Stroke Dash Array | yes | -| +-------------------+-----------+ -| | Stroke Dash Offset| yes | -| +-------------------+-----------+ -| | Stroke Line Join | no | -+------------------+-------------------+-----------+ -| TextSymbolizer | ALL | no | -+------------------+-------------------+-----------+ -| RasterSymbolizer | ALL | no | -+------------------+-------------------+-----------+ -| Transformation | ALL | no | -| Functions | | | -+------------------+-------------------+-----------+ -| Zoom | ALL | yes | -| Denominators | | | -+------------------+-------------------+-----------+ - - -WMS GetMap considerations -^^^^^^^^^^^^^^^^^^^^^^^^^ - -By default, each layer/style pair that is requested via the GetMap parameters is composed into a single ...... structure as exemplified above. - -If the 'Represent multi-layer requests as multiple elements' checkbox from the global WMS Settings page is checked as described above, a request for multiple layers or layer groups in MapML format will result in the serialization of a MapML document containing multiple elements. Each layer/style pair is represented by a element in the response. The elements are represented in the client viewer layer control settings as sub-layers, which turn on and off independently of each other, but which are controlled by the parent element's state (checked / unchecked, opacity etc) (right-click or Shift+F10 to obtain context menus): - -.. figure:: images/mapml_wms_multi_extent.png - -With 'Represent multi-layer requests as multiple elements' checked, if two or more layers are requested in MapML format via the GetMap 'layers' parameter, the MapML extension serialize each layer's according to its "Use Features" and "Use Tiles" settings. Note that there is currently no "Use Features" setting available for layer groups. - -Tile Caching -^^^^^^^^^^^^ - -In the Tile Caching tab panel of the Edit Layer or Edit Layer Group page, at the bottom of the page you will see the table of GridSets that are assigned to the layer or layer group. - -The values ``WGS84`` and ``OSMTILE`` are equivalent to the EPSG:4326 and EPSG:900913 built in GeoWebCache GridSets. -However, for the MapML module to recognize these GridSets, you must select and use the MapML names. For new layers or layer groups, or newly created grid subsets for a layer or layer group, the MapML values are selected by default. For existing layers that you wish to enable the use of cached tile references by the MapML service, you will have to select and add those values you wish to support from the dropdown of available GridSets. The set of recognized values for MapML is ``WGS84`` (equivalent to EPSG:4326), ``OSMTILE`` (equivalent to EPSG:900913), ``CBMTILE`` (Canada Base Map) and ``APSTILE`` (Alaska Polar Stereographic). - -The MapML client will normally request image tiles against WMTS, but if configured to use feature output, -it will try to use tiles in ``text/mapml`` format, which should be configured as a cacheable format -in order to enable WMTS requests. - -.. figure:: images/mapml_tile_caching_panel_ui.png - -Starting with version 2.26.x of GeoServer, Sharding support and related configuration has been removed. - -Dimension Config -^^^^^^^^^^^^^^^^ - -**Dimension** - The selected dimension (if any) is advertised in the mapml as an input with the appropriate value options or ranges, as configured in the *Dimension* tab of the Layer Configuration page. Only dimensions enabled in the *Dimension* tab are available as options. - -Attribute to mapping -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**List of attributes** -The list allows you to read the names of the layer attributes, it doesn't really do more than that. - -**Feature Caption Template String** - -To cause an attribute to be serialized in MapML vector content as the element value, -you must enter its name as a ${placeholder} in the text box immediately below the attributes list. You can also add (a small amount of) plain text that will be -copied verbatim into the content. is used as the accessible name of features by screen reader software, which will often -read out this value without the user having to expand a popup; in other words, it will be used as a visual and audible tooltip when the -feature is focused. - - -MapML Resources ---------------- - -MapML resources will be available for any published WMS layers by making a GetMap request with the WMS output format to ``format=text/mapml``. See :ref:`WMS` for further WMS details, :ref:`wms_getmap` for GetMap details, and :ref:`wms_output_formats` for output format reference information. - -**SRS/CRS** - -Note that the WMS SRS or CRS must be one of the projections supported by MapML: - -- 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** - -The output image format for the MapML resource should be specified using the format_options parameter with a key called ``mapml-wms-format``. If provided, the provided mime type must be a valid WMS format specifier. If not provided, it defaults to to the format set with the Default Mime Type dropdown under MapML Settings in the Publishing tab of the Edit Layer settings page. - -Example:: - - http://localhost:8080/geoserver/tiger/wms?service=WMS&version=1.1.0&request=GetMap&layers=tiger:giant_polygon&bbox=-180.0,-90.0,180.0,90.0&width=768&height=384&srs=EPSG:4326&styles=&format=text/mapml&format_options=mapml-wms-format:image/jpeg - -MapML Visualization -------------------- - -With the MapML Extension module installed, the GeoServer Layer Preview page is modified to add a WMS GetMap link to the MapML resources for each layer or layer group. The MapML link in the Layer Preview table is generated by the MapML extension to an HTML Web map page that is created on the fly which refers to the GeoServer resource: - -.. figure:: images/mapml_preview_ui.png - -You can add layers to the map as you like, by dragging the URL bar value generated by the the Layer Preview WMS formats dropdown menu selection of "MapML" as shown below, and dropping it onto another layer's MapML preview: - -.. figure:: images/mapml_wms_format_dropdown.png - -If all goes well, you should see the layers stacked on the map and in the layer control. - -MapML visualization is supported by the Web-Map-Custom-Element project. The MapML viewer is built into the GeoServer layer and layer group preview facility. You can find out more about the Web-Map-Custom-Element at the project `website `. Here is a simple, self-contained example of an HTML page that uses the and elements: - -.. code-block:: html - - - - - - MapML Test Map - - - - - - - - - - - -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 - - - -.. warning:: Note that the MapML WFS output will automatically set a default max feature limit. Removing that limit can lead to browser issues. - + installation + template diff --git a/doc/en/user/source/extensions/mapml/installation.rst b/doc/en/user/source/extensions/mapml/installation.rst new file mode 100644 index 00000000000..c01992e4e16 --- /dev/null +++ b/doc/en/user/source/extensions/mapml/installation.rst @@ -0,0 +1,319 @@ +Installation +-------------------- + +#. Visit the :website:`website download ` page, locate your release, and download: :download_extension:`mapml` + + .. warning:: Make sure to match the version of the extension (for example |release| above) to the version of the GeoServer instance! + +#. Extract the contents of the archive into the :file:`WEB-INF/lib` directory of the GeoServer installation. + +#. Restart GeoServer. + +Configuration +------------- + +Configuration can be done using the Geoserver administrator GUI. The MapML configuration is accessible in the *MapML Settings* section under the *Publishing* tab of the Layer or Layer Group Configuration page for the layer or layer group being configured. Here is the MapML Settings section, with some example values filled in: + +.. figure:: images/mapml_config_ui.png + +There is also a MapML-specific global WMS setting in the *MapML Extension* section of the ``WMS`` Services Settings Page. This setting is used to control the handling of multi-layer requests. + +.. figure:: images/mapml_config_wms.png + +If the ``Represent multi-layer requests as multiple elements`` is checked (and the configuration is saved), an individually accessible element will be generated for each requested layer. The default is to represent the layers as a single (hidden) . + +.. figure:: images/mapml_wms_multi_extent.png + +Styles +------ + +Like any WMS layer or layer group available from GeoServer, a comma-separated list of styles may be supplied in the WMS GetMap `styles` parameter. If no style name is requested, the default style will be used for that layer. For single-layer or single-layer group requests, the set of alternate styles is presented as an option list in the layer preview map's layer control, with the currently requested style indicated. + +.. figure:: images/mapml_preview_multiple_styles_menu.png + +Note that in order to ensure that the default layer style is properly available to the preview map's option list, make sure that the style is moved to the ``Available Styles`` list in the ``Publishing`` tab of the Layer Configuration page. If the style is set to ``Default`` but not explicitly made ``Available``, the style will not be available to MapML. Similarly but a with a slight variation in requirement, for Layer Groups, the 'default' layer group style must be copied and given a name matching `default-style-` plus the layer group name. + +License Info +^^^^^^^^^^^^ + +Together these two attributes allow the administrator to define the contents of the ```` element in the MapML header. Here is an example of the resulting XML: + + + +**License Title** + The License Title will be included as the value of ``title`` attribute of the ```` element in the MapML header. + +**License Link** + The License Link will be included as the value of ``href`` attribute of the ```` element in the MapML header, and should be a valid URL referencing the license document. + + +Tile Settings +^^^^^^^^^^^^^ + +Using tiles to access the layer can increase the performance of your web map. This is especially true if there is a tile cache mechanism in use between GeoServer and the browser client. + +**Use Tiles** + If the "Use Tiles" checkbox is checked, by default the output MapML will define a tile-based reference to the WMS server. Otherwise, an image-based reference will be used. If one or more of the MapML-defined GridSets is referenced by the layer or layer group in its "Tile Caching" profile, GeoServer will generate tile references instead of generating WMS GetMap URLs in the MapML document body. + +Vector Settings +^^^^^^^^^^^^^^^ + +MapML supports the serving of vector feature representations of the data. This results in a smoother user navigation experience, smaller bandwidth requirements, and more options for dynamic styling on the client-side. + +**Use Features** + If the "Use Features" checkbox is checked, by default the output MapML will define a feature-based reference to the WMS server. Otherwise, an image-based reference will be used. Note that this option is only available for vector source data. MapML element with a feature link: + +.. code-block:: html + + + + + + + + + + + + +When both "Use Tiles" and "Use Features" are checked, the MapML extension will request tiled maps in ``text/mapml`` format. +The contents of the tiles will be clipped to the requested area, and feature attributes will be skiipped, as the MapML client cannot leverage them for the moment. + + +**Feature Styling** + Basic styling of vector features is supported by the MapML extension. The style is defined in the WMS GetMap request, and the MapML extension will convert the rules and style attributes defined in the SLD into CSS classes and apply those classes to the appropriate features. Note that this conversion is currently limited to basic styling and does not include transformation functions, external graphics, or styling dependent on individual feature attributes (non-static style values). See below for a more detailed compatibility table: + ++------------------+-------------------+-----------+ +| Symbolizer | Style Attribute | Supported | ++==================+===================+===========+ +| PointSymbolizer | Opacity | yes | +| +-------------------+-----------+ +| | Default Radius | yes | +| +-------------------+-----------+ +| | Radius | yes | +| +-------------------+-----------+ +| | Rotation | no | +| +-------------------+-----------+ +| | Displacement | no | +| +-------------------+-----------+ +| | Anchor Point | no | +| +-------------------+-----------+ +| | Gap | no | +| +-------------------+-----------+ +| | Initial Gap | no | +| +-------------------+-----------+ +| | Well Known Name | yes | +| +-------------------+-----------+ +| | External Mark | no | +| +-------------------+-----------+ +| | Graphic Fill | no | +| +-------------------+-----------+ +| | Fill Color | yes | +| +-------------------+-----------+ +| | Fill Opacity | yes | +| +-------------------+-----------+ +| | Stroke Color | yes | +| +-------------------+-----------+ +| | Stroke Opacity | yes | +| +-------------------+-----------+ +| | Stroke Width | yes | +| +-------------------+-----------+ +| | Stroke Linecap | yes | +| +-------------------+-----------+ +| | Stroke Dash Array | yes | +| +-------------------+-----------+ +| | Stroke Dash Offset| yes | +| +-------------------+-----------+ +| | Stroke Line Join | no | ++------------------+-------------------+-----------+ +| LineSymbolizer | Stroke Linecap | yes | +| +-------------------+-----------+ +| | Stroke Dash Array | yes | +| +-------------------+-----------+ +| | Stroke Dash Offset| yes | +| +-------------------+-----------+ +| | Stroke Line Join | no | ++------------------+-------------------+-----------+ +| PolygonSymbolizer| Displacement | no | +| +-------------------+-----------+ +| | Perpendicular Offs| no | +| +-------------------+-----------+ +| | Graphic Fill | no | +| +-------------------+-----------+ +| | Fill Color | yes | +| +-------------------+-----------+ +| | Fill Opacity | yes | +| +-------------------+-----------+ +| | Stroke Color | yes | +| +-------------------+-----------+ +| | Stroke Opacity | yes | +| +-------------------+-----------+ +| | Stroke Width | yes | +| +-------------------+-----------+ +| | Stroke Linecap | yes | +| +-------------------+-----------+ +| | Stroke Dash Array | yes | +| +-------------------+-----------+ +| | Stroke Dash Offset| yes | +| +-------------------+-----------+ +| | Stroke Line Join | no | ++------------------+-------------------+-----------+ +| TextSymbolizer | ALL | no | ++------------------+-------------------+-----------+ +| RasterSymbolizer | ALL | no | ++------------------+-------------------+-----------+ +| Transformation | ALL | no | +| Functions | | | ++------------------+-------------------+-----------+ +| Zoom | ALL | yes | +| Denominators | | | ++------------------+-------------------+-----------+ + + +WMS GetMap considerations +^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, each layer/style pair that is requested via the GetMap parameters is composed into a single ...... structure as exemplified above. + +If the 'Represent multi-layer requests as multiple elements' checkbox from the global WMS Settings page is checked as described above, a request for multiple layers or layer groups in MapML format will result in the serialization of a MapML document containing multiple elements. Each layer/style pair is represented by a element in the response. The elements are represented in the client viewer layer control settings as sub-layers, which turn on and off independently of each other, but which are controlled by the parent element's state (checked / unchecked, opacity etc) (right-click or Shift+F10 to obtain context menus): + +.. figure:: images/mapml_wms_multi_extent.png + +With 'Represent multi-layer requests as multiple elements' checked, if two or more layers are requested in MapML format via the GetMap 'layers' parameter, the MapML extension serialize each layer's according to its "Use Features" and "Use Tiles" settings. Note that there is currently no "Use Features" setting available for layer groups. + +Tile Caching +^^^^^^^^^^^^ + +In the Tile Caching tab panel of the Edit Layer or Edit Layer Group page, at the bottom of the page you will see the table of GridSets that are assigned to the layer or layer group. + +The values ``WGS84`` and ``OSMTILE`` are equivalent to the EPSG:4326 and EPSG:900913 built in GeoWebCache GridSets. +However, for the MapML module to recognize these GridSets, you must select and use the MapML names. For new layers or layer groups, or newly created grid subsets for a layer or layer group, the MapML values are selected by default. For existing layers that you wish to enable the use of cached tile references by the MapML service, you will have to select and add those values you wish to support from the dropdown of available GridSets. The set of recognized values for MapML is ``WGS84`` (equivalent to EPSG:4326), ``OSMTILE`` (equivalent to EPSG:900913), ``CBMTILE`` (Canada Base Map) and ``APSTILE`` (Alaska Polar Stereographic). + +The MapML client will normally request image tiles against WMTS, but if configured to use feature output, +it will try to use tiles in ``text/mapml`` format, which should be configured as a cacheable format +in order to enable WMTS requests. + +.. figure:: images/mapml_tile_caching_panel_ui.png + +Sharding Config +^^^^^^^^^^^^^^^^ + +The Sharding Config options are intended to allow for parallel access to tiles via different server names. The actual server names must be configured in the DNS to refer to either the same server or different servers with the same GeSserver layer configuration. In the example above, the mapML client would alternate between the servers a.geoserver.org, b.geoserver.org, and c.geoserver.org to access the map images. The values in the example above would result in the following MapML: + +.. code-block:: html + + + + + + + +**Enable Sharding** + If Enable Sharding is checked, and values are provided for the Shard List and Shard Server Pattern fields, then a hidden shard list input will be included in the MapML. + +**Shard List** + If Enable Sharding is checked, the Shard List should be populated with a comma-separated list of shard names which will be used to populate the shard data list element. + +**Shard Server Pattern** + The Shard Server Pattern should be a valid DNS name including the special placeholder string {s} which will be replace with the Shard Names from the Shard List in requests to the server. This pattern should not include any slashes, the protocol string (http://) or the port number (:80), as these are all determined automatically from the URL used to access the MapML resource. + + +Dimension Config +^^^^^^^^^^^^^^^^ + +**Dimension** + The selected dimension (if any) is advertised in the mapml as an input with the appropriate value options or ranges, as configured in the *Dimension* tab of the Layer Configuration page. Only dimensions enabled in the *Dimension* tab are available as options. + +Attribute to mapping +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**List of attributes** +The list allows you to read the names of the layer attributes, it doesn't really do more than that. + +**Feature Caption Template String** + +To cause an attribute to be serialized in MapML vector content as the element value, +you must enter its name as a ${placeholder} in the text box immediately below the attributes list. You can also add (a small amount of) plain text that will be +copied verbatim into the content. is used as the accessible name of features by screen reader software, which will often +read out this value without the user having to expand a popup; in other words, it will be used as a visual and audible tooltip when the +feature is focused. + + +MapML Resources +--------------- + +MapML resources will be available for any published WMS layers by making a GetMap request with the WMS output format to ``format=text/mapml``. See :ref:`WMS` for further WMS details, :ref:`wms_getmap` for GetMap details, and :ref:`wms_output_formats` for output format reference information. + +**SRS/CRS** + +Note that the WMS SRS or CRS must be one of the projections supported by MapML: + +- 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** + +The output image format for the MapML resource should be specified using the format_options parameter with a key called ``mapml-wms-format``. If provided, the provided mime type must be a valid WMS format specifier. If not provided, it defaults to ``image/png``. + +Example:: + + http://localhost:8080/geoserver/tiger/wms?service=WMS&version=1.1.0&request=GetMap&layers=tiger:giant_polygon&bbox=-180.0,-90.0,180.0,90.0&width=768&height=384&srs=EPSG:4326&styles=&format=text/mapml&format_options=mapml-wms-format:image/jpeg + +MapML Visualization +------------------- + +With the MapML Extension module installed, the GeoServer Layer Preview page is modified to add a WMS GetMap link to the MapML resources for each layer or layer group. The MapML link in the Layer Preview table is generated by the MapML extension to an HTML Web map page that is created on the fly which refers to the GeoServer resource: + +.. figure:: images/mapml_preview_ui.png + +You can add layers to the map as you like, by dragging the URL bar value generated by the the Layer Preview WMS formats dropdown menu selection of "MapML" as shown below, and dropping it onto another layer's MapML preview: + +.. figure:: images/mapml_wms_format_dropdown.png + +If all goes well, you should see the layers stacked on the map and in the layer control. + +MapML visualization is supported by the Web-Map-Custom-Element project. The MapML viewer is built into the GeoServer layer and layer group preview facility. You can find out more about the Web-Map-Custom-Element at the project `website `. Here is a simple, self-contained example of an HTML page that uses the and elements: + +.. code-block:: html + + + + + + MapML Test Map + + + + + + + + + + + +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. diff --git a/doc/en/user/source/extensions/mapml/template.rst b/doc/en/user/source/extensions/mapml/template.rst new file mode 100644 index 00000000000..5700c8cf5f5 --- /dev/null +++ b/doc/en/user/source/extensions/mapml/template.rst @@ -0,0 +1,442 @@ +Templates With FreeMarker +------------------------- + +MapML templates are written in `Freemarker `_ , a Java-based template engine. The templates below are feature type specific and will not be applied in multi-layer WMS requests. See :ref:`tutorial_freemarkertemplate` for general information about FreeMarker implementation in GeoServer. + +MapML supports the following template types: + ++----------------------------+--------------------------------------------------------------------------------------+ +| Template File Name | Purpose | ++============================+======================================================================================+ +| ``mapml-preview-head.ftl`` | Used to insert stylesheet links or elements into the MapML HTML preview viewer. | ++----------------------------+--------------------------------------------------------------------------------------+ +| ``mapml-head.ftl`` | Used to insert ``mapml-link`` elements into the MapML map-head section. | ++----------------------------+--------------------------------------------------------------------------------------+ +| ``mapml-feature-head.ftl`` | Used to insert ``map-style`` elements into a MapML feature document. | ++----------------------------+--------------------------------------------------------------------------------------+ +| ``mapml-feature.ftl`` | Used to rewrite MapML features, with ability to change attributes, styles, | +| | geometries, and add links | ++----------------------------+--------------------------------------------------------------------------------------+ + +GetMap MapML HTML Preview/Layer Preview Head Stylesheet Templating +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The preview is returned when the format includes ``subtype=mapml``. The preview is an HTML document that includes a ``head`` section with a link to the stylesheet. The default preview viewer is a simple viewer that includes a link to the default stylesheet. +A template can be created to insert links to whole stylesheet or actual stylesheet elements. +We can do this by creating a file called ``mapml-preview-head.ftl`` in the GeoServer data directory in the directory for the layer that we wish to append links to. For example we could create this file under ``workspaces/topp/states_shapefile/states``. To add stylesheet links and stylesheet elements, we enter the following text inside this new file: + +.. code-block:: html + + + + + + +This would result in a head section that would resemble: + +.. code-block:: html + + + USA Population + + + + + + + + + + +GetMap MapML Head Stylesheet Templating +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The MapML format includes a map-head element that includes map-link elements to link to other resources, including map style variants. Additional map-link elements can be added to the map-head element by creating a ``mapml-head.ftl`` template in the GeoServer data directory in the directory for the layer we wish to append map-links to. For example we could create the ``mapml-head.ftl`` file under ``workspaces/tiger/nyc/poly_landmarks_shapefile/poly_landmarks``: + +.. code-block:: bash + + + .polygon-r1-s1{stroke-opacity:3.0; stroke-dashoffset:4; stroke-width:2.0; fill:#AAAAAA; fill-opacity:3.0; stroke:#DD0000; stroke-linecap:butt} + + + +This would result in a map-head section that would resemble (note the inserted css styles and map-link): + +.. code-block:: html + + + Manhattan (NY) landmarks + + + + + + + + + + + .bbox {display:none} .poly_landmarks-r1-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:1.0; fill:#B4DFB4; fill-opacity:1.0; stroke:#88B588; stroke-linecap:butt} .poly_landmarks-r2-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:1.0; fill:#8AA9D1; fill-opacity:1.0; stroke:#436C91; stroke-linecap:butt} .poly_landmarks-r3-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:1.0; fill:#FDE5A5; fill-opacity:0.75; stroke:#6E6E6E; stroke-linecap:butt} .polygon-r1-s1{stroke-opacity:3.0; stroke-dashoffset:4; stroke-width:2.0; fill:#AAAAAA; fill-opacity:3.0; stroke:#DD0000; stroke-linecap:butt} + + +GetMap Features Inline Style Class Templating +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +MapML in feature format (when the parameter format_options=mapmlfeatures:true is set) has a map-head element that includes map-style elements where the style classes are defined. +Within the map-body, map-feature elements include map-geometry with map-coordinates. + +The ``mapml-feature-head.ftl`` is a file that can be used to insert map-style elements with the style class definitions. +This file is placed in the GeoServer data directory in the directory for the layer we wish to append style classes to. For example we could create the file under ``workspaces/tiger/nyc/poly_landmarks_shapefile/poly_landmarks``. + +The ``mapml-feature-head.ftl`` file would look like:: + + + + .desired {stroke-dashoffset:3} + + + +This would result in a MapML feature output header that would resemble: + +.. code-block:: xml + + + + poi + + + + + + .bbox {display:none} .polygon-r1-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:1.0; fill:#AAAAAA; fill-opacity:1.0; stroke:#000000; stroke-linecap:butt} + .desired {stroke-dashoffset:3} + + + +The ``mapml-feature.ftl`` is a file can be used to insert map-style elements with the style class definitions into the map-head. Note that this section of the template adds the styles listed but does not remove any existing styles. +It can be used to edit map-property names and values in a manner similar to :ref:`tutorials_getfeatureinfo_geojson`. Note that this template represents a full replacement of the feature. If there are attributes that need to be included without change, they need to be referenced in the template. It also can be used to add style class identifiers to map-feature elements based on the feature identifier or to wrap groupings of map-coordinates with spans that specify the style class based on an index of coordinate order (zero based index that starts at the first coordinate pair of each feature). +This file is placed in the GeoServer data directory in the directory for the layer we wish to append style classes to. For example we could create the file under ``workspaces/tiger/poly_landmarks_shapefile/poly_landmarks``. + +An example ``mapml-feature.ftl`` file to modify a point layer would look like:: + + + + + + + <#list attributes as attribute> + <#if attribute.name == "MAINPAGE"> + + <#else> + + + + <#list attributes as gattribute> + <#if gattribute.isGeometry> + + <#if attributes.NAME.value == "museam"> + + + <#list gattribute.rawValue.coordinates as coord>${coord.x} ${coord.y} + + + <#if attributes.NAME.value == "museam"> + + + + + + + +This would result in a MapML feature output body that would resemble this fragment:: + + + + poi + + + + + + .bbox {display:none} .poi-r1-s1{r:88.0; well-known-name:circle; opacity:1.0; fill:#FF0000; fill-opacity:1.0} .poi-r1-s2{r:56.0; well-known-name:circle; opacity:1.0; fill:#FFFFFF; fill-opacity:1.0} + + + + + + + -74.01046109936 40.70758762626 + + + + + + + + + + + + + + + + + +
Property nameProperty value
CHANGED MAINPAGEUPDATED pics/22037827-L.jpg
+
+
+ +Note that in addition to tagging the coordinates with a style class, the template also changes the name of the MAINPAGE property to "UPDATED MAINPAGE" and the value to "CHANGED pics/22037827-L.jpg". + +For linestring features the template would look like:: + + + + + + + <#list attributes as attribute> + <#if attribute.isGeometry> + + <#if attributes.NAME.value == "Washington Sq W"> + + + <#list attribute.rawValue.coordinates as coord> ${coord.x} ${coord.y} + + <#if attributes.NAME.value == "Washington Sq W"> + + + + + + +For polygon features the template would look like:: + + + + + + + <#list attributes as attribute> + <#if attribute.isGeometry> + + + + <#assign shell = attribute.rawValue.getExteriorRing()> + + <#list shell.coordinates as coord> ${coord.x} ${coord.y} + + <#list 0 ..< attribute.rawValue.getNumInteriorRing() as index> + <#assign hole = attribute.rawValue.getInteriorRingN(index)><#list hole.coordinates as coord> ${coord.x} ${coord.y} + + + + + + + + + +For multipoint features the template would look like:: + + + + + + + <#list attributes as gattribute> + <#if gattribute.isGeometry> + + + + <#list 0 ..< gattribute.rawValue.getNumGeometries() as index> + <#assign point = gattribute.rawValue.getGeometryN(index)> + <#list point.coordinates as coord> + ${coord.x} ${coord.y} + + + + + + + + + + +For multiline features the template would like:: + + + + + + + <#list attributes as attribute> + <#if attribute.isGeometry> + + + + <#list 0 ..< attribute.rawValue.getNumGeometries() as index> + <#assign line = attribute.rawValue.getGeometryN(index)> + <#list line.coordinates as coord> ${coord.x} ${coord.y} + + + + + + + + + + +For multipolygon features the template would like:: + + + + + + + <#if attributes.LAND.value == "72.0"> + <#list attributes as attribute> + <#if attribute.isGeometry> + + + + <#list 0 ..< attribute.rawValue.getNumGeometries() as index> + <#assign polygon = attribute.rawValue.getGeometryN(index)> + + <#assign shell = polygon.getExteriorRing()> + <#list shell.coordinates as coord> ${coord.x} ${coord.y} + <#list 0 ..< polygon.getNumInteriorRing() as index> + <#assign hole = polygon.getInteriorRingN(index)> + <#list hole.coordinates as coord> ${coord.x} ${coord.y} + + + + + + + + <#else> + <#list attributes as attribute> + <#if attribute.isGeometry> + + + <#list 0 ..< attribute.rawValue.getNumGeometries() as index> + <#assign polygon = attribute.rawValue.getGeometryN(index)> + + <#assign shell = polygon.getExteriorRing()> + <#list shell.coordinates as coord> ${coord.x} ${coord.y} + <#list 0 ..< polygon.getNumInteriorRing() as index> + <#assign hole = polygon.getInteriorRingN(index)> + <#list hole.coordinates as coord> ${coord.x} ${coord.y} + + + + + + + + + + + + +Templates can also be used to create MapML GeometryCollections that consist of multiple geometry types. For example, a template that creates a GeometryCollection that contains points and linestring representations of the NYC TIGER POI sample data would look like:: + + + + + + + <#list attributes as attribute> + <#if attribute.isGeometry> + + + + + <#list attribute.rawValue.coordinates as coord> ${coord.x} ${coord.y} + + + <#list attribute.rawValue.coordinates as coord> ${coord.x} ${coord.y} + + + + + + + + + + +This would result in a MapML feature output body that would resemble:: + + + + poi + + + + + + .bbox {display:none} .poi-r1-s1{r:88.0; well-known-name:circle; opacity:1.0; fill:#FF0000; fill-opacity:1.0} .poi-r1-s2{r:56.0; well-known-name:circle; opacity:1.0; fill:#FFFFFF; fill-opacity:1.0} + + + + + + + + -74.00857344353 40.71194564907 + + + -74.00857344353 40.71194564907 + + + + + + + + + + + + + + + + + + + + + + + + + + +
Property nameProperty value
NAMElox
THUMBNAILpics/22037884-Ti.jpg
MAINPAGEpics/22037884-L.jpg
+
+
+
+
\ No newline at end of file diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java index d7ddd2592f5..f852427ab56 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java @@ -13,11 +13,17 @@ import static org.geoserver.mapml.MapMLConstants.MAPML_USE_FEATURES; import static org.geoserver.mapml.MapMLConstants.MAPML_USE_TILES; import static org.geoserver.mapml.MapMLHTMLOutput.PREVIEW_TCRS_MAP; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_PREVIEW_HEAD_FTL; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_XML_HEAD_FTL; +import freemarker.template.TemplateMethodModelEx; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -29,6 +35,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -47,6 +54,7 @@ import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geoserver.mapml.tcrs.Bounds; import org.geoserver.mapml.tcrs.TiledCRS; +import org.geoserver.mapml.template.MapMLMapTemplate; import org.geoserver.mapml.xml.AxisType; import org.geoserver.mapml.xml.Base; import org.geoserver.mapml.xml.BodyContent; @@ -66,14 +74,18 @@ import org.geoserver.mapml.xml.Select; import org.geoserver.mapml.xml.UnitType; import org.geoserver.ows.Dispatcher; +import org.geoserver.ows.Request; import org.geoserver.ows.URLMangler; import org.geoserver.ows.util.ResponseUtils; import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetMapRequest; +import org.geoserver.wms.MapLayerInfo; import org.geoserver.wms.WMS; import org.geoserver.wms.WMSInfo; import org.geoserver.wms.WMSMapContent; import org.geoserver.wms.capabilities.CapabilityUtil; +import org.geoserver.wms.featureinfo.FeatureTemplate; +import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.api.referencing.FactoryException; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.api.referencing.operation.TransformException; @@ -137,6 +149,17 @@ public class MapMLDocumentBuilder { private ReferencedEnvelope projectedBox; private String bbox; + private static final String MAP_STYLE_OPEN_TAG = ""; + private static final String MAP_STYLE_CLOSE_TAG = ""; + private static final Pattern MAP_STYLE_REGEX = + Pattern.compile(MAP_STYLE_OPEN_TAG + "(.+?)" + MAP_STYLE_CLOSE_TAG, Pattern.DOTALL); + private static final Pattern MAP_LINK_REGEX = + Pattern.compile("", Pattern.DOTALL); + + private static final Pattern MAP_LINK_HREF_REGEX = Pattern.compile("href=\"(.+?)\""); + + private static final Pattern MAP_LINK_TITLE_REGEX = Pattern.compile("title=\"(.+?)\""); + private List extentList; private Input zoomInput; @@ -146,6 +169,14 @@ public class MapMLDocumentBuilder { private Mapml mapml; private Boolean isMultiExtent = MAPML_MULTILAYER_AS_MULTIEXTENT_DEFAULT; + private MapMLMapTemplate mapMLMapTemplate = new MapMLMapTemplate(); + + 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 @@ -828,10 +859,78 @@ private HeadContent prepareHead() throws IOException { } } String styles = buildStyles(); + // get the styles and links from the head template + List stylesAndLinks = getHeaderTemplates(MAPML_XML_HEAD_FTL, getFeatureTypes()); + styles = appendStylesFromHeadTemplate(styles, stylesAndLinks); if (styles != null) head.setStyle(styles); + links.addAll(getLinksFromHeadTemplate(stylesAndLinks)); return head; } + /** + * Get Links generated from the head template + * + * @param stylesAndLinks Styles and links from the head template + * @return List of Link objects + */ + private List getLinksFromHeadTemplate(List stylesAndLinks) { + List outLinks = new ArrayList<>(); + List extractedLinks = extractLinks(stylesAndLinks); + for (String extractedLink : extractedLinks) { + Link link = new Link(); + Matcher matcherTitle = MAP_LINK_TITLE_REGEX.matcher(extractedLink); + if (matcherTitle.find()) { + link.setTitle(matcherTitle.group(1)); + } + Matcher matcherHref = MAP_LINK_HREF_REGEX.matcher(extractedLink); + if (matcherHref.find()) { + link.setRel(RelType.STYLE); + link.setHref(matcherHref.group(1)); + // only add if mandatory href attribute is found + outLinks.add(link); + } + } + return outLinks; + } + + private String appendStylesFromHeadTemplate(String styles, List stylesAndLinks) { + + List extractedStyles = extractStyles(stylesAndLinks); + for (String extractedStyle : extractedStyles) { + if (styles == null) { + styles = extractedStyle; + } else { + styles = styles + " " + extractedStyle; + } + } + return styles; + } + + private List extractLinks(List stylesAndLinks) { + List extractedStyles = new ArrayList<>(); + for (String stylesAndLink : stylesAndLinks) { + Matcher matcher = MAP_LINK_REGEX.matcher(stylesAndLink); + while (matcher.find()) { + extractedStyles.add(matcher.group()); + } + } + return extractedStyles; + } + + private List extractStyles(List stylesAndLinks) { + List extractedStyles = new ArrayList<>(); + for (String stylesAndLink : stylesAndLinks) { + Matcher matcher = MAP_STYLE_REGEX.matcher(stylesAndLink); + while (matcher.find()) { + extractedStyles.add( + matcher.group() + .replaceAll(MAP_STYLE_OPEN_TAG, "") + .replace(MAP_STYLE_CLOSE_TAG, "")); + } + } + return extractedStyles; + } + /** Builds the CSS styles for all the layers involved in this GetMap */ private String buildStyles() throws IOException { List cssStyles = new ArrayList<>(); @@ -1595,6 +1694,7 @@ public String getMapMLHTMLDocument() { Double longitude = 0.0; ReferencedEnvelope projectedBbox = this.projectedBox; ReferencedEnvelope geographicBox = new ReferencedEnvelope(DefaultGeographicCRS.WGS84); + List headerContent = getPreviewTemplates(MAPML_PREVIEW_HEAD_FTL, getFeatureTypes()); for (MapMLLayerMetadata mapMLLayerMetadata : mapMLLayerMetadataList) { layer += mapMLLayerMetadata.getLayerName() + ","; styleName += mapMLLayerMetadata.getStyleName() + ","; @@ -1658,10 +1758,99 @@ public String getMapMLHTMLDocument() { .setRequest(request) .setProjectedBbox(projectedBbox) .setLayerLabel(layerLabel) + .setTemplateHeader(String.join("\n", headerContent)) .build(); return htmlOutput.toHTML(); } + /** + * Get FeatureTypes based on requested layers + * + * @return list of SimpleFeatureType + */ + private List getFeatureTypes() { + List featureTypes = new ArrayList<>(); + try { + for (MapLayerInfo mapLayerInfo : mapContent.getRequest().getLayers()) { + if (mapLayerInfo.getType() == MapLayerInfo.TYPE_VECTOR + && mapLayerInfo.getFeature() != null + && mapLayerInfo.getFeature().getFeatureType() != null + && mapLayerInfo.getFeature().getFeatureType() + instanceof SimpleFeatureType) { + featureTypes.add( + (SimpleFeatureType) mapLayerInfo.getFeature().getFeatureType()); + } else if (mapLayerInfo.getType() == MapLayerInfo.TYPE_RASTER) { + LOGGER.fine( + "Templating not supported for raster layers: " + + mapLayerInfo.getName()); + } + } + } catch (IOException | ClassCastException e) { + LOGGER.fine("Error getting feature types: " + e.getMessage()); + } + return featureTypes; + } + + /** + * Get Preview Header Content from templates + * + * @param templateName template name + * @param featureTypes list of feature types + * @return list of head content + */ + private List getPreviewTemplates( + String templateName, List featureTypes) { + List templates = new ArrayList<>(); + for (SimpleFeatureType featureType : featureTypes) { + try { + if (!mapMLMapTemplate.isTemplateEmpty( + featureType, templateName, FeatureTemplate.class, "0\n")) { + templates.add(mapMLMapTemplate.preview(featureType)); + } + + } catch (IOException e) { + LOGGER.fine( + "Template not found: " + + templateName + + " for schema: " + + featureType.getTypeName()); + } + } + return templates; + } + + /** + * Get the MapML head content from templates + * + * @param templateName template name + * @param featureTypes list of feature types + * @return list of head content + */ + private List getHeaderTemplates( + String templateName, List featureTypes) { + List templates = new ArrayList<>(); + + for (SimpleFeatureType featureType : featureTypes) { + try { + Map model = + getMapRequestElementsToModel( + layersCommaDelimited, bbox, format, width, height); + if (!mapMLMapTemplate.isTemplateEmpty( + featureType, templateName, FeatureTemplate.class, "0\n")) { + templates.add(mapMLMapTemplate.head(model, featureType)); + } + + } catch (IOException e) { + LOGGER.fine( + "Template not found: " + + templateName + + " for schema: " + + featureType.getTypeName()); + } + } + return templates; + } + /** Builds the GetMap backlink to get MapML */ private String buildGetMap( String layer, @@ -1728,6 +1917,99 @@ String getLabel(PublishedInfo p, String def, HttpServletRequest request) { } } + /** + * Converts URL query string to a map of key value pairs + * + * @param query URL query string + * @return Map of key value pairs + */ + private Map getParametersFromQuery(String query) { + return Arrays.stream(query.split("&")) + .map(this::splitQueryParameter) + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue(), (v1, v2) -> v2)); + } + + private AbstractMap.SimpleImmutableEntry splitQueryParameter(String parameter) { + final int idx = parameter.indexOf("="); + final String key = idx > 0 ? parameter.substring(0, idx) : parameter; + + try { + String value = null; + if (idx > 0 && parameter.length() > idx + 1) { + final String encodedValue = parameter.substring(idx + 1); + value = URLDecoder.decode(encodedValue, "UTF-8"); + } + return new AbstractMap.SimpleImmutableEntry<>(key, value); + } catch (UnsupportedEncodingException e) { + // UTF-8 not supported?? + throw new RuntimeException(e); + } + } + + /** + * Builds a link from the arguments passed into the template + * + * @param arguments List of arguments, the first argument is the base URL, the second is the + * path, and the third is the query string + * @return URL string + */ + private String serviceLink(List arguments) { + Request request = Dispatcher.REQUEST.get(); + String baseURL = + arguments.get(0) != null + ? arguments.get(0).toString() + : ResponseUtils.baseURL(request.getHttpRequest()); + Map kvp = + arguments.get(2) != null + ? getParametersFromQuery(arguments.get(2).toString()) + : request.getKvp().entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, e -> e.getValue().toString())); + + return ResponseUtils.buildURL(baseURL, request.getPath(), kvp, URLMangler.URLType.SERVICE); + } + + /** + * Convert GetMapRequest elements to a map model for the template + * + * @param layersCommaDelimited Comma delimited list of layer names + * @param bbox + * @param format + * @param width + * @param height + * @return + */ + private Map getMapRequestElementsToModel( + String layersCommaDelimited, + String bbox, + Optional format, + int width, + int height) { + HashMap model = new HashMap<>(); + Request request = Dispatcher.REQUEST.get(); + String baseURL = ResponseUtils.baseURL(request.getHttpRequest()); + String kvp = + request.getKvp().entrySet().stream() + .map( + p -> + URLEncoder.encode(p.getKey(), StandardCharsets.UTF_8) + + "=" + + URLEncoder.encode( + p.getValue().toString(), + StandardCharsets.UTF_8)) + .reduce((p1, p2) -> p1 + "&" + p2) + .orElse(""); + String path = request.getPath(); + model.put("base", baseURL); + model.put("path", path); + model.put("kvp", kvp); + model.put("rel", "style"); + model.put("serviceLink", (TemplateMethodModelEx) arguments -> serviceLink(arguments)); + return model; + } + /** Raw KVP layer info */ static class RawLayer { diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java index 4395093568d..f9f58b0097c 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLEncoder.java @@ -72,6 +72,8 @@ public void encode(Mapml mapml, OutputStream output) { public Mapml decode(Reader reader) { try { Unmarshaller unmarshaller = context.createUnmarshaller(); + unmarshaller.setEventHandler( + new javax.xml.bind.helpers.DefaultValidationEventHandler()); return (Mapml) unmarshaller.unmarshal(reader); } catch (JAXBException e) { throw new ServiceException(e); diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLFeatureUtil.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLFeatureUtil.java index 6e4b0a9d0d0..5fe33a7dec8 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLFeatureUtil.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLFeatureUtil.java @@ -7,8 +7,12 @@ import static org.geoserver.mapml.MapMLConstants.MAPML_FEATURE_FO; import static org.geoserver.mapml.MapMLConstants.MAPML_SKIP_ATTRIBUTES_FO; import static org.geoserver.mapml.MapMLConstants.MAPML_SKIP_STYLES_FO; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_FEATURE_FTL; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_FEATURE_HEAD_FTL; +import freemarker.template.TemplateNotFoundException; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -19,12 +23,14 @@ import java.util.StringJoiner; import java.util.logging.Level; import java.util.logging.Logger; +import javax.xml.bind.JAXBException; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.MetadataMap; import org.geoserver.catalog.ResourceInfo; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geoserver.mapml.tcrs.TiledCRSConstants; import org.geoserver.mapml.tcrs.TiledCRSParams; +import org.geoserver.mapml.template.MapMLMapTemplate; import org.geoserver.mapml.xml.BodyContent; import org.geoserver.mapml.xml.Feature; import org.geoserver.mapml.xml.HeadContent; @@ -37,6 +43,7 @@ import org.geoserver.ows.URLMangler; import org.geoserver.ows.util.ResponseUtils; import org.geoserver.platform.ServiceException; +import org.geoserver.wms.featureinfo.FeatureTemplate; import org.geotools.api.feature.simple.SimpleFeature; import org.geotools.api.referencing.FactoryException; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; @@ -56,6 +63,16 @@ public class MapMLFeatureUtil { public static final String STYLE_CLASS_PREFIX = "."; public static final String STYLE_CLASS_DELIMITER = " "; public static final String BBOX_DISPLAY_NONE = ".bbox {display:none}"; + private static final MapMLMapTemplate mapMLMapTemplate = new MapMLMapTemplate(); + private static final MapMLEncoder encoder; + + static { + try { + encoder = new MapMLEncoder(); + } catch (JAXBException e) { + throw new ServiceException(e); + } + } /** * Convert a feature collection to a MapML document @@ -91,6 +108,24 @@ public static Mapml featureCollectionToMapML( throw new ServiceException("MapML OutputFormat does not support Complex Features."); } SimpleFeatureCollection fc = (SimpleFeatureCollection) featureCollection; + boolean hasTemplate = false; + boolean hasHeadTemplate = false; + try { + if (!mapMLMapTemplate.isTemplateEmpty( + fc.getSchema(), MAPML_FEATURE_HEAD_FTL, FeatureTemplate.class, "0\n")) { + hasHeadTemplate = true; + } + } catch (TemplateNotFoundException e) { + LOGGER.log(Level.FINEST, MAPML_FEATURE_HEAD_FTL + " Template not found", e); + } + try { + if (!mapMLMapTemplate.isTemplateEmpty( + fc.getSchema(), MAPML_FEATURE_FTL, FeatureTemplate.class, "0\n")) { + hasTemplate = true; + } + } catch (TemplateNotFoundException e) { + LOGGER.log(Level.FINEST, MAPML_FEATURE_FTL + " Template not found", e); + } ResourceInfo resourceInfo = layerInfo.getResource(); MetadataMap layerMeta = resourceInfo.getMetadata(); @@ -132,7 +167,12 @@ public static Mapml featureCollectionToMapML( if (!skipHeadStyles) { String style = getCSSStylesFull(styles); head.setStyle(style); + if (hasHeadTemplate) { + getInterpolatedStylesFromTemplate(fc) + .ifPresent(interpolated -> appendTemplateCSSStyle(head, interpolated)); + } } + String fCaptionTemplate = layerMeta.get("mapml.featureCaption", String.class); mapml.setHead(head); @@ -151,18 +191,26 @@ public static Mapml featureCollectionToMapML( try (SimpleFeatureIterator iterator = fc.features()) { while (iterator.hasNext()) { SimpleFeature feature = iterator.next(); + Optional interpolatedOptional = Optional.empty(); + if (hasTemplate) { + interpolatedOptional = getInterpolatedFromTemplate(fc, feature); + } // convert feature to xml if (styles != null) { List applicableStyles = getApplicableStyles(feature, styles); Optional f = featureBuilder.buildFeature( - feature, fCaptionTemplate, applicableStyles); + feature, + fCaptionTemplate, + applicableStyles, + interpolatedOptional); // feature will be skipped if geometry incompatible with style symbolizer f.ifPresent(features::add); } else { // WFS GETFEATURE request with no styles Optional f = - featureBuilder.buildFeature(feature, fCaptionTemplate, null); + featureBuilder.buildFeature( + feature, fCaptionTemplate, null, interpolatedOptional); f.ifPresent(features::add); } } @@ -170,6 +218,55 @@ public static Mapml featureCollectionToMapML( return mapml; } + private static Optional getInterpolatedFromTemplate( + SimpleFeatureCollection fc, SimpleFeature feature) { + String templateOutput = "Error parsing template output"; + try { + templateOutput = mapMLMapTemplate.features(fc.getSchema(), feature); + Mapml out = encoder.decode(new StringReader(templateOutput)); + return Optional.of(out); + } catch (Exception e) { + LOGGER.info( + "Error unmarshalling template output for MapML features " + + "Output from template: " + + templateOutput + + " Error: " + + e.getLocalizedMessage()); + throw new ServiceException(e, templateOutput); + } + } + + /** + * Append the CSS style from the template to the feature + * + * @param head the head content + * @param interpolated the interpolated object from the template + */ + private static void appendTemplateCSSStyle(HeadContent head, Mapml interpolated) { + if (head != null) { + if (interpolated.getHead() != null && interpolated.getHead().getStyle() != null) { + String interpolatedCSSStyle = interpolated.getHead().getStyle(); + if (head.getStyle() == null) { + head.setStyle(interpolatedCSSStyle); + } else { + head.setStyle(head.getStyle() + " " + interpolatedCSSStyle); + } + } + } + } + + private static Optional getInterpolatedStylesFromTemplate(SimpleFeatureCollection fc) + throws IOException { + String templateOutput = mapMLMapTemplate.featureHead(fc.getSchema()); + StringReader reader = new StringReader(templateOutput); + try { + return Optional.of(encoder.decode(reader)); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error unmarshalling template output: " + templateOutput, e); + throw new ServiceException(e, templateOutput); + } + } + /** * Get an empty MapML document populated with basic request related metadata * diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGenerator.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGenerator.java index 3409a8c2cb6..0882aff0c05 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGenerator.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGenerator.java @@ -7,17 +7,21 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.StringJoiner; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -import javax.xml.bind.JAXBElement; +import java.util.stream.IntStream; import org.apache.commons.text.StringEscapeUtils; +import org.geoserver.mapml.xml.BodyContent; import org.geoserver.mapml.xml.Coordinates; import org.geoserver.mapml.xml.Feature; import org.geoserver.mapml.xml.GeometryContent; +import org.geoserver.mapml.xml.Mapml; import org.geoserver.mapml.xml.ObjectFactory; import org.geoserver.mapml.xml.PropertyContent; import org.geoserver.mapml.xml.Span; @@ -74,7 +78,10 @@ public class MapMLGenerator { * @throws IOException - IOException */ public Optional buildFeature( - SimpleFeature sf, String featureCaptionTemplate, List mapMLStyles) + SimpleFeature sf, + String featureCaptionTemplate, + List mapMLStyles, + Optional templateOptional) throws IOException { if (mapMLStyles != null && mapMLStyles.isEmpty()) { // no applicable styles, probably because of scale @@ -82,6 +89,8 @@ public Optional buildFeature( } Feature f = new Feature(); f.setId(sf.getID()); + Optional> replacmentAttsOptional = + getTemplateAttributes(templateOptional); if (featureCaptionTemplate != null && !featureCaptionTemplate.isEmpty()) { AttributeValueResolver attributeResolver = new AttributeValueResolver(sf); String caption = @@ -95,7 +104,7 @@ public Optional buildFeature( if (!skipAttributes) { PropertyContent pc = new PropertyContent(); f.setProperties(pc); - pc.setAnyElement(collectAttributes(sf)); + pc.setAnyElement(collectAttributes(sf, replacmentAttsOptional)); } // if clipping is enabled, clip the geometry and return null if the clip removed it entirely @@ -143,12 +152,137 @@ public Optional buildFeature( } if (g == null || g.isEmpty()) return Optional.empty(); - f.setGeometry(buildGeometry(g)); + // if there is an template geometry and the original geometry is not tagged, use it instead + // of the original geometry + GeometryContent geometryContent = null; + if (templateOptional.isPresent() && g.getUserData() == null) { + geometryContent = templateOptional.get().getBody().getFeatures().get(0).getGeometry(); + // format the geometry coming from the template using the formatter + Object geometry = geometryContent.getGeometryContent().getValue(); + formatGeometry(geometry); + } else { + geometryContent = buildGeometry(g); + } + + f.setGeometry(geometryContent); return Optional.of(f); } + /** + * Formats the geometry using the formatter including the number of decimals + * + * @param geometry the geometry + */ + private void formatGeometry(Object geometry) { + if (geometry instanceof org.geoserver.mapml.xml.Point) { + org.geoserver.mapml.xml.Point point = (org.geoserver.mapml.xml.Point) geometry; + formatCoordinates(point.getCoordinates()); + } else if (geometry instanceof org.geoserver.mapml.xml.MultiPoint) { + org.geoserver.mapml.xml.MultiPoint multiPoint = + (org.geoserver.mapml.xml.MultiPoint) geometry; + formatCoordinates(multiPoint.getCoordinates()); + } else if (geometry instanceof org.geoserver.mapml.xml.LineString) { + org.geoserver.mapml.xml.LineString lineString = + (org.geoserver.mapml.xml.LineString) geometry; + formatCoordinates(lineString.getCoordinates()); + } else if (geometry instanceof org.geoserver.mapml.xml.MultiLineString) { + org.geoserver.mapml.xml.MultiLineString multiLineString = + (org.geoserver.mapml.xml.MultiLineString) geometry; + formatCoordinates(multiLineString.getTwoOrMoreCoordinatePairs()); + } else if (geometry instanceof org.geoserver.mapml.xml.Polygon) { + org.geoserver.mapml.xml.Polygon polygon = (org.geoserver.mapml.xml.Polygon) geometry; + formatCoordinates(polygon.getThreeOrMoreCoordinatePairs()); + } else if (geometry instanceof org.geoserver.mapml.xml.MultiPolygon) { + org.geoserver.mapml.xml.MultiPolygon multiPolygon = + (org.geoserver.mapml.xml.MultiPolygon) geometry; + for (org.geoserver.mapml.xml.Polygon polygon : multiPolygon.getPolygon()) { + formatCoordinates(polygon.getThreeOrMoreCoordinatePairs()); + } + + } else if (geometry instanceof org.geoserver.mapml.xml.GeometryCollection) { + org.geoserver.mapml.xml.GeometryCollection geometryCollection = + (org.geoserver.mapml.xml.GeometryCollection) geometry; + for (Object geom : geometryCollection.getPointOrLineStringOrPolygon()) { + formatGeometry(geom); + } + } else if (geometry instanceof org.geoserver.mapml.xml.A) { + org.geoserver.mapml.xml.A a = (org.geoserver.mapml.xml.A) geometry; + formatGeometry(a.getGeometryContent().getValue()); + } + } + + /** + * Formats the coordinates using the formatter including the number of decimals + * + * @param coordinates the coordinates + */ + private void formatCoordinates(List coordinates) { + for (Coordinates coords : coordinates) { + List coordList = coords.getCoordinates(); + for (Object coord : coordList) { + if (coord instanceof Span) { + Span span = (Span) coord; + List spanCoords = span.getCoordinates(); + for (int i = 0; i < spanCoords.size(); i++) { + String[] xyArray = formatCoordStrings(spanCoords.get(i)); + spanCoords.set(i, String.join(" ", xyArray)); + } + } else { + String xy = coord.toString(); + String[] xyArray = formatCoordStrings(xy); + coord = String.join(" ", xyArray); + } + } + } + } + + /** + * Formats the coordinates using the formatter including the number of decimals + * + * @param xy the coordinates + * @return the formatted coordinates + */ + private String[] formatCoordStrings(String xy) { + String[] xyArray = + Arrays.asList(xy.split("\\s+")).stream() + .filter(s -> !s.trim().isEmpty()) + .toArray(String[]::new); + for (int i = 0; i < xyArray.length; i++) { + xyArray[i] = formatter.format(Double.parseDouble(xyArray[i])); + } + return xyArray; + } + + private static Optional> getTemplateAttributes( + Optional templateOptional) { + + return templateOptional + .map(Mapml::getBody) + .map(BodyContent::getFeatures) + .filter(features -> features != null && !features.isEmpty()) + .map(features -> features.get(0)) + .map(Feature::getProperties) + .map(PropertyContent::getOtherAttributes) + .filter( + attributes -> + attributes != null + && !attributes.isEmpty() + && attributes.values().size() % 2 == 0) + .map( + attributes -> { + List values = new ArrayList<>(attributes.values()); + return IntStream.range(0, values.size() / 2) + .boxed() + .collect( + Collectors.toMap( + i -> values.get(i * 2), + i -> values.get(i * 2 + 1))); + }); + } + /** Collects the attributes representation as a HTML table */ - private String collectAttributes(SimpleFeature sf) { + private String collectAttributes( + SimpleFeature sf, Optional> replacmentAttsOptional) { StringBuilder sb = new StringBuilder( "" @@ -156,20 +290,35 @@ private String collectAttributes(SimpleFeature sf) { + "" + ""); - for (AttributeDescriptor attr : sf.getFeatureType().getAttributeDescriptors()) { - if (!(attr.getType() instanceof GeometryType)) { - String escapedName = StringEscapeUtils.escapeXml10(attr.getLocalName()); - String value = - sf.getAttribute(attr.getName()) != null - ? sf.getAttribute(attr.getName()).toString() - : ""; + if (replacmentAttsOptional.isEmpty()) { + for (AttributeDescriptor attr : sf.getFeatureType().getAttributeDescriptors()) { + if (!(attr.getType() instanceof GeometryType)) { + String escapedName = StringEscapeUtils.escapeXml10(attr.getLocalName()); + String value = + sf.getAttribute(attr.getName()) != null + ? sf.getAttribute(attr.getName()).toString() + : ""; + sb.append("") + .append(""); + } + } + } else { + for (Map.Entry entry : replacmentAttsOptional.get().entrySet()) { + String escapedName = StringEscapeUtils.escapeXml10(entry.getKey()); + String value = StringEscapeUtils.escapeXml10(entry.getValue()); sb.append("") .append(""); } } @@ -395,13 +544,11 @@ private org.geoserver.mapml.xml.MultiPolygon buildMultiPolygon(MultiPolygon mpg) private org.geoserver.mapml.xml.MultiLineString buildMultiLineString(MultiLineString ml) { org.geoserver.mapml.xml.MultiLineString multiLine = new org.geoserver.mapml.xml.MultiLineString(); - List>> coordLists = multiLine.getTwoOrMoreCoordinatePairs(); + List coordLists = multiLine.getTwoOrMoreCoordinatePairs(); for (int i = 0; i < ml.getNumGeometries(); i++) { - coordLists.add( - factory.createMultiLineStringCoordinates( - buildCoordinates( - ((LineString) (ml.getGeometryN(i))).getCoordinateSequence(), - null))); + LineString ls = (LineString) ml.getGeometryN(i); + String coordList = buildCoordinates(ls.getCoordinateSequence()); + coordLists.add(new Coordinates(coordList)); } return multiLine; } @@ -412,7 +559,7 @@ private org.geoserver.mapml.xml.MultiLineString buildMultiLineString(MultiLineSt */ private org.geoserver.mapml.xml.LineString buildLineString(LineString l) { org.geoserver.mapml.xml.LineString lineString = new org.geoserver.mapml.xml.LineString(); - List lsCoords = lineString.getCoordinates(); + List lsCoords = lineString.getCoordinates(); buildCoordinates(l.getCoordinateSequence(), lsCoords); return lineString; } @@ -423,7 +570,7 @@ private org.geoserver.mapml.xml.LineString buildLineString(LineString l) { */ private org.geoserver.mapml.xml.MultiPoint buildMultiPoint(MultiPoint mp) { org.geoserver.mapml.xml.MultiPoint multiPoint = new org.geoserver.mapml.xml.MultiPoint(); - List mpCoords = multiPoint.getCoordinates(); + List mpCoords = multiPoint.getCoordinates(); buildCoordinates(new CoordinateArraySequence(mp.getCoordinates()), mpCoords); return multiPoint; } @@ -435,7 +582,11 @@ private org.geoserver.mapml.xml.MultiPoint buildMultiPoint(MultiPoint mp) { private org.geoserver.mapml.xml.Point buildPoint(Point p) { org.geoserver.mapml.xml.Point point = new org.geoserver.mapml.xml.Point(); point.getCoordinates() - .add(this.formatter.format(p.getX()) + SPACE + this.formatter.format(p.getY())); + .add( + new Coordinates( + this.formatter.format(p.getX()) + + SPACE + + this.formatter.format(p.getY()))); return point; } @@ -496,7 +647,7 @@ private Object buildTaggedCoordinateSequence(TaggedPolygon.TaggedCoordinateSeque } else { return new Span( "bbox", - buildCoordinates( + buildSpanCoordinates( new CoordinateArraySequence( cs.getCoordinates().toArray(n -> new Coordinate[n])), null)); @@ -508,7 +659,7 @@ private Object buildTaggedCoordinateSequence(TaggedPolygon.TaggedCoordinateSeque * @param coordList a list of coordinate strings to add to * @return */ - private List buildCoordinates(CoordinateSequence cs, List coordList) { + private List buildSpanCoordinates(CoordinateSequence cs, List coordList) { if (coordList == null) { coordList = new ArrayList<>(cs.size()); } @@ -519,6 +670,25 @@ private List buildCoordinates(CoordinateSequence cs, List coordL return coordList; } + /** + * @param cs a JTS CoordinateSequence + * @param coordList a list of coordinate strings to add to + * @return + */ + private List buildCoordinates(CoordinateSequence cs, List coordList) { + if (coordList == null) { + coordList = new ArrayList<>(cs.size()); + } + StringJoiner joiner = new StringJoiner(" "); + for (int i = 0; i < cs.size(); i++) { + joiner.add( + this.formatter.format(cs.getX(i)) + SPACE + this.formatter.format(cs.getY(i))); + } + Coordinates coords = new Coordinates(joiner.toString()); + coordList.add(coords); + return coordList; + } + /** * Builds a space separated representation of the coordinate sequence * diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java index 9c7f647d2b7..bb662b1f829 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLGetFeatureInfoOutputFormat.java @@ -150,7 +150,8 @@ public void write( featureBuilder.buildFeature( feature, captionTemplates.get(fc.getSchema().getName()), - null); + null, + Optional.empty()); // might be interesting to be able to put features // from different layers into a layer-specific div f.ifPresent(features::add); diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLHTMLOutput.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLHTMLOutput.java index 75ee2d3e066..5971afcc3bc 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLHTMLOutput.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLHTMLOutput.java @@ -37,6 +37,7 @@ public class MapMLHTMLOutput { private Double latitude = 0.0; private Double longitude = 0.0; private ReferencedEnvelope projectedBbox; + private String templateHeader; private MapMLHTMLOutput(HTMLOutputBuilder builder) { this.latitude = builder.latitude; @@ -47,6 +48,7 @@ private MapMLHTMLOutput(HTMLOutputBuilder builder) { this.projType = builder.projType; this.sourceUrL = builder.sourceUrL; this.projectedBbox = builder.projectedBbox; + this.templateHeader = builder.templateHeader; } public static class HTMLOutputBuilder { @@ -58,6 +60,7 @@ public static class HTMLOutputBuilder { private int zoom = 0; private Double latitude = 0.0; private Double longitude = 0.0; + private String templateHeader = ""; public HTMLOutputBuilder setLayerLabel(String layerLabel) { this.layerLabel = layerLabel; @@ -79,6 +82,11 @@ public HTMLOutputBuilder setSourceUrL(String sourceUrL) { return this; } + public HTMLOutputBuilder setTemplateHeader(String templateHeader) { + this.templateHeader = templateHeader; + return this; + } + public HTMLOutputBuilder setProjectedBbox(ReferencedEnvelope projectedBbox) { this.projectedBbox = projectedBbox; return this; @@ -124,6 +132,7 @@ public String toHTML() { .append("mapml-viewer:not(:defined) > :not(layer-) { display: initial; }\n") .append("\n") .append("\n") + .append(templateHeader) .append("\n") .append("\n") .append(" WRITER = new ThreadLocal<>(); + + /** + * Returns a {@link CharArrayWriter} attached to the current thread (or a new one, in case this + * methods it's called outside the context of an OGC request). The writer is guaranteed to be + * either new, or freshly reset. + * + * @return + */ + public static CharArrayWriter getWriter() { + // just in case we're not on a OGC request + if (Dispatcher.REQUEST.get() == null) return new CharArrayWriter(); + + CharArrayWriter writer = WRITER.get(); + if (writer == null) { + writer = new CharArrayWriter(); + WRITER.set(writer); + } else { + writer.reset(); + } + return writer; + } + + @Override + public void finished(Request request) { + WRITER.remove(); + } +} diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/template/MapMLMapTemplate.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/template/MapMLMapTemplate.java new file mode 100644 index 00000000000..df225c621a2 --- /dev/null +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/template/MapMLMapTemplate.java @@ -0,0 +1,288 @@ +/* (c) 2024 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.mapml.template; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.geoserver.catalog.Catalog; +import org.geoserver.platform.GeoServerExtensions; +import org.geoserver.platform.GeoServerResourceLoader; +import org.geoserver.template.DirectTemplateFeatureCollectionFactory; +import org.geoserver.template.FeatureWrapper; +import org.geoserver.template.GeoServerTemplateLoader; +import org.geoserver.template.TemplateUtils; +import org.geoserver.wms.featureinfo.FeatureTemplate; +import org.geotools.api.feature.Feature; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.api.feature.simple.SimpleFeatureType; + +/** A template engine for generating MapML content. */ +public class MapMLMapTemplate { + /** The template configuration */ + static Configuration templateConfig; + + static DirectTemplateFeatureCollectionFactory FC_FACTORY = + new DirectTemplateFeatureCollectionFactory(); + + static { + // initialize the template engine, this is static to maintain a cache + templateConfig = TemplateUtils.getSafeConfiguration(); + + templateConfig.setLocale(Locale.US); + templateConfig.setNumberFormat("0.###########"); + templateConfig.setObjectWrapper(new FeatureWrapper(FC_FACTORY)); + + // encoding + templateConfig.setDefaultEncoding("UTF-8"); + } + + /** The template used to add to the head of the preview viewer. */ + public static final String MAPML_PREVIEW_HEAD_FTL = "mapml-preview-head.ftl"; + + /** The template used to add to the head of the xml representation */ + public static final String MAPML_XML_HEAD_FTL = "mapml-head.ftl"; + + public static final String MAPML_FEATURE_HEAD_FTL = "mapml-feature-head.ftl"; + + public static final String MAPML_FEATURE_FTL = "mapml-feature.ftl"; + + /** Template cache used to avoid paying the cost of template lookup for each GetMap call */ + Map templateCache = new ConcurrentHashMap<>(); + + /** + * Generates the preview content for the given feature type. + * + * @param model the model to use for the template + * @param featureType the feature type to use for the template + * @param writer the writer to write the output to + * @throws IOException in case of an error + */ + public void preview(Map model, SimpleFeatureType featureType, Writer writer) + throws IOException { + execute(model, featureType, writer, MAPML_PREVIEW_HEAD_FTL); + } + + /** + * Generates the preview content for the given feature type. + * + * @param featureType the feature type to use for the template + * @return the preview content + * @throws IOException in case of an error + */ + public String preview(SimpleFeatureType featureType) throws IOException { + CharArrayWriter caw = CharArrayWriterPool.getWriter(); + preview(Collections.emptyMap(), featureType, caw); + + return caw.toString(); + } + + public String features(SimpleFeatureType featureType, SimpleFeature feature) + throws IOException { + CharArrayWriter caw = CharArrayWriterPool.getWriter(); + + features(featureType, feature, caw); + return caw.toString(); + } + + public void features(SimpleFeatureType featureType, SimpleFeature feature, Writer writer) + throws IOException { + execute(feature, featureType, writer, MAPML_FEATURE_FTL); + } + + /** + * Generates the head content for the given feature type. + * + * @param model the model to use for the template + * @param featureType the feature type to use for the template + * @param writer the writer to write the output to + * @throws IOException in case of an error + */ + public void head(Map model, SimpleFeatureType featureType, Writer writer) + throws IOException { + execute(model, featureType, writer, MAPML_XML_HEAD_FTL); + } + + public String featureHead(SimpleFeatureType featureType) throws IOException { + CharArrayWriter caw = CharArrayWriterPool.getWriter(); + featureHead(featureType, caw); + return caw.toString(); + } + + public void featureHead(SimpleFeatureType featureType, Writer writer) throws IOException { + execute(featureType, writer, MAPML_FEATURE_HEAD_FTL); + } + + /** + * Generates the head content for the given feature type. + * + * @param model the model to use for the template + * @param featureType the feature type to use for the template + * @return the head content + * @throws IOException in case of an error + */ + public String head(Map model, SimpleFeatureType featureType) + throws IOException { + CharArrayWriter caw = CharArrayWriterPool.getWriter(); + head(model, featureType, caw); + + return caw.toString(); + } + + /* + * Internal helper method to exceute the template against feature or + * feature collection. + */ + private void execute( + Map model, + SimpleFeatureType featureType, + Writer writer, + String template) + throws IOException { + + Template t = lookupTemplate(featureType, template, null); + + try { + t.process(model, writer); + } catch (TemplateException e) { + String msg = "Error occured processing template."; + throw (IOException) new IOException(msg).initCause(e); + } + } + + /* + * Internal helper method to exceute the template against feature or + * feature collection. + */ + private void execute( + Feature feature, SimpleFeatureType featureType, Writer writer, String template) + throws IOException { + + Template t = lookupTemplate(featureType, template, null); + + try { + t.process(feature, writer); + } catch (TemplateException e) { + String msg = "Error occured processing template."; + throw (IOException) new IOException(msg).initCause(e); + } + } + + /* + * Internal helper method to exceute the template against feature or + * feature collection. + */ + private void execute(SimpleFeatureType featureType, Writer writer, String template) + throws IOException { + + Template t = lookupTemplate(featureType, template, null); + + try { + t.process(null, writer); + } catch (TemplateException e) { + String msg = "Error occured processing template."; + throw (IOException) new IOException(msg).initCause(e); + } + } + + /** + * Returns the template for the specified feature type. Looking up templates is pretty + * expensive, so we cache templates by feture type and template. + */ + private Template lookupTemplate(SimpleFeatureType featureType, String template, Class lookup) + throws IOException { + + // lookup the cache first + TemplateKey key = new TemplateKey(featureType, template); + Template t = templateCache.get(key); + if (t != null) return t; + + // otherwise, build a loader and do the lookup + GeoServerTemplateLoader templateLoader = + new GeoServerTemplateLoader( + lookup != null ? lookup : getClass(), + GeoServerExtensions.bean(GeoServerResourceLoader.class)); + Catalog catalog = (Catalog) GeoServerExtensions.bean("catalog"); + templateLoader.setFeatureType(catalog.getFeatureTypeByName(featureType.getName())); + + // Configuration is not thread safe + synchronized (templateConfig) { + templateConfig.setTemplateLoader(templateLoader); + t = templateConfig.getTemplate(template); + } + templateCache.put(key, t); + return t; + } + + /** Returns true if the required template is empty or has its default content */ + public boolean isTemplateEmpty( + SimpleFeatureType featureType, + String template, + Class lookup, + String defaultContent) + throws IOException { + Template t = lookupTemplate(featureType, template, lookup); + if (t == null) { + return true; + } + // check if the template is empty + StringWriter sw = new StringWriter(); + t.dump(sw); + // an empty template canonical form is "0\n".. weird! + String templateText = sw.toString(); + return "".equals(templateText) + || (defaultContent != null && defaultContent.equals(templateText)); + } + + /** Template key class used to cache templates by feature type and template name. */ + private static class TemplateKey { + SimpleFeatureType type; + String template; + + /** + * Template key constructor + * + * @param type the feature type + * @param template the template name + */ + public TemplateKey(SimpleFeatureType type, String template) { + super(); + this.type = type; + this.template = template; + } + + @Override + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = PRIME * result + ((template == null) ? 0 : template.hashCode()); + result = PRIME * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + final MapMLMapTemplate.TemplateKey other = (MapMLMapTemplate.TemplateKey) obj; + if (template == null) { + if (other.template != null) return false; + } else if (!template.equals(other.template)) return false; + if (type == null) { + if (other.type != null) return false; + } else if (!type.equals(other.type)) return false; + return true; + } + } +} diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/A.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/A.java new file mode 100644 index 00000000000..ace938474d3 --- /dev/null +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/A.java @@ -0,0 +1,59 @@ +package org.geoserver.mapml.xml; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType( + name = "A", + propOrder = {"geometryContent"}) +public class A { + @XmlAttribute(name = "href") + protected String href; + + @XmlElementRef( + name = "GeometryContent", + type = JAXBElement.class, + namespace = "http://www.w3.org/1999/xhtml") + protected JAXBElement geometryContent; + + public String getHref() { + return href; + } + + public void setHref(String value) { + this.href = value; + } + + /** + * Gets the value of the geometryContent property. + * + * @return possible object is {@link JAXBElement }{@code <}{@link MultiPolygon }{@code >} {@link + * JAXBElement }{@code <}{@link LineString }{@code >} {@link JAXBElement }{@code <}{@link + * GeometryCollection }{@code >} {@link JAXBElement }{@code <}{@link MultiPoint }{@code >} + * {@link JAXBElement }{@code <}{@link Object }{@code >} {@link JAXBElement }{@code <}{@link + * Point }{@code >} {@link JAXBElement }{@code <}{@link MultiLineString }{@code >} {@link + * JAXBElement }{@code <}{@link Polygon }{@code >} + */ + public JAXBElement getGeometryContent() { + return geometryContent; + } + + /** + * Sets the value of the geometryContent property. + * + * @param value allowed object is {@link JAXBElement }{@code <}{@link MultiPolygon }{@code >} + * {@link JAXBElement }{@code <}{@link LineString }{@code >} {@link JAXBElement }{@code + * <}{@link GeometryCollection }{@code >} {@link JAXBElement }{@code <}{@link MultiPoint + * }{@code >} {@link JAXBElement }{@code <}{@link Object }{@code >} {@link JAXBElement + * }{@code <}{@link Point }{@code >} {@link JAXBElement }{@code <}{@link MultiLineString + * }{@code >} {@link JAXBElement }{@code <}{@link Polygon }{@code >} + */ + public void setGeometryContent(JAXBElement value) { + this.geometryContent = value; + } +} diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/GeometryCollection.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/GeometryCollection.java index dcf56eeb2ee..c47eb7a5456 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/GeometryCollection.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/GeometryCollection.java @@ -45,7 +45,10 @@ public class GeometryCollection { @XmlElements({ - @XmlElement(name = "point", type = Point.class, namespace = "http://www.w3.org/1999/xhtml"), + @XmlElement( + name = "map-point", + type = Point.class, + namespace = "http://www.w3.org/1999/xhtml"), @XmlElement( name = "map-linestring", type = LineString.class, @@ -65,7 +68,8 @@ public class GeometryCollection { @XmlElement( name = "map-multipolygon", type = MultiPolygon.class, - namespace = "http://www.w3.org/1999/xhtml") + namespace = "http://www.w3.org/1999/xhtml"), + @XmlElement(name = "map-a", type = A.class, namespace = "http://www.w3.org/1999/xhtml") }) protected List pointOrLineStringOrPolygon; diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/LineString.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/LineString.java index 3733fd225b7..05ea0e77773 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/LineString.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/LineString.java @@ -12,8 +12,8 @@ import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlList; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlMixed; import javax.xml.bind.annotation.XmlType; /** @@ -37,12 +37,12 @@ propOrder = {"coordinates"}) public class LineString { - @XmlList - @XmlElement( - required = true, + @XmlMixed + @XmlElementRef( name = "map-coordinates", + type = Coordinates.class, namespace = "http://www.w3.org/1999/xhtml") - protected List coordinates; + protected List coordinates; /** * Gets the value of the coordinates property. @@ -61,7 +61,7 @@ public class LineString { * * @return list of coordinates elements */ - public List getCoordinates() { + public List getCoordinates() { if (coordinates == null) { coordinates = new ArrayList<>(); } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiLineString.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiLineString.java index 0dd6ea40093..8bf460aa094 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiLineString.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiLineString.java @@ -14,6 +14,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlMixed; import javax.xml.bind.annotation.XmlType; /** @@ -37,11 +38,12 @@ propOrder = {"twoOrMoreCoordinatePairs"}) public class MultiLineString { + @XmlMixed @XmlElementRef( name = "map-coordinates", - type = JAXBElement.class, + type = Coordinates.class, namespace = "http://www.w3.org/1999/xhtml") - protected List>> twoOrMoreCoordinatePairs; + protected List twoOrMoreCoordinatePairs; /** * Gets the value of the twoOrMoreCoordinatePairs property. @@ -61,7 +63,7 @@ public class MultiLineString { * * @return two or more coordinate pairs */ - public List>> getTwoOrMoreCoordinatePairs() { + public List getTwoOrMoreCoordinatePairs() { if (twoOrMoreCoordinatePairs == null) { twoOrMoreCoordinatePairs = new ArrayList<>(); } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiPoint.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiPoint.java index b4b09fc1a3e..4c2209fbed0 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiPoint.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/MultiPoint.java @@ -12,8 +12,8 @@ import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlList; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlMixed; import javax.xml.bind.annotation.XmlType; /** @@ -37,12 +37,12 @@ propOrder = {"coordinates"}) public class MultiPoint { - @XmlList - @XmlElement( - required = true, + @XmlMixed + @XmlElementRef( name = "map-coordinates", + type = Coordinates.class, namespace = "http://www.w3.org/1999/xhtml") - protected List coordinates; + protected List coordinates; /** * Gets the value of the map-coordinates property. Exception Description: The property or field @@ -63,7 +63,7 @@ public class MultiPoint { * * @return list of coordinates strings */ - public List getCoordinates() { + public List getCoordinates() { if (coordinates == null) { coordinates = new ArrayList<>(); } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/ObjectFactory.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/ObjectFactory.java index 1918d038406..035375d7562 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/ObjectFactory.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/ObjectFactory.java @@ -55,6 +55,7 @@ public class ObjectFactory { new QName("http://www.w3.org/1999/xhtml", "map-properties"); private static final QName _MultiPointCoordinates_QNAME = new QName("http://www.w3.org/1999/xhtml", "map-coordinates"); + private static final QName _A_QNAME = new QName("http://www.w3.org/1999/xhtml", "map-a"); /** * Create a new ObjectFactory that can be used to create new instances of schema derived classes @@ -296,6 +297,16 @@ public JAXBElement createPolygon(Polygon value) { return new JAXBElement<>(_Polygon_QNAME, Polygon.class, null, value); } + /** Create an instance of {@link JAXBElement }{@code <}{@link A }{@code >}} */ + @XmlElementDecl( + namespace = "http://www.w3.org/1999/xhtml", + name = "map-a", + substitutionHeadNamespace = "http://www.w3.org/1999/xhtml", + substitutionHeadName = "GeometryContent") + public JAXBElement createA(A value) { + return new JAXBElement<>(_A_QNAME, A.class, null, value); + } + /** Create an instance of {@link JAXBElement }{@code <}{@link PropertyContent }{@code >}} */ @XmlElementDecl(namespace = "http://www.w3.org/1999/xhtml", name = "map-properties") public JAXBElement createProperties(PropertyContent value) { @@ -339,11 +350,11 @@ public JAXBElement createPolygonCoordinates(List value) { namespace = "http://www.w3.org/1999/xhtml", name = "map-coordinates", scope = MultiLineString.class) - public JAXBElement> createMultiLineStringCoordinates(List value) { + public JAXBElement createMultiLineStringCoordinates(List value) { return new JAXBElement<>( _MultiPointCoordinates_QNAME, ((Class) List.class), MultiLineString.class, - ((List) value)); + ((List) value)); } } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/Point.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/Point.java index adcdf04f7ad..007a5ba4651 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/Point.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/xml/Point.java @@ -12,8 +12,8 @@ import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlList; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlMixed; import javax.xml.bind.annotation.XmlType; /** @@ -37,12 +37,12 @@ propOrder = {"coordinates"}) public class Point { - @XmlList - @XmlElement( - required = true, + @XmlMixed + @XmlElementRef( name = "map-coordinates", + type = Coordinates.class, namespace = "http://www.w3.org/1999/xhtml") - protected List coordinates; + protected List coordinates; /** * Gets the value of the coordinates property. @@ -61,7 +61,7 @@ public class Point { * * @return list of coordinates strings */ - public List getCoordinates() { + public List getCoordinates() { if (coordinates == null) { coordinates = new ArrayList<>(); } diff --git a/src/extension/mapml/src/main/resources/applicationContext.xml b/src/extension/mapml/src/main/resources/applicationContext.xml index 25a143f0415..72d68fb9218 100644 --- a/src/extension/mapml/src/main/resources/applicationContext.xml +++ b/src/extension/mapml/src/main/resources/applicationContext.xml @@ -91,4 +91,5 @@ + diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSFeatureTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSFeatureTest.java index 65bca1aacd1..2bad13e25fa 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSFeatureTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSFeatureTest.java @@ -6,29 +6,37 @@ import static org.geoserver.mapml.MapMLConstants.MAPML_USE_FEATURES; import static org.geoserver.mapml.MapMLConstants.MAPML_USE_TILES; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_FEATURE_FTL; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_FEATURE_HEAD_FTL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.awt.Rectangle; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import javax.xml.bind.JAXBElement; +import org.apache.commons.io.FileUtils; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; +import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; +import org.geoserver.mapml.xml.Coordinates; import org.geoserver.mapml.xml.Feature; import org.geoserver.mapml.xml.LineString; import org.geoserver.mapml.xml.Mapml; import org.geoserver.mapml.xml.MultiLineString; +import org.geoserver.mapml.xml.MultiPolygon; +import org.geoserver.mapml.xml.Point; import org.geoserver.mapml.xml.Polygon; +import org.geoserver.mapml.xml.Span; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.MapLayerInfo; import org.geoserver.wms.WMSMapContent; @@ -245,12 +253,13 @@ public void testCoordinateSimplification() throws Exception { // all lines are small enough that they are simplified to start/end if (geometry instanceof LineString) { LineString ls = (LineString) geometry; - assertEquals(4, ls.getCoordinates().size()); + String lscoords = ls.getCoordinates().get(0).getCoordinates().get(0).toString(); + assertEquals(4, lscoords.split(" ").length); } else if (geometry instanceof MultiLineString) { MultiLineString mls = (MultiLineString) geometry; - for (JAXBElement je : mls.getTwoOrMoreCoordinatePairs()) { - List coordinates = (List) je.getValue(); - assertEquals(4, coordinates.size()); + for (Coordinates je : mls.getTwoOrMoreCoordinatePairs()) { + String mlscoords = je.getCoordinates().get(0).toString(); + assertEquals(2, mlscoords.split(" ").length); } } } @@ -289,6 +298,321 @@ public void testMapMLGetStyleQuery() throws Exception { qElse.getFilter().toString().contains("ADDRESS = 123 Main Street")); } + @Test + public void testTemplateHeaderStyle() throws Exception { + File template = null; + try { + Catalog cat = getCatalog(); + LayerInfo li = cat.getLayerByName(MockData.BRIDGES.getLocalPart()); + li.getResource().getMetadata().put(MAPML_USE_FEATURES, true); + li.getResource().getMetadata().put(MAPML_USE_TILES, false); + cat.save(li); + String layerId = getLayerId(MockData.BRIDGES); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_FEATURE_HEAD_FTL); + FileUtils.write( + template, + "\n" + + "\n" + + " .desired {stroke-dashoffset:3}\n" + + "\n" + + "\n", + "UTF-8"); + Mapml mapmlFeatures = + new MapMLWMSRequest() + .name(MockData.BRIDGES.getLocalPart()) + .bbox("-180,-90,180,90") + .srs("EPSG:4326") + .feature(true) + .getAsMapML(); + + String mapmlStyle = mapmlFeatures.getHead().getStyle(); + assertTrue(mapmlStyle.contains(".desired {stroke-dashoffset:3}")); + } finally { + if (template != null) { + template.delete(); + } + } + } + + @Test + public void testMapMLFeaturePointHasClass() throws Exception { + File template = null; + try { + Catalog cat = getCatalog(); + LayerInfo li = cat.getLayerByName(MockData.BRIDGES.getLocalPart()); + li.getResource().getMetadata().put(MAPML_USE_FEATURES, true); + li.getResource().getMetadata().put(MAPML_USE_TILES, false); + cat.save(li); + String layerId = getLayerId(MockData.BRIDGES); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_FEATURE_FTL); + FileUtils.write( + template, + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " <#list attributes as attribute>\n" + + " <#if attribute.name == \"NAME\">\n" + + " \n" + + " \n" + + " \n" + + " <#list attributes as gattribute>\n" + + " <#if gattribute.isGeometry>\n" + + " " + + " " + + " <#list gattribute.rawValue.coordinates as coord>" + + " <#if coord?index == 0>${coord.x} ${coord.y}<#else>${coord.x} ${coord.y}" + + " " + + " \n" + + " \n" + + "\n" + + "\n" + + "\n", + "UTF-8"); + Mapml mapmlFeatures = + new MapMLWMSRequest() + .name(MockData.BRIDGES.getLocalPart()) + .bbox("-180,-90,180,90") + .srs("EPSG:4326") + .feature(true) + .getAsMapML(); + + Feature feature2 = + mapmlFeatures + .getBody() + .getFeatures() + .get(0); // get the first feature, which has a class + String attributes = feature2.getProperties().getAnyElement(); + assertTrue(attributes.contains("UPDATED NAME")); + Point featurePoint = (Point) feature2.getGeometry().getGeometryContent().getValue(); + Span span = ((Span) featurePoint.getCoordinates().get(0).getCoordinates().get(0)); + assertEquals("desired", span.getClazz()); + } finally { + if (template != null) { + template.delete(); + } + } + } + + @Test + public void testMapMLFeatureLineHasClass() throws Exception { + File template = null; + try { + Catalog cat = getCatalog(); + LayerInfo li = cat.getLayerByName(MockData.MLINES.getLocalPart()); + li.getResource().getMetadata().put(MAPML_USE_FEATURES, true); + li.getResource().getMetadata().put(MAPML_USE_TILES, false); + cat.save(li); + String layerId = getLayerId(MockData.MLINES); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_FEATURE_FTL); + FileUtils.write( + template, + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " <#list attributes as attribute>\n" + + " <#if attribute.isGeometry>\n" + + " <#list attribute.rawValue.coordinates as coord><#if coord?index == 2> ${coord.x} ${coord.y}<#elseif coord?index == 3>${coord.x} ${coord.y}<#else> ${coord.x} ${coord.y}\n" + + " \n" + + " \n" + + "\n" + + "\n" + + "\n", + "UTF-8"); + Mapml mapmlFeatures = + new MapMLWMSRequest() + .name(MockData.MLINES.getLocalPart()) + .bbox("500000,500000,500999,500999") + .srs("EPSG:32615") + .feature(true) + .getAsMapML(); + + Feature feature2 = + mapmlFeatures + .getBody() + .getFeatures() + .get(0); // get the second feature, which has a class + LineString featureLine = + (LineString) feature2.getGeometry().getGeometryContent().getValue(); + Span span = (Span) featureLine.getCoordinates().get(0).getCoordinates().get(1); + assertEquals("desired", span.getClazz()); + } finally { + if (template != null) { + template.delete(); + } + } + } + + @Test + public void testMapMLFeaturePolygonHasClass() throws Exception { + File template = null; + try { + Catalog cat = getCatalog(); + LayerInfo li = cat.getLayerByName(MockData.POLYGONS.getLocalPart()); + li.getResource().getMetadata().put(MAPML_USE_FEATURES, true); + li.getResource().getMetadata().put(MAPML_USE_TILES, false); + cat.save(li); + String layerId = getLayerId(MockData.POLYGONS); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_FEATURE_FTL); + FileUtils.write( + template, + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " <#list attributes as attribute>\n" + + " <#if attribute.isGeometry>\n" + + " \n" + + " " + + " <#assign shell = attribute.rawValue.getExteriorRing()><#list shell.coordinates as coord><#if coord?index == 0>${coord.x} ${coord.y}<#elseif coord?index == 4> ${coord.x} ${coord.y}<#else> ${coord.x} ${coord.y}" + + " <#list 0 ..< attribute.rawValue.getNumInteriorRing() as index>" + + " <#assign hole = attribute.rawValue.getInteriorRingN(index)><#list hole.coordinates as coord><#if coord?index == 0>${coord.x} ${coord.y} <#elseif coord?index == 4> ${coord.x} ${coord.y}<#else> ${coord.x} ${coord.y}" + + " " + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "\n", + "UTF-8"); + Mapml mapmlFeatures = + new MapMLWMSRequest() + .name(MockData.POLYGONS.getLocalPart()) + .bbox("500000,500000,500999,500999") + .srs("EPSG:32615") + .feature(true) + .getAsMapML(); + + Feature feature2 = + mapmlFeatures + .getBody() + .getFeatures() + .get(0); // get the second feature, which has a class + Polygon featurePolygon = + (Polygon) feature2.getGeometry().getGeometryContent().getValue(); + Span span = + (Span) + featurePolygon + .getThreeOrMoreCoordinatePairs() + .get(0) + .getCoordinates() + .get(0); + assertEquals("desired", span.getClazz()); + } finally { + if (template != null) { + template.delete(); + } + } + } + + @Test + public void testMapMLFeatureMultiPolygonHasClass() throws Exception { + File template = null; + try { + Catalog cat = getCatalog(); + LayerInfo li = cat.getLayerByName(MockData.NAMED_PLACES.getLocalPart()); + li.getResource().getMetadata().put(MAPML_USE_FEATURES, true); + li.getResource().getMetadata().put(MAPML_USE_TILES, false); + cat.save(li); + String layerId = getLayerId(MockData.NAMED_PLACES); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_FEATURE_FTL); + FileUtils.write( + template, + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "<#if attributes.FID.value == \"117\">\n" + + " <#list attributes as attribute>\n" + + " <#if attribute.isGeometry>\n" + + " \n" + + " " + + " <#list 0 ..< attribute.rawValue.getNumGeometries() as index>" + + " <#assign polygon = attribute.rawValue.getGeometryN(index)>" + + " " + + " <#assign shell = polygon.getExteriorRing()><#list shell.coordinates as coord><#if coord?index == 0>${coord.x} ${coord.y}<#elseif coord?index == 4> ${coord.x} ${coord.y}<#else> ${coord.x} ${coord.y}" + + " <#list 0 ..< polygon.getNumInteriorRing() as index>" + + " <#assign hole = polygon.getInteriorRingN(index)><#list hole.coordinates as coord><#if coord?index == 0>${coord.x} ${coord.y} <#elseif coord?index == 4> ${coord.x} ${coord.y}<#else> ${coord.x} ${coord.y}" + + " " + + " " + + " " + + " \n" + + " \n" + + " \n" + + "<#else>\n" + + " <#list attributes as attribute>\n" + + " <#if attribute.isGeometry>\n" + + " \n" + + " " + + " <#list 0 ..< attribute.rawValue.getNumGeometries() as index>" + + " <#assign polygon = attribute.rawValue.getGeometryN(index)>" + + " " + + " <#assign shell = polygon.getExteriorRing()><#list shell.coordinates as coord> ${coord.x} ${coord.y} " + + " <#list 0 ..< polygon.getNumInteriorRing() as index>" + + " <#assign hole = polygon.getInteriorRingN(index)><#list hole.coordinates as coord> ${coord.x} ${coord.y} " + + " " + + " " + + " " + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "\n" + + "\n", + "UTF-8"); + Mapml mapmlFeatures = + new MapMLWMSRequest() + .name(MockData.NAMED_PLACES.getLocalPart()) + .bbox("-180,-90,180,90") + .srs("EPSG:4326") + .feature(true) + .getAsMapML(); + + Feature feature2 = + mapmlFeatures + .getBody() + .getFeatures() + .get(0); // get the first feature, which has a class + MultiPolygon featureMultiPolygon = + (MultiPolygon) feature2.getGeometry().getGeometryContent().getValue(); + Span span = + (Span) + featureMultiPolygon + .getPolygon() + .get(0) + .getThreeOrMoreCoordinatePairs() + .get(0) + .getCoordinates() + .get(0); + assertEquals("desired", span.getClazz()); + } finally { + if (template != null) { + template.delete(); + } + } + } + @Test public void testExceptionBecauseMoreThanOneFeatureType() throws Exception { Catalog cat = getCatalog(); diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java index c4b474a9645..bfb2e881b82 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java @@ -8,6 +8,8 @@ import static org.custommonkey.xmlunit.XMLAssert.assertXpathExists; import static org.geoserver.mapml.MapMLConstants.MAPML_USE_FEATURES; import static org.geoserver.mapml.MapMLConstants.MAPML_USE_TILES; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_PREVIEW_HEAD_FTL; +import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_XML_HEAD_FTL; import static org.geowebcache.grid.GridSubsetFactory.createGridSubSet; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.startsWith; @@ -23,6 +25,7 @@ import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; @@ -40,10 +43,12 @@ import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.namespace.QName; +import org.apache.commons.io.FileUtils; import org.custommonkey.xmlunit.XMLUnit; import org.custommonkey.xmlunit.XpathEngine; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; +import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.MetadataMap; @@ -1575,6 +1580,96 @@ public void testHTMLWorkspaceQualified() throws Exception { assertThat(layerSrc, containsString("LAYERS=Lakes")); } + @Test + public void testXMLHeadTemplate() throws Exception { + File template = null; + try { + String layerId = getLayerId(MockData.ROAD_SEGMENTS); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_XML_HEAD_FTL); + FileUtils.write( + template, + ".polygon-r1-s1{stroke-opacity:3.0; stroke-dashoffset:4; stroke-width:2.0; fill:#AAAAAA; fill-opacity:3.0; stroke:#DD0000; stroke-linecap:butt}\n" + + "", + "UTF-8"); + + MockRequestResponse requestResponse = + getMockRequestResponse( + MockData.ROAD_SEGMENTS.getPrefix() + + ":" + + MockData.ROAD_SEGMENTS.getLocalPart(), + null, + null, + "EPSG:3857", + null); + Mapml mapml = parseMapML(requestResponse); + List styleLinks = getLinkByRelType(mapml.getHead().getLinks(), RelType.STYLE); + Link templateStyleLink = styleLinks.get(0); + assertEquals("templateinsertedstyle", templateStyleLink.getTitle()); + assertEquals( + "http://localhost:8080/geoserver/wms?SRS=EPSG%3A3857&REQUEST=GetMap&FORMAT=text%2Fmapml&FORMAT_OPTIONS=%7BMAPML-WMS-FORMAT%3Dimage%2Fpng%7D&BBOX=SRSEnvelope%5B0.0%20%3A%20111319.49079327357%2C%20-7.081154551613622E-10%20%3A%20111325.14286638486%5D&VERSION=1.3.0&WIDTH=150&SERVICE=WMS&HEIGHT=150&LAYERS=cite%3ARoadSegments", + templateStyleLink.getHref()); + String templateStyle = mapml.getHead().getStyle(); + assertEquals( + ".bbox {display:none} .RoadSegments-r1-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:4.0; stroke:#C0A000; stroke-linecap:butt} .RoadSegments-r2-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:4.0; stroke:#000000; stroke-linecap:butt} .RoadSegments-r3-s1{stroke-opacity:1.0; stroke-dashoffset:0; stroke-width:4.0; stroke:#E04000; stroke-linecap:butt} .polygon-r1-s1{stroke-opacity:3.0; stroke-dashoffset:4; stroke-width:2.0; fill:#AAAAAA; fill-opacity:3.0; stroke:#DD0000; stroke-linecap:butt}", + templateStyle); + } finally { + if (template != null) { + template.delete(); + } + } + } + + @Test + public void testPreviewHeadTemplate() throws Exception { + File template = null; + try { + String layerId = getLayerId(MockData.LAKES); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_PREVIEW_HEAD_FTL); + FileUtils.write(template, "", "UTF-8"); + FileUtils.write( + template, + "", + "UTF-8", + true); + + String path = + "cite/wms?LAYERS=Lakes" + + "&STYLES=&FORMAT=" + + MapMLConstants.MAPML_HTML_MIME_TYPE + + "&SERVICE=WMS&VERSION=1.3.0" + + "&REQUEST=GetMap" + + "&SRS=epsg:3857" + + "&BBOX=-13885038,2870337,-7455049,6338174" + + "&WIDTH=150" + + "&HEIGHT=150" + + "&format_options=" + + MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION + + ":image/png"; + Document doc = getAsJSoup(path); + Element link = doc.select("link").first(); + String linkRel = link.attr("rel"); + String linkSrc = link.attr("href"); + assertThat(linkRel, containsString("stylesheet")); + assertThat(linkSrc, containsString("mystyle.css")); + Element style = doc.select("style").get(2); + assertEquals("body {\n" + " background-color: linen;\n" + " }", style.html()); + } finally { + if (template != null) { + template.delete(); + } + } + } + @Test public void testInvalidProjectionHTML() throws Exception { String path = From 30fe085feae0ad25e8c0fa66b203fbdb2b1e4d19 Mon Sep 17 00:00:00 2001 From: Daniele Romagnoli Date: Wed, 17 Jul 2024 15:09:49 +0200 Subject: [PATCH 6/6] [GEOS-11486]: MapML Adding support for custom dimensions --- .../geoserver/mapml/MapMLDocumentBuilder.java | 382 ++++++++++++++---- .../geoserver/mapml/MapMLDimensionsTest.java | 216 ++++++++-- .../wms/capabilities/DimensionHelper.java | 191 +++++---- 3 files changed, 613 insertions(+), 176 deletions(-) diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java index f852427ab56..1e72e9f9135 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java @@ -15,6 +15,7 @@ import static org.geoserver.mapml.MapMLHTMLOutput.PREVIEW_TCRS_MAP; import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_PREVIEW_HEAD_FTL; import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_XML_HEAD_FTL; +import static org.geoserver.wms.capabilities.DimensionHelper.getDataType; import freemarker.template.TemplateMethodModelEx; import java.io.IOException; @@ -26,6 +27,7 @@ import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -33,12 +35,17 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.geoserver.catalog.Catalog; +import org.geoserver.catalog.CoverageInfo; +import org.geoserver.catalog.CoverageStoreInfo; import org.geoserver.catalog.DimensionInfo; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.LayerGroupInfo; @@ -49,6 +56,7 @@ import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.impl.LayerGroupStyle; +import org.geoserver.catalog.util.ReaderDimensionsAccessor; import org.geoserver.config.GeoServer; import org.geoserver.gwc.GWC; import org.geoserver.gwc.layer.GeoServerTileLayer; @@ -84,12 +92,14 @@ import org.geoserver.wms.WMSInfo; import org.geoserver.wms.WMSMapContent; import org.geoserver.wms.capabilities.CapabilityUtil; +import org.geoserver.wms.capabilities.DimensionHelper; import org.geoserver.wms.featureinfo.FeatureTemplate; import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.api.referencing.FactoryException; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.api.referencing.operation.TransformException; import org.geotools.api.style.Style; +import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; @@ -97,6 +107,7 @@ import org.geotools.renderer.crs.ProjectionHandlerFinder; import org.geotools.util.NumberRange; import org.geotools.util.logging.Logging; +import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.grid.GridSubset; import org.locationtech.jts.geom.Envelope; @@ -171,13 +182,6 @@ public class MapMLDocumentBuilder { private Boolean isMultiExtent = MAPML_MULTILAYER_AS_MULTIEXTENT_DEFAULT; private MapMLMapTemplate mapMLMapTemplate = new MapMLMapTemplate(); - 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 * @@ -1009,10 +1013,6 @@ private List prepareExtents() throws IOException { List extents = new ArrayList<>(); for (MapMLLayerMetadata mapMLLayerMetadata : mapMLLayerMetadataList) { Extent extent = new Extent(); - if (isMultiExtent) { - extent.setHidden(null); // not needed for multi-extent - extent.setLabel(mapMLLayerMetadata.layerTitle); - } extent.setUnits(projType); extentList = extent.getInputOrDatalistOrLink(); @@ -1054,12 +1054,27 @@ private List prepareExtents() throws IOException { String dimension = layerMeta.get("mapml.dimension", String.class); prepareExtentForLayer(mapMLLayerMetadata, dimension); generateTemplatedLinks(mapMLLayerMetadata); + if (isMultiExtent || isSingleLayerWithDimensionOptions(mapMLLayerMetadataList)) { + extent.setHidden(null); // not needed for multi-extent + extent.setLabel(mapMLLayerMetadata.layerTitle); + } extents.add(extent); } return extents; } + private boolean isSingleLayerWithDimensionOptions( + List mapMLLayerMetadataList) { + if (mapMLLayerMetadataList.size() == 1) { + MapMLLayerMetadata metadata = mapMLLayerMetadataList.get(0); + return metadata.isTimeEnabled() + || metadata.isElevationEnabled() + || StringUtils.isNotBlank(metadata.getCustomDimension()); + } + return false; + } + /** * Prepare the extent for a layer * @@ -1074,49 +1089,136 @@ private void prepareExtentForLayer(MapMLLayerMetadata mapMLLayerMetadata, String } LayerInfo layerInfo = mapMLLayerMetadata.getLayerInfo(); ResourceInfo resourceInfo = layerInfo.getResource(); + if (resourceInfo instanceof FeatureTypeInfo) { + prepareFeatureExtent((FeatureTypeInfo) resourceInfo, mapMLLayerMetadata, dimension); + } else if (resourceInfo instanceof CoverageInfo) { + prepareCoverageExtent((CoverageInfo) resourceInfo, mapMLLayerMetadata, dimension); + } + } + + @SuppressWarnings("unchecked") + private void prepareFeatureExtent( + FeatureTypeInfo typeInfo, MapMLLayerMetadata layerMetadata, String dimension) + throws IOException { + MetadataMap metadataMap = typeInfo.getMetadata(); + DimensionOptions options; if ("Time".equalsIgnoreCase(dimension)) { - if (resourceInfo instanceof FeatureTypeInfo) { - FeatureTypeInfo typeInfo = (FeatureTypeInfo) resourceInfo; - DimensionInfo timeInfo = - typeInfo.getMetadata().get(ResourceInfo.TIME, DimensionInfo.class); - if (timeInfo.isEnabled()) { - mapMLLayerMetadata.setTimeEnabled(true); - Set dates = wms.getFeatureTypeTimes(typeInfo); - Select select = new Select(); - select.setId("time"); - select.setName("time"); - extentList.add(select); - List
Property value
") + .append(escapedName) + .append("") + .append(StringEscapeUtils.escapeXml10(value)) + .append("
") .append(escapedName) .append("") - .append(StringEscapeUtils.escapeXml10(value)) + .append(value) .append("