/*
* 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.text.Text;
import org.json.simple.JSONObject;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.style.Displacement;
import org.opengis.style.GraphicFill;
import org.opengis.style.SemanticType;
import javax.measure.unit.NonSI;
import java.awt.*;
import java.util.List;
import java.util.*;
/**
* MBLayer wrapper for "Fill" layers.
* <p>
* Example of line JSON:
*
* <pre>
* { "type": "line",
* "source": "http://localhost:8080/geoserver/ne/roads",
* "source-layer": "road"
* "id": "roads",
* "paint": {
* "fill-anitalias": true,
* "fill-opacity": 1
* "fill-color": "#6655ae",
* "fill-outline-color": "#000000",
* "fill-translate": [0,0],
* "fill-translate-anchor": "map",
* "fill-pattern": "triangle" // Name of image in sprite to use for drawing image fills. For seamless patterns, image width and height must be a factor of two (2, 4, 8, ..., 512).
* },
* },
* </pre>
*
* @author Reggie Beckwith (Boundless)
*
*/
public class FillMBLayer extends MBLayer {
private JSONObject paint;
private JSONObject layout;
private static String TYPE = "fill";
/**
* Controls the translation reference point.
*/
public static enum FillTranslateAnchor {
/** The fill is translated relative to the map. */
MAP,
/** The fill is translated relative to the viewport. */
VIEWPORT
}
public FillMBLayer(JSONObject json) {
super(json,new MBObjectParser(FillMBLayer.class));
paint = paint();
layout = layout();
}
@Override
protected SemanticType defaultSemanticType() {
return SemanticType.POLYGON;
}
/**
* (Optional) Whether or not the fill should be antialiased.
*
* Defaults to true.
*
* @return Whether the fill should be antialiased.
*/
public Expression getFillAntialias() {
return parse.bool(paint, "fill-antialias", true);
}
/**
* (Optional) The opacity of the entire fill layer. In contrast to the fill-color, this value will also affect the
* 1px stroke around the fill, if the stroke is used.
*
* Defaults to 1.
*
* @return The opacity of the layer.
* @throws MBFormatException
*/
public Number getFillOpacity() throws MBFormatException {
return parse.optional(Double.class, paint, "fill-opacity", 1.0 );
}
/**
* Access fill-opacity, defaults to 1.
*
* @return The opacity of the layer.
* @throws MBFormatException
*/
public Expression fillOpacity() throws MBFormatException {
return parse.percentage( paint, "fill-opacity", 1 );
}
/**
* (Optional). The color of the filled part of this layer. This color can be specified as rgba with an alpha
* component and the color's opacity will not affect the opacity of the 1px stroke, if it is used.
*
* Colors are written as JSON strings in a variety of permitted formats.
*
* Defaults to #000000. Disabled by fill-pattern.
*
* @return The fill color.
*/
public Color getFillColor(){
return parse.convertToColor(parse.optional(String.class, paint, "fill-color", "#000000"));
}
/**
* Access fill-color as literal or function expression, defaults to black.
*
* @return The fill color.
*/
public Expression fillColor() {
return parse.color(paint, "fill-color", Color.BLACK);
}
/**
* (Optional). Requires fill-antialias = true. The outline color of the fill.
*
* Matches the value of fill-color if unspecified. Disabled by fill-pattern.
*
* @return The outline color of the fill.
*/
public Color getFillOutlineColor(){
if (paint.get("fill-outline-color") != null) {
return parse.convertToColor(parse.optional(String.class, paint, "fill-outline-color", "#000000"));
} else {
return getFillColor();
}
}
/**
* Access fill-outline-color as literal or function expression, defaults to black.
*
* @return The outline color of the fill.
*/
public Expression fillOutlineColor() {
if (paint.get("fill-outline-color") != null) {
return parse.color(paint, "fill-outline-color", Color.BLACK);
} else {
return fillColor();
}
}
/**
* (Optional) The geometry's offset. Values are [x, y] where negatives indicate left and up,
* respectively. Units in pixels. Defaults to 0, 0.
*
* @return The geometry's offset
*/
public int[] getFillTranslate(){
return parse.array( paint, "fill-translate", new int[]{ 0, 0 } );
}
/**
* Access fill-translate as Point.
*
* @return The geometry's offset, in pixels.
*/
public Point fillTranslate() {
int[] translate = getFillTranslate();
return new Point(translate[0], translate[1]);
}
/**
* Processes the filter-translate into a Displacement.
* <p>
* This should handle both literals and function stops:</p>
* <pre>
* filter-translate: [0,0]
* filter-translate: { property: "building-height", "stops": [[0,[0,0]],[5,[1,2]]] }
* filter-translate: [ 0, { property: "building-height", "TYPE":"exponential","stops": [[0,0],[30, 5]] }
* </pre>
* @return The geometry displacement
*/
public Displacement fillTranslateDisplacement() {
return parse.displacement(paint, "fill-translate", sf.displacement(ff.literal(0), ff.literal(0)));
}
/**
* (Optional) Controls the translation reference point.
*
* <ul>
* <li>{@link FillTranslateAnchor#MAP}: The fill is translated relative to the map.</li>
* <li>{@link FillTranslateAnchor#VIEWPORT}: The fill is translated relative to the viewport.</li>
* </ul>
*
* Requires fill-translate.
*
* @return One of 'map','viewport', defaults to 'map'.
*/
public FillTranslateAnchor getFillTranslateAnchor() {
Object value = paint.get("fill-translate-anchor");
if (value != null && "viewport".equalsIgnoreCase((String) value)) {
return FillTranslateAnchor.VIEWPORT;
} else {
return FillTranslateAnchor.MAP;
}
}
/**
* (Optional) Name of image in a sprite to use for drawing image fills. For seamless patterns, image width and
* height must be a factor of two (2, 4, 8, ..., 512).
*
* @return name of the sprite for the fill pattern, or null if not defined.
*/
public Expression fillPattern() {
return parse.string(paint, "fill-pattern", null);
}
/**
*
* @return True if the layer has a fill-pattern explicitly provided.
*/
public boolean hasFillPattern() {
return parse.isPropertyDefined(paint, "fill-pattern");
}
/**
* Transform MBFillLayer to GeoTools FeatureTypeStyle.
* <p>
* Notes:</p>
* <ul>
* <li>stroke-width is assumed to be 1 (not specified by MapBox style)
* </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);
PolygonSymbolizer symbolizer;
// use factory to avoid defaults values
org.geotools.styling.Stroke stroke = sf.stroke(
fillOutlineColor(),
fillOpacity(),
ff.literal(1),
ff.literal("miter"),
ff.literal("butt"),
null,
null);
// from fill pattern or fill color
Fill fill;
if (hasFillPattern()) {
// If the fill-pattern is a literal string (not a function), then
// we need to support Mapbox {token} replacement.
Expression fillPatternExpr = fillPattern();
if (fillPatternExpr instanceof Literal) {
String text = fillPatternExpr.evaluate(null, String.class);
if (text.trim().isEmpty()) {
fillPatternExpr = ff.literal(" ");
} else {
fillPatternExpr = transformer.cqlExpressionFromTokens(text);
}
}
ExternalGraphic eg = transformer.createExternalGraphicForSprite(fillPatternExpr, styleContext);
GraphicFill gf = sf.graphicFill(Arrays.asList(eg), fillOpacity(), null, null, null, fillTranslateDisplacement());
fill = sf.fill(gf, null, null);
} else {
fill = sf.fill(null, fillColor(), fillOpacity());
}
symbolizer = sf.polygonSymbolizer(
getId(),
ff.property((String)null),
sf.description(Text.text("fill"),null),
NonSI.PIXEL,
stroke,
fill,
fillTranslateDisplacement(),
ff.literal(0));
MBFilter filter = getFilter();
Rule rule = sf.rule(
getId(),
null,
null,
0.0,
Double.POSITIVE_INFINITY,
Arrays.asList(symbolizer),
filter.filter());
// Set legend graphic to null.
//How do other style transformers set a null legend? SLD/SE difference - fix setLegend(null) to empty list.
rule.setLegendGraphic(new Graphic[0]);
return Collections.singletonList(sf.featureTypeStyle(
getId(),
sf.description(
Text.text("MBStyle "+getId()),
Text.text("Generated for "+getSourceLayer())),
null, // (unused)
Collections.emptySet(),
filter.semanticTypeIdentifiers(),
Arrays.asList(rule)
));
}
/**
* Rendering type of this layer.
*
* @return {@link #TYPE}
*/
@Override
public String getType() {
return TYPE;
}
}