/*
* Copyright (C) 2009 Camptocamp
*
* This file is part of MapFish Server
*
* MapFish Server 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, either version 3 of the License, or
* (at your option) any later version.
*
* MapFish Server 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MapFish Server. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapfish.print.config.layout;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import org.mapfish.print.*;
import org.mapfish.print.config.ColorWrapper;
import org.mapfish.print.scalebar.Direction;
import org.mapfish.print.scalebar.Label;
import org.mapfish.print.scalebar.ScalebarDrawer;
import org.mapfish.print.scalebar.Type;
import org.mapfish.print.utils.DistanceUnit;
import org.mapfish.print.utils.PJsonObject;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* Block for drawing a !scalebar block.
* <p/>
* See http://trac.mapfish.org/trac/mapfish/wiki/PrintModuleServer#Scalebarblock
*/
public class ScalebarBlock extends FontBlock {
private int maxSize = 150;
private Type type = Type.LINE;
private int intervals = 3;
private boolean subIntervals = false;
private DistanceUnit units = null;
private Integer barSize = null;
private Direction barDirection = Direction.UP;
private Direction textDirection = Direction.UP;
private Integer labelDistance = null;
private String color = "black";
/**
* The background color of the odd intervals (only for the scalebar of type "bar")
*/
private String barBgColor = null;
private Double lineWidth = null;
public void render(PJsonObject params, PdfElement target, RenderingContext context) throws DocumentException {
final PJsonObject globalParams = context.getGlobalParams();
final DistanceUnit mapUnits = DistanceUnit.fromString(globalParams.getString("units"));
if (mapUnits == null) {
throw new InvalidJsonValueException(globalParams, "units", globalParams.getString("units"));
}
DistanceUnit scaleUnit = (units != null ? units : mapUnits);
final int scale = context.getLayout().getMainPage().getMap().createTransformer(context, params).getScale();
final double maxWidthIntervaleDistance = DistanceUnit.PT.convertTo(maxSize, scaleUnit) * scale / intervals;
final double intervalDistance = getNearestNiceValue(maxWidthIntervaleDistance, scaleUnit);
final Font pdfFont = getPdfFont();
tryLayout(context, target, pdfFont, scaleUnit, scale, intervalDistance, 0);
}
/**
* Try recursively to find the correct layout.
*/
private void tryLayout(RenderingContext context, PdfElement target, Font pdfFont, DistanceUnit scaleUnit, int scale, double intervalDistance, int tryNumber) throws DocumentException {
if (tryNumber > 3) {
//noinspection ThrowableInstanceNeverThrown
context.addError(new InvalidValueException("maxSize too small", maxSize));
return;
}
DistanceUnit intervalUnit = DistanceUnit.getBestUnit(intervalDistance, scaleUnit);
final float intervalPaperWidth = (float) scaleUnit.convertTo(intervalDistance / scale, DistanceUnit.PT);
//compute the label positions
final List<Label> labels = new ArrayList<Label>(intervals + 1);
final float leftLabelMargin;
final float rightLabelMargin;
final BaseFont baseFont = pdfFont.getCalculatedBaseFont(false);
if (intervals > 1 || subIntervals) {
//the label will be centered under each tick marks
for (int i = 0; i <= intervals; ++i) {
String labelText = createLabelText(scaleUnit, intervalDistance * i, intervalUnit);
if (i == intervals) {
labelText += intervalUnit;
}
labels.add(new Label(intervalPaperWidth * i, labelText, baseFont, getFontSize(),
!barDirection.isSameOrientation(textDirection)));
}
leftLabelMargin = labels.get(0).width / 2.0f;
rightLabelMargin = labels.get(labels.size() - 1).width / 2.0f;
} else {
//if there is only one interval, place the label centered between the two tick marks
final Label label = new Label(intervalPaperWidth / 2.0f,
createLabelText(scaleUnit, intervalDistance, intervalUnit) + intervalUnit, baseFont, getFontSize(), !barDirection.isSameOrientation(textDirection));
labels.add(label);
leftLabelMargin = rightLabelMargin = Math.max(0.0f, label.width - intervalPaperWidth) / 2.0f;
}
if (intervals * intervalPaperWidth + leftLabelMargin + rightLabelMargin <= maxSize) {
//the layout fits the maxSize
doLayout(context, target, pdfFont, labels, intervalPaperWidth, scaleUnit, intervalDistance, intervalUnit,
leftLabelMargin, rightLabelMargin);
} else {
//not enough room because of the labels, try a smaller bar
double nextIntervalDistance = getNearestNiceValue(intervalDistance * 0.9, scaleUnit);
tryLayout(context, target, pdfFont, scaleUnit, scale, nextIntervalDistance, tryNumber + 1);
}
}
/**
* Called when the position of the labels and their content is known.
* <p/>
* Creates the drawer and schedule it for drawing when the position of the block is known.
*/
private void doLayout(RenderingContext context, PdfElement target, Font pdfFont, List<Label> labels,
float intervalWidth, DistanceUnit scaleUnit, double intervalDistance,
DistanceUnit intervalUnit, float leftLabelPaperMargin, float rightLabelPaperMargin) throws DocumentException {
float maxLabelHeight = 0.0f;
float maxLabelWidth = 0.0f;
for (int i = 0; i < labels.size(); i++) {
Label label = labels.get(i);
maxLabelHeight = Math.max(maxLabelHeight, label.height);
maxLabelWidth = Math.max(maxLabelWidth, label.width);
}
final float straightWidth = intervalWidth * intervals + leftLabelPaperMargin + rightLabelPaperMargin;
final float straightHeight = getBarSize() + getLabelDistance() + maxLabelHeight;
final float width;
final float height;
if (barDirection == Direction.DOWN || barDirection == Direction.UP) {
width = straightWidth;
height = straightHeight;
} else {
//noinspection SuspiciousNameCombination
width = straightHeight;
//noinspection SuspiciousNameCombination
height = straightWidth;
}
int numSubIntervals = 1;
if (subIntervals) {
numSubIntervals = getNbSubIntervals(scaleUnit, intervalDistance, intervalUnit);
}
ChunkDrawer drawer = ScalebarDrawer.create(context.getCustomBlocks(), this, type, labels, getBarSize(), getLabelDistance(),
numSubIntervals, intervalWidth, pdfFont, leftLabelPaperMargin, rightLabelPaperMargin, maxLabelWidth, maxLabelHeight);
target.add(PDFUtils.createPlaceholderTable(width, height, spacingAfter, drawer, align, context.getCustomBlocks()));
}
/**
* Format the label text.
*/
private String createLabelText(DistanceUnit scaleUnit, double value, DistanceUnit intervalUnit) {
final double scaledValue = scaleUnit.convertTo(value, intervalUnit);
return Long.toString(Math.round(scaledValue));
}
/**
* Reduce the given value to the nearest 1 significant digit number starting
* with 1, 2 or 5.
*/
private double getNearestNiceValue(double value, DistanceUnit scaleUnit) {
DistanceUnit bestUnit = DistanceUnit.getBestUnit(value, scaleUnit);
double factor = scaleUnit.convertTo(1.0, bestUnit);
// nearest power of 10 lower than value
int digits = (int) (Math.log(value * factor) / Math.log(10));
double pow10 = Math.pow(10, digits);
// ok, find first character
double firstChar = value * factor / pow10;
// right, put it into the correct bracket
int barLen;
if (firstChar >= 10.0) {
barLen = 10;
} else if (firstChar >= 5.0) {
barLen = 5;
} else if (firstChar >= 2.0) {
barLen = 2;
} else {
barLen = 1;
}
// scale it up the correct power of 10
return barLen * pow10 / factor;
}
/**
* @return The "nicest" number of sub intervals in function of the interval distance.
*/
private int getNbSubIntervals(DistanceUnit scaleUnit, double intervalDistance, DistanceUnit intervalUnit) {
double value = scaleUnit.convertTo(intervalDistance, intervalUnit);
int digits = (int) (Math.log(value) / Math.log(10));
double pow10 = Math.pow(10, digits);
// ok, find first character
int firstChar = (int) (value / pow10);
switch (firstChar) {
case 1:
return 2;
case 2:
return 2;
case 5:
return 5;
case 10:
return 2;
default:
throw new RuntimeException("Invalid interval: " + value + intervalUnit + " (" + firstChar + ")");
}
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
if (maxSize <= 0) throw new InvalidValueException("maxSize", maxSize);
}
public void setType(Type type) {
this.type = type;
}
public void setIntervals(int intervals) {
if (intervals < 1) {
throw new InvalidValueException("intervals", intervals);
}
this.intervals = intervals;
}
public void setSubIntervals(boolean subIntervals) {
this.subIntervals = subIntervals;
}
public void setUnits(DistanceUnit units) {
this.units = units;
}
public void setBarSize(int barSize) {
this.barSize = barSize;
if (barSize < 0) throw new InvalidValueException("barSize", barSize);
}
public void setBarDirection(Direction barDirection) {
this.barDirection = barDirection;
}
public void setTextDirection(Direction textDirection) {
this.textDirection = textDirection;
}
public void setLabelDistance(int labelDistance) {
this.labelDistance = labelDistance;
}
public void setBarBgColor(String barBgColor) {
this.barBgColor = barBgColor;
}
public void setColor(String color) {
this.color = color;
}
public void setLineWidth(double lineWidth) {
this.lineWidth = lineWidth;
if (lineWidth < 0) throw new InvalidValueException("lineWidth", lineWidth);
}
public int getBarSize() {
if (barSize != null) {
return barSize;
} else {
return maxSize / 30;
}
}
public int getLabelDistance() {
if (labelDistance != null) {
return labelDistance;
} else {
return maxSize / 40;
}
}
public double getFontSize() {
if (fontSize != null) {
return fontSize;
} else {
return maxSize * 10.0 / 200.0;
}
}
public double getLineWidth() {
if (lineWidth != null) {
return lineWidth;
} else {
return maxSize / 150.0;
}
}
public Direction getBarDirection() {
return barDirection;
}
public Direction getTextDirection() {
return textDirection;
}
public int getIntervals() {
return intervals;
}
public Color getBarBgColorVal() {
return ColorWrapper.convertColor(barBgColor);
}
public Color getColorVal() {
return ColorWrapper.convertColor(color);
}
}