/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008 - 2009, Geomatys
*
* 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.geotoolkit.display2d.ext.scalebar;
import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.FieldPosition;
import java.util.Arrays;
import javax.measure.IncommensurableException;
import javax.measure.UnitConverter;
import javax.measure.Unit;
import org.geotoolkit.display.PortrayalException;
import org.geotoolkit.display2d.ext.BackgroundTemplate;
import org.geotoolkit.display2d.ext.BackgroundUtilities;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.Units;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.datum.DefaultEllipsoid;
import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
/**
* Utility class to render scalebars using a provided template and geographic information.
*
* @author Martin Desruisseaux
* @author Johann Sorel (Geomatys)
* @module
*/
public class J2DScaleBarUtilities {
/**
* Round numbers for map scale, between 1 and 10. The map scale length in "real world"
* units will be rounded to one of those numbers at rendering time.
*/
private static final double[] SNAP = {1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0};
private J2DScaleBarUtilities(){}
/**
* Paint a scalebar using Java2D.
*
* @param objectiveCRS : objective CRS of the map
* @param displayCRS : display CRS of the map
* @param geoPosition : Geographic position in objective CRS where to calculate the scale
* @param scaleUnit : scalebar unit
* @param g2d : Graphics2D used to render
* @param bounds : Rectangle where the scale must be painted
* @param template : scalebar template
* @return length in given unit of the scale bar
* @throws org.geotoolkit.display.exception.PortrayalException
*/
public static double paint(CoordinateReferenceSystem objectiveCRS,
final CoordinateReferenceSystem displayCRS,
final Point2D geoPosition,
final Graphics2D g2d,
final int x, final int y,
final ScaleBarTemplate template) throws PortrayalException{
try {
objectiveCRS = CRSUtilities.getCRS2D(objectiveCRS);
} catch (TransformException ex) {
throw new PortrayalException(ex);
}
final Dimension estimation = estimate(g2d, template, false);
final Dimension bounds = template.getSize();
int X = x;
int Y = y;
final BackgroundTemplate background = template.getBackground();
if(background != null){
final Rectangle area = new Rectangle(estimation);
area.x = x;
area.y = y;
Insets insets = background.getBackgroundInsets();
area.width += insets.left + insets.right;
area.height += insets.top + insets.bottom;
X += insets.left;
Y += insets.top;
BackgroundUtilities.paint(g2d, area, background);
}
final Unit scaleUnit = template.getUnit();
////////////////////////////////////////////////////////////////////////////
////// ////
////// BLOCK 1 - Compute the map scale length. No painting occurs ////
////// here. No Graphics2D modification for now. ////
////// ////
////////////////////////////////////////////////////////////////////////////
/*
* Gets an estimation of the map scale in linear units (usually kilometers). First,
* we get an estimation of the map scale position in screen coordinates. We use a
* coordinate system local to the legend in which the upper-left corner of the map
* scale is located at (0,0). Ticks labels and scale title locations will be relative
* to the map scale.
*/
Point2D P1, P2;
CoordinateReferenceSystem mapCRS = objectiveCRS;
if (template.calculateUsingGeodesic() && (mapCRS instanceof ProjectedCRS)) {
mapCRS = ((ProjectedCRS) mapCRS).getBaseCRS();
}
final MathTransform2D displayToObjective;
final MathTransform2D objectiveToDisplay;
try{
final MathTransform transform = CRS.findOperation(displayCRS, objectiveCRS, null).getMathTransform();
final MathTransform inverse = transform.inverse();
if(!(transform instanceof MathTransform2D) || !(inverse instanceof MathTransform2D)){
throw new PortrayalException("MathTransform is not 2D.");
}
displayToObjective = (MathTransform2D) transform;
objectiveToDisplay = (MathTransform2D) inverse;
//move points to the provided objective position
final Point2D displayGeo = objectiveToDisplay.transform(geoPosition, null);
P1 = new Point2D.Double(displayGeo.getX(), displayGeo.getY());
P2 = new Point2D.Double(displayGeo.getX()+bounds.getWidth(), displayGeo.getY());
P1 = displayToObjective.transform(P1, P1);
P2 = displayToObjective.transform(P2, P2);
}catch(FactoryException ex){
throw new PortrayalException(ex);
}catch(TransformException ex){
throw new PortrayalException(ex);
}
/*
* Convert the position from pixels to "real world" coordinates. Then, measures its length
* using orthodromic distance computation if the rendering units were angular units. Then,
* "snap" the length to some number easier to read. For example the length 2371 will be
* snapped to 2500. Finally, the new "snapped" length will be converted bach to pixel units.
*/
final Unit<?> mapUnitX = mapCRS.getCoordinateSystem().getAxis(0).getUnit();
final Unit<?> mapUnitY = mapCRS.getCoordinateSystem().getAxis(1).getUnit();
if (mapUnitX == null || mapUnitY == null) {
throw new NullPointerException("no unit for one axi.");
}
double logicalLength;
final Ellipsoid ellipsoid = ReferencingUtilities.getEllipsoidOfGeographicCRS(mapCRS);
try {
if (ellipsoid != null && ellipsoid instanceof DefaultEllipsoid) {
final UnitConverter xConverter = mapUnitX.getConverterToAny(Units.DEGREE);
final UnitConverter yConverter = mapUnitY.getConverterToAny(Units.DEGREE);
P1.setLocation(xConverter.convert(P1.getX()), yConverter.convert(P1.getY()));
P2.setLocation(xConverter.convert(P2.getX()), yConverter.convert(P2.getY()));
logicalLength = ((DefaultEllipsoid)ellipsoid).orthodromicDistance(P1.getX(), P1.getY(), P2.getX(), P2.getY());
logicalLength = ellipsoid.getAxisUnit().getConverterToAny(scaleUnit).convert(logicalLength);
} else {
final UnitConverter xConverter = mapUnitX.getConverterToAny(scaleUnit);
final UnitConverter yConverter = mapUnitY.getConverterToAny(scaleUnit);
P1.setLocation(xConverter.convert(P1.getX()), yConverter.convert(P1.getY()));
P2.setLocation(xConverter.convert(P2.getX()), yConverter.convert(P2.getY()));
logicalLength = P1.distance(P2);
}
} catch (IncommensurableException exception) {
// Should not occurs, unless the user is using a very particular coordinate system.
throw new PortrayalException(exception);
}
final double maximumLength = bounds.getWidth();
final double scaleFactor = logicalLength / maximumLength;
logicalLength /= template.getDivisionCount()+0.5f;
// If the current logical length is between two values in the SNAP array, then select
// the lowest value. It produces a more compact scale than selecting the highest value.
final double factor = MathFunctions.pow10((int) Math.floor(Math.log10(logicalLength)));
logicalLength /= factor;
int index = Arrays.binarySearch(SNAP, logicalLength);
if (index < 0) {
index = ~index; // Highest value (really ~, not -)
if (index > 0) {
index--; // Choose lowest value instead, if such a value exists.
}
}
logicalLength = SNAP[index];
logicalLength *= factor;
final int visualLength = (int) Math.ceil(logicalLength / scaleFactor);
////////////////////////////////////////////////////////////////////////////
////// ////
////// BLOCK 2 - Compute the content. No painting occurs here. ////
////// No Graphics2D modification, except through ////
////// RenderingContext.setCoordinateSystem(...). ////
////// ////
////////////////////////////////////////////////////////////////////////////
final float thickness = template.getThickness();
final GlyphVector[] tickGlyphs = new GlyphVector[template.getDivisionCount() + 2];
final Rectangle2D[] tickBounds = new Rectangle2D[template.getDivisionCount() + 2];
final FontRenderContext fontContext = g2d.getFontRenderContext();
final Font font = template.getFont();
final StringBuffer buffer = new StringBuffer(16);
final FieldPosition pos = new FieldPosition(0);
for (int i=0,n=template.getDivisionCount(); i<=n; i++) {
String text = template.getNumberFormat().format(logicalLength * i, buffer, pos).toString();
GlyphVector glyphs = font.createGlyphVector(fontContext, text);
Rectangle2D rect = glyphs.getVisualBounds();
rect.setRect(0, thickness + 5, rect.getWidth(), rect.getHeight());
if (i == n) {
buffer.append(' ');
buffer.append(scaleUnit);
text = buffer.toString();
glyphs = font.createGlyphVector(fontContext, text);
}
tickBounds[i] = rect;
tickGlyphs[i] = glyphs;
buffer.setLength(0);
}
////////////////////////////////////////////////////////////////////////////
////// ////
////// BLOCK 3 - Paint the content. ////
////// ////
////////////////////////////////////////////////////////////////////////////
g2d.translate(X, Y);
g2d.setStroke(new BasicStroke(1));
final Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, visualLength, thickness);
//apply an offset of half the width of the first text
rect.x = tickBounds[0].getWidth()/2.0;
for (int i=0,n=template.getDivisionCount(); i<n; i++) {
final Paint fill = ((i & 1) != 0) ? template.getFirstRectanglePaint() :
template.getSecondRectanglePaint() ;
g2d.setPaint(fill);
g2d.fill(rect);
g2d.setPaint(template.getForeground());
g2d.draw(rect);
rect.x += visualLength;
}
/*
* Writes tick labels, units and map scale legend.
*/
double tickX = tickBounds[0].getWidth()/2.0;
g2d.setPaint(template.getForeground());
for (int i = 0; i < tickGlyphs.length; i++) {
if (tickGlyphs[i] != null) {
final Rectangle2D tick = tickBounds[i];
g2d.drawGlyphVector(tickGlyphs[i], (float)(tickX - (tick.getWidth()/2.0)),(float) tick.getMaxY());
}
tickX += visualLength;
}
g2d.translate(-X, -Y);
return logicalLength * template.getDivisionCount();
}
public static Dimension estimate(final Graphics2D g, final ScaleBarTemplate template, final boolean considerBackground){
final Dimension dim = new Dimension(0, 0);
dim.width = template.getSize().width;
dim.height = template.getSize().height;
if(considerBackground && template.getBackground() != null){
final Insets insets = template.getBackground().getBackgroundInsets();
dim.width += insets.left + insets.right;
dim.height += insets.bottom + insets.top;
}
return dim;
}
}