/*
* 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.styling.*;
import org.geotools.styling.Stroke;
import org.geotools.text.Text;
import org.json.simple.JSONObject;
import org.opengis.filter.expression.Expression;
import org.opengis.style.Displacement;
import org.opengis.style.SemanticType;
import javax.measure.unit.NonSI;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* A filled circle.
* <p>
* MBLayer wrapper around a {@link JSONObject} representation of a "circle" type layer. All
* methods act as accessors on provided JSON layer, no other state is maintained. This allows
* modifications to be made cleanly with out chance of side-effect.
*
* <ul>
* <li>get methods: access the json directly</li>
* <li>query methods: provide logic / transforms to GeoTools classes as required.</li>
* </ul>
*
* @author Reggie Beckwith (Boundless)
*/
public class CircleMBLayer extends MBLayer {
private JSONObject paint;
private JSONObject layout;
private static String TYPE = "circle";
public CircleMBLayer(JSONObject json) {
super(json, new MBObjectParser(CircleMBLayer.class));
paint = paint();
layout = layout();
}
@Override
protected SemanticType defaultSemanticType() {
return SemanticType.POINT;
}
/**
* (Optional) Circle radius in pixels. Defaults to 5.
*
* @return The circle radius
* @throws MBFormatException
*/
public Number getCircleRadius() throws MBFormatException {
return parse.optional(Double.class, paint, "circle-radius", 5.0);
}
/**
* Access circle-radius as literal or function expression, defaults to 5
*
* @return The circle radius as literal or function expression
* @throws MBFormatException
*/
public Expression circleRadius() throws MBFormatException {
return parse.percentage(paint, "circle-radius", 5);
}
/**
* (Optional) The fill color of the circle. Defaults to #000000.
*
* @return The fill color of the circle
* @throws MBFormatException
*
*/
public Color getCircleColor() throws MBFormatException {
return parse.optional(Color.class, paint, "circle-color", Color.BLACK);
}
/**
* Access circle-color as literal or function expression, defaults to black.
*
* @return The circle color as literal or function expression
* @throws MBFormatException
*/
public Expression circleColor() throws MBFormatException {
return parse.color(paint, "circle-color", Color.BLACK);
}
/**
* (Optional) Amount to blur the circle. 1 blurs the circle such that only the centerpoint is
* full opacity. Defaults to 0.
*
* @return The amount to blur the circle.
* @throws MBFormatException
*
*/
public Number getCircleBlur() throws MBFormatException {
return parse.optional(Double.class, paint, "circle-blur", 0.0);
}
/**
* Access circle-blur as literal or function expression, defaults to 0
*
* @return The amount to blur the circle, as literal or function expression
* @throws MBFormatException
*/
public Expression circleBlur() throws MBFormatException {
return parse.percentage(paint, "circle-blur", 0);
}
/**
* (Optional) The opacity at which the circle will be drawn. Defaults to 1.
*
* @return The opacity at which the circle will be drawn.
* @throws MBFormatException
*
*/
public Number getCircleOpacity() throws MBFormatException {
return parse.optional(Double.class, paint, "circle-opacity", 1.0);
}
/**
* Access circle-opacity, defaults to 1.
*
* @return The opacity at which the circle will be drawn as literal or function expression.
* @throws MBFormatException
*/
public Expression circleOpacity() throws MBFormatException {
return parse.percentage(paint, "circle-opacity", 1);
}
/**
* (Optional) The geometry's offset. Values are [x, y] where negatives indicate left and up,
* respectively. Units in pixels. Defaults to 0, 0.
*
* @return x and y offset in pixels.
* @throws MBFormatException
*/
public int[] getCircleTranslate() throws MBFormatException {
return parse.array(paint, "circle-translate", new int[] { 0, 0 });
}
/**
* Access circle-translate
*
* @return x and y offset in pixels as Point
* @throws MBFormatException
*/
public Point circleTranslate() throws MBFormatException {
int[] circleTranslate = getCircleTranslate();
return new Point(circleTranslate[0], circleTranslate[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
*/
public Displacement circleTranslateDisplacement() {
return parse.displacement(paint, "circle-translate", sf.displacement(ff.literal(0), ff.literal(0)));
}
/**
* Controls the translation reference point.
*
* Map: The circle is translated relative to the map.
* Viewport: The circle is translated relative to the viewport.
*/
public enum CircleTranslateAnchor {
MAP, VIEWPORT
}
/**
* (Optional) Controls the translation reference point.
*
* {@link CircleTranslateAnchor#MAP}: The circle is translated relative to the map.
* {@link CircleTranslateAnchor#VIEWPORT}: The circle is translated relative to the viewport.
*
* Defaults to {@link CircleTranslateAnchor#MAP}. Requires circle-translate.
*
* @return The translation reference point.
*/
public CircleTranslateAnchor getCircleTranslateAnchor() {
Object value = paint.get("circle-translate-anchor");
if (value != null && "viewport".equalsIgnoreCase((String) value)) {
return CircleTranslateAnchor.VIEWPORT;
} else {
return CircleTranslateAnchor.MAP;
}
}
/**
* Controls the translation reference point.
*
* Map: The circle is translated relative to the map.
* Viewport: The circle is translated relative to the viewport.
*
*/
public enum CirclePitchScale {
MAP, VIEWPORT
}
/**
* (Optional) Controls the scaling behavior of the circle when the map is pitched.
*
* {@link CirclePitchScale#MAP}: Circles are scaled according to their apparent distance to the
* camera.
* {@link CirclePitchScale#VIEWPORT}: Circles are not scaled.
*
* Defaults to {@link CirclePitchScale#MAP}.
*
* @return The circle scaling behavior.
*/
public CirclePitchScale getCirclePitchScale() {
Object value = paint.get("circle-pitch-scale");
if (value != null && "viewport".equalsIgnoreCase((String) value)) {
return CirclePitchScale.VIEWPORT;
} else {
return CirclePitchScale.MAP;
}
}
/**
* (Optional) The width of the circle's stroke. Strokes are placed outside of the circle-radius.
*
* Units in pixels. Defaults to 0.
*
* @return The circle stroke width.
* @throws MBFormatException
*
*/
public Number getCircleStrokeWidth() throws MBFormatException {
return parse.optional(Double.class, paint, "circle-stroke-width", 0.0);
}
/**
* Access circle-stroke-width, defaults to 0.
*
* @return The circle stroke width.
* @throws MBFormatException
*/
public Expression circleStrokeWidth() throws MBFormatException {
return parse.percentage(paint, "circle-stroke-width", 0);
}
/**
* (Optional) The stroke color of the circle.
*
* Defaults to #000000.
*
* @return The color of the circle stroke.
* @throws MBFormatException
*
*/
public Color getCircleStrokeColor() throws MBFormatException {
return parse.optional(Color.class, paint, "circle-stroke-color", Color.BLACK);
}
/**
* Access circle-stroke-color as literal or function expression, defaults to black.
*
* @return The color of the circle stroke.
* @throws MBFormatException
*/
public Expression circleStrokeColor() throws MBFormatException {
return parse.color(paint, "circle-stroke-color", Color.BLACK);
}
/**
* (Optional) The opacity of the circle's stroke.
*
* Defaults to 1.
*
* @return Number representing the stroke opacity.
* @throws MBFormatException
*
*/
public Number getCircleStrokeOpacity() throws MBFormatException {
return parse.optional(Double.class, paint, "circle-stroke-opacity", 1.0);
}
/**
* Access circle-stroke-opacity, defaults to 1.
*
* @return Number representing the stroke opacity.
* @throws MBFormatException
*/
public Expression circleStrokeOpacity() throws MBFormatException {
return parse.percentage(paint, "circle-stroke-opacity", 1);
}
/**
* Transform {@link CircleMBLayer} 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) {
// default linecap because StrokeImpl.getOpacity has a bug. If lineCap == null, it returns a default opacity.
Stroke s = sf.stroke(circleStrokeColor(), circleStrokeOpacity(),
circleStrokeWidth(), null, Stroke.DEFAULT.getLineCap(), null, null);
Fill f = sf.fill(null, circleColor(), circleOpacity());
Mark m = sf.mark(ff.literal("circle"), f, s);
Graphic gr = sf.graphic(Arrays.asList(m), null,
ff.multiply(ff.literal(2), circleRadius()), null, null,
circleTranslateDisplacement());
gr.graphicalSymbols().clear();
gr.graphicalSymbols().add(m);
PointSymbolizer ps = sf
.pointSymbolizer(getId(), ff.property((String) null),
sf.description(Text.text("MBStyle " + getId()),
Text.text("Generated for " + getSourceLayer())),
NonSI.PIXEL, gr);
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(ps),
filter.filter());
rules.add(rule);
rule.setLegendGraphic(new Graphic[0]);
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;
}
}