/*
* 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.transform;
import org.geotools.filter.text.ecql.ECQL;
import org.geotools.mbstyle.MBStyle;
import org.geotools.mbstyle.layer.SymbolMBLayer.TextAnchor;
import org.geotools.mbstyle.parse.MBObjectParser;
import org.geotools.mbstyle.sprite.SpriteGraphicFactory;
import org.geotools.renderer.style.ExpressionExtractor;
import org.geotools.styling.*;
import org.geotools.util.logging.Logging;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Responsible for traverse {@link MBStyle} and generating {@link StyledLayerDescriptor}.
*
* @author Jody Garnett (Jody Garnett)
*/
public class MBStyleTransformer {
private final FilterFactory2 ff;
private final StyleFactory sf;
private final StyleBuilder sb;
private final List<String> defaultFonts;
protected static Pattern mapboxTokenPattern = Pattern.compile("\\{(.*?)\\}");
private static final Logger LOGGER = Logging.getLogger(MBStyleTransformer.class);
public MBStyleTransformer(MBObjectParser parse) {
defaultFonts = new ArrayList<>();
defaultFonts.add("Open Sans Regular");
defaultFonts.add("Arial Unicode MS Regular");
ff = parse.getFilterFactory();
sf = parse.getStyleFactory();
sb = new StyleBuilder();
}
/**
* <p>
* Takes the name of an icon, and an {@link MBStyle} as a context, and returns an External Graphic referencing the full URL of the image for
* consumption by the {@link SpriteGraphicFactory}. (The format of the image will be {@link SpriteGraphicFactory#FORMAT}).
* </p>
*
* @see {@link SpriteGraphicFactory} for more information.
*
* @param iconName The name of the icon inside the spritesheet.
* @param styleContext The style context in which to resolve the icon name to the full sprite URL (for consumption by the
* {@link SpriteGraphicFactory}).
* @return An external graphic with the full URL of the mage for the {@link SpriteGraphicFactory}.
*/
public ExternalGraphic createExternalGraphicForSprite(Expression iconName, MBStyle styleContext) {
String spriteUrl;
String iconNameCql = ECQL.toCQL(iconName);
/*
* Note: The provided iconName {@link Expression} will be embedded in the {@link ExternalGraphic}'s URL as a CQL string, in order to support
* Mapbox functions. The {@link SLDStyleFactory} will transform it back into a proper {@link Expression} before sending it to the {@link
* SpriteGraphicFactory}.
*/
if (styleContext != null && styleContext.getSprite() != null) {
String spriteBase = styleContext.getSprite().trim() + "#";
spriteUrl = spriteBase + "${" + iconNameCql + "}";
} else {
spriteUrl = iconNameCql;
}
return sf.createExternalGraphic(spriteUrl, SpriteGraphicFactory.FORMAT);
}
/**
* Given a string of "bottom-right" or "top-left" find the x,y coordinates and create an AnchorPoint
* @param textAnchor The value of the "text-anchor" property in the mapbox style.
* @return AnchorPoint
*/
AnchorPoint getAnchorPoint(String textAnchor) {
TextAnchor anchor = TextAnchor.parse(textAnchor);
return sb.createAnchorPoint(anchor.getX(), anchor.getY());
}
/**
* <p>Take a string that may contain Mapbox-style tokens, and convert it to a CQL expression string.</p>
*
* <p>E.g., convert "<code>String with {tokens}</code>" to a CQL Expression (String) "<code>String with ${tokens}</code>".</p>
*
* <p>See documentation of Mapbox {token} values, linked below.</p>
*
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-icon-image">Mapbox Style Spec: {token} values for icon-image</a>
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-text-field">Mapbox Style Spec: {token} values for text-field</a>
*
* @param tokenStr A string with mapbox-style tokens
* @return A CQL Expression
*/
public String cqlStringFromTokens(String tokenStr) {
// Find all {tokens} and turn them into CQL ${expressions}
Matcher m = mapboxTokenPattern.matcher(tokenStr);
return m.replaceAll("\\${$1}");
}
/**
* <p>Take a string that may contain Mapbox-style tokens, and convert it to a CQL expression.</p>
*
* <p>E.g., convert "<code>String with {tokens}</code>" to a CQL Expression: "<code>String with ${tokens}</code>".</p>
*
* <p>See documentation of Mapbox {token} values, linked below.</p>
*
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-icon-image">Mapbox Style Spec: {token} values for icon-image</a>
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#layout-symbol-text-field">Mapbox Style Spec: {token} values for text-field</a>
*
* @param tokenStr A string with mapbox-style tokens
* @return A CQL Expression
*/
public Expression cqlExpressionFromTokens(String tokenStr) {
try {
return ExpressionExtractor.extractCqlExpressions(cqlStringFromTokens(tokenStr));
} catch (IllegalArgumentException iae) {
LOGGER.warning(
"Exception converting Mapbox token string to CQL expression. Mapbox token string was: \""
+ tokenStr + "\". Exception was: " + iae.getMessage());
return ff.literal(tokenStr);
}
}
/**
* Utility method for getting a concrete value out of an expression, used by transformer methods when GeoTools is unable to accept an expression.
* <ul>
* <li>If the provided {@link Expression} is a {@link Literal}, evaluates it and returns the value.</li>
* <li>Otherwise, returns the provided fallback value and logs a warning that dynamic styling is not yet supported for this property.</li>
* </ul>
*
* @param expression The expression
* @param clazz The type to provide as the context for the expression's evaluation.
* @param fallback The value to return if the expression is not a literal
* @param propertyName The name of the property that the expression corresponds to, for logging purposes.
* @param layerId The ID of the layer that the expression corresponds to, for logging purposes.
* @return The evaluated value of the provided {@link Expression}, or the provided fallback value.
*/
public static <T> T requireLiteral(Expression expression, Class<T> clazz, T fallback,
String propertyName, String layerId) {
if (expression instanceof Literal) {
T value = expression.evaluate(null, clazz);
if (value != null) {
return value;
} else {
return fallback;
}
} else {
LOGGER.warning("Mapbox '" + propertyName
+ "' property: functions not yet supported for this property, falling back to default value."
+ " (layerId = '" + layerId + "')");
return fallback;
}
}
/**
*
* @return The list of default font names
*/
public List<String> getDefaultFonts() {
return defaultFonts;
}
}