/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2017, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.mbstyle.layer; import org.geotools.mbstyle.MBStyle; import org.geotools.mbstyle.parse.MBFilter; import org.geotools.mbstyle.parse.MBFormatException; import org.geotools.mbstyle.parse.MBObjectParser; import org.geotools.mbstyle.transform.MBStyleTransformer; import org.geotools.styling.*; import org.geotools.styling.Font; import org.geotools.text.Text; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.style.SemanticType; import org.opengis.style.Symbolizer; import javax.measure.unit.NonSI; import java.awt.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * A symbol. * <p> * MBLayer wrapper around a {@link JSONObject} representation of a "symbol" type layer. All * methods act as accessors on provided JSON layer, no other state is maintained. This allows * modifications to be made cleanly with out chance of side-effect. * * <ul> * <li>get methods: access the json directly</li> * <li>query methods: provide logic / transforms to GeoTools classes as required.</li> * </ul> */ public class SymbolMBLayer extends MBLayer { private JSONObject layout; private JSONObject paint; private static String TYPE = "symbol"; public enum SymbolPlacement { /** * The label is placed at the point where the geometry is located. */ POINT, /** * The label is placed along the line of the geometry. Can only be used on LineString and Polygon geometries. */ LINE } /** * Interpreted differently when applied to different fields. */ public enum Alignment { MAP, VIEWPORT, AUTO } public enum IconTextFit { /** * The icon is displayed at its intrinsic aspect ratio. */ NONE, /** * The icon is scaled in the x-dimension to fit the width of the text. */ WIDTH, /** * The icon is scaled in the y-dimension to fit the height of the text. */ HEIGHT, /** * The icon is scaled in both x- and y-dimensions. */ BOTH } public enum Justification { /** * The text is aligned to the left. */ LEFT, /** * The text is centered. */ CENTER, /** * The text is aligned to the right. */ RIGHT } /** * Text justification options. */ public enum TextAnchor { /** * The center of the text is placed closest to the anchor. */ CENTER(0.5, 0.5), /** * The left side of the text is placed closest to the anchor. */ LEFT(0.0, 0.5), /** * The right side of the text is placed closest to the anchor. */ RIGHT(1.0, 0.5), /** * The top of the text is placed closest to the anchor. */ TOP(0.5, 1.0), /** * The bottom of the text is placed closest to the anchor. */ BOTTOM(0.5, 0.0), /** * The top left corner of the text is placed closest to the anchor. */ TOP_LEFT(0.0, 1.0), /** * The top right corner of the text is placed closest to the anchor. */ TOP_RIGHT(1.0, 1.0), /** * The bottom left corner of the text is placed closest to the anchor. */ BOTTOM_LEFT(0.0, 0.0), /** * The bottom right corner of the text is placed closest to the anchor. */ BOTTOM_RIGHT(1.0, 0.0); /** horizontal justification */ final private double x; /** vertical justification */ final private double y; TextAnchor(double x, double y) { this.x = x; this.y = y; } /** * Horizontal justification. * * @return horizontal alignment between 0.0 and 1.0. */ public double getX() { return x; } /** * Vertical justification. * * @return vertical alignment between 0.0 and 1.0. */ public double getY() { return y; } /** * Parse provided jsonString as a TextAnchor. * <p> * One of center, left, right, top, bottom, top-left, top-right, bottom-left, bottom-right. Defaults to center.</p> * * @param jsonString text anchor definition * @return TextAnchor, defaults TextAnchor#CENTER if undefined */ public static TextAnchor parse(String jsonString){ if( jsonString == null ){ return CENTER; } String name = jsonString.toUpperCase().trim().replace('-', '_'); try { return TextAnchor.valueOf(name); } catch (IllegalArgumentException invalid){ throw new MBFormatException("Invalid text-alginment '"+jsonString+"' expected one of" + "center, left, right, top, bottom, top-left, top-right, bottom-left, bottom-right"); } } /** * The json representation of this TextAnchor. * * @return json representation */ public String json(){ return name().toLowerCase().replace('_', '-'); } /** * Quickly grab y justification for jsonString. * * @param jsonString * @return vertical anchor, defaults to 0.5 */ public static double getAnchorY(String jsonString){ return TextAnchor.parse(jsonString).getY(); } /** * Quickly grab x justification for jsonString. * * @param jsonString * @return horizontal anchor, defaults to 0.5 */ public static double getAnchorX(String jsonString){ return TextAnchor.parse(jsonString).getX(); } } public enum TextTransform { /** * The text is not altered. */ NONE, /** * Forces all letters to be displayed in uppercase. */ UPPERCASE, /** * Forces all letters to be displayed in lowercase. */ LOWERCASE, } public enum TranslateAnchor { /** * Translation relative to the map. */ MAP, /** * Translation relative to the viewport. */ VIEWPORT } /** * * @param json */ public SymbolMBLayer(JSONObject json) { super(json,new MBObjectParser(SymbolMBLayer.class)); paint = super.getPaint(); layout = super.getLayout(); } /** * * @return The default semantic type. */ @Override protected SemanticType defaultSemanticType() { return SemanticType.ANY; } /** * (Optional) One of point, line. Defaults to point. * * Label placement relative to its geometry. * * @return SymbolPlacement */ public SymbolPlacement getSymbolPlacement() { Object value = layout.get("symbol-placement"); if (value != null && "line".equalsIgnoreCase((String) value)) { return SymbolPlacement.LINE; } else { return SymbolPlacement.POINT; } } /** * (Optional) One of point, line. Defaults to point. * * Label placement relative to its geometry. * * @return SymbolPlacement */ public Expression symbolPlacement() { return parse.string(layout, "symbol-placement", "point"); } /** * (Optional) Units in pixels. Defaults to 250. Requires SymbolPlacement.LINE * * Distance between two symbol anchors. * * @return Number representing distance between two symbol anchors * @throws MBFormatException */ public Number getSymbolSpacing() throws MBFormatException { return parse.optional(Number.class, layout, "symbol-spacing", 250); } /** * Access symbol-spacing, defaults to 250. * * @return Number representing distance between two symbol anchors * @throws MBFormatException */ public Expression symbolSpacing() throws MBFormatException { return parse.percentage(layout, "symbol-spacing", 250); } /** * (Optional) Defaults to false. * * If true, the symbols will not cross tile edges to avoid mutual collisions. Recommended in layers that don't have * enough padding in the vector tile to prevent collisions, or if it is a point symbol layer placed after a line * symbol layer. * * @return Whether or not the symbols should avoid edges. * @throws MBFormatException */ public Boolean getSymbolAvoidEdges() throws MBFormatException { return parse.getBoolean(layout, "symbol-avoid-edges", false); } /** * Wraps {@link #getSymbolAvoidEdges()} in a GeoTools expression. * * (Optional) Defaults to false. If true, the symbols will not cross tile edges to avoid mutual collisions. Recommended in layers that don't have * enough padding in the vector tile to prevent collisions, or if it is a point symbol layer placed after a line symbol layer. * * @return Whether or not the symbols should avoid edges. * @throws MBFormatException */ public Expression symbolAvoidEdges() { return parse.bool(layout, "symbol-avoid-edges", false); } /** * (Optional) Defaults to false. Requires icon-image. * * If true, the icon will be visible even if it collides with other previously drawn symbols. * * @return Whether or not the symbols should be allowed to overlap other symbols * @throws MBFormatException */ public Boolean getIconAllowOverlap() throws MBFormatException { return parse.getBoolean(layout, "icon-allow-overlap", false); } /** * Wraps {@link #getIconAllowOverlap()} in a GeoTools expression. * * (Optional) Defaults to false. Requires icon-image. * * If true, the icon will be visible even if it collides with other previously drawn symbols. * * @return Whether or not the symbols should be allowed to overlap other symbols */ public Expression iconAllowOverlap() throws MBFormatException { return parse.bool(layout, "icon-allow-overlap", false); } /** * (Optional) Defaults to false. Requires icon-image. * * If true, other symbols can be visible even if they collide with the icon. * * @return Whether or not other symbols should be allowed to overlap symbols in this layer. * @throws MBFormatException */ public Boolean getIconIgnorePlacement() throws MBFormatException { return parse.getBoolean(layout, "icon-ignore-placement", false); } /** * * Wraps {@link #getIconIgnorePlacement()} in a GeoTools expression. * * (Optional) Defaults to false. Requires icon-image. If true, other symbols can be visible even if they collide with the icon. * * @return Whether or not other symbols should be allowed to overlap symbols in this layer. * @throws MBFormatException */ public Expression iconIgnorePlacement() { return parse.bool(layout, "icon-ignore-placement", false); } /** * (Optional) Defaults to false. Requires icon-image. Requires text-field. * * If true, text will display without their corresponding icons when the icon collides with other symbols and the * text does not. * * @return Whether or not the label may be drawn when the icon is not drawn due to collisions * @throws MBFormatException */ public Boolean getIconOptional() throws MBFormatException { return parse.getBoolean(layout, "icon-optional", false); } /** * Optional enum. One of map, viewport, auto. Defaults to auto. Requires icon-image. In combination with * symbol-placement, determines the rotation * * Wraps {@link #getIconOptional()} in a GeoTools expression. (Optional) Defaults to false. Requires icon-image. Requires text-field. * * If true, text will display without their corresponding icons when the icon collides with other symbols and the text does not. * * @return Whether or not the label may be drawn when the icon is not drawn due to collisions * @throws MBFormatException */ public Expression iconOptional() { return parse.bool(layout, "icon-optional", false); } /** * Optional enum. One of map, viewport, auto. Defaults to auto. Requires icon-image. In combination with symbol-placement, determines the rotation * behavior of icons. * * Possible values: * * {@link Alignment#MAP} When symbol-placement is set to point, aligns icons east-west. When symbol-placement is set * to line, aligns icon x-axes with the line. * * {@link Alignment#VIEWPORT} Produces icons whose x-axes are aligned with the x-axis of the viewport, regardless of * the value of symbol-placement. * * {@link Alignment#AUTO} When symbol-placement is set to point, this is equivalent to viewport. When * symbol-placement is set to line, this is equivalent to map. * * @return The icon rotation alignment */ public Alignment getIconRotationAlignment() { Object value = layout.get("icon-rotation-alignment"); if (value != null && "map".equalsIgnoreCase((String) value)) { return Alignment.MAP; } else if (value != null && "viewport".equalsIgnoreCase((String) value)){ return Alignment.VIEWPORT; } else { return Alignment.AUTO; } } /** * Converts {@link #getIconRotationAlignment()} to a GeoTools expression. Returns an expression that evaluates to one of "map", "viewport", or "auto". */ public Expression iconRotationAlignment() { return parse.enumToExpression(layout, "icon-rotation-alignment", Alignment.class, Alignment.AUTO); } /** * (Optional) Defaults to 1. Requires icon-image. * * Scale factor for icon. 1 is original size, 3 triples the size. * * @return The icon size. * @throws MBFormatException */ public Number getIconSize() throws MBFormatException{ return parse.optional(Number.class, layout, "icon-size", 1.0); } /** * Access icon-size, defaults to 1. * * @return The icon size. * @throws MBFormatException */ public Expression iconSize() { return parse.percentage(layout, "icon-size", 1.0); } /** * (Optional) One of none, width, height, both. Defaults to none. Requires icon-image. Requires text-field. * Scales the icon to fit around the associated text. * * @return How the icon should be scaled to fit the associated text */ public IconTextFit getIconTextFit() { Object value = layout.get("icon-text-fit"); if (value != null && "width".equalsIgnoreCase((String) value)){ return IconTextFit.WIDTH; } else if (value != null && "height".equalsIgnoreCase((String) value)){ return IconTextFit.HEIGHT; } else if (value != null && "both".equalsIgnoreCase((String) value)){ return IconTextFit.BOTH; } else { return IconTextFit.NONE; } } /** * Wraps {@link #getIconTextFit()} in a GeoTools expression. * * (Optional) One of none, width, height, both. Defaults to none. Requires icon-image. Requires text-field. * Scales the icon to fit around the associated text. * * @return How the icon should be scaled to fit the associated text */ public Expression iconTextFit() { return parse.string(layout, "icon-text-fit", "none"); } /** * (Optional) Units in pixels. Defaults to 0,0,0,0. Requires icon-image. Requires text-field. Requires * icon-text-fit = one of both, width, height. * * Size of the additional area added to dimensions determined by icon-text-fit, in clockwise * order: top, right, bottom, left. * * @return The padding to add to icon-text-fit */ public List<Number> getIconTextFitPadding() { // Not currently supported. (GT padding does not support multiple values). // json.get("icon-text-fit-padding") return null; } /** * (Optional) Units in pixels. Defaults to 0,0,0,0. Requires icon-image. Requires text-field. Requires * icon-text-fit = one of both, width, height. * * Size of the additional area added to dimensions determined by icon-text-fit, in clockwise * order: top, right, bottom, left. * * @return The padding to add to icon-text-fit */ public Expression iconTextFitPadding() { // Not curreently supported. (GT padding does not support multiple values). // json.get("icon-text-fit-padding") return null; } /** * (Optional) A string with {tokens} replaced, referencing the data property to pull from. * * @return The name of the icon image * @throws MBFormatException */ public String getIconImage() throws MBFormatException { return parse.optional(String.class, layout, "icon-image", null); } /** * * @return True if the layer has a icon-image explicitly provided. */ public boolean hasIconImage() throws MBFormatException { return parse.isPropertyDefined(layout, "icon-image"); } /** * Access icon-image as literal or function expression * * @return The name of the icon image * @throws MBFormatException * */ public Expression iconImage() { return parse.string(layout, "icon-image", ""); } /** * (Optional) Units in degrees. Defaults to 0. Requires icon-image. * * Rotates the icon clockwise. * * @return The icon rotation * @throws MBFormatException */ public Number getIconRotate() throws MBFormatException { return parse.optional(Number.class, layout, "icon-rotate", 0.0); } /** * Access icon-rotate as literal or function expression * * @return The icon rotation * @throws MBFormatException */ public Expression iconRotate() throws MBFormatException { return parse.percentage(layout, "icon-rotate", 0); } /** * (Optional) Units in pixels. Defaults to 2. Requires icon-image. * * Size of the additional area around the icon bounding box used for detecting symbol collisions. * * @return Padding around the icon for collision-detection. * @throws MBFormatException */ public Number getIconPadding() throws MBFormatException { return parse.optional(Number.class, layout, "icon-padding", 2.0); } /** * Access icon-padding as literal or function expression * * @return Padding around the icon for collision-detection. * @throws MBFormatException */ public Expression iconPadding() throws MBFormatException { return parse.percentage(layout, "icon-padding", 2.0); } /** * (Optional) Defaults to false. Requires icon-image. Requires icon-rotation-alignment = map. Requires symbol-placement = line. * * If true, the icon may be flipped to prevent it from being rendered upside-down. * * @return Whether to flip the icon if the orientation of the geometry would cause it to be rendered upside-down * @throws MBFormatException */ public Boolean getIconKeepUpright() throws MBFormatException { return parse.getBoolean(layout, "icon-keep-upright", false); } /** * Wraps {@link #getIconKeepUpright()} in a GeoTools expression. * * (Optional) Defaults to false. Requires icon-image. Requires * icon-rotation-alignment = map. Requires symbol-placement = line. * * If true, the icon may be flipped to prevent it from being rendered upside-down. * * @return Whether to flip the icon if the orientation of the geometry would cause it to be rendered upside-down * @throws MBFormatException */ public Expression iconKeepUpright() { return parse.bool(layout, "icon-keep-upright", false); } /** * (Optional) Defaults to 0,0. Requires icon-image. * * Offset distance of icon from its anchor. Positive values indicate right and down, while negative values indicate left and up. When combined with icon-rotate the offset will be as if the rotated direction was up. * @return Offset of the icon from its anchor * @throws MBFormatException */ public double[] getIconOffset() throws MBFormatException { return parse.array(layout, "icon-offset", new double[] { 0.0, 0.0 }); } /** * Access icon-offset * * @return Offset of the icon from its anchor * @throws MBFormatException */ public Point iconOffset() throws MBFormatException { if (layout.get("icon-offset") != null) { JSONArray array = (JSONArray) layout.get("icon-offset"); Number x = (Number) array.get(0); Number y = (Number) array.get(1); return new Point(x.intValue(), y.intValue()); } else { return new Point(0, 0); } } /** * Maps {@link #getIconOffset()} to a {@link Displacement} * * (Optional) Defaults to 0,0. Requires icon-image. Offset distance of icon from its anchor. Positive values indicate right and down, while * negative values indicate left and up. When combined with icon-rotate the offset will be as if the rotated direction was up. * */ public Displacement iconOffsetDisplacement() { return parse.displacement(layout, "icon-offset", sf.displacement(ff.literal(0), ff.literal(0))); } /** * * Optional enum. One of map, viewport, auto. Defaults to auto. Requires text-field. Orientation of text when map is pitched. * * Possible values: * * {@link Alignment#MAP} The text is aligned to the plane of the map. * * {@link Alignment#VIEWPORT} The text is aligned to the plane of the viewport. * * {@link Alignment#AUTO} Automatically matches the value of text-rotation-alignment. * * @return Text alignment when the map is pitched. */ public Alignment getTextPitchAlignment() { Object value = layout.get("text-pitch-alignment"); if (value != null && "map".equalsIgnoreCase((String) value)) { return Alignment.MAP; } else if (value != null && "viewport".equalsIgnoreCase((String) value)){ return Alignment.VIEWPORT; } else { return Alignment.AUTO; } } /** * Converts {@link #getTextPitchAlignment()} to a GeoTools expression. Returns an expression that evaluates to one of "map", "viewport", or "auto". */ public Expression textPitchAlignment() { return parse.enumToExpression(layout, "text-pitch-alignment", Alignment.class, Alignment.AUTO); } /** * * Optional enum. One of map, viewport, auto. Defaults to auto. Requires text-field. In combination with symbol-placement, determines the rotation * behavior of the individual glyphs forming the text. * * Possible values: * * {@link Alignment#MAP} When symbol-placement is set to point, aligns text east-west. When symbol-placement is set to line, aligns text x-axes * with the line. * * {@link Alignment#VIEWPORT} Produces glyphs whose x-axes are aligned with the x-axis of the viewport, regardless of the value of * symbol-placement. * * {@link Alignment#AUTO} When symbol-placement is set to point, this is equivalent to viewport. When symbol-placement is set to line, this is * equivalent to map. * * @return Text alignment when the map is rotated. */ public Alignment getTextRotationAlignment() { Object value = layout.get("text-rotation-alignment"); if (value != null && "map".equalsIgnoreCase((String) value)) { return Alignment.MAP; } else if (value != null && "viewport".equalsIgnoreCase((String) value)){ return Alignment.VIEWPORT; } else { return Alignment.AUTO; } } /** * Converts {@link #getTextRotationAlignment()} to a GeoTools expression. * * @return A GeoTools expression that evaluates to "map", "viewport", or "auto". * @see {@link #getTextRotationAlignment()}}. */ public Expression textRotationAlignment() { return parse.enumToExpression(layout, "text-rotation-alignment", Alignment.class, Alignment.AUTO); } /** * (Optional) Value to use for a text label. Feature properties are specified using tokens like {field_name}. * * @return Value to use for a text label * @throws MBFormatException */ public String getTextField() throws MBFormatException { return parse.optional(String.class, layout, "text-field", ""); } /** * Access text-field as literal or function expression * @return Value to use for a text label * @throws MBFormatException */ public Expression textField() throws MBFormatException { return parse.string(layout, "text-field", ""); } /** * (Optional) Font stack to use for displaying text. * * Defaults to <code>["Open Sans Regular","Arial Unicode MS Regular"]</code>. Requires text-field. * * @return The font to use for the label */ public List<String> getTextFont() { String[] fonts = parse.array(String.class, layout, "text-font", new String[] { "Open Sans Regular", "Arial Unicode MS Regular" }); return Arrays.asList(fonts); } /** * Access text-font as a literal or function expression. * * @return The font to use for the label */ public List<Expression> textFont() { List<Expression> fontExpressions = new ArrayList<>(); String[] fonts = parse.array(String.class, layout, "text-font", new String[] {"Open Sans Regular","Arial Unicode MS Regular"}); for (int i = 0; i < fonts.length; i++) { fontExpressions.add(ff.literal(fonts[i])); } return fontExpressions; } /** * (Optional) Units in pixels. Defaults to 16. Requires text-field. * * Font size. * * @return The font size * @throws MBFormatException */ public Number getTextSize() throws MBFormatException { return parse.optional(Number.class, layout, "text-size", 16.0); } /** * Access text-size as literal or function expression * * @return The font size * @throws MBFormatException */ public Expression textSize() throws MBFormatException { return parse.percentage(layout, "text-size", 16.0); } /** * (Optional) Units in ems. Defaults to 10. Requires text-field. * * The maximum line width for text wrapping. * * @return Maximum label width * @throws MBFormatException */ public Number getTextMaxWidth() throws MBFormatException { return parse.optional(Number.class, layout, "text-max-width", 10.0); } /** * Access text-max-width as literal or function expression * * @return Maximum label width * @throws MBFormatException */ public Expression textMaxWidth() throws MBFormatException { return parse.percentage(layout, "text-max-width", 10.0); } /** * (Optional) Units in ems. Defaults to 1.2. Requires text-field. * * Text leading value for multi-line text. * * @return Label line height * @throws MBFormatException */ public Number getTextLineHeight() throws MBFormatException { return parse.optional(Number.class, layout, "text-line-height", 1.2); } /** * Access text-line-height as literal or function expression * * @return Label line height * @throws MBFormatException */ public Expression textLineHeight() throws MBFormatException { return parse.percentage(layout, "text-line-height", 1.2); } /** * (Optional) Units in ems. Defaults to 0. Requires text-field. * * Text tracking amount. * * @return Spacing between label characters * @throws MBFormatException */ public Number getTextLetterSpacing() throws MBFormatException { return parse.optional(Number.class, layout, "text-letter-spacing", 0.0); } /** * Access text-line-height as literal or function expression * * @return Spacing between label characters * @throws MBFormatException */ public Expression textLetterSpacing() throws MBFormatException { return parse.percentage(layout, "text-letter-spacing", 0.0); } /** * * Optional enum. One of left, center, right. Defaults to center. Requires text-field. * * Text justification options: * * {@link Justification#LEFT} The text is aligned to the left. * * {@link Justification#CENTER} The text is centered. * * {@link Justification#RIGHT} The text is aligned to the right. * * @return The label justification. */ public Justification getTextJustify() { Object value = layout.get("text-justify"); if (value != null && "left".equalsIgnoreCase((String) value)) { return Justification.LEFT; } else if (value != null && "right".equalsIgnoreCase((String) value)){ return Justification.RIGHT; } else { return Justification.CENTER; } } /** * Converts {@link #getTextJustify()} to a GeoTools expression. Returns an expression that evaluates to one of "left", "right", or "center". * * @see {@link #getTextJustify()} */ public Expression textJustify() { return parse.enumToExpression(layout, "text-justify", Justification.class, Justification.CENTER); } /** * Part of the text placed closest to the anchor (requires text-field). * <p> * Optional enum. One of center, left, right, top, bottom, top-left, top-right, bottom-left, * bottom-right. Defaults to center. Requires text-field. Part of the text placed closest to the * anchor. * * {@link TextAnchor#CENTER} The center of the text is placed closest to the anchor. * * {@link TextAnchor#LEFT} The left side of the text is placed closest to the anchor. * * {@link TextAnchor#RIGHT} The right side of the text is placed closest to the anchor. * * {@link TextAnchor#TOP} The top of the text is placed closest to the anchor. * * {@link TextAnchor#BOTTOM} The bottom of the text is placed closest to the anchor. * * {@link TextAnchor#TOP_LEFT} The top left corner of the text is placed closest to the anchor. * * {@link TextAnchor#TOP_RIGHT} The top right corner of the text is placed closest to the * anchor. * * {@link TextAnchor#BOTTOM_LEFT} The bottom left corner of the text is placed closest to the * anchor. * * {@link TextAnchor#BOTTOM_RIGHT} The bottom right corner of the text is placed closest to the * anchor. * * @return part of the text placed closest to the anchor. */ public TextAnchor getTextAnchor() { String json = parse.get(layout, "text-anchor", "center"); if (json == null) { return null; } return TextAnchor.parse(json); } /** * Converts {@link #getTextAnchor()} to a GeoTools expression. Returns an expression that evaluates to one of "center", "left", or "right", "top", * "bottom", "top_left", "top_right", "bottom_left", "bottom_right". * * @see {@link #getTextAnchor()} */ public Expression textAnchor() { return parse.enumToExpression(layout, "text-anchor", TextAnchor.class, TextAnchor.CENTER); } /** * Layout "text-anchor" provided as {@link AnchorPoint}. * * @return AnchorPoint defined by "text-anchor". */ public AnchorPoint anchorPoint() { TextAnchor anchor = getTextAnchor(); if (anchor == null) { return null; } return sf.anchorPoint(ff.literal(anchor.getX()), ff.literal(anchor.getY())); } /** * (Optional) Units in degrees. Defaults to 45. Requires text-field. Requires symbol-placement = line. * * Maximum angle change between adjacent characters. * * @return Maximum label angle between characters when following a line * @throws MBFormatException */ public Number getTextMaxAngle() throws MBFormatException { return parse.optional(Number.class, layout, "text-max-angle", 45.0); } /** * Access text-max-angle as literal or function expression * * @return Maximum label angle between characters when following a line * @throws MBFormatException */ public Expression textMaxAngle() { return parse.percentage(layout, "text-max-angle", 45.0); } /** * (Optional) Units in degrees. Defaults to 0. Requires text-field. * * Rotates the text clockwise. * * @return Rotation angle of the label * @throws MBFormatException */ public Number getTextRotate() throws MBFormatException { return parse.optional(Number.class, layout, "text-rotate", 0.0); } /** * Access text-rotate as literal or function expression * * @return Rotation angle of the label * @throws MBFormatException */ public Expression textRotate() throws MBFormatException { return parse.percentage(layout, "text-rotate", 0.0); } /** * (Optional) Units in pixels. Defaults to 2. Requires text-field. * * Size of the additional area around the text bounding box used for detecting symbol collisions. * * @return Padding around the label for detecting collisions * @throws MBFormatException */ public Number getTextPadding() throws MBFormatException { return parse.optional(Number.class, layout, "text-padding", 2.0); } /** * Access text-padding as literal or function expression * * @return Padding around the label for detecting collisions * @throws MBFormatException */ public Expression textPadding() throws MBFormatException { return parse.percentage(layout, "text-padding", 2.0); } /** * (Optional) Defaults to true. Requires text-field. Requires text-rotation-alignment = map. Requires symbol-placement = line. * * If true, the text may be flipped vertically to prevent it from being rendered upside-down. * * @return Whether to flip the label if the orientation of the geometry would cause it to be rendered upside-down * @throws MBFormatException */ public Boolean getTextKeepUpright() throws MBFormatException { return parse.getBoolean(layout, "text-keep-upright", true); } /** * Wraps {@link #getTextKeepUpright()} in a GeoTools expression (Optional) Defaults to true. Requires text-field. Requires text-rotation-alignment * = map. Requires symbol-placement = line. * * If true, the text may be flipped vertically to prevent it from being rendered upside-down. * * @return Boolean * @throws MBFormatException */ public Expression textKeepUpright() { return parse.bool(layout, "text-keep-upright", true); } /** * One of none, uppercase, lowercase. Defaults to none. Requires text-field. * * Specifies how to capitalize text, similar to the CSS text-transform property. * * {@link TextTransform#NONE} The text is not altered. * * {@link TextTransform#UPPERCASE} Forces all letters to be displayed in uppercase. * * {@link TextTransform#LOWERCASE} Forces all letters to be displayed in lowercase. * * @return The tranformation to apply to the label */ public TextTransform getTextTransform() { Object value = layout.get("text-transform"); if (value != null && "uppercase".equalsIgnoreCase((String) value)) { return TextTransform.UPPERCASE; } else if (value != null && "lowercase".equalsIgnoreCase((String) value)){ return TextTransform.LOWERCASE; } else { return TextTransform.NONE; } } /** * Converts {@link #getTextTransform()} to a GeoTools expression. Returns an expression that evaluates to one of "uppercase", "lowercase", "none". * * @see {@link #getTextTransform()} */ public Expression textTransform() { return parse.enumToExpression(layout, "text-transform", TextTransform.class, TextTransform.NONE); } /** * (Optional) Units in ems. Defaults to 0,0. Requires text-field. * * Offset distance of text from its anchor. Positive values indicate right and down, while negative values indicate left and up. * * @return Offset of the label from its anchor. * @throws MBFormatException */ public double[] getTextOffset() throws MBFormatException { return parse.array(layout, "text-offset", new double[] { 0.0, 0.0 }); } /** * Access text-offset * * @return Offset of the label from its anchor. * @throws MBFormatException */ public Point textOffset() throws MBFormatException { if (layout.get("text-offset") != null) { JSONArray array = (JSONArray) layout.get("text-offset"); Number x = (Number) array.get(0); Number y = (Number) array.get(1); return new Point(x.intValue(), y.intValue()); } else { return new Point(0, 0); } } /** * Maps {@link #getTextOffset()} to a {@link Displacement}. * * (Optional) Units in ems. Defaults to 0,0. Requires text-field. */ public Displacement textOffsetDisplacement() { return parse.displacement(layout, "text-offset", sf.displacement(ff.literal(0), ff.literal(0))); } /** * (Optional) Defaults to false. Requires text-field. * * If true, the text will be visible even if it collides with other previously drawn symbols. * * @return Whether or not the text should be allowed to overlap other symbols * @throws MBFormatException */ public Boolean getTextAllowOverlap() throws MBFormatException { return parse.getBoolean(layout, "text-allow-overlap", false); } /** * Wraps {@link #getTextAllowOverlap()} in a GeoTools {@link Expression}. * * (Optional) Defaults to false. Requires text-field. * * If true, the text will be visible even if it collides with other previously drawn symbols. * * @return Whether or not the symbols should be allowed to overlap other symbols */ public Expression textAllowOverlap() throws MBFormatException { return parse.bool(layout, "text-allow-overlap", false); } /** * Defaults to false. Requires text-field. * * If true, other symbols can be visible even if they collide with the text. * * @return Whether or not other symbols should be allowed to overlap text in this layer. * @throws MBFormatException */ public Boolean getTextIgnorePlacement() throws MBFormatException { return parse.getBoolean(layout, "text-ignore-placement", false); } /** * Wraps {@link #getTextIgnorePlacement()} in a GeoTools expression Defaults to false. Requires text-field. * * If true, other symbols can be visible even if they collide with the text. * * @return Boolean * @throws MBFormatException */ public Expression textIgnorePlacement() { return parse.bool(layout, "text-ignore-placement", false); } /** * Defaults to false. Requires text-field. Requires icon-image. * * If true, icons will display without their corresponding text when the text collides with other symbols and the icon does not. * * @return Whether or not the symbol may be drawn when the label is not drawn due to collisions * @throws MBFormatException */ public Boolean getTextOptional() throws MBFormatException { return parse.getBoolean(layout, "text-optional", false); } /** * Wraps {@link #getTextOptional()} in a GeoTools expression. * * Defaults to false. Requires text-field. Defaults to false. Requires text-field. Requires icon-image. * * If true, icons will display without their corresponding text when the text collides with other symbols and the icon does not. * * @return Boolean * @throws MBFormatException */ public Expression textOptional() { return parse.bool(layout, "text-optional", false); } /** * (Optional) Defaults to 1. Requires icon-image. * * The opacity at which the icon will be drawn. * * @return Opacity of the icon * @throws MBFormatException */ public Number getIconOpacity() throws MBFormatException { return parse.optional(Number.class, paint, "icon-opacity", 1.0); } /** * Access icon-opacity as literal or function expression * * @return Opacity of the icon * @throws MBFormatException */ public Expression iconOpacity() throws MBFormatException { return parse.percentage(paint, "icon-opacity", 1.0); } /** * (Optional) Defaults to #000000. Requires icon-image. * * The color of the icon. This can only be used with sdf icons. * * @link Color of the icon. */ public Color getIconColor() { return parse.optional(Color.class, paint, "icon-color", Color.BLACK ); } /** * Access icon-color as literal or function expression, defaults to black. * * @link Color of the icon. */ public Expression iconColor() { return parse.color(paint, "icon-color", Color.BLACK); } /** * (Optional) Defaults to rgba(0, 0, 0, 0). Requires icon-image. * * The color of the icon's halo. Icon halos can only be used with SDF icons. * * @return Color of the icon's halo. */ public Color getIconHaloColor() { return parse.optional(Color.class, paint, "icon-halo-color", new Color(0,0,0,0)); } /** * Access icon-halo-color as literal or function expression, defaults to black. * * @return Color of the icon's halo. */ public Expression iconHaloColor() { return parse.color(paint, "icon-halo-color", Color.BLACK); } /** * (Optional) Units in pixels. Defaults to 0. Requires icon-image. * * Distance of halo to the icon outline. * * @return Width of the icon halo * @throws MBFormatException */ public Number getIconHaloWidth() throws MBFormatException { return parse.optional(Number.class, paint, "icon-halo-width", 0.0); } /** * Access icon-halo-width as literal or function expression * * @return Width of the icon halo * @throws MBFormatException */ public Expression iconHaloWidth() { return parse.percentage(paint, "icon-halo-width", 0.0); } /** * (Optional) Units in pixels. Defaults to 0. Requires icon-image. * * Fade out the halo towards the outside. * * @return Size of the halo fade * @throws MBFormatException */ public Number getIconHaloBlur() throws MBFormatException { return parse.optional(Number.class, paint, "icon-halo-blur", 0.0); } /** * Access icon-halo-blur as literal or function expression * * @return Size of the halo fade * @throws MBFormatException */ public Expression iconHaloBlur() { return parse.percentage(paint, "icon-halo-blur", 0.0); } /** * (Optional) Units in pixels. Defaults to 0,0. Requires icon-image. * * Distance that the icon's anchor is moved from its original placement. Positive values indicate right and down, * while negative values indicate left and up. * * @return Translation of the icon from its origin * @throws MBFormatException */ public int[] getIconTranslate() throws MBFormatException { return parse.array( paint, "icon-translate", new int[]{ 0, 0 } ); } /** * Units in pixels. Defaults to 0,0. Requires icon-image. * * Distance that the icon's anchor is moved from its original placement. Positive values indicate right and down, * while negative values indicate left and up. * * @return Translation of the icon from its origin * @throws MBFormatException */ public Point iconTranslate() { int[] translate = getIconTranslate(); return new Point(translate[0], translate[1]); } /** * Maps {@link #getIconTranslate()} to a {@link Displacement} * * (Optional) Units in pixels. Defaults to 0,0. Requires icon-image. Distance that the icon's anchor is moved from its original placement. Positive values * indicate right and down, while negative values indicate left and up. * */ public Displacement iconTranslateDisplacement() { return parse.displacement(paint, "icon-translate", sf.displacement(ff.literal(0), ff.literal(0))); } /** * (Optional) One of map, viewport. Defaults to map. Requires icon-image. Requires icon-translate. * * Controls the translation reference point. * * {@link TranslateAnchor#MAP}: Icons are translated relative to the map. * * {@link TranslateAnchor#VIEWPORT}: Icons are translated relative to the viewport. * * Defaults to {@link TranslateAnchor#MAP}. * * @return The location of the translation anchor. */ public TranslateAnchor getIconTranslateAnchor() { Object value = paint.get("icon-translate-anchor"); if (value != null && "viewport".equalsIgnoreCase((String) value)) { return TranslateAnchor.VIEWPORT; } else { return TranslateAnchor.MAP; } } /** * Converts {@link #getIconTranslateAnchor()} to a GeoTools expression. Returns an expression that evaluates to one of "map", "viewport". * * @see {@link #getIconTranslateAnchor()} */ public Expression iconTranslateAnchor() { return parse.enumToExpression(layout, "icon-translate-anchor", TranslateAnchor.class, TranslateAnchor.MAP); } /** * (Optional) Defaults to 1. Requires text-field. * * The opacity at which the text will be drawn. * * @return Opacity of the label * @throws MBFormatException */ public Number getTextOpacity() throws MBFormatException { return parse.optional(Number.class, paint, "text-opacity", 1.0); } /** * Access text-opacity as literal or function expression * * @return Opacity of the label * @throws MBFormatException */ public Expression textOpacity() throws MBFormatException { return parse.percentage(paint, "text-opacity", 1.0); } /** * Defaults to #000000. Requires text-field. * * The color with which the text will be drawn. * * @return The label color. * @throws MBFormatException */ public Color getTextColor() throws MBFormatException { return parse.convertToColor(parse.optional(String.class, paint, "text-color", "#000000")); } /** * Access text-color as literal or function expression, defaults to black. * * @return The label color. */ public Expression textColor() { return parse.color(paint, "text-color", Color.BLACK); } /** * Defaults to rgba(0, 0, 0, 0). Requires text-field. * * The color of the text's halo, which helps it stand out from backgrounds. * * @return The label halo color. * @throws MBFormatException */ public Color getTextHaloColor() throws MBFormatException { if (!paint.containsKey("text-halo-color")) { return new Color(0,0,0,0); } else { return parse.convertToColor(parse.optional(String.class, paint, "text-halo-color", "#000000")); } } /** * Access text-halo-color as literal or function expression, defaults to black. * * @return The label halo color. */ public Expression textHaloColor() { return parse.color(paint, "text-halo-color", Color.BLACK); } /** * (Optional) Units in pixels. Defaults to 0. Requires text-field. * * Distance of halo to the font outline. Max text halo width is 1/4 of the font-size. * * @return Size of the label halo * @throws MBFormatException */ public Number getTextHaloWidth() throws MBFormatException { return parse.optional(Number.class, paint, "text-halo-width", 0.0); } /** * Access text-halo-width as literal or function expression * * @return Size of the label halo * @throws MBFormatException */ public Expression textHaloWidth() throws MBFormatException { return parse.percentage(paint, "text-halo-width", 0.0); } /** * (Optional) Units in pixels. Defaults to 0. Requires text-field. * * The halo's fadeout distance towards the outside. * * @return Size of the label halo fade * @throws MBFormatException */ public Number getTextHaloBlur() throws MBFormatException { return parse.optional(Number.class, paint, "text-halo-blur", 0.0); } /** * Access text-halo-blur as literal or function expression * * @return Size of the label halo fade * @throws MBFormatException */ public Expression textHaloBlur() throws MBFormatException { return parse.percentage(paint, "text-halo-blur", 0.0); } /** * (Optional) Units in pixels. Defaults to 0,0. Requires text-field. * * Distance that the text's anchor is moved from its original placement. Positive values indicate right and down, * while negative values indicate left and up. * * @return The translation of hte lable form its anchor. * @throws MBFormatException */ public int[] getTextTranslate() { return parse.array( paint, "text-translate", new int[]{ 0, 0 } ); } /** * (Optional) Units in pixels. Defaults to 0,0. Requires text-field. * * Distance that the text's anchor is moved from its original placement. Positive values indicate right and down, * while negative values indicate left and up. * * @return The translation of hte lable form its anchor. * @throws MBFormatException */ public Point textTranslate() { int[] translate = getTextTranslate(); return new Point(translate[0], translate[1]); } /** * Maps {@link #getTextTranslate()} to a {@link Displacement}. * * Distance that the text's anchor is moved from its original placement. Positive values indicate right and down, while negative values indicate * left and up. (Optional) Units in pixels. Defaults to 0,0. Requires text-field. */ public Displacement textTranslateDisplacement() { return parse.displacement(paint, "text-translate", sf.displacement(ff.literal(0), ff.literal(0))); } /** * (Optional) One of map, viewport. Defaults to map. Requires text-field. Requires text-translate. * * Controls the translation reference point. * * {@link TranslateAnchor#MAP}: The text is translated relative to the map. * * {@link TranslateAnchor#VIEWPORT}: The text is translated relative to the viewport. * * Defaults to {@link TranslateAnchor#MAP}. * * @return The anchor the tect is translated relative to */ public TranslateAnchor getTextTranslateAnchor() { Object value = paint.get("text-translate-anchor"); if (value != null && "viewport".equalsIgnoreCase((String) value)) { return TranslateAnchor.VIEWPORT; } else { return TranslateAnchor.MAP; } } /** * Converts {@link #getTextTranslateAnchor()} to a GeoTools expression. Returns an expression that evaluates to one of "map", "viewport". * * @see {@link #getTextTranslateAnchor()} */ public Expression textTranslateAnchor() { return parse.enumToExpression(layout, "text-translate-anchor", TranslateAnchor.class, TranslateAnchor.MAP); } /** * Transform {@link SymbolMBLayer} to GeoTools FeatureTypeStyle. * <p> * Notes: * </p> * <ul> * </ul> * * @param styleContext The MBStyle to which this layer belongs, used as a context for things like resolving sprite and glyph names to full urls. * @return FeatureTypeStyle */ public List<FeatureTypeStyle> transformInternal(MBStyle styleContext) { MBStyleTransformer transformer = new MBStyleTransformer(parse); StyleBuilder sb = new StyleBuilder(); List<Symbolizer> symbolizers = new ArrayList<Symbolizer>(); LabelPlacement labelPlacement; // Create point or line placement // Functions not yet supported for symbolPlacement, so try to evaluate or use default. String symbolPlacementVal = transformer.requireLiteral(symbolPlacement(), String.class, "point", "symbol-placement", getId()); if ("point".equalsIgnoreCase(symbolPlacementVal.trim())) { // Point Placement (default) PointPlacement pointP = sb.createPointPlacement(); // Set anchor point (translated by text-translate) // GeoTools AnchorPoint doesn't seem to have an effect on PointPlacement pointP.setAnchorPoint(anchorPoint()); // MapBox text-offset: +y means down Displacement textTranslate = textTranslateDisplacement(); textTranslate.setDisplacementY(ff.multiply(ff.literal(-1), textTranslate.getDisplacementY())); pointP.setDisplacement(textTranslate); pointP.setRotation(textRotate()); labelPlacement = pointP; } else { // Line Placement LinePlacement lineP = sb.createLinePlacement(null); lineP.setRepeated(true); // pixels (geotools) vs ems (mapbox) for text-offset lineP.setPerpendicularOffset( ff.multiply(ff.literal(-1), textOffsetDisplacement().getDisplacementY())); labelPlacement = lineP; } Halo halo = sf.halo(sf.fill(null, textHaloColor(), null), textHaloWidth()); Fill fill = sf.fill(null, textColor(), textOpacity()); Font font = sb.createFont(ff.literal(""), ff.literal("normal"), ff.literal("normal"), textSize()); if (getTextFont() != null) { font.getFamily().clear(); for (String textFont : getTextFont()) { font.getFamily().add(ff.literal(textFont)); } } // If the textField is a literal string (not a function), then // we need to support Mapbox token replacement. Expression textExpression = textField(); if (textExpression instanceof Literal) { String text = textExpression.evaluate(null, String.class); if (text.trim().isEmpty()) { textExpression = ff.literal(" "); } else { textExpression = transformer.cqlExpressionFromTokens(text); } } TextSymbolizer2 symbolizer = (TextSymbolizer2) sf.textSymbolizer(getId(), ff.property((String) null), sf.description(Text.text("text"), null), NonSI.PIXEL, textExpression, font, labelPlacement, halo, fill); Number symbolSpacing = transformer.requireLiteral(symbolSpacing(), Number.class, 250, "symbol-spacing", getId()); symbolizer.getOptions().put("repeat", String.valueOf(symbolSpacing)); // text max angle // layer.getTextMaxAngle(); // symbolizer.getOptions().put("maxAngleDelta", "40"); // conflictResolution // Mapbox allows text overlap and icon overlap separately. GeoTools only has conflictResolution. Boolean textAllowOverlap = transformer.requireLiteral(textAllowOverlap(), Boolean.class, false, "text-allow-overlap", getId()); Boolean iconAllowOverlap = transformer.requireLiteral(iconAllowOverlap(), Boolean.class, false, "icon-allow-overlap", getId()); symbolizer.getOptions().put("conflictResolution", String.valueOf(!(textAllowOverlap || iconAllowOverlap))); String textFitVal = transformer.requireLiteral(iconTextFit(), String.class, "none", "icon-text-fit", getId()).trim(); if ("height".equalsIgnoreCase(textFitVal) || "width".equalsIgnoreCase(textFitVal)) { symbolizer.getOptions().put("graphic-resize", "stretch"); } else if ("both".equalsIgnoreCase(textFitVal)) { symbolizer.getOptions().put("graphic-resize", "proportional"); } else { // Default symbolizer.getOptions().put("graphic-resize", "none"); } //Mapbox allows you to sapecify an array of values, one for each side if (getIconTextFitPadding() != null && !getIconTextFitPadding().isEmpty()) { symbolizer.getOptions().put("graphic-margin", String.valueOf(getIconTextFitPadding().get(0))); } else { symbolizer.getOptions().put("graphic-margin", "0"); } // halo blur // layer.textHaloBlur(); // auto wrap // symbolizer.getOptions().put("autoWrap", layer.textMaxWidth()); // Pixels (GS) vs ems (MB); Vendor options with expressions? // If the layer has an icon image, add it to our symbolizer if (hasIconImage()) { // If the iconImage is a literal string (not a function), then // we need to support Mapbox token replacement. // Note: the URL is expected to be a CQL STRING ... Expression iconExpression = iconImage(); if (iconExpression instanceof Literal) { iconExpression = transformer.cqlExpressionFromTokens(iconExpression.evaluate(null, String.class)); } ExternalGraphic eg = transformer.createExternalGraphicForSprite(iconExpression, styleContext); // layer.iconSize() - MapBox uses multiplier, GeoTools uses pixels Graphic g = sf.graphic(Arrays.asList(eg), iconOpacity(), null, iconRotate(), null, null); Displacement d = iconOffsetDisplacement(); d.setDisplacementY(d.getDisplacementY()); g.setDisplacement(d); symbolizer.setGraphic(g); } symbolizers.add(symbolizer); MBFilter filter = getFilter(); // List of opengis rules here (needed for constructor) List<org.opengis.style.Rule> rules = new ArrayList<>(); Rule rule = sf.rule(getId(), null, null, 0.0, Double.POSITIVE_INFINITY, symbolizers, filter.filter()); rule.setLegendGraphic(new Graphic[0]); rules.add(rule); return Collections.singletonList(sf.featureTypeStyle(getId(), sf.description(Text.text("MBStyle " + getId()), Text.text("Generated for " + getSourceLayer())), null, // (unused) Collections.emptySet(), filter.semanticTypeIdentifiers(), // we only expect this to be applied to polygons rules)); } /** * Rendering type of this layer. * * @return {@link #TYPE} */ @Override public String getType() { return TYPE; } }