/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2016, 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.styling.visitor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.measure.quantity.Length;
import javax.measure.unit.NonSI;
import javax.measure.unit.Unit;
import org.geotools.styling.Displacement;
import org.geotools.styling.Fill;
import org.geotools.styling.Font;
import org.geotools.styling.Graphic;
import org.geotools.styling.LabelPlacement;
import org.geotools.styling.LinePlacement;
import org.geotools.styling.LineSymbolizer;
import org.geotools.styling.Mark;
import org.geotools.styling.PointPlacement;
import org.geotools.styling.PointSymbolizer;
import org.geotools.styling.PolygonSymbolizer;
import org.geotools.styling.Stroke;
import org.geotools.styling.TextSymbolizer;
import org.geotools.styling.TextSymbolizer2;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.style.GraphicalSymbol;
/**
* Visitor used for rescaling a Style given a map scale (e.g., meters per pixel) and taking into
* consideration the Unit of Measure (UOM, e.g., SI.METER, NonSI.FOOT) of each symbolizer. The
* resulting Style's Symbolizer sizes will all be given in PIXELS, so that they can be directly used
* by a renderer that is unaware of units of measure or the current map scale. For example, points
* with size == 100 meters could be rescaled to 10 pixels for higher levels of zoom and 2 pixels for
* a lower level of zoom.
* <p>
* This visitor extends {@link DuplicatingStyleVisitor} and as such yields a copy of the original
* Style. Usage is simply to call the desired visit() method and then call getCopy() to retrieve the
* result.
*
* @author milton
* @author Andrea Aime - GeoSolutions
*
*
* @source $URL$
*/
public class UomRescaleStyleVisitor extends DuplicatingStyleVisitor {
double mapScale;
/**
* Constructor: requires the current mapScale to inform the window to viewport (world to screen)
* relation in order to correctly rescaleDashArray sizes according to units of measure given in world
* units (e.g., SI.METER, NonSI.FOOT, etc).
*
* @param mapScale The specified map scale, given in pixels per meter.
*/
public UomRescaleStyleVisitor(double mapScale) {
if (mapScale <= 0)
throw new IllegalArgumentException("The mapScale is out of range. Value is "
+ Double.toString(mapScale) + ". It must be positive.");
this.mapScale = mapScale;
}
/**
* Used to rescaleDashArray the provided unscaled value.
*
* @param unscaled the unscaled value.
* @param mapScale the mapScale in pixels per meter.
* @param uom the unit of measure that will be used to scale.
* @return the expression multiplied by the provided scale.
*/
protected Expression rescale(Expression unscaled, Unit<Length> uom) {
if (unscaled == null || unscaled.equals(Expression.NIL)) {
return unscaled;
}
Measure m = new Measure(unscaled, uom);
return RescalingMode.RealWorld.rescaleToExpression(ff.literal(mapScale), m);
}
/**
* Rescale a list of expressions, can handle null.
*/
protected List<Expression> rescaleDashArray(List<Expression> expressions, Unit<Length> uom) {
if (expressions == null || expressions.isEmpty()) {
return expressions;
}
List<Expression> rescaled = new ArrayList<>(expressions.size());
final Expression scale = rescale(ff.literal(1), uom);
for (Expression expression : expressions) {
Expression rescale = ff.function("listMultiply", scale, expression);
if (expression instanceof Literal) {
rescaled.add(ff.literal(rescale.evaluate(null)));
} else {
rescaled.add(rescale);
}
}
return rescaled;
}
/**
* Used to rescaleDashArray the provided unscaled value.
*
* @param unscaled the unscaled value.
* @param mapScale the mapScale in pixels per meter.
* @param uom the unit of measure that will be used to scale.
* @return the expression multiplied by the provided scale.
*/
protected String rescale(String unscaled, Unit<Length> uom) {
if (unscaled == null) {
return unscaled;
}
Measure v = new Measure(unscaled, uom);
return RescalingMode.RealWorld.rescaleToString(mapScale, v);
}
/**
* Used to rescaleDashArray the provided stroke.
*
* @param stroke the unscaled stroke, which will be modified in-place.
* @param mapScale the mapScale in pixels per meter.
* @param uom the unit of measure that will be used to scale.
*/
protected void rescaleStroke(Stroke stroke, Unit<Length> uom) {
if (stroke != null) {
stroke.setWidth(rescale(stroke.getWidth(), uom));
stroke.setDashArray(rescaleDashArray(stroke.dashArray(), uom));
stroke.setDashOffset(rescale(stroke.getDashOffset(), uom));
rescale(stroke.getGraphicFill(), uom);
rescale(stroke.getGraphicStroke(), uom);
}
}
@Override
public void visit(PointSymbolizer ps) {
super.visit(ps);
PointSymbolizer copy = (PointSymbolizer) pages.peek();
Unit<Length> uom = copy.getUnitOfMeasure();
Graphic copyGraphic = copy.getGraphic();
rescale(copyGraphic, uom);
copy.setUnitOfMeasure(NonSI.PIXEL);
}
private void rescale(Graphic graphic, Unit<Length> unit) {
if (graphic != null) {
graphic.setSize(rescale(graphic.getSize(), unit));
graphic.setGap(rescale(graphic.getGap(), unit));
Displacement disp = graphic.getDisplacement();
if (disp != null) {
disp.setDisplacementX(rescale(disp.getDisplacementX(), unit));
disp.setDisplacementY(rescale(disp.getDisplacementY(), unit));
graphic.setDisplacement(disp);
}
if (graphic.graphicalSymbols() != null) {
for (GraphicalSymbol gs : graphic.graphicalSymbols()) {
if (gs instanceof Mark) {
Mark mark = (Mark) gs;
rescaleStroke(mark.getStroke(), unit);
rescaleFill(mark.getFill(), unit);
}
}
}
}
}
@Override
public void visit(LineSymbolizer line) {
super.visit(line);
LineSymbolizer copy = (LineSymbolizer) pages.peek();
Unit<Length> uom = copy.getUnitOfMeasure();
Stroke copyStroke = copy.getStroke();
rescaleStroke(copyStroke, uom);
copy.setPerpendicularOffset(rescale(copy.getPerpendicularOffset(), uom));
copy.setUnitOfMeasure(NonSI.PIXEL);
}
@Override
public void visit(PolygonSymbolizer poly) {
super.visit(poly);
PolygonSymbolizer copy = (PolygonSymbolizer) pages.peek();
Unit<Length> uom = copy.getUnitOfMeasure();
rescaleStroke(copy.getStroke(), uom);
rescaleFill(copy.getFill(), uom);
scaleIntArrayOption(copy.getOptions(), PolygonSymbolizer.GRAPHIC_MARGIN_KEY, uom);
copy.setUnitOfMeasure(NonSI.PIXEL);
}
private void rescaleFill(Fill copyFill, Unit<Length> unit) {
// rescale the graphic fill, if any
if (copyFill != null) {
rescale(copyFill.getGraphicFill(), unit);
}
}
@SuppressWarnings("deprecation")
@Override
public void visit(TextSymbolizer text) {
super.visit(text);
TextSymbolizer copy = (TextSymbolizer) pages.peek();
Unit<Length> uom = copy.getUnitOfMeasure();
// rescales fonts
for (Font font : copy.fonts()) {
font.setSize(rescale(font.getSize(), uom));
}
// rescales label placement
LabelPlacement placement = copy.getLabelPlacement();
if (placement instanceof PointPlacement) {
// rescales point label placement
PointPlacement pointPlacement = (PointPlacement) placement;
Displacement disp = pointPlacement.getDisplacement();
if (disp != null) {
disp.setDisplacementX(rescale(disp.getDisplacementX(), uom));
disp.setDisplacementY(rescale(disp.getDisplacementY(), uom));
pointPlacement.setDisplacement(disp);
}
} else if (placement instanceof LinePlacement) {
// rescales line label placement
LinePlacement linePlacement = (LinePlacement) placement;
linePlacement.setGap(rescale(linePlacement.getGap(), uom));
linePlacement.setInitialGap(rescale(linePlacement.getInitialGap(), uom));
linePlacement.setPerpendicularOffset(rescale(linePlacement.getPerpendicularOffset(),
uom));
}
copy.setLabelPlacement(placement);
// rescale the halo
if (copy.getHalo() != null) {
copy.getHalo().setRadius(rescale(copy.getHalo().getRadius(), uom));
}
if (copy instanceof TextSymbolizer2) {
TextSymbolizer2 copy2 = (TextSymbolizer2) copy;
rescale(copy2.getGraphic(), uom);
}
// scale various options as well
Map<String, String> options = copy.getOptions();
scaleIntOption(options, TextSymbolizer.MAX_DISPLACEMENT_KEY, uom);
scaleIntOption(options, TextSymbolizer.SPACE_AROUND_KEY, uom);
scaleIntOption(options, TextSymbolizer.MIN_GROUP_DISTANCE_KEY, uom);
scaleIntOption(options, TextSymbolizer.LABEL_REPEAT_KEY, uom);
scaleIntOption(options, TextSymbolizer.AUTO_WRAP_KEY, uom);
scaleIntArrayOption(options, TextSymbolizer.GRAPHIC_MARGIN_KEY, uom);
copy.setUnitOfMeasure(NonSI.PIXEL);
}
private void scaleIntOption(Map<String, String> options, String optionName, Unit<Length> uom) {
if (options.containsKey(optionName)) {
String rescaled = rescale(options.get(optionName), uom);
options.put(optionName, toInt(rescaled));
}
}
private void scaleIntArrayOption(Map<String, String> options, String optionName,
Unit<Length> uom) {
if (options.containsKey(optionName)) {
String strValue = options.get(optionName);
String[] splitted = strValue.split("\\s+");
StringBuilder sb = new StringBuilder();
for (String value : splitted) {
String rescaled = rescale(value, uom);
sb.append(toInt(rescaled)).append(" ");
}
sb.setLength(sb.length() - 1);
options.put(optionName, sb.toString());
}
}
String toInt(String value) {
Double dv = Double.valueOf(value);
return String.valueOf(dv.intValue());
}
}