package com.revolsys.swing.map.border; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import javax.measure.Measure; import javax.measure.quantity.Angle; import javax.measure.quantity.Length; import javax.measure.quantity.Quantity; import javax.measure.unit.BaseUnit; import javax.measure.unit.NonSI; import javax.measure.unit.SI; import javax.measure.unit.Unit; import javax.swing.border.AbstractBorder; import com.revolsys.geometry.cs.CoordinateSystem; import com.revolsys.geometry.cs.GeographicCoordinateSystem; import com.revolsys.geometry.cs.ProjectedCoordinateSystem; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.segment.LineSegment; import com.revolsys.geometry.model.segment.LineSegmentDoubleGF; import com.revolsys.swing.map.Viewport2D; import com.revolsys.util.Property; public class MapRulerBorder extends AbstractBorder implements PropertyChangeListener { private static final List<Unit<Length>> IMPERIAL_FOOT_STEPS = newSteps(NonSI.FOOT.times(1000000), NonSI.FOOT.times(100000), NonSI.FOOT.times(10000), NonSI.FOOT.times(1000), NonSI.FOOT.times(100), NonSI.FOOT.times(10), NonSI.FOOT); private static final List<Unit<Length>> IMPERIAL_MILE_STEPS = newSteps(NonSI.MILE.times(1000), NonSI.MILE.times(100), NonSI.MILE.times(10), NonSI.MILE, NonSI.MILE.divide(10), NonSI.MILE.divide(100)); private static final List<Unit<Length>> IMPERIAL_PROJECTED_STEPS = newSteps( NonSI.MILE.times(1000), NonSI.MILE.times(100), NonSI.MILE.times(10), NonSI.MILE, NonSI.MILE.divide(16), NonSI.MILE.divide(32), NonSI.FOOT, NonSI.INCH); private static final List<Unit<Angle>> METRIC_GEOGRAPHICS_STEPS = newSteps(NonSI.DEGREE_ANGLE, 30, 10, 1, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15); private static final List<Unit<Length>> METRIC_PROJECTED_STEPS = newSteps(SI.METRE, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15); /** * */ private static final long serialVersionUID = -3070841484052913548L; public static <U extends Quantity> List<Unit<U>> newSteps(final Unit<U>... steps) { final List<Unit<U>> stepList = new ArrayList<>(); for (final Unit<U> step : steps) { stepList.add(step); } return stepList; } /** * Construct a new list of steps in measurable units from the double array. * * @param <U> The type of unit (e.g. {@link Angle} or {@link Length}). * @param unit The unit of measure. * @param steps The list of steps. * @return The list of step measures. */ public static <U extends Quantity> List<Unit<U>> newSteps(final Unit<U> unit, final double... steps) { final List<Unit<U>> stepList = new ArrayList<>(); for (final double step : steps) { if (step == 1) { stepList.add(unit); } else { stepList.add(unit.times(step)); } } return stepList; } private double areaMaxX; private double areaMaxY; private double areaMinX; private double areaMinY; @SuppressWarnings("rawtypes") private Unit baseUnit; private int labelHeight; private CoordinateSystem rulerCoordinateSystem; private GeometryFactory rulerGeometryFactory; private final int rulerSize = 25; private final Viewport2D viewport; public MapRulerBorder(final Viewport2D viewport) { this.viewport = viewport; final GeometryFactory geometryFactory = viewport.getGeometryFactory(); setRulerGeometryFactory(geometryFactory); Property.addListener(viewport, "geometryFactory", this); } private <Q extends Quantity> void drawLabel(final Graphics2D graphics, final int textX, final int textY, final Unit<Q> displayUnit, final double displayValue, final Unit<Q> scaleUnit) { DecimalFormat format; if (displayValue - Math.floor(displayValue) == 0) { format = new DecimalFormat("#,###,###,###"); } else { final StringBuilder formatString = new StringBuilder("#,###,###,###."); final double stepSize = Measure.valueOf(1, scaleUnit).doubleValue(displayUnit); final int numZeros = (int)Math.abs(Math.round(Math.log10(stepSize % 1.0))); for (int j = 0; j < numZeros; j++) { formatString.append("0"); } format = new DecimalFormat(formatString.toString()); } final String label = String.valueOf(format.format(displayValue) + displayUnit); graphics.setColor(Color.BLACK); graphics.drawString(label, textX, textY); } /** * Returns the insets of the border. * @param c the component for which this border insets value applies */ @Override public Insets getBorderInsets(final Component c) { return new Insets(this.rulerSize, this.rulerSize, this.rulerSize, this.rulerSize); } /** * Reinitialize the insets parameter with this Border's current Insets. * @param c the component for which this border insets value applies * @param insets the object to be reinitialized */ @Override public Insets getBorderInsets(final Component c, final Insets insets) { insets.left = this.rulerSize; insets.top = this.rulerSize; insets.right = this.rulerSize; insets.bottom = this.rulerSize; return insets; } public GeometryFactory getRulerGeometryFactory() { return this.rulerGeometryFactory; } private <Q extends Quantity> int getStepLevel(final List<Unit<Q>> steps, final Measure<Q> modelUnitsPer10ViewUnits) { for (int i = 0; i < steps.size(); i++) { final Unit<Q> stepUnit = steps.get(i); final Measure<Q> step = Measure.valueOf(1, stepUnit); final int compare = modelUnitsPer10ViewUnits.compareTo(step); if (compare > 0) { if (i == 0) { return 0; } else { return i - 1; } } } return steps.size() - 1; } private void paintBackground(final Graphics2D g, final int x, final int y, final int width, final int height) { g.setColor(Color.WHITE); g.fillRect(x, y, this.rulerSize - 1, height); // left g.fillRect(x + width - this.rulerSize + 1, y, this.rulerSize - 1, height - 1); // right g.fillRect(x + this.rulerSize - 1, y, width - 2 * this.rulerSize + 2, this.rulerSize - 1); // top g.fillRect(x + this.rulerSize - 1, y + height - this.rulerSize + 1, width - 2 * this.rulerSize + 2, this.rulerSize - 1); // bottom } @Override public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) { final Graphics2D graphics = (Graphics2D)g; graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10)); final FontMetrics fontMetrics = graphics.getFontMetrics(); this.labelHeight = fontMetrics.getHeight(); paintBackground(graphics, x, y, width, height); final BoundingBox boundingBox = this.viewport.getBoundingBox(); if (this.rulerCoordinateSystem instanceof GeographicCoordinateSystem) { final Unit<Angle> displayUnit = NonSI.DEGREE_ANGLE; paintRuler(graphics, boundingBox, displayUnit, METRIC_GEOGRAPHICS_STEPS, true, x, y, width, height); } else if (this.rulerCoordinateSystem instanceof ProjectedCoordinateSystem) { if (this.baseUnit.equals(NonSI.FOOT)) { final Unit<Length> displayUnit = NonSI.FOOT; paintRuler(graphics, boundingBox, displayUnit, IMPERIAL_FOOT_STEPS, true, x, y, width, height); } else { final BaseUnit<Length> displayUnit = SI.METRE; paintRuler(graphics, boundingBox, displayUnit, METRIC_PROJECTED_STEPS, true, x, y, width, height); } } graphics.setColor(Color.BLACK); graphics.drawRect(this.rulerSize - 1, this.rulerSize - 1, width - 2 * this.rulerSize + 1, height - 2 * this.rulerSize + 1); } private <Q extends Quantity> void paintHorizontalRuler(final Graphics2D g, final BoundingBox boundingBox, final Unit<Q> displayUnit, final List<Unit<Q>> steps, final int x, final int y, final int width, final int height, final boolean top) { final AffineTransform transform = g.getTransform(); final Shape clip = g.getClip(); try { int textY; LineSegment line; final double x1 = boundingBox.getMinX(); final double x2 = boundingBox.getMaxX(); double y0; if (top) { g.translate(this.rulerSize, 0); textY = this.labelHeight; y0 = boundingBox.getMaxY(); } else { g.translate(this.rulerSize, height - this.rulerSize); textY = this.rulerSize - 3; y0 = boundingBox.getMinY(); } line = new LineSegmentDoubleGF(boundingBox.getGeometryFactory(), 2, x1, y0, x2, y0); line = line.convertGeometry(this.rulerGeometryFactory); g.setClip(0, 0, width - 2 * this.rulerSize, this.rulerSize); final double mapSize = boundingBox.getWidth(); final double viewSize = this.viewport.getViewWidthPixels(); final double minX = line.getX(0); double maxX = line.getX(1); if (maxX > this.areaMaxX) { maxX = this.areaMaxX; } if (mapSize > 0) { final Unit<Q> screenToModelUnit = this.viewport.getViewToModelUnit(this.baseUnit); final Measure<Q> modelUnitsPer6ViewUnits = Measure.valueOf(6, screenToModelUnit); final int stepLevel = getStepLevel(steps, modelUnitsPer6ViewUnits); final Unit<Q> stepUnit = steps.get(stepLevel); final double step = toBaseUnit(Measure.valueOf(1, stepUnit)); final double pixelsPerUnit = viewSize / mapSize; final long minIndex = (long)Math.floor(this.areaMinX / step); final long maxIndex = (long)Math.floor(maxX / step); long startIndex = (long)Math.floor(minX / step); if (startIndex < minIndex) { startIndex = minIndex; } for (long index = startIndex; index < maxIndex; index++) { final Measure<Q> measureValue = Measure.valueOf(index, stepUnit); final double value = toBaseUnit(measureValue); final double displayValue = measureValue.doubleValue(displayUnit); final int pixel = (int)((value - minX) * pixelsPerUnit); boolean found = false; int barSize = 4; g.setColor(Color.LIGHT_GRAY); for (int i = 0; !found && i < stepLevel; i++) { final Unit<Q> scaleUnit = steps.get(i); final double stepValue = measureValue.doubleValue(scaleUnit); if (Math.abs(stepValue - Math.round(stepValue)) < 0.000001) { barSize = 4 + (int)((this.rulerSize - 4) * (((double)stepLevel - i) / stepLevel)); found = true; drawLabel(g, pixel + 3, textY, displayUnit, displayValue, scaleUnit); } } if (top) { g.drawLine(pixel, this.rulerSize - 1 - barSize, pixel, this.rulerSize - 1); } else { g.drawLine(pixel, 0, pixel, barSize); } } } } finally { g.setTransform(transform); g.setClip(clip); } } private <Q extends Quantity> void paintRuler(final Graphics2D g, final BoundingBox boundingBox, final Unit<Q> displayUnit, final List<Unit<Q>> steps, final boolean horizontal, final int x, final int y, final int width, final int height) { paintHorizontalRuler(g, boundingBox, displayUnit, steps, x, y, width, height, true); paintHorizontalRuler(g, boundingBox, displayUnit, steps, x, y, width, height, false); paintVerticalRuler(g, boundingBox, displayUnit, steps, x, y, width, height, true); paintVerticalRuler(g, boundingBox, displayUnit, steps, x, y, width, height, false); } private <Q extends Quantity> void paintVerticalRuler(final Graphics2D g, final BoundingBox boundingBox, final Unit<Q> displayUnit, final List<Unit<Q>> steps, final int x, final int y, final int width, final int height, final boolean left) { final AffineTransform transform = g.getTransform(); final Shape clip = g.getClip(); try { int textX; LineSegment line; final double y1 = boundingBox.getMinY(); final double y2 = boundingBox.getMaxY(); double x0; if (left) { g.translate(0, -this.rulerSize); textX = this.labelHeight; x0 = boundingBox.getMinX(); } else { g.translate(width - this.rulerSize, -this.rulerSize); textX = this.rulerSize - 3; x0 = boundingBox.getMaxX(); } line = new LineSegmentDoubleGF(boundingBox.getGeometryFactory(), 2, x0, y1, x0, y2); line = line.convertGeometry(this.rulerGeometryFactory); g.setClip(0, this.rulerSize * 2, this.rulerSize, height - 2 * this.rulerSize); final double mapSize = boundingBox.getHeight(); final double viewSize = this.viewport.getViewHeightPixels(); final double minY = line.getY(0); double maxY = line.getY(1); if (maxY > this.areaMaxY) { maxY = this.areaMaxY; } if (mapSize > 0) { final Unit<Q> screenToModelUnit = this.viewport.getViewToModelUnit(this.baseUnit); final Measure<Q> modelUnitsPer6ViewUnits = Measure.valueOf(6, screenToModelUnit); final int stepLevel = getStepLevel(steps, modelUnitsPer6ViewUnits); final Unit<Q> stepUnit = steps.get(stepLevel); final double step = toBaseUnit(Measure.valueOf(1, stepUnit)); final double pixelsPerUnit = viewSize / mapSize; final long minIndex = (long)Math.ceil(this.areaMinY / step); final long maxIndex = (long)Math.ceil(maxY / step); long startIndex = (long)Math.floor(minY / step); if (startIndex < minIndex) { startIndex = minIndex; } for (long index = startIndex; index < maxIndex; index++) { final Measure<Q> measureValue = Measure.valueOf(index, stepUnit); final double value = toBaseUnit(measureValue); final double displayValue = measureValue.doubleValue(displayUnit); final int pixel = (int)((value - minY) * pixelsPerUnit); boolean found = false; int barSize = 4; g.setColor(Color.LIGHT_GRAY); for (int i = 0; !found && i < stepLevel; i++) { final Unit<Q> scaleUnit = steps.get(i); final double stepValue = measureValue.doubleValue(scaleUnit); if (Math.abs(stepValue - Math.round(stepValue)) < 0.000001) { barSize = 4 + (int)((this.rulerSize - 4) * (((double)stepLevel - i) / stepLevel)); found = true; final AffineTransform transform2 = g.getTransform(); try { g.translate(textX, height - pixel - 3); g.rotate(-Math.PI / 2); drawLabel(g, 0, 0, displayUnit, displayValue, scaleUnit); } finally { g.setTransform(transform2); } } } if (left) { g.drawLine(this.rulerSize - 1 - barSize, height - pixel, this.rulerSize - 1, height - pixel); } else { g.drawLine(0, height - pixel, barSize, height - pixel); } } } } finally { g.setTransform(transform); g.setClip(clip); } } @Override public void propertyChange(final PropertyChangeEvent event) { final GeometryFactory geometryFactory = (GeometryFactory)event.getNewValue(); setRulerGeometryFactory(geometryFactory); } public void setRulerGeometryFactory(final GeometryFactory rulerGeometryFactory) { this.rulerGeometryFactory = rulerGeometryFactory; if (rulerGeometryFactory == null) { this.rulerGeometryFactory = this.viewport.getGeometryFactory(); } else { this.rulerGeometryFactory = rulerGeometryFactory; } this.rulerCoordinateSystem = this.rulerGeometryFactory.getCoordinateSystem(); this.baseUnit = this.rulerCoordinateSystem.getUnit(); final BoundingBox areaBoundingBox = this.rulerCoordinateSystem.getAreaBoundingBox(); this.areaMinX = areaBoundingBox.getMinX(); this.areaMaxX = areaBoundingBox.getMaxX(); this.areaMinY = areaBoundingBox.getMinY(); this.areaMaxY = areaBoundingBox.getMaxY(); } @SuppressWarnings("unchecked") private <Q extends Quantity> double toBaseUnit(final Measure<Q> value) { return value.doubleValue(this.baseUnit); } }