// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.print;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ComponentListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import org.openstreetmap.gui.jmapviewer.tilesources.AbstractOsmTileSource;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.SystemOfMeasurement;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
/**
* The PrintableMapView class implements a "Printable" perspective on
* the main MapView.
* @author Kai Pastor
*/
public class PrintableMapView extends MapView implements Printable {
/**
* A fixed map scale if greater than zero.
*/
protected int fixedMapScale = 0;
/**
* The factor for scaling the printing graphics to the desired resolution
*/
protected double g2dFactor;
/**
* The font size for text added by PrintableMapView
*/
public static final int FONT_SIZE = 8;
/**
* Create a new PrintableMapView.
*/
public PrintableMapView() {
/* Initialize MapView with a dummy parent */
super(new PrintableLayerManager(), null);
/* Disable MapView's ComponentLister,
* as it will interfere with the main MapView. */
ComponentListener[] listeners = getComponentListeners();
for (int i = 0; i < listeners.length; i++) {
removeComponentListener(listeners[i]);
}
}
/**
* Set a fixed map scale 1 : "scale"
*
* @param scale the fixed map scale
*/
public void setFixedMapScale(int scale) {
this.fixedMapScale = scale;
rezoomToFixedScale();
}
/**
* Unset the fixed map scale
*
* The map scaling will be chosen automatically such that the
* main windows map view fits on the page format.
*/
public void unsetFixedMapScale() {
setFixedMapScale(0);
rezoomToFixedScale();
}
/**
* Get the map scale that will be used for rendering
*/
public int getMapScale() {
if (fixedMapScale > 0 || g2dFactor == 0.0) {
return fixedMapScale;
}
double dist100px = getDist100Pixel() / g2dFactor;
int mapScale = (int) (dist100px * 72.0 / 2.54);
return mapScale;
}
/**
* Initialize the PrintableMapView for a particular combination of
* main MapView, PageFormat and target resolution
*
* @param pageformat the size and orientation of the page being drawn
*/
public void initialize(PageFormat pageFormat) {
int resolution = Main.pref.getInteger("print.resolution.dpi", PrintPlugin.DEF_RESOLUTION_DPI);
g2dFactor = 72.0/resolution;
setSize((int) (pageFormat.getImageableWidth()/g2dFactor), (int) (pageFormat.getImageableHeight()/g2dFactor));
}
/**
* Resizes this component.
*/
@Override
public void setSize(int width, int height) {
Dimension dim = getSize();
if (dim.width != width || dim.height != height) {
super.setSize(width, height);
zoomTo(Main.map.mapView.getRealBounds());
rezoomToFixedScale();
}
}
/**
* Resizes this component.
*/
@Override
public void setSize(Dimension newSize) {
Dimension dim = getSize();
if (dim.width != newSize.width || dim.height != newSize.height) {
super.setSize(newSize);
zoomTo(Main.map.mapView.getRealBounds());
rezoomToFixedScale();
}
}
/**
* Adjust the zoom as necessary to establish the fixed scale.
*/
protected void rezoomToFixedScale() {
if (fixedMapScale > 0) {
double dist100px = getDist100Pixel() / g2dFactor;
double mapScale = dist100px * 72.0 / 2.54;
double mapFactor = fixedMapScale / mapScale;
zoomToFactor(mapFactor);
}
}
/**
* Render a page for the printer
*
* Implements java.awt.print.Printable.
*
* @param g the context into which the page is drawn
* @param pageFormat the size and orientation of the page being drawn
* @param page the zero based index of the page to be drawn
*
* @return PAGE_EXISTS for page==0 or NO_SUCH_PAGE for page>0
*
* @throws PrinterException thrown when the print job is terminated
*
*/
@Override
public int print(Graphics g, PageFormat pageFormat, int page) throws
PrinterException {
if (page > 0) { /* stop after first page */
return NO_SUCH_PAGE;
}
initialize(pageFormat);
Graphics2D g2d = (Graphics2D) g;
g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
paintMap(g2d, pageFormat);
paintMapScale(g2d, pageFormat);
paintMapAttribution(g2d, pageFormat);
return PAGE_EXISTS;
}
/**
* Paint the map
*
* This implementation is derived from MapView's paint and
* from other JOSM core components.
*
* @param g2d the graphics context to use for painting
* @param pageFormat the size and orientation of the page being drawn
*/
public void paintMap(Graphics2D g2d, PageFormat pageFormat) {
AffineTransform at = g2d.getTransform();
g2d.scale(g2dFactor, g2dFactor);
Bounds box = getRealBounds();
for (Layer l : getLayerManager().getVisibleLayersInZOrder()) {
if (l.getOpacity() < 1) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) l.getOpacity()));
}
l.paint(g2d, this, box);
g2d.setPaintMode();
}
g2d.setTransform(at);
}
/**
* Paint a linear scale and a lexical scale
*
* This implementation is derived from JOSM's MapScaler,
* NavigatableComponent and SystemOfMeasurement.
*
* @param g2d the graphics context to use for painting
* @param pageFormat the size and orientation of the page being drawn
*/
public void paintMapScale(Graphics2D g2d, PageFormat pageFormat) {
SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
double dist100px = getDist100Pixel() / g2dFactor;
double dist = dist100px / som.aValue;
if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && dist > som.bValue / som.aValue) {
dist = dist100px / som.bValue;
}
long distExponent = (long) Math.floor(Math.log(dist) / Math.log(10));
double distMantissa = dist / Math.pow(10, distExponent);
double distScale;
if (distMantissa <= 2.5) {
distScale = 2.5 / distMantissa;
} else if (distMantissa <= 4.0) {
distScale = 5.0 / distMantissa;
} else {
distScale = 10.0 / distMantissa;
}
Font labelFont = new Font("Arial", Font.PLAIN, FONT_SIZE);
g2d.setFont(labelFont);
/* length of scale */
int x = (int) (100.0 * distScale);
/* offset from the left paper border to the left end of the bar */
Rectangle2D bound = g2d.getFontMetrics().getStringBounds("0", g2d);
int xLeft = (int) (bound.getWidth()/2);
/* offset from the left paper border to the right label */
String rightLabel = som.getDistText(dist100px * distScale);
bound = g2d.getFontMetrics().getStringBounds(rightLabel, g2d);
int xRight = xLeft+(int) Math.max(0.95*x, x-bound.getWidth()/2);
// CHECKSTYLE.OFF: SingleSpaceSeparator
int h = FONT_SIZE / 2; // raster, height of the bar
int yLexical = 3 * h; // baseline of the lexical scale
int yBar = 4 * h; // top of the bar
int yLabel = 8 * h; // baseline of the labels
int w = (int) (distScale * 100.0); // length of the bar
int ws = (int) (distScale * 20.0); // length of a segment
// CHECKSTYLE.ON: SingleSpaceSeparator
/* white background */
g2d.setColor(Color.WHITE);
g2d.fillRect(xLeft-1, yBar-1, w+2, h+2);
/* black foreground */
g2d.setColor(Color.BLACK);
g2d.drawRect(xLeft, yBar, w, h);
g2d.fillRect(xLeft, yBar, ws, h);
g2d.fillRect(xLeft+(int) (distScale * 40.0), yBar, ws, h);
g2d.fillRect(xLeft+w-ws, yBar, ws, h);
g2d.setFont(labelFont);
paintText(g2d, "0", 0, yLabel);
paintText(g2d, rightLabel, xRight, yLabel);
/* lexical scale */
int mapScale = getMapScale();
String lexicalScale = tr("Scale") + " 1 : " + mapScale;
Font scaleFront = new Font("Arial", Font.BOLD, FONT_SIZE);
g2d.setFont(scaleFront);
bound = g2d.getFontMetrics().getStringBounds(lexicalScale, g2d);
int xLexical = Math.max(0, xLeft + (w - (int) bound.getWidth()) / 2);
paintText(g2d, lexicalScale, xLexical, yLexical);
}
/**
* Paint an attribution text
*
* @param g2d the graphics context to use for painting
* @param pageFormat the size and orientation of the page being drawn
*/
public void paintMapAttribution(Graphics2D g2d, PageFormat pageFormat) {
String text = Main.pref.get("print.attribution", AbstractOsmTileSource.DEFAULT_OSM_ATTRIBUTION);
if (text == null) {
return;
}
Font attributionFont = new Font("Arial", Font.PLAIN, FONT_SIZE * 8 / 10);
g2d.setFont(attributionFont);
text += "\n";
int y = FONT_SIZE * 3 / 2;
int from = 0;
int to = text.indexOf('\n', from);
while (to >= from) {
String line = text.substring(from, to);
Rectangle2D bound = g2d.getFontMetrics().getStringBounds(line, g2d);
int x = (int) ((pageFormat.getImageableWidth() - bound.getWidth()) - FONT_SIZE/2);
paintText(g2d, line, x, y);
y += FONT_SIZE * 5 / 4;
from = to + 1;
to = text.indexOf('\n', from);
}
}
/**
* Paint a text.
*
* This method will not only draw the letters but also a background which improves redability.
*
* @param g2d the graphics context to use for painting
* @param text the text to be drawn
* @param x the x coordinate
* @param y the y coordinate
*/
public void paintText(Graphics2D g2d, String text, int x, int y) {
AffineTransform ax = g2d.getTransform();
g2d.translate(x, y);
FontRenderContext frc = g2d.getFontRenderContext();
GlyphVector gv = g2d.getFont().createGlyphVector(frc, text);
Shape textOutline = gv.getOutline();
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
g2d.setColor(Color.WHITE);
g2d.draw(textOutline);
g2d.setStroke(new BasicStroke());
g2d.setColor(Color.BLACK);
g2d.drawString(text, 0, 0);
g2d.setTransform(ax);
}
@Override
public void layerAdded(LayerAddEvent e) {
// Don't mess with global stuff done by MapView
}
@Override
public void layerRemoving(LayerRemoveEvent e) {
// Don't mess with global stuff done by MapView
}
@Override
public void layerOrderChanged(LayerOrderChangeEvent e) {
// Don't mess with global stuff done by MapView
}
}