All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mapfish.print.map.style.json.JsonStyleParserHelper Maven / Gradle / Ivy

There is a newer version: 3.22.0
Show newest version
package org.mapfish.print.map.style.json;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.ecql.ECQL;
import org.geotools.styling.AnchorPoint;
import org.geotools.styling.Displacement;
import org.geotools.styling.ExternalGraphic;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Fill;
import org.geotools.styling.Font;
import org.geotools.styling.Graphic;
import org.geotools.styling.Halo;
import org.geotools.styling.LabelPlacement;
import org.geotools.styling.LinePlacement;
import org.geotools.styling.LineSymbolizer;
import org.geotools.styling.Mark;
import org.geotools.styling.PointPlacement;
import org.geotools.styling.PointSymbolizer;
import org.geotools.styling.PolygonSymbolizer;
import org.geotools.styling.Rule;
import org.geotools.styling.Stroke;
import org.geotools.styling.Style;
import org.geotools.styling.StyleBuilder;
import org.geotools.styling.Symbolizer;
import org.geotools.styling.TextSymbolizer;
import org.mapfish.print.ExceptionUtils;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.map.DistanceUnit;
import org.mapfish.print.url.data.Handler;
import org.mapfish.print.wrapper.json.PJsonObject;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;

import static org.mapfish.print.FileUtils.testForLegalFileUrl;
import static org.springframework.http.HttpMethod.HEAD;

/**
 * Methods shared by various style versions for creating geotools SLD styles from the json format mapfish
 * supports.
 */
public final class JsonStyleParserHelper {
    static final String JSON_FONT_FAMILY = "fontFamily";
    static final String JSON_FONT_SIZE = "fontSize";
    static final String JSON_FONT_WEIGHT = "fontWeight";
    static final String JSON_FONT_STYLE = "fontStyle";
    static final String JSON_LABEL = "label";
    static final String JSON_LABEL_ANCHOR_POINT_X = "labelAnchorPointX";
    static final String JSON_LABEL_ANCHOR_POINT_Y = "labelAnchorPointY";
    static final String JSON_LABEL_ALIGN = "labelAlign";
    static final String JSON_LABEL_X_OFFSET = "labelXOffset";
    static final String JSON_LABEL_Y_OFFSET = "labelYOffset";
    static final String JSON_LABEL_ROTATION = "labelRotation";
    static final String JSON_LABEL_PERPENDICULAR_OFFSET = "labelPerpendicularOffset";
    static final String JSON_LABEL_OUTLINE_COLOR = "labelOutlineColor";
    static final String JSON_LABEL_OUTLINE_WIDTH = "labelOutlineWidth";
    static final String JSON_LABEL_ALLOW_OVERRUNS = "allowOverruns";
    static final String JSON_LABEL_AUTO_WRAP = "autoWrap";
    static final String JSON_LABEL_CONFLICT_RESOLUTION = "conflictResolution";
    static final String JSON_LABEL_FOLLOW_LINE = "followLine";
    static final String JSON_LABEL_GOODNESS_OF_FIT = "goodnessOfFit";
    static final String JSON_LABEL_GROUP = "group";
    static final String JSON_LABEL_MAX_DISPLACEMENT = "maxDisplacement";
    static final String JSON_LABEL_SPACE_AROUND = "spaceAround";
    static final String JSON_FONT_COLOR = "fontColor";
    static final String JSON_FONT_OPACITY = "fontOpacity";
    static final String JSON_FILL_COLOR = "fillColor";
    static final String JSON_STROKE_COLOR = "strokeColor";
    static final String JSON_STROKE_OPACITY = "strokeOpacity";
    static final String JSON_STROKE_WIDTH = "strokeWidth";
    static final String JSON_STROKE_DASHSTYLE = "strokeDashstyle";
    static final String JSON_STROKE_LINECAP = "strokeLinecap";
    static final String JSON_FILL_OPACITY = "fillOpacity";
    static final String JSON_EXTERNAL_GRAPHIC = "externalGraphic";
    static final String JSON_GRAPHIC_NAME = "graphicName";
    static final String JSON_GRAPHIC_OPACITY = "graphicOpacity";
    static final String JSON_GRAPHIC_Y_OFFSET = "graphicYOffset";
    static final String JSON_GRAPHIC_X_OFFSET = "graphicXOffset";
    static final String JSON_POINT_RADIUS = "pointRadius";
    static final String JSON_GRAPHIC_WIDTH = "graphicWidth";
    static final String JSON_ROTATION = "rotation";
    static final String JSON_HALO_RADIUS = "haloRadius";
    static final String JSON_HALO_COLOR = "haloColor";
    static final String JSON_HALO_OPACITY = "haloOpacity";
    static final String JSON_GRAPHIC_FORMAT = "graphicFormat";
    static final String STROKE_DASHSTYLE_SOLID = "solid";
    static final String STROKE_DASHSTYLE_DOT = "dot";
    static final String STROKE_DASHSTYLE_DASH = "dash";
    static final String STROKE_DASHSTYLE_DASHDOT = "dashdot";
    static final String STROKE_DASHSTYLE_LONGDASH = "longdash";
    static final String STROKE_DASHSTYLE_LONGDASHDOT = "longdashdot";
    static final Set> COMPATIBLE_MIMETYPES = Sets.newIdentityHashSet();
    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(JsonStyleParserHelper.class);
    private static final String[] SUPPORTED_MIME_TYPES = ImageIO.getReaderMIMETypes();
    private static final String DEFAULT_POINT_MARK = "circle";
    private static final Pattern VALUE_UNIT_PATTERN = Pattern.compile("^([0-9.]+)([a-z]*)");
    private static final Pattern DATA_FORMAT_PATTERN = Pattern.compile("^data:([^;,]+)[;,]");

    static {
        COMPATIBLE_MIMETYPES.add(Sets.newHashSet("image/jpeg", "image/jpg"));
        COMPATIBLE_MIMETYPES.add(Sets.newHashSet("image/jpeg1000", "image/jpg2000"));
        COMPATIBLE_MIMETYPES.add(Sets.newHashSet("image/tiff", "image/tif"));
    }

    private final Configuration configuration;
    private final ClientHttpRequestFactory requestFactory;
    private boolean allowNullSymbolizer;
    private StyleBuilder styleBuilder;

    /**
     * Constructor.
     *
     * @param configuration the configuration to use for resolving relative files or other settings.
     * @param requestFactory Request factory for making the request.
     * @param styleBuilder a style builder to use for creating the style objects.
     * @param allowNullSymbolizer If true then create*Symbolizer() methods can return null if expected
     *         params are missing.
     */
    public JsonStyleParserHelper(
            @Nullable final Configuration configuration,
            @Nonnull final ClientHttpRequestFactory requestFactory,
            @Nonnull final StyleBuilder styleBuilder,
            final boolean allowNullSymbolizer) {
        this.configuration = configuration;
        this.requestFactory = requestFactory;
        this.styleBuilder = styleBuilder;
        this.allowNullSymbolizer = allowNullSymbolizer;
    }

    void setAllowNullSymbolizer(final boolean allowNullSymbolizer) {
        this.allowNullSymbolizer = allowNullSymbolizer;
    }

    /**
     * Create a style from a list of rules.
     *
     * @param styleRules the rules
     */
    public Style createStyle(final List styleRules) {
        final Rule[] rulesArray = styleRules.toArray(new Rule[styleRules.size()]);
        final FeatureTypeStyle featureTypeStyle = this.styleBuilder.createFeatureTypeStyle(null, rulesArray);
        final Style style = this.styleBuilder.createStyle();
        style.featureTypeStyles().add(featureTypeStyle);
        return style;
    }

    /**
     * Add a point symbolizer definition to the rule.
     *
     * @param styleJson The old style.
     */
    @Nullable
    public PointSymbolizer createPointSymbolizer(final PJsonObject styleJson) {

        if (this.allowNullSymbolizer && !(
                styleJson.has(JSON_EXTERNAL_GRAPHIC) || styleJson.has(JSON_GRAPHIC_NAME) ||
                        styleJson.has(JSON_POINT_RADIUS))) {
            return null;
        }

        Graphic graphic = this.styleBuilder.createGraphic();
        graphic.graphicalSymbols().clear();
        if (styleJson.has(JSON_EXTERNAL_GRAPHIC)) {
            String externalGraphicUrl = validateURL(styleJson.getString(JSON_EXTERNAL_GRAPHIC));

            try {
                final URI uri = URI.create(externalGraphicUrl);
                if (uri.getScheme().startsWith("http")) {
                    final ClientHttpRequest request = this.requestFactory.createRequest(uri, HttpMethod.GET);
                    externalGraphicUrl = request.getURI().toString();
                }
            } catch (IOException ignored) {
                // ignored
            }

            final String graphicFormat = getGraphicFormat(externalGraphicUrl, styleJson);
            ExternalGraphic externalGraphic = null;
            if (externalGraphicUrl.startsWith("data:")) {
                try {
                    externalGraphic = this.styleBuilder.createExternalGraphic(
                            new URL(null, externalGraphicUrl, new Handler()),
                            graphicFormat
                    );
                } catch (MalformedURLException e) {
                    // ignored
                }
            } else {
                externalGraphic =
                        this.styleBuilder.createExternalGraphic(externalGraphicUrl, graphicFormat);
            }
            if (externalGraphic != null) {
                graphic.graphicalSymbols().add(externalGraphic);
            }
        }

        if (styleJson.has(JSON_GRAPHIC_NAME)) {
            Expression graphicName =
                    parseProperty(styleJson.getString(JSON_GRAPHIC_NAME), new Function() {
                        public Object apply(final String input) {
                            return input;
                        }
                    });
            Fill fill = createFill(styleJson);
            Stroke stroke = createStroke(styleJson, false);

            final Mark mark = this.styleBuilder.createMark(graphicName, fill, stroke);
            graphic.graphicalSymbols().add(mark);
        }

        if (graphic.graphicalSymbols().isEmpty()) {
            Fill fill = createFill(styleJson);
            Stroke stroke = createStroke(styleJson, false);

            final Mark mark = this.styleBuilder.createMark(DEFAULT_POINT_MARK, fill, stroke);
            graphic.graphicalSymbols().add(mark);
        }

        graphic.setOpacity(
                parseExpression(null, styleJson, JSON_GRAPHIC_OPACITY, new Function() {
                    @Nullable
                    @Override
                    public Object apply(final String opacityString) {
                        return Double.parseDouble(opacityString);
                    }
                }));

        if (!Strings.isNullOrEmpty(styleJson.optString(JSON_POINT_RADIUS))) {
            Expression size =
                    parseExpression(null, styleJson, JSON_POINT_RADIUS, new Function() {
                        @Nullable
                        @Override
                        public Object apply(final String input) {
                            return Double.parseDouble(input) * 2;
                        }
                    });
            graphic.setSize(size);
        } else if (!Strings.isNullOrEmpty(styleJson.optString(JSON_GRAPHIC_WIDTH))) {
            Expression size =
                    parseExpression(null, styleJson, JSON_GRAPHIC_WIDTH, new Function() {
                        @Nullable
                        @Override
                        public Object apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });
            graphic.setSize(size);
        }

        if (!Strings.isNullOrEmpty(styleJson.optString(JSON_GRAPHIC_Y_OFFSET)) &&
                !Strings.isNullOrEmpty(styleJson.optString(JSON_GRAPHIC_X_OFFSET))) {
            Expression dy =
                    parseExpression(null, styleJson, JSON_GRAPHIC_Y_OFFSET, new Function() {
                        @Nullable
                        @Override
                        public Object apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });
            Expression dx =
                    parseExpression(null, styleJson, JSON_GRAPHIC_X_OFFSET, new Function() {
                        @Nullable
                        @Override
                        public Object apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });
            Displacement offset = this.styleBuilder.createDisplacement(dx, dy);
            graphic.setDisplacement(offset);
        }

        if (!Strings.isNullOrEmpty(styleJson.optString(JSON_ROTATION))) {
            final Expression rotation =
                    parseExpression(null, styleJson, JSON_ROTATION, new Function() {
                        @Nullable
                        @Override
                        public Object apply(final String rotation) {
                            return Double.parseDouble(rotation);
                        }
                    });
            graphic.setRotation(rotation);
        }

        return this.styleBuilder.createPointSymbolizer(graphic);
    }

    private String validateURL(final String externalGraphicUrl) {
        try {
            new URL(externalGraphicUrl);
        } catch (MalformedURLException e) {
            // not a url so assume a file or data url and verify that it is valid
            try {
                if (externalGraphicUrl.startsWith("data:")) {
                    new URL(null, externalGraphicUrl, new Handler());
                    return externalGraphicUrl;
                }
                final URL fileURL = new URL("file://" + externalGraphicUrl);
                return testForLegalFileUrl(this.configuration, fileURL).toExternalForm();
            } catch (MalformedURLException e1) {
                // unable to convert to file url so give up and throw exception;
                throw ExceptionUtils.getRuntimeException(e);
            }
        }
        return externalGraphicUrl;
    }

    /**
     * Add a line symbolizer definition to the rule.
     *
     * @param styleJson The old style.
     */
    @VisibleForTesting
    @Nullable
    protected LineSymbolizer createLineSymbolizer(final PJsonObject styleJson) {
        final Stroke stroke = createStroke(styleJson, true);
        if (stroke == null) {
            return null;
        } else {
            return this.styleBuilder.createLineSymbolizer(stroke);
        }
    }

    /**
     * Add a polygon symbolizer definition to the rule.
     *
     * @param styleJson The old style.
     */
    @Nullable
    @VisibleForTesting
    protected PolygonSymbolizer createPolygonSymbolizer(final PJsonObject styleJson) {
        if (this.allowNullSymbolizer && !styleJson.has(JSON_FILL_COLOR)) {
            return null;
        }

        final PolygonSymbolizer symbolizer = this.styleBuilder.createPolygonSymbolizer();
        symbolizer.setFill(createFill(styleJson));

        symbolizer.setStroke(createStroke(styleJson, false));

        return symbolizer;
    }

    /**
     * Add a text symbolizer definition to the rule.
     *
     * @param styleJson The old style.
     */
    @VisibleForTesting
    protected TextSymbolizer createTextSymbolizer(final PJsonObject styleJson) {
        final TextSymbolizer textSymbolizer = this.styleBuilder.createTextSymbolizer();

        // make sure that labels are also rendered if a part of the text would be outside
        // the view context, see http://docs.geoserver.org/stable/en/user/styling/sld-reference/labeling
        // .html#partials
        textSymbolizer.getOptions().put("partials", "true");

        if (styleJson.has(JSON_LABEL)) {
            final Expression label =
                    parseExpression(null, styleJson, JSON_LABEL, new Function() {
                        @Nullable
                        @Override
                        public Object apply(final String labelValue) {
                            return labelValue.replace("${", "").replace("}", "");
                        }
                    });

            textSymbolizer.setLabel(label);
        } else {
            return null;
        }

        textSymbolizer.setFont(createFont(textSymbolizer.getFont(), styleJson));

        if (styleJson.has(JSON_LABEL_ANCHOR_POINT_X) ||
                styleJson.has(JSON_LABEL_ANCHOR_POINT_Y) ||
                styleJson.has(JSON_LABEL_ALIGN) ||
                styleJson.has(JSON_LABEL_X_OFFSET) ||
                styleJson.has(JSON_LABEL_Y_OFFSET) ||
                styleJson.has(JSON_LABEL_ROTATION) ||
                styleJson.has(JSON_LABEL_PERPENDICULAR_OFFSET)) {
            textSymbolizer.setLabelPlacement(createLabelPlacement(styleJson));
        }

        if (!Strings.isNullOrEmpty(styleJson.optString(JSON_HALO_RADIUS)) ||
                !Strings.isNullOrEmpty(styleJson.optString(JSON_HALO_COLOR)) ||
                !Strings.isNullOrEmpty(styleJson.optString(JSON_HALO_OPACITY)) ||
                !Strings.isNullOrEmpty(styleJson.optString(JSON_LABEL_OUTLINE_WIDTH)) ||
                !Strings.isNullOrEmpty(styleJson.optString(JSON_LABEL_OUTLINE_COLOR))) {
            textSymbolizer.setHalo(createHalo(styleJson));
        }

        if (!Strings.isNullOrEmpty(styleJson.optString(JSON_FONT_COLOR)) ||
                !Strings.isNullOrEmpty(styleJson.optString(JSON_FONT_OPACITY))) {
            textSymbolizer.setFill(addFill(
                    styleJson.optString(JSON_FONT_COLOR, "black"),
                    styleJson.optString(JSON_FONT_OPACITY, "1.0")));
        }

        this.addVendorOptions(JSON_LABEL_ALLOW_OVERRUNS, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_AUTO_WRAP, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_CONFLICT_RESOLUTION, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_FOLLOW_LINE, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_GOODNESS_OF_FIT, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_GROUP, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_MAX_DISPLACEMENT, styleJson, textSymbolizer);
        this.addVendorOptions(JSON_LABEL_SPACE_AROUND, styleJson, textSymbolizer);

        return textSymbolizer;
    }

    private void addVendorOptions(
            final String key, final PJsonObject styleJson, final Symbolizer symbolizer) {
        final String value = styleJson.optString(key);
        if (!Strings.isNullOrEmpty(value)) {
            symbolizer.getOptions().put(key, value);
        }
    }

    private double getPxSize(final String size) {
        Matcher matcher = VALUE_UNIT_PATTERN.matcher(size);
        matcher.find();

        double value = Double.parseDouble(matcher.group(1));
        String unit = matcher.group(2);

        if (unit.length() == 0) {
            return value;
        } else {
            return DistanceUnit.fromString(unit).convertTo(value, DistanceUnit.PX);
        }
    }

    private Font createFont(final Font defaultFont, final PJsonObject styleJson) {

        Expression fontFamily =
                parseExpression(null, styleJson, JSON_FONT_FAMILY, new Function() {
                    @Override
                    public String apply(final String fontFamily) {
                        return fontFamily;
                    }
                });
        if (fontFamily == null) {
            fontFamily = defaultFont.getFamily().get(0);
        }
        List fontFamilies = getFontExpressions(fontFamily);

        Expression fontSize =
                parseExpression(null, styleJson, JSON_FONT_SIZE, new Function() {
                    @Nullable
                    @Override
                    public Object apply(final String input) {
                        return getPxSize(input);
                    }
                });
        if (fontSize == null) {
            fontSize = defaultFont.getSize();
        }

        Expression fontWeight = parseExpression(null, styleJson, JSON_FONT_WEIGHT,
                                                Functions.identity());
        if (fontWeight == null) {
            fontWeight = defaultFont.getWeight();
        }

        Expression fontStyle = parseExpression(null, styleJson, JSON_FONT_STYLE,
                                               Functions.identity());
        if (fontStyle == null) {
            fontStyle = defaultFont.getStyle();
        }

        Font font = this.styleBuilder.createFont(fontFamilies.get(0), fontStyle, fontWeight, fontSize);
        if (fontFamilies.size() > 1) {
            // add remaining "fallback" fonts
            for (int i = 1; i < fontFamilies.size(); i++) {
                font.getFamily().add(fontFamilies.get(i));
            }
        }

        return font;
    }

    private List getFontExpressions(final Expression fontFamily) {
        List fontFamilies = new LinkedList<>();
        if (fontFamily instanceof Literal) {
            String fonts = (String) ((Literal) fontFamily).getValue();
            for (String font: fonts.split(",")) {
                font = font.trim();
                // translate SVG/CSS font expressions to Java logical fonts
                if (font.equalsIgnoreCase("serif")) {
                    font = "Serif";
                } else if (font.equalsIgnoreCase("sans-serif")) {
                    font = "SansSerif";
                } else if (font.equalsIgnoreCase("monospace")) {
                    font = "Monospaced";
                }
                fontFamilies.add(this.styleBuilder.literalExpression(font));
            }
        } else {
            fontFamilies.add(fontFamily);
        }
        return fontFamilies;
    }

    private LabelPlacement createLabelPlacement(final PJsonObject styleJson) {
        if ((styleJson.has(JSON_LABEL_ANCHOR_POINT_X) ||
                styleJson.has(JSON_LABEL_ANCHOR_POINT_Y) ||
                styleJson.has(JSON_LABEL_ALIGN) ||
                styleJson.has(JSON_LABEL_X_OFFSET) ||
                styleJson.has(JSON_LABEL_Y_OFFSET) ||
                styleJson.has(JSON_LABEL_ROTATION))
                && Strings.isNullOrEmpty(styleJson.optString(JSON_LABEL_PERPENDICULAR_OFFSET))) {
            return createPointPlacement(styleJson);
        }

        return createLinePlacement(styleJson);
    }

    private LinePlacement createLinePlacement(final PJsonObject styleJson) {
        Expression linePlacement = parseExpression(null, styleJson,
                                                   JSON_LABEL_PERPENDICULAR_OFFSET,
                                                   new Function() {
                                                       @Nullable
                                                       @Override
                                                       public Object apply(final String input) {
                                                           return Double.parseDouble(input);
                                                       }
                                                   });

        if (linePlacement != null) {
            return this.styleBuilder.createLinePlacement(linePlacement);
        }
        return null;
    }

    @Nullable
    private PointPlacement createPointPlacement(final PJsonObject styleJson) {
        AnchorPoint anchorPoint = createAnchorPoint(styleJson);

        Displacement displacement = null;
        if (styleJson.has(JSON_LABEL_X_OFFSET) || styleJson.has(JSON_LABEL_Y_OFFSET)) {

            Expression xOffset =
                    parseExpression(0.0, styleJson, JSON_LABEL_X_OFFSET, new Function() {
                        @Nullable
                        @Override
                        public Double apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });
            Expression yOffset =
                    parseExpression(0.0, styleJson, JSON_LABEL_Y_OFFSET, new Function() {
                        @Nullable
                        @Override
                        public Double apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });

            displacement = this.styleBuilder.createDisplacement(xOffset, yOffset);
        }

        Expression rotation =
                parseExpression(0.0, styleJson, JSON_LABEL_ROTATION, new Function() {
                    @Nullable
                    @Override
                    public Double apply(final String input) {
                        return Double.parseDouble(input);
                    }
                });

        if (anchorPoint == null) {
            anchorPoint = this.styleBuilder.createAnchorPoint(0, 0);
        }

        if (displacement == null) {
            displacement = this.styleBuilder.createDisplacement(0, 0);
        }

        return this.styleBuilder.createPointPlacement(anchorPoint, displacement, rotation);
    }

    private AnchorPoint createAnchorPoint(final PJsonObject styleJson) {
        Expression anchorX =
                parseExpression(null, styleJson, JSON_LABEL_ANCHOR_POINT_X, new Function() {
                    @Nullable
                    @Override
                    public Double apply(final String input) {
                        return Double.parseDouble(input);
                    }
                });
        Expression anchorY =
                parseExpression(null, styleJson, JSON_LABEL_ANCHOR_POINT_Y, new Function() {
                    @Nullable
                    @Override
                    public Double apply(final String input) {
                        return Double.parseDouble(input);
                    }
                });

        if (anchorX == null && anchorY == null && styleJson.has(JSON_LABEL_ALIGN)) {
            String labelAlign = styleJson.getString(JSON_LABEL_ALIGN);
            String xAlign = labelAlign.substring(0, 1);
            String yAlign = labelAlign.substring(1, 2);

            final double anchorInMiddle = 0.5;
            if ("l".equals(xAlign)) {
                anchorX = this.styleBuilder.literalExpression(0.0);
            } else if ("c".equals(xAlign)) {
                anchorX = this.styleBuilder.literalExpression(anchorInMiddle);
            } else if ("r".equals(xAlign)) {
                anchorX = this.styleBuilder.literalExpression(1.0);
            }
            if ("b".equals(yAlign)) {
                anchorY = this.styleBuilder.literalExpression(0.0);
            } else if ("m".equals(yAlign)) {
                anchorY = this.styleBuilder.literalExpression(anchorInMiddle);
            } else if ("t".equals(yAlign)) {
                anchorY = this.styleBuilder.literalExpression(1.0);
            }
        }
        boolean returnNull = true;
        if (anchorX == null) {
            anchorX = this.styleBuilder.literalExpression(0.0);
        } else {
            returnNull = false;
        }
        if (anchorY == null) {
            anchorY = this.styleBuilder.literalExpression(0.0);
        } else {
            returnNull = false;
        }

        if (returnNull) {
            return null;
        }

        return this.styleBuilder.createAnchorPoint(anchorX, anchorY);
    }

    private Halo createHalo(final PJsonObject styleJson) {
        if (styleJson.has(JSON_HALO_RADIUS)) {
            Expression radius =
                    parseExpression(1.0, styleJson, JSON_HALO_RADIUS, new Function() {
                        @Nullable
                        @Override
                        public Double apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });

            final Fill fill;
            if (styleJson.has(JSON_HALO_COLOR) || styleJson.has(JSON_HALO_OPACITY)) {
                fill = addFill(
                        styleJson.optString(JSON_HALO_COLOR, "white"),
                        styleJson.optString(JSON_HALO_OPACITY, "1.0"));
                return this.styleBuilder.createHalo(fill, radius);
            }
        }

        // labelOutlineColor and labelOutlineWidth are aliases for halo that is used by some V2 Clients
        if (styleJson.has(JSON_LABEL_OUTLINE_COLOR) || styleJson.has(JSON_LABEL_OUTLINE_WIDTH)) {
            Expression radius =
                    parseExpression(1.0, styleJson, JSON_LABEL_OUTLINE_WIDTH, new Function() {
                        @Nullable
                        @Override
                        public Double apply(final String input) {
                            return Double.parseDouble(input);
                        }
                    });
            Fill fill = addFill(styleJson.optString(JSON_LABEL_OUTLINE_COLOR, "white"), "1.0");
            return this.styleBuilder.createHalo(fill, radius);
        }

        return null;
    }

    private Fill createFill(final PJsonObject styleJson) {
        if (this.allowNullSymbolizer && !styleJson.has(JSON_FILL_COLOR)) {
            return null;
        }
        final String fillColor = styleJson.optString(JSON_FILL_COLOR, "black");
        return addFill(fillColor, styleJson.optString(JSON_FILL_OPACITY, "1.0"));
    }

    private Fill addFill(final String fillColor, final String fillOpacity) {
        final Expression finalFillColor = parseProperty(fillColor, new Function() {
            @Nullable
            @Override
            public Object apply(final String fillColor) {
                return toColorExpression(fillColor);
            }
        });
        final Expression opacity = parseProperty(fillOpacity, new Function() {
            @Nullable
            @Override
            public Object apply(final String fillOpacity) {
                return Double.parseDouble(fillOpacity);
            }
        });
        return this.styleBuilder.createFill(finalFillColor, opacity);
    }

    private Object toColorExpression(final String color) {
        return ((Literal) JsonStyleParserHelper.this.styleBuilder.colorExpression(
                ColorParser.toColor(color))).getValue();
    }

    @Nullable
    @VisibleForTesting
    Stroke createStroke(final PJsonObject styleJson, final boolean allowNull) {
        final float defaultDashSpacing = 0.1f;
        final int doubleWidth = 2;
        final int tripleWidth = 3;
        final int quadrupleWidth = 4;
        final int quintupleWidth = 5;

        if (this.allowNullSymbolizer && allowNull && !styleJson.has(JSON_STROKE_COLOR)) {
            return null;
        }
        Expression strokeColor =
                parseExpression(Color.black, styleJson, JSON_STROKE_COLOR, new Function() {
                    @Nullable
                    @Override
                    public Object apply(final String input) {
                        return toColorExpression(input);
                    }
                });
        Expression strokeOpacity =
                parseExpression(1.0, styleJson, JSON_STROKE_OPACITY, new Function() {
                    @Nullable
                    @Override
                    public Object apply(final String input) {
                        return Double.parseDouble(styleJson.getString(JSON_STROKE_OPACITY));
                    }
                });
        Expression widthExpression =
                parseExpression(1, styleJson, JSON_STROKE_WIDTH, new Function() {
                    @Nullable
                    @Override
                    public Object apply(final String input) {
                        return Double.parseDouble(styleJson.getString(JSON_STROKE_WIDTH));
                    }
                });

        float[] dashArray = null;
        if (styleJson.has(JSON_STROKE_DASHSTYLE) && !STROKE_DASHSTYLE_SOLID.equals(
                styleJson.getString(JSON_STROKE_DASHSTYLE))) {
            double width = 1.0;
            if (widthExpression instanceof Literal) {
                Literal expression = (Literal) widthExpression;
                width = ((Number) expression.getValue()).doubleValue();
            }
            String dashStyle = styleJson.getString(JSON_STROKE_DASHSTYLE);
            if (dashStyle.equalsIgnoreCase(STROKE_DASHSTYLE_DOT)) {
                dashArray = new float[]{defaultDashSpacing, (float) (doubleWidth * width)};
            } else if (dashStyle.equalsIgnoreCase(STROKE_DASHSTYLE_DASH)) {
                dashArray = new float[]{(float) (doubleWidth * width), (float) (doubleWidth * width)};
            } else if (dashStyle.equalsIgnoreCase(STROKE_DASHSTYLE_DASHDOT)) {
                dashArray = new float[]{
                        (float) (tripleWidth * width),
                        (float) (doubleWidth * width),
                        defaultDashSpacing,
                        (float) (doubleWidth * width)
                };
            } else if (dashStyle.equalsIgnoreCase(STROKE_DASHSTYLE_LONGDASH)) {
                dashArray = new float[]{
                        (float) (quadrupleWidth * width),
                        (float) (doubleWidth * width)
                };
            } else if (dashStyle.equalsIgnoreCase(STROKE_DASHSTYLE_LONGDASHDOT)) {
                dashArray = new float[]{
                        (float) (quintupleWidth * width),
                        (float) (doubleWidth * width),
                        defaultDashSpacing,
                        (float) (doubleWidth * width)
                };
            } else if (dashStyle.contains(" ")) {
                //check for pattern if empty array, throw.
                try {
                    String[] x = dashStyle.split(" ");
                    if (x.length > 1) {
                        dashArray = new float[x.length];
                        for (int i = 0; i < x.length; i++) {
                            dashArray[i] = Float.parseFloat(x[i]);
                        }
                    }
                } catch (NumberFormatException e) {
                    //assume solid!
                }
            } else {
                throw new IllegalArgumentException(
                        "strokeDashstyle does not have a legal value: " + dashStyle);
            }
        }

        Expression lineCap = parseExpression(null, styleJson, JSON_STROKE_LINECAP,
                                             Functions.identity());

        final Stroke stroke = this.styleBuilder.createStroke(strokeColor, widthExpression);
        stroke.setLineCap(lineCap);
        stroke.setOpacity(strokeOpacity);
        stroke.setDashArray(dashArray);
        return stroke;
    }

    @VisibleForTesting
    String getGraphicFormat(final String externalGraphicFile, final PJsonObject styleJson) {
        String mimeType = null;
        if (!Strings.isNullOrEmpty(styleJson.optString(JSON_GRAPHIC_FORMAT))) {
            mimeType = styleJson.getString(JSON_GRAPHIC_FORMAT);
        } else {
            Matcher matcher = DATA_FORMAT_PATTERN.matcher(externalGraphicFile);
            if (matcher.find()) {
                mimeType = matcher.group(1);
            }

            if (mimeType == null) {
                int separatorPos = externalGraphicFile.lastIndexOf(".");
                if (separatorPos >= 0) {
                    mimeType = "image/" + externalGraphicFile.substring(separatorPos + 1).toLowerCase();
                }
            }

            if (mimeType == null) {
                try {
                    URI uri;
                    try {
                        uri = new URI(externalGraphicFile);
                    } catch (URISyntaxException e) {
                        uri = new File(externalGraphicFile).toURI();
                    }

                    ClientHttpResponse httpResponse = this.requestFactory.createRequest(
                            uri, HEAD).execute();
                    List contentTypes = httpResponse.getHeaders().get("Content-Type");
                    if (contentTypes.size() == 1) {
                        String contentType = contentTypes.get(0);
                        int index = contentType.lastIndexOf(";");
                        mimeType = index >= 0 ? contentType.substring(0, index) : contentType;
                    } else {
                        LOGGER.info("No content type found for: {}", externalGraphicFile);
                    }
                } catch (IOException e) {
                    throw new RuntimeException("Unable to get a mime type for the external graphic", e);
                }
            }
        }
        mimeType = toSupportedMimeType(mimeType);
        return mimeType;
    }

    private String toSupportedMimeType(final String mimeType) {
        for (Set compatibleMimeType: COMPATIBLE_MIMETYPES) {
            if (compatibleMimeType.contains(mimeType.toLowerCase())) {
                for (String compatible: compatibleMimeType) {
                    if (isSupportedMimetype(compatible)) {
                        return compatible;
                    }
                }
            }
        }
        return mimeType;
    }

    private boolean isSupportedMimetype(final String mimeType) {
        for (String supported: SUPPORTED_MIME_TYPES) {
            if (supported.equalsIgnoreCase(mimeType)) {
                return true;
            }
        }
        return false;
    }

    private  Expression parseExpression(
            final T defaultValue,
            final PJsonObject styleJson,
            final String propertyName,
            final Function staticParser) {
        if (!styleJson.has(propertyName)) {
            if (defaultValue == null) {
                return null;
            }
            return this.styleBuilder.literalExpression(defaultValue);
        }

        String propertyValue = styleJson.getString(propertyName);
        if (isExpression(propertyValue)) {
            return toExpressionFromCQl(propertyValue);
        }
        return this.styleBuilder.literalExpression(staticParser.apply(propertyValue));
    }

    private  Expression parseProperty(final String property, final Function literalSupplier) {
        if (isExpression(property)) {
            return toExpressionFromCQl(property);
        } else {
            return this.styleBuilder.literalExpression(literalSupplier.apply(property));
        }
    }

    private Expression toExpressionFromCQl(final String property) {
        try {
            return ECQL.toExpression(property, this.styleBuilder.getFilterFactory());
        } catch (CQLException e) {
            throw ExceptionUtils.getRuntimeException(e);
        }
    }

    private boolean isExpression(final String property) {
        return property != null && property.startsWith("[") && property.endsWith("]");
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy