/*
* 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.filter.function.RecodeFunction;
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.JSONArray;
import org.json.simple.JSONObject;
import org.opengis.filter.expression.Expression;
import org.opengis.style.GraphicFill;
import org.opengis.style.SemanticType;
import org.opengis.style.Stroke;
import javax.measure.unit.NonSI;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* MBLayer wrapper for "line" layers.
* <p>
* Example of line JSON:
*
* <pre>
* { "type": "line",
* "source": "http://localhost:8080/geoserver/ne/roads",
* "source-layer": "road"
* "id": "roads",
* "paint": {
* "line-color": "#6655ae",
* "line-width": 2,
* "line-opacity": 1
* },
* },
* </pre>
*
* @author Reggie Beckwith (Boundless)
*
*/
public class LineMBLayer extends MBLayer {
private JSONObject layout;
private JSONObject paint;
private static String TYPE = "line";
public LineMBLayer(JSONObject json) {
super(json, new MBObjectParser(LineMBLayer.class));
paint = super.getPaint();
layout = super.getLayout();
}
@Override
protected SemanticType defaultSemanticType() {
return SemanticType.LINE;
}
/**
* The display of line endings.
*/
public enum LineCap {
/** A cap with a squared-off end which is drawn to the exact endpoint of the line. */
BUTT,
/**
* A cap with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the
* endpoint of the line.
*/
ROUND,
/**
* A cap with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width.
*
*/
SQUARE
}
/**
* Display of line endings.
* <p>
* Supports piecewise constant functions.
* </p>
*
* @return One of butt, round, square, optional defaults to butt.
*/
public LineCap getLineCap() {
return parse.getEnum(layout, "line-cap", LineCap.class, LineCap.BUTT);
}
/**
* Maps {@link #getLineCap()} to {@link Stroke#getLineCap()} values of "butt", "round", and "square" Literals. Defaults to butt.
* <p>
* Since piecewise constant functions is supported a {@link RecodeFunction} may be generated.
*
* @return Expression for {@link Stroke#getLineCap()} use.
*/
public Expression lineCap() {
return parse.enumToExpression(layout, "line-cap", LineCap.class, LineCap.BUTT);
}
/**
* (Optional) The display of lines when joining.
*
* Bevel - A join with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width.
*
* Round - A join with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on
* the endpoint of the line.
*
* Miter - A join with a sharp, angled corner which is drawn with the outer sides beyond the endpoint of the path until they meet.
*/
public enum LineJoin {
BEVEL, ROUND, MITER
}
/**
* Optional enum. One of bevel, round, miter. Defaults to miter. The display of lines when joining.
*
* @return The line join
*/
public LineJoin getLineJoin() {
return parse.getEnum(layout, "line-join", LineJoin.class, LineJoin.MITER);
}
/**
* Maps {@link #getLineJoin()} to {@link Stroke#getLineJoin()} values of "mitre", "round", and "bevel" Literals. Defaults to "mitre".
* <p>
* Since piecewise constant functions is supported a {@link RecodeFunction} may be generated.
*
* @return Expression for {@link Stroke#getLineJoin()()} use.
*/
public Expression lineJoin() {
return parse.enumToExpression(layout, "line-join", LineJoin.class, LineJoin.MITER);
}
/**
* (Optional) Used to automatically convert miter joins to bevel joins for sharp angles.
*
* Defaults to 2. Requires line-join = miter.
*
* @return The threshold at which miter joins are converted to bevel joins.
*/
public Number getLineMiterLimit() {
return parse.optional(Number.class, layout, "line-miter-limit", 2);
}
/**
* Maps {@link #getLineMiterLimit()} to an {@link Expression}. (Optional) Used to automatically convert miter joins to bevel joins for sharp
* angles.
*
* Defaults to 2. Requires line-join = miter.
*
* @return Expression for {@link #getLineMiterLimit()}
*/
public Expression lineMiterLimit() {
return parse.number(layout, "line-miter-limit", 2);
}
/**
* (Optional) Used to automatically convert round joins to bevel joins for sharp angles.
*
* Defaults to 1.05. Requires line-join = round.
*
* @return The threshold at which round joins are converted to bevel joins.
*/
public Number getLineRoundLimit() {
return parse.optional(Number.class, layout, "line-round-limit", 1.05);
}
/**
* Maps {@link #getLineRoundLimit()} to an {@link Expression}.
*
* (Optional) Used to automatically convert round joins to bevel joins for sharp angles.
*
* Defaults to 1.05. Requires line-join = round.
*
*/
public Expression lineRoundLimit() {
return parse.number(layout, "line-round-limit", 1.05);
}
/**
* (Optional) The opacity at which the line will be drawn.
*
* Defaults to 1.
*
* @return The line opacity
*/
public Number getLineOpacity() {
return parse.optional(Number.class, paint, "line-opacity", 1);
}
/**
* Maps {@link #getLineOpacity()} to an {@link Expression}.
*
* (Optional) The opacity at which the line will be drawn.
*
* Defaults to 1.
*
* @return opacity for line (literal or function), defaults to 1.
*
*/
public Expression lineOpacity() {
return parse.number(paint, "line-opacity", 1);
}
/**
* (Optional) The color with which the line will be drawn.
*
* Defaults to {@link Color#BLACK}, disabled by line-pattern.
*
* @return color to draw the line, optional defaults to black.
*/
public Color getLineColor() {
if (paint.containsKey("line-pattern")) {
return null; // disabled
}
return parse.convertToColor(parse.optional(String.class, paint, "line-color", "#000000"));
}
/**
*
* Maps {@link #getLineColor()} to an {@link Expression}.
*
* (Optional) The color with which the line will be drawn.
*
* Defaults to {@link Color#BLACK}, disabled by line-pattern.
*
* @return color to draw the line, optional defaults to black.
*/
public Expression lineColor() {
if (paint.containsKey("line-pattern")) {
return null; // disabled
}
return parse.color(paint, "line-color", Color.BLACK);
}
/**
* (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[] getLineTranslate() {
return parse.array( paint, "line-translate", new int[]{ 0, 0 } );
}
/**
* Maps {@link #getLineTranslate()} to a {@link Displacement}.
*
* (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, as a Displacement.
*/
public Displacement lineTranslateDisplacement() {
return parse.displacement(paint, "line-translate", sf.displacement(ff.literal(0), ff.literal(0)));
}
/**
* Controls the translation reference point.
*
* Map: The fill is translated relative to the map.
*
* Viewport: The fill is translated relative to the viewport.
*
*/
public enum LineTranslateAnchor {
MAP, VIEWPORT
}
/**
* (Optional) Controls the translation reference point.
*
* {@link LineTranslateAnchor#MAP}: The fill is translated relative to the map.
*
* {@link LineTranslateAnchor#VIEWPORT}: The fill is translated relative to the viewport.
*
* Defaults to {@link LineTranslateAnchor#MAP}. Requires fill-translate.
*
* @return The translation reference point.
*/
public LineTranslateAnchor getLineTranslateAnchor() {
return parse.getEnum(paint, "line-translate-anchor", LineTranslateAnchor.class, LineTranslateAnchor.MAP);
}
/**
* Wraps {@link #getLineTranslateAnchor()} in a GeoTools expression. Returns an expression that evaluates to "map" or "viewport".
*
*/
public Expression lineTranslateAnchor() {
return parse.enumToExpression(paint, "line-translate-anchor", LineTranslateAnchor.class, LineTranslateAnchor.MAP);
}
/**
* (Optional) Stroke thickness.
*
* Units in pixels. Defaults to 1.
*
* @return The stroke thickness.
*/
public Number getLineWidth() {
if (paint.get("line-width") != null) {
return (Number) paint.get("line-width");
} else {
return 1;
}
}
/**
*
* Convert {@link #getLineWidth()} to an Expression.
*
* (Optional) Stroke thickness. Units in pixels. Defaults to 1.
*
* @return The stroke thickness.
*/
public Expression lineWidth() {
return parse.number(paint, "line-width", 1);
}
/**
* (Optional) Draws a line casing outside of a line's actual path. Value indicates the width of the inner gap.
*
* Units in pixels. Defaults to 0.
*
* @return The inner gap between the sides of the line casing
*/
public Number getLineGapWidth() {
return parse.optional(Number.class, paint, "line-gap-width", 0);
}
/**
* Converts {@link #getLineGapWidth()} to an Expression.
*
* (Optional) Draws a line casing outside of a line's actual path. Value indicates the width of the inner gap.
*
* Units in pixels. Defaults to 0.
*
* @return The inner gap between the sides of the line casing
*/
public Expression lineGapWidth() {
return parse.number(paint, "line-gap-width", 0);
}
/**
* (Optional) The line's offset. For linear features, a positive value offsets the line to the right, relative to the direction of the line, and a
* negative value to the left. For polygon features, a positive value results in an inset, and a negative value results in an outset.
*
* Units in pixels. Defaults to 0.
*
* @return The line's offset.
*/
public Number getLineOffset() {
return parse.optional(Number.class, paint, "line-offset", 0);
}
/**
* Converts {@link #getLineOffset()} to an Expression.
*
* (Optional) The line's offset. For linear features, a positive value offsets the line to the right, relative to the direction of the line, and a
* negative value to the left. For polygon features, a positive value results in an inset, and a negative value results in an outset.
*
* Units in pixels. Defaults to 0.
*
* @return The line's offset.
*/
public Expression lineOffset() {
return parse.number(paint, "line-offset", 0);
}
/**
* (Optional) Blur applied to the line, in pixels.
*
* Units in pixels. Defaults to 0.
*
* @return The line blur.
*/
public Number getLineBlur() {
return parse.optional(Number.class, paint, "line-blur", 0);
}
/**
* Converts {@link #getLineBlur()} to an Expression.
*
* (Optional) Blur applied to the line, in pixels.
*
* Units in pixels. Defaults to 0.
*
* @return The line blur.
*/
public Expression lineBlur() {
return parse.number(paint, "line-blur", 0);
}
/**
* (Optional) Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width.
* To convert a dash length to pixels, multiply the length by the current line width.
*
* Units in line widths. Disabled by line-pattern.
*
* @return A list of dash and gap lengths defining the pattern for a dashed line.
*/
public List<Double> getLineDasharray() {
List<Double> ret = new ArrayList<>();
if (paint.get("line-dasharray") != null
&& paint.get("line-dasharray") instanceof JSONArray) {
JSONArray a = (JSONArray) paint.get("line-dasharray");
for (Object o : a) {
Number n = (Number) o;
ret.add(n.doubleValue());
}
return ret;
} else {
return null;
}
}
/**
* Converts {@link #getLineDasharray()} to a List of Expressions
*
* (Optional) Specifies the lengths of the alternating dashes and gaps that form the dash pattern. The lengths are later scaled by the line width.
* To convert a dash length to pixels, multiply the length by the current line width.
*
* Units in line widths. Disabled by line-pattern.
*
* @return A list of dash and gap lengths defining the pattern for a dashed line.
*/
public List<Expression> lineDasharray() {
Object defn = paint.get("line-dasharray");
if (defn == null) {
return null;
} else if (defn instanceof JSONArray) {
JSONArray array = (JSONArray) defn;
List<Expression> expressionList = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
expressionList.add(parse.number(array, i, 0));
}
return expressionList;
} else if (defn instanceof JSONObject) {
throw new MBFormatException("\"line-dasharray\": Functions not supported yet.");
} else {
throw new MBFormatException("\"line-dasharray\": Expected array or function, but was "
+ defn.getClass().getSimpleName());
}
}
/**
* (Optional) Name of image in sprite to use for drawing image lines. For seamless patterns, image width must be a
* factor of two (2, 4, 8, ..., 512).
*
* Units in line widths. Disabled by line-pattern.
*
* The name of the sprite to use for the line pattern.
*/
public String getLinePattern() {
return parse.optional(String.class, paint, "line-pattern", null);
}
/**
*
* Converts {@link #getLinePattern()} to an Expression.
*
* (Optional) Name of image in sprite to use for drawing image lines. For seamless patterns, image width must be a
* factor of two (2, 4, 8, ..., 512).
*
* Units in line widths. Disabled by line-pattern.
*
* The name of the sprite to use for the line pattern.
*/
public Expression linePattern() {
return parse.string(paint, "line-pattern", null);
}
/**
*
* @return True if the layer has a line-pattern explicitly provided.
*/
public boolean hasLinePattern() {
return parse.isPropertyDefined(paint, "line-pattern");
}
/**
* Transform {@link LineMBLayer} 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);
org.geotools.styling.Stroke stroke = sf.stroke(lineColor(), lineOpacity(), lineWidth(),
lineJoin(), lineCap(), null, null); // last "offset" is really "dash offset"
stroke.setDashArray(lineDasharray());
LineSymbolizer ls = sf.lineSymbolizer(getId(), null,
sf.description(Text.text("line"), null), NonSI.PIXEL, stroke, lineOffset());
if (hasLinePattern()) {
ExternalGraphic eg = transformer.createExternalGraphicForSprite(linePattern(), styleContext);
GraphicFill fill = sf.graphicFill(Arrays.asList(eg), lineOpacity(), null, null, null, null);
stroke.setGraphicFill(fill);
}
MBFilter filter = getFilter();
List<org.opengis.style.Rule> rules = new ArrayList<>();
Rule rule = sf.rule(
getId(),
null,
null,
0.0,
Double.POSITIVE_INFINITY,
Arrays.asList(ls),
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, Collections.emptySet(), filter.semanticTypeIdentifiers(), rules));
}
/**
* Rendering type of this layer.
*
* @return {@link #TYPE}
*/
@Override
public String getType() {
return TYPE;
}
}