package org.mapfish.print.map.style.json;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import org.geotools.styling.Style;
import org.geotools.styling.StyleBuilder;
import org.json.JSONObject;
import org.mapfish.print.Constants;
import org.mapfish.print.ExceptionUtils;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.map.style.ParserPluginUtils;
import org.mapfish.print.map.style.SLDParserPlugin;
import org.mapfish.print.map.style.StyleParserPlugin;
import org.mapfish.print.wrapper.json.PJsonObject;
import org.springframework.http.client.ClientHttpRequestFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.mapfish.print.map.style.json.MapfishJsonStyleVersion1.DEFAULT_GEOM_ATT_NAME;
/**
* Supports all style format.
* <p>
* This style parser support two versions of JSON and SLD formatting. Both versions use the same
* parameter names for configuring the values of the various properties of the style but the layout
* differs between the two and version 2 is more flexible and powerful than version 1.
* </p>
* <h2 id="stylev1">Mapfish JSON Style Version 1 <a class="headerlink" href="#stylev1">¶</a></h2>
* <p>
* Version 1 is compatible with mapfish print <= v2 and is based on the OpenLayers v2 styling.
* The layout is as follows:
* </p>
* <pre><code>
* {
* "version" : "1",
* "styleProperty":"_gx_style",
* "1": {
* "fillColor":"#FF0000",
* "fillOpacity":0,
* "rotation" : "30",
*
* "externalGraphic" : "mark.png"
* "graphicName": "circle",
* "graphicOpacity": 0.4,
* "pointRadius": 5,
*
* "strokeColor":"#FFA829",
* "strokeOpacity":1,
* "strokeWidth":5,
* "strokeLinecap":"round",
* "strokeDashstyle":"dot",
*
* "fontColor":"#000000",
* "fontFamily": "sans-serif",
* "fontSize": "12px",
* "fontStyle": "normal",
* "fontWeight": "bold",
* "haloColor": "#123456",
* "haloOpacity": "0.7",
* "haloRadius": "3.0",
* "label": "${name}",
* "labelAlign": "cm",
* "labelRotation": "45",
* "labelXOffset": "-25.0",
* "labelYOffset": "-35.0"
* }
* }
* </code></pre>
* <p></p>
* <h2 id="stylev2">Mapfish JSON Style Version 2 <a class="headerlink" href="#stylev2">¶</a></h2>
* <p>
* Version 2 uses the same property names as version 1 but has a different structure.
* The layout is as follows:
* </p>
* <pre><code>
* {
* "version" : "2",
* // shared values can be declared here (at top level)
* // and used in form ${constName} later in json
* "val1" : "#FFA829",
* // default values for properties can be defined here
* " strokeDashstyle" : "dot"
* "[population > 300]" : {
* // default values for current rule can be defined here
* // they will override default values defined at
* // higher level
* "rotation" : "30",
*
* //min and max scale denominator are optional
* "maxScale" : 1000000,
* "minScale" : 100000,
* "symbolizers" : [{
* // values defined in symbolizer will override defaults
* "type" : "point",
* "fillColor":"#FF0000",
* "fillOpacity":0,
* "rotation" : "30",
* "externalGraphic" : "mark.png",
*
* "graphicName": "circle",
* "graphicOpacity": 0.4,
* "pointRadius": 5,
*
* "strokeColor":"${val1}",
* "strokeOpacity":1,
* "strokeWidth":5,
* "strokeLinecap":"round",
* "strokeDashstyle":"dot"
* }, {
* "type" : "line",
* "strokeColor":"${val1}",
* "strokeOpacity":1,
* "strokeWidth":5,
* "strokeLinecap":"round",
* "strokeDashstyle":"dot"
* }, {
* "type" : "polygon",
* "fillColor":"#FF0000",
* "fillOpacity":0,
*
* "strokeColor":"${val1}",
* "strokeOpacity":1,
* "strokeWidth":5,
* "strokeLinecap":"round",
* "strokeDashstyle":"dot"
* }, {
* "type" : "text",
* "fontColor":"#000000",
* "fontFamily": "sans-serif",
* "fontSize": "12px",
* "fontStyle": "normal",
* "fontWeight": "bold",
* "haloColor": "#123456",
* "haloOpacity": "0.7",
* "haloRadius": "3.0",
* "label": "[name]",
* "fillColor":"#FF0000",
* "fillOpacity":0,
* "labelAlign": "cm",
* "labelRotation": "45",
* "labelXOffset": "-25.0",
* "labelYOffset": "-35.0"
* }
* ]}
* }
* </code></pre>
* <p>
* As illustrated above the style consists of:
* </p>
* <ul>
* <li>The version number (2) (required)</li>
* <li>
* Common values which can be referenced in symbolizer property values.(optional)
* <p>Values can be referenced in the value of a property with the pattern: ${valName}</p>
* <p>Value names can only contain numbers, characters, _ or -</p>
* <p>
* Values do not have to be the full property they will be interpolated. For example:
* <code>The value is ${val}</code>
* </p>
* </li>
* <li>
* Defaults property definitions(optional):
* <p>
* In order to reduce duplication and keep the style definitions small, default values can be
* specified.
* The default values in the root (style level) will be used in all symbolizers if
* the value is not defined.
* The style level default will apply to all symbolizers defined in the system.
* </p>
* <p>
* The only difference between a value and a default is that the default has a well known name,
* therefore defaults can also be used as values.
* </p>
* </li>
* <li>
* All the styling rules (At least one is required)
* <p>
* A styling rule has a key which is the filter which selects the features that the rule will
* be used to draw and the rule definition object.
* </p>
* <p>The filter is either <code>*</code> or an
* <a href="http://docs.geoserver.org/stable/en/user/filter/ecql_reference.html#filter-ecql-reference">
* ECQL Expression</a>) surrounded by square brackets. For example: [att < 23].
* </p>
* <p>
* <em>WARNING:</em> At the moment DWITHIN and BEYOND spatial functions take a unit parameter.
* However it is ignored by geotools and the distance is always in the crs of the geometry
* projection.
* </p>
* The rule definition is as follows:
* <ul>
* <li>
* Default property values (optional):
* <p>
* Each rule can also have defaults. If the style and the rule have a default for
* the same property the rule will override the style default. All defaults can be
* (of course) overridden by a value in a symbolizer.
* </p>
* </li>
* <li>
* minScale (optional)
* <p>
* The minimum scale that the rule should evaluate to true
* </p>
* </li>
* <li>
* maxScale (optional)
* <p>
* The maximum scale that the rule should evaluate to true
* </p>
* </li>
* <li>
* An array of symbolizers. (at least one required).
* <p>
* A symbolizer must have a type property (point, line, polygon, text) which
* indicates the type of symbolizer and it has the attributes for that type of
* symbolizer. All values have defaults so it is possible to define a symbolizer
* as with only the type property. The only exception is that the "text" symbolizer
* needs a label property.
* </p>
* </li>
* </ul>
* </li>
* </ul>
* <p></p>
* <h2 id="config">Configuration Elements <a class="headerlink" href="#config">¶</a></h2>
* The items in the list below are the properties that can be set on the different symbolizers.
* In brackets list the symbolizers the values can apply to.
* <p>
* Most properties can be static values or ECQL expressions. If the property has <code>[ ]</code>
* around the property value then it will be interpreted as an ECQL expression. Otherwise it is
* assumed to be static text. If you need static text that start and ends with <code>[ ]</code> then
* you will have to enter: <code>['propertyValue']</code> (where propertyValue
* start and ends with <code>[ ]</code>.
* </p>
* <p>
* The items below with (ECQL) can have ECQL expressions.
* </p>
* <ul>
* <li><strong>fillColor</strong>(ECQL) - (polygon, point, text) The color used to fill the point
* graphic, polygon or text.</li>
* <li><strong>fillOpacity</strong>(ECQL) - (polygon, point, text) The opacity used when fill the
* point graphic, polygon or text.</li>
* <li><strong>rotation</strong>(ECQL) - (point) The rotation of the point graphic</li>
* <li>
* <strong>externalGraphic</strong> - (point) one of the two options for declaring the point
* graphic to use. This can
* be a URL to the icon to use or, if just a string it will be assumed to refer to a file in the
* configuration directory (or subdirectory). Only files in the configuration directory (or
* subdirectory) will be allowed.
* </li>
* <li>
* <strong>graphicName</strong>(ECQL) - (point) one of the two options for declaring the point
* graphic to use. This is the
* default and will be a square if not specified. The option are any of the Geotools Marks.
* <p>Geotools has by default 3 types of marks:</p>
* <ul>
* <li>WellKnownMarks: cross, star, triangle, arrow, X, hatch, square</li>
* <li>ShapeMarks: shape://vertline, shape://horline, shape://slash, shape://backslash,
* shape://dot, shape://plus, shape://times, shape://oarrow, shape://carrow,
* shape://coarrow, shape://ccarrow</li>
* <li>TTFMarkFactory: ttf://fontName#code (where fontName is a TrueType font and the code is
* the code number of thecharacter to render for the point.</li>
* </ul>
* </li>
* <li><strong>graphicOpacity</strong>(ECQL) - (point) the opacity to use when drawing the point
* graphic</li>
* <li><strong>pointRadius</strong>(ECQL) - (point) the size at which to draw the point graphic</li>
* <li>
* <strong>strokeColor</strong>(ECQL) - (line, point, polygon) the color to use when drawing a line
* or the outline of a polygon or point graphic
* </li>
* <li><strong>strokeOpacity</strong>(ECQL) - (line, point, polygon) the opacity to use when drawing
* the line/stroke</li>
* <li><strong>strokeWidth</strong>(ECQL) - (line, point, polygon) the widh of the line/stroke</li>
* <li>
* <strong>strokeLinecap</strong>(ECQL) - (line, point, polygon) the style used when drawing the
* end of a line.
* <p>
* Options: butt (sharp square edge), round (rounded edge), and square (slightly elongated
* square edge). Default is butt
* </p>
* </li>
* <li>
* <strong>strokeDashstyle</strong> - (line, point, polygon) A string describing how to draw the
* line or an array of floats describing the line lengths and space lengths:
* <ul>
* <li>dot - translates to dash array: [0.1, 2 * strokeWidth]</li>
* <li>dash - translates to dash array: [2 * strokeWidth, 2 * strokeWidth]</li>
* <li>dashdot - translates to dash array: [3 * strokeWidth, 2 * strokeWidth, 0.1, 2 *
* strokeWidth]</li>
* <li>longdash - translates to dash array: [4 * strokeWidth, 2 * strokeWidth]</li>
* <li>longdashdot - translates to dash array: [5 * strokeWidth, 2 * strokeWidth, 0.1, 2 *
* strokeWidth]</li>
* <li>{string containing spaces to delimit array elements} - Example: [1 2 3 1 2]</li>
* </ul>
* </li>
* <li><strong>fontColor</strong>(ECQL) - (text) the color of the text drawn</li>
* <li><strong>fontFamily</strong>(ECQL) - (text) the font of the text drawn</li>
* <li><strong>fontSize</strong>(ECQL) - (text) the font size of the text drawn</li>
* <li><strong>fontStyle</strong>(ECQL) - (text) the font style of the text drawn</li>
* <li><strong>fontWeight</strong>(ECQL) - (text) the font weight of the text drawn</li>
* <li><strong>haloColor</strong>(ECQL) - (text) the color of the halo around the text</li>
* <li><strong>haloOpacity</strong>(ECQL) - (text) the opacity of the halo around the text</li>
* <li><strong>haloRadius</strong>(ECQL) - (text) the radius of the halo around the text</li>
* <li>
* <strong>label</strong>(ECQL) - (text) the expression used to create the label e. See the
* section on labelling for more details
* </li>
* <li>
* <strong>labelAlign</strong> - (Point Placement) the indicator of how to align the text with
* respect to the geometry.
* This property must have 2 characters, the x-align and the y-align.
* <p>
* X-Align options:
* </p>
* <ul>
* <li>l - align to the left of the geometric center</li>
* <li>c - align on the center of the geometric center</li>
* <li>r - align to the right of the geometric center</li>
* </ul>
* <p>
* Y-Align options:
* </p>
* <ul>
* <li>b - align to the bottom of the geometric center</li>
* <li>m - align on the middle of the geometric center</li>
* <li>t - align to the top of the geometric center</li>
* </ul>
* </li>
*
* <li><strong>labelRotation</strong>(ECQL) - (Point Placement) the rotation of the label</li>
* <li><strong>labelXOffset</strong>(ECQL) - (Point Placement) the amount to offset the label along the
* x axis. negative number offset to the left</li>
* <li><strong>labelYOffset</strong>(ECQL) - (Point Placement) the amount to offset the label along the
* y axis. negative number offset to the top of the printing</li>
* <li><strong>labelAnchorPointX</strong>(ECQL) - (Point Placement) The point along the x axis that the
* label is started at anchored). Offset and rotation is relative to this point. Only one of
* labelAnchorPointX/Y or labelAlign will be respected, since they are both ways of defining the anchor
* Point</li>
* <li><strong>labelAnchorPointY</strong>(ECQL) - (Point Placement) The point along the y axis that the
* label is started at (anchored). Offset and rotation is relative to this point. Only one of
* labelAnchorPointX/Y or labelAlign will be respected, since they are both ways of defining the anchor
* Point</li>
* <li><strong>labelPerpendicularOffset</strong>(ECQL) - (Line Placement) If this property is defined
* it will be assumed that the geometry is a line and this property defines how far from the center of
* the line the label should be drawn.</li>
* </ul>
* <p></p>
* <h2 id="labels">Labelling: <a class="headerlink" href="#labels">¶</a></h2>
* <p>
* Labelling in this style format is done by defining a text symbolizer ("type":"text"). All text
* symbolizers consist of:
* </p>
* <ul>
* <li><a href="#labelproperties">Label Property</a></li>
* <li><a href="#haloproperties">Halo Properties</a></li>
* <li><a href="#otherproperties">Font/weight/style/color/opacity</a></li>
* <li><a href="#placementproperties">Placement Properties</a></li>
* <li><a href="#vendoroptions">Vendor Options</a></li>
* </ul>
*
* <h3 id="labelproperties">Label Property <a class="headerlink" href="#labelproperties">¶</a></h3>
* <p>
* The label property defines what label will be drawn for a given feature. The value is either a
* string which will be the static label for all features that the symbolizer will be drawn on or a
* string surrounded by [] which indicates that it is an ECQL Expression. Examples:
* </p>
* <ul>
* <li>Static label</li>
* <li>[attributeName]</li>
* <li>['Static Label Again']</li>
* <li>[5]</li>
* <li>5</li>
* <li>env('java.home')</li>
* <li>centroid(geomAtt)</li>
* </ul>
*
* <h3 id="haloproperties">Halo Properties <a class="headerlink" href="#haloproperties">¶</a></h3>
* <p>
* A halo is a space around the drawn label text that is color (using the halo properties). A label
* with a halo is like the drawn label text with a buffer around the label text drawn using the halo
* properties. This allows the label to be clearly visible regardless of the background. For example
* if the text is black and the halo is with, then the text will always be readable thanks to the white
* buffer around the label text.
* </p>
*
* <h3 id="otherproperties">Font/weight/style/color/opacity
* <a class="headerlink" href="#otherproperties">¶</a></h3>
* <p>
* The Font/weight/style/color/opacity properties define how the label text is drawn. They are for the
* most part equivalent to the similarly named css and SLD properties.
* </p>
*
* <h3 id="placementproperties">Placement Properties
* <a class="headerlink" href="#placementproperties">¶</a></h3>
* <p>
* An important part of defining a text symbolizer is defining where the text/label will be drawn. The
* placement properties are used for this purpose. There are two types of placements, Point and Line
* placement and <em>only one</em> type of placement can be used. The type of placement is determined
* by inspecting the properties in the text symbolizer and if the <em>labelPerpendicularOffset</em>
* property is defined then a line placement will be created for the text symbolizer.
* </p>
* <p>
* It is important to realize that since only one type of placement can be used, an error will be
* reported if <em>labelPerpendicularOffset</em> is defined in the text symbolizer along with
* <em>any</em> of the point placement properties.
* </p>
* <p><strong>Point Placement</strong></p>
* <p>
* Point placement defines an <em>anchor point</em> which is the point to draw the text relative to.
* For example an anchor point of 0.5, 0.5 ("labelAnchorPointX" : "0.5", "labelAnchorPointY" : "0.5")
* would position the start of the label at the center of the geometry.
* </p>
* <p>
* After <em>anchor point</em>, comes <em>displacement</em> displacement defines the distance
* from the anchor point to start the label. The combination of the two values determines the final
* location of the label.
* </p>
* <p>Lastly, there is a label rotation which defines the orientation of the label.</p>
* <p>
* There are two ways to define the anchor point, either the <em>labelAnchorPointX/Y</em> properties
* are set or the <em>labelAlign</em> property is set. If both are defined then the
* <em>labelAlign</em> will be ignored.
* </p>
*
* <h3 id="vendoroptions">Vendor Options <a class="headerlink" href="#vendoroptions">¶</a></h3>
* <p>For text symbolizers the following vendor options are available:</p>
* <ul>
* <li><strong>allowOverruns</strong> (false): When false does not allow labels on lines to get beyond the
* beginning/end of the line. By default a partial overrun is tolerated, set to false to disallow it.
* </li>
* <li><strong>autoWrap</strong> (400): Number of pixels are which a long label should be split into
* multiple lines. Works on all geometries, on lines it is mutually exclusive with the followLine option.
* </li>
* <li><strong>conflictResolution</strong> (true): Enables conflict resolution (default, true) meaning no
* two labels will be allowed to overlap. Symbolizers with conflict resolution off are considered
* outside of the conflict resolution game, they don’t reserve area and can overlap with other labels.
* </li>
* <li><strong>followLine</strong> (true): When true activates curved labels on linear geometries. The
* label will follow the shape of the current line, as opposed to being drawn a tangent straight line
* </li>
* <li><strong>goodnessOfFit</strong> (90): Sets the percentage of the label that must sit inside the
* geometry to allow drawing the label. Works only on polygons.
* </li>
* <li><strong>group</strong> (false): If true, geometries with the same labels are grouped and considered
* a single entity to be labelled. This allows to avoid or control repeated labels.
* </li>
* <li><strong>maxDisplacement</strong> (400): The distance, in pixel, a label can be displaced from its
* natural position in an attempt to find a position that does not conflict with already drawn labels.
* </li>
* <li><strong>spaceAround</strong> (50): The minimum distance between two labels, in pixels.</li>
* </ul>
* <p>Example</p>
* <pre><code>
* {
* "version" : "2",
* "*" : {
* "symbolizers" : [{
* "type" : "text",
* "fontColor":"#000000",
* "label": "[name]",
* "goodnessOfFit": 0.1,
* "spaceAround": 10
* }
* ]}
* }
* </code></pre>
* <p>For more information, please refer to the
* <a href="http://docs.geotools.org/latest/userguide/library/render/style.html#textsymbolizer">GeoTools
* documentation</a>.
* </p>
* <p></p>
* <h2>ECQL references:</h2>
* <ul>
* <li><a href="http://docs.geoserver.org/stable/en/user/filter/ecql_reference.html#ecql-expr">
* http://docs.geoserver.org/stable/en/user/filter/ecql_reference.html#ecql-expr</a></li>
* <li><a href="http://udig.refractions.net/files/docs/latest/user/Constraint%20Query%20Language.html">
* http://udig.refractions.net/files/docs/latest/user/Constraint%20Query%20Language.html</a></li>
* <li><a href="http://docs.geoserver.org/stable/en/user/filter/function_reference.html#filter-function-reference">
* http://docs.geoserver.org/stable/en/user/filter/function_reference.html#filter-function-reference</a></li>
* <li><a href="http://docs.geotools.org/stable/userguide/library/cql/ecql.html">
* http://docs.geotools.org/stable/userguide/library/cql/ecql.html</a></li>
* <li><a href="http://docs.geoserver.org/latest/en/user/tutorials/cql/cql_tutorial.html">
* http://docs.geoserver.org/latest/en/user/tutorials/cql/cql_tutorial.html</a></li>
* </ul>
*/
public final class MapfishStyleParserPlugin implements StyleParserPlugin {
enum Versions {
ONE("1") {
@Override
Style parseStyle(
@Nonnull final PJsonObject json,
@Nonnull final StyleBuilder styleBuilder,
@Nullable final Configuration configuration,
@Nonnull final ClientHttpRequestFactory requestFactory) {
return new MapfishJsonStyleVersion1(
json, styleBuilder, configuration, requestFactory, DEFAULT_GEOM_ATT_NAME).parseStyle();
}
}, TWO("2") {
@Override
Style parseStyle(
@Nonnull final PJsonObject json,
@Nonnull final StyleBuilder styleBuilder,
@Nullable final Configuration configuration,
@Nonnull final ClientHttpRequestFactory requestFactory) {
return new MapfishJsonStyleVersion2(json, styleBuilder, configuration, requestFactory)
.parseStyle();
}
};
private final String versionNumber;
Versions(final String versionNumber) {
this.versionNumber = versionNumber;
}
abstract Style parseStyle(
PJsonObject json, StyleBuilder styleBuilder, Configuration configuration,
ClientHttpRequestFactory requestFactory);
}
static final String JSON_VERSION = "version";
private StyleBuilder sldStyleBuilder = new StyleBuilder();
@Override
public Optional<Style> parseStyle(
@Nullable final Configuration configuration,
@Nonnull final ClientHttpRequestFactory clientHttpRequestFactory,
@Nonnull final String styleString) throws Throwable {
final Optional<Style> styleOptional = tryParse(
configuration, styleString, clientHttpRequestFactory);
if (styleOptional.isPresent()) {
return styleOptional;
}
return ParserPluginUtils.loadStyleAsURI(clientHttpRequestFactory, styleString, new Function<byte[],
Optional<Style>>() {
@Override
public Optional<Style> apply(final byte[] input) {
try {
return tryParse(
configuration, new String(input, Constants.DEFAULT_CHARSET),
clientHttpRequestFactory);
} catch (Throwable e) {
throw ExceptionUtils.getRuntimeException(e);
}
}
});
}
private Optional<Style> tryParse(
@Nullable final Configuration configuration,
@Nonnull final String styleString,
@Nonnull final ClientHttpRequestFactory clientHttpRequestFactory)
throws Throwable {
String trimmed = styleString.trim();
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
final PJsonObject json = new PJsonObject(new JSONObject(styleString), "style");
final String jsonVersion = json.optString(JSON_VERSION, "1");
for (Versions versions : Versions.values()) {
if (versions.versionNumber.equals(jsonVersion)) {
return Optional.of(versions.parseStyle(
json, this.sldStyleBuilder, configuration, clientHttpRequestFactory));
}
}
} else if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
final SLDParserPlugin parser = new SLDParserPlugin();
return parser.parseStyle(configuration, clientHttpRequestFactory, styleString);
}
return Optional.absent();
}
}