package org.mapfish.print.map.style.json; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; import org.geotools.styling.Rule; import org.geotools.styling.Style; import org.geotools.styling.StyleBuilder; import org.geotools.styling.Symbolizer; import org.json.JSONException; import org.json.JSONObject; import org.mapfish.print.ExceptionUtils; import org.mapfish.print.config.Configuration; import org.mapfish.print.wrapper.json.PJsonArray; import org.mapfish.print.wrapper.json.PJsonObject; import org.opengis.filter.Filter; import org.springframework.http.client.ClientHttpRequestFactory; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import static org.mapfish.print.map.style.json.MapfishStyleParserPlugin.Versions; /** * Support a more flexible json styling than that which is supported by version 1. */ public final class MapfishJsonStyleVersion2 { static final String JSON_SYMB = "symbolizers"; private static final String JSON_TYPE = "type"; private static final Pattern VALUE_EXPR_PATTERN = Pattern.compile("\\$\\{([\\w\\d_-]+)\\}"); private static final String JSON_MIN_SCALE = "minScale"; private static final String JSON_MAX_SCALE = "maxScale"; private static final String JSON_FILTER_INCLUDE = "*"; enum SymbolizerType { POINT { @Override protected Symbolizer parseJson( final JsonStyleParserHelper parser, final PJsonObject symbolizerJson) { return parser.createPointSymbolizer(symbolizerJson); } }, LINE { @Override protected Symbolizer parseJson( final JsonStyleParserHelper parser, final PJsonObject symbolizerJson) { return parser.createLineSymbolizer(symbolizerJson); } }, POLYGON { @Override protected Symbolizer parseJson( final JsonStyleParserHelper parser, final PJsonObject symbolizerJson) { return parser.createPolygonSymbolizer(symbolizerJson); } }, TEXT { @Override protected Symbolizer parseJson( final JsonStyleParserHelper parser, final PJsonObject symbolizerJson) { return parser.createTextSymbolizer(symbolizerJson); } }; protected abstract Symbolizer parseJson( final JsonStyleParserHelper parser, final PJsonObject symbolizerJson); } private final PJsonObject json; private final StyleBuilder styleBuilder; private final ClientHttpRequestFactory clientHttpRequestFactory; private final JsonStyleParserHelper parserHelper; MapfishJsonStyleVersion2( @Nonnull final PJsonObject json, @Nonnull final StyleBuilder styleBuilder, @Nullable final Configuration configuration, @Nonnull final ClientHttpRequestFactory requestFactory) { this.json = json; this.styleBuilder = styleBuilder; this.parserHelper = new JsonStyleParserHelper(configuration, requestFactory, styleBuilder, false, Versions.TWO); this.clientHttpRequestFactory = requestFactory; } Style parseStyle() { List<Rule> rules = Lists.newArrayList(); final Iterator<String> keys = this.json.keys(); while (keys.hasNext()) { String next = keys.next().trim(); if (isRule(next)) { rules.add(createRule(next)); } } if (rules.isEmpty()) { throw new IllegalArgumentException(String.format( "No rules found in style. Rules are json objects that have the key %s or have the " + "form: [ecql]", JSON_FILTER_INCLUDE)); } return this.parserHelper.createStyle(rules); } private Rule createRule(final String jsonKey) { PJsonObject ruleJson = this.json.getJSONObject(jsonKey); Filter filter = Filter.INCLUDE; if (!jsonKey.equals(JSON_FILTER_INCLUDE)) { try { filter = ECQL.toFilter(jsonKey, this.styleBuilder.getFilterFactory()); } catch (CQLException e) { throw new RuntimeException("Error compiling rule filter: " + jsonKey, e); } } PJsonArray symbolizerJsonArray = ruleJson.getJSONArray(JSON_SYMB); Symbolizer[] symbolizers = new Symbolizer[symbolizerJsonArray.size()]; for (int i = 0; i < symbolizerJsonArray.size(); i++) { final PJsonObject symbolizerJson = symbolizerJsonArray.getJSONObject(i); updateSymbolizerProperties(ruleJson, symbolizerJson); SymbolizerType type = SymbolizerType.valueOf(symbolizerJson.getString(JSON_TYPE).toUpperCase()); symbolizers[i] = type.parseJson(this.parserHelper, symbolizerJson); if (symbolizers[i] == null) { throw new RuntimeException("Error creating symbolizer " + symbolizerJson.getString(JSON_TYPE) + " in rule " + jsonKey); } } Map<String, String> ruleValues = buildValuesMap(ruleJson, new PJsonObject(new JSONObject(), "empty")); double minScale = getScaleDenominator(ruleValues, JSON_MIN_SCALE, Double.MIN_VALUE); double maxScale = getScaleDenominator(ruleValues, JSON_MAX_SCALE, Double.MAX_VALUE); final Rule rule = this.styleBuilder.createRule(symbolizers, minScale, maxScale); rule.setFilter(filter); return rule; } private void updateSymbolizerProperties(final PJsonObject ruleJson, final PJsonObject symbolizerJson) { Map<String, String> values = buildValuesMap(ruleJson, symbolizerJson); for (Map.Entry<String, String> entry : values.entrySet()) { try { symbolizerJson.getInternalObj().put(entry.getKey(), entry.getValue()); } catch (JSONException e) { throw ExceptionUtils.getRuntimeException(e); } } } private double getScaleDenominator(final Map<String, String> ruleValues, final String keyName, final double defaultValue) { String scaleString = ruleValues.get(keyName); if (scaleString != null) { return Double.parseDouble(scaleString); } return defaultValue; } private Map<String, String> buildValuesMap(final PJsonObject ruleJson, final PJsonObject symbolizerJson) { Map<String, String> values = Maps.newHashMap(); Iterator<String> keys = this.json.keys(); while (keys.hasNext()) { String key = keys.next().trim(); if (!isRule(key)) { values.put(key, this.json.getString(key)); } } keys = ruleJson.keys(); while (keys.hasNext()) { String key = keys.next().trim(); if (!key.equals(JSON_SYMB)) { values.put(key, ruleJson.getString(key)); } } keys = symbolizerJson.keys(); while (keys.hasNext()) { String key = keys.next().trim(); if (!key.equals(JSON_SYMB)) { values.put(key, symbolizerJson.getString(key)); } } return resolveAllValues(values); } @VisibleForTesting static Map<String, String> resolveAllValues(final Map<String, String> values) { Map<String, String> toResolve = Maps.newHashMap(values); Map<String, String> resolved = Maps.newHashMapWithExpectedSize(values.size()); while (!toResolve.isEmpty()) { Iterator<Map.Entry<String, String>> entries = toResolve.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, String> next = entries.next(); String value = next.getValue(); final String resolve = resolve(values, value); if (resolve == null) { resolved.put(next.getKey(), value); entries.remove(); } else { next.setValue(resolve); } } } return resolved; } private static String resolve(final Map<String, String> values, final String value) { int lastEnd = 0; final Matcher matcher = VALUE_EXPR_PATTERN.matcher(value); boolean changed = false; StringBuilder updatedValue = new StringBuilder(); while (matcher.find()) { final String valName = matcher.group(1); String replacement; if (values.containsKey(valName)) { changed = true; replacement = values.get(valName); } else { replacement = matcher.group(0); } updatedValue.append(value.substring(lastEnd, matcher.start())); updatedValue.append(replacement); lastEnd = matcher.end(); } if (changed) { updatedValue.append(value.substring(lastEnd)); return updatedValue.toString(); } else { return null; } } private boolean isRule(final String jsonKey) { return this.json.optJSONObject(jsonKey) != null; } }