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 1fac542963e..c6c476f3555 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 @@ -12,8 +12,8 @@ import static org.geoserver.mapml.MapMLConstants.MAPML_SKIP_STYLES_FO; 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_HEAD_FTL; 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; @@ -121,7 +121,6 @@ public class MapMLDocumentBuilder { private static final int BYTES_PER_PIXEL_TRANSPARENT = 4; private static final int BYTES_PER_KILOBYTE = 1024; public static final String DEFAULT_MIME_TYPE = "image/png"; - public static final String MAPML_XML_HEAD_FTL = "mapml-head.ftl"; private final WMS wms; @@ -901,7 +900,7 @@ private HeadContent prepareHead() throws IOException { } String styles = buildStyles(); // get the styles and links from the head template - List stylesAndLinks = getHeaderTemplates(MAPML_HEAD_FTL, getFeatureTypes()); + List stylesAndLinks = getHeaderTemplates(MAPML_XML_HEAD_FTL, getFeatureTypes()); styles = appendStylesFromHeadTemplate(styles, stylesAndLinks); if (styles != null) head.setStyle(styles); links.addAll(getLinksFromHeadTemplate(stylesAndLinks)); 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..20a797bb653 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,6 +7,7 @@ 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 java.io.IOException; import java.util.ArrayList; @@ -25,6 +26,7 @@ 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 +39,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 +59,7 @@ 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(); /** * Convert a feature collection to a MapML document @@ -91,6 +95,11 @@ public static Mapml featureCollectionToMapML( throw new ServiceException("MapML OutputFormat does not support Complex Features."); } SimpleFeatureCollection fc = (SimpleFeatureCollection) featureCollection; + boolean hasTemplate = false; + if (!mapMLMapTemplate.isTemplateEmpty( + fc.getSchema(), MAPML_FEATURE_FTL, FeatureTemplate.class, "0\n")) { + hasTemplate = true; + } ResourceInfo resourceInfo = layerInfo.getResource(); MetadataMap layerMeta = resourceInfo.getMetadata(); @@ -151,6 +160,9 @@ public static Mapml featureCollectionToMapML( try (SimpleFeatureIterator iterator = fc.features()) { while (iterator.hasNext()) { SimpleFeature feature = iterator.next(); + if (hasTemplate) { + String templateOutput = mapMLMapTemplate.features(fc.getSchema(), feature); + } // convert feature to xml if (styles != null) { List applicableStyles = getApplicableStyles(feature, styles); 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 index 1f9f8bd5aaf..c98a43fa5c9 100644 --- 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 @@ -18,22 +18,32 @@ 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; +import org.geotools.api.feature.type.FeatureType; +import org.geotools.feature.FeatureCollection; /** 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"); @@ -43,7 +53,9 @@ public class MapMLMapTemplate { 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_HEAD_FTL = "mapml-head.ftl"; + public static final String MAPML_XML_HEAD_FTL = "mapml-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 HashMap<>(); @@ -81,6 +93,18 @@ public String preview(SimpleFeatureType featureType) throws IOException { return caw.toString(); } + public String features(SimpleFeatureType featureType, SimpleFeature feature) + throws IOException { + caw.reset(); + 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. * @@ -91,7 +115,7 @@ public String preview(SimpleFeatureType featureType) throws IOException { */ public void head(Map model, SimpleFeatureType featureType, Writer writer) throws IOException { - execute(model, featureType, writer, MAPML_HEAD_FTL); + execute(model, featureType, writer, MAPML_XML_HEAD_FTL); } /** @@ -131,6 +155,24 @@ private void execute( } } + /* + * 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); + } + } + /** * Returns the template for the specified feature type. Looking up templates is pretty * expensive, so we cache templates by feture type and template. 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..356dbfb1b9b 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,11 +6,13 @@ 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.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; @@ -18,8 +20,11 @@ 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; @@ -289,6 +294,71 @@ public void testMapMLGetStyleQuery() throws Exception { qElse.getFilter().toString().contains("ADDRESS = 123 Main Street")); } + @Test + public void testMapMLFeatureHasClass() throws Exception { + File template = null; + try { + Catalog cat = getCatalog(); + LayerInfo li = cat.getLayerByName(MockData.BUILDINGS.getLocalPart()); + li.getResource().getMetadata().put(MAPML_USE_FEATURES, true); + li.getResource().getMetadata().put(MAPML_USE_TILES, false); + cat.save(li); + String layerId = getLayerId(MockData.BUILDINGS); + FeatureTypeInfo resource = + getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); + File parent = getDataDirectory().get(resource).dir(); + template = new File(parent, MAPML_FEATURE_FTL); + FileUtils.write( + template, + "\n" + + " .desired {stroke-dashoffset:3}\n" + + "\n" + + " <#list attributes as attribute>\n" + + " <#if attribute.name == \"ADDRESS\">\n" + + " \n" + + " <#elseif !attribute.isGeometry>\n" + + " \n" + + " \n" + + " <#if attribute.isGeometry>\n" + + " \n" + + " \n" + + " <#list attribute.rawValue.coordinates as coord>\n" + + " <#if coord?index == 3>\n" + + " ${coord.x} ${coord.y}\n" + + " <#elseif coord?index == 4>\n" + + " ${coord.x} ${coord.y}\n" + + " <#else>\n" + + " ${coord.x} ${coord.y}\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n", + "UTF-8"); + Mapml mapmlFeatures = + new MapMLWMSRequest() + .name(MockData.BUILDINGS.getLocalPart()) + .bbox("-180,-90,180,90") + .srs("EPSG:4326") + .feature(true) + .getAsMapML(); + + String mapmlStyle = mapmlFeatures.getHead().getStyle(); + + Feature feature2 = + mapmlFeatures + .getBody() + .getFeatures() + .get(1); // get the second feature, which has a class + assertEquals("desired", feature2.getStyle()); + } 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 3258e311534..d847dc4aa62 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,8 +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.MapMLDocumentBuilder.MAPML_XML_HEAD_FTL; 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;