package org.simantics.maps.sg; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Locale; import org.geotools.referencing.CRS; import org.geotools.referencing.GeodeticCalculator; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.simantics.scenegraph.g2d.G2DNode; import org.simantics.scenegraph.utils.GridUtils; public class MapScaleNode extends G2DNode { private static final long serialVersionUID = -2738682328944298290L; private static final Color GRAY = new Color(100, 100, 100); protected Boolean enabled = true; protected double gridSize = 1.0; @Override public void render(Graphics2D g) { if (!enabled) return; AffineTransform ot = g.getTransform(); g.transform(transform); AffineTransform tr = g.getTransform(); double scaleX = Math.abs(tr.getScaleX()); double scaleY = Math.abs(tr.getScaleY()); if (scaleX <= 0 || scaleY <= 0) { // Make sure that we don't end up in an eternal loop below. return; } double offsetX = tr.getTranslateX(); double offsetY = tr.getTranslateY(); g.setTransform(new AffineTransform()); Font rulerFont = new Font("Tahoma", Font.PLAIN, 9); //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setStroke(new BasicStroke(1)); g.setColor(new Color(0.9f, 0.9f, 0.9f, 0.75f)); Rectangle2D bounds = g.getClipBounds(); if(bounds == null) return; // FIXME double previousText = -100; double minY = bounds.getMaxY() - 40; double scaleRight = bounds.getMaxX() - 20; double meterPerPixel = getMeterPerPixel(scaleRight - offsetX, minY - offsetY, scaleX, scaleY); double pixels = 0; double value = 0; for (int i = 0; i < SCALE_VALUES.length; i++) { value = SCALE_VALUES[i]; pixels = value / meterPerPixel; if (pixels > 100) { break; } } double newScaleLeft = scaleRight - pixels; g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); Rectangle2D vertical = new Rectangle2D.Double(newScaleLeft, bounds.getMaxY() - 40, pixels, 20); g.fill(vertical); g.setColor(GRAY); g.setFont(rulerFont); // stepX and stepY should be something between 50 and 100 double stepX = 50; stepX = GridUtils.limitedEvenGridSpacing(stepX, scaleX, 100, gridSize, true); //while(stepX * scaleX > 100) stepX /= 2; //while(stepY * scaleY > 100) stepY /= 2; while(stepX * scaleX < 50) stepX *= 2; stepX *= scaleX; double gap = scaleRight -newScaleLeft; stepX = gap / 2 - 0.00001; // Horizontal ruler double label = 0; FontMetrics fm = g.getFontMetrics(); for(double x = newScaleLeft; x < scaleRight; x += stepX) { String str = formatValue(label * meterPerPixel); Rectangle2D r = fm.getStringBounds(str, g); if((x - r.getWidth() / 2) > previousText) { g.setColor(Color.BLACK); g.drawString(str, (int)(x-r.getWidth()/2), (int)(minY+1+r.getHeight())); previousText = x+r.getWidth()/2+stepX/4; } g.setColor(GRAY); g.drawLine((int)x, (int)minY+12, (int)x, (int)minY+19); if (x + 0.1 < scaleRight) { if(stepX/5 > 2) { for(double x2 = x+stepX/5; x2 < x+stepX; x2+=stepX/5) { if(x2 > 20) { g.drawLine((int)x2, (int)minY+15, (int)x2, (int)minY+19); } } for(double x2 = x+stepX/10; x2 < x+stepX; x2+=stepX/5) { if(x2 > 20) { g.drawLine((int)x2, (int)minY+17, (int)x2, (int)minY+19); } } } } label += stepX; } g.setTransform(ot); } @Override public Rectangle2D getBoundsInLocal() { return null; } private static final transient int MAX_DIGITS = 0; private static final transient double EPSILON = 0.01; private static final transient double TRIM_THRESHOLD_MAX_VALUE = Math.pow(10, 2); private static final transient String[] SI_UNIT_LARGE_PREFIXES = { "m", "km" }; private static final transient double[] SCALE_VALUES = { 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000, 50000000 }; public static String formatValue(double value) { int magnitude = (int) Math.round(Math.log10(value)); //System.out.println("magnitude: " + magnitude + ", " + value); int allowedDecimals = MAX_DIGITS; allowedDecimals -= Math.abs(magnitude); if (allowedDecimals < 0) allowedDecimals = 0; String valueStr = String.format(Locale.US, "%." + allowedDecimals + "f", value); if (allowedDecimals > 0) { for (int trunc = valueStr.length() - 1; trunc > 0; --trunc) { char ch = valueStr.charAt(trunc); if (ch == '.') { valueStr = valueStr.substring(0, trunc); break; } if (valueStr.charAt(trunc) != '0') { valueStr = valueStr.substring(0, trunc + 1); break; } } if (Math.abs(value) + EPSILON > TRIM_THRESHOLD_MAX_VALUE) { // Cut anything beyond a possible decimal dot out since they // should not show anyway. This is a complete hack that tries to // circumvent floating-point inaccuracy problems. int dotIndex = valueStr.lastIndexOf('.'); if (dotIndex > -1) { valueStr = valueStr.substring(0, dotIndex); } } } if (valueStr.equals("-0")) valueStr = "0"; // if (!valueStr.equals("0")) { // double trimValue = value; // for (int i = 0; i < SI_UNIT_LARGE_PREFIXES.length; i++) { // double trim = trimValue / 1000; // if (Math.abs(trim)-EPSILON < TRIM_THRESHOLD_MAX_VALUE) { // valueStr = valueStr.substring(0, valueStr.length() - (i + 1) * 3); // valueStr += SI_UNIT_LARGE_PREFIXES[i]; // break; // } // trimValue = trim; // } // } return valueStr; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public void init() { try { EPSG4326 = CRS.decode("EPSG:4326"); calculator = new GeodeticCalculator(EPSG4326); } catch (FactoryException e) { e.printStackTrace(); } super.init(); } GeodeticCalculator calculator; CoordinateReferenceSystem EPSG4326; public Point2D scaleLeftmostPoint(double startLon, double startLat, double distance, double azimuth) { GeodeticCalculator calculator = new GeodeticCalculator(); calculator.setStartingGeographicPoint(startLon, startLat); calculator.setDirection(azimuth, distance); return calculator.getDestinationGeographicPoint(); } public double getMeterPerPixel(double screenX, double screenY, double scaleX, double scaleY) { double startLon = (screenX / scaleX); double val = (screenY / scaleY); val = Math.toDegrees(Math.atan(Math.sinh(Math.toRadians(val)))); double startLat = val; double endLon = ((screenX + 1) / scaleX); double endLat = val; calculator.setStartingGeographicPoint(startLon, startLat); calculator.setDestinationGeographicPoint(endLon, endLat); double distance = calculator.getOrthodromicDistance(); return distance; } }