/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.scenegraph.g2d.nodes; 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.Rectangle2D; import java.util.Locale; import org.simantics.scenegraph.g2d.G2DNode; import org.simantics.scenegraph.utils.GridUtils; public class RulerNode extends G2DNode { /** * */ private static final long serialVersionUID = 2490944880914577411L; /** * FIXME: this is a hack for the map UI that has to be solved some other way. */ private static final boolean MAP_Y_SCALING = false; private static final Color GRAY = new Color(100, 100, 100); protected Boolean enabled = true; protected double gridSize = 1.0; @SyncField("enabled") public void setEnabled(Boolean enabled) { this.enabled = enabled; } @SyncField("gridSize") public void setGridSize(double gridSize) { if (gridSize < 1e-6) gridSize = 1e-6; this.gridSize = gridSize; } @Override public void render(Graphics2D g) { if (!enabled) return; 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 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); Rectangle2D vertical = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMinY()+20); g.fill(vertical); Rectangle2D horizontal = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY()+20, bounds.getMinX()+20, bounds.getMaxY()); g.fill(horizontal); // stepX and stepY should be something between 50 and 100 double stepX = 50; double stepY = 50; stepX = GridUtils.limitedEvenGridSpacing(stepX, scaleX, 100, gridSize, true); stepY = GridUtils.limitedEvenGridSpacing(stepY, scaleY, 100, gridSize, true); //while(stepX * scaleX > 100) stepX /= 2; //while(stepY * scaleY > 100) stepY /= 2; while(stepX * scaleX < 50) stepX *= 2; while(stepY * scaleY < 50) stepY *= 2; stepX *= scaleX; stepY *= scaleY; g.setColor(GRAY); g.setFont(rulerFont); double previousText = -100; // Vertical ruler for(double x = offsetX%stepX-stepX; x < bounds.getMaxX(); x+=stepX) { if(x > 20) { String str = formatValue((x-offsetX)/scaleX); FontMetrics fm = g.getFontMetrics(); 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)(bounds.getMinY()+1+r.getHeight())); previousText = x+r.getWidth()/2+stepX/4; } g.setColor(GRAY); g.drawLine((int)x, (int)bounds.getMinY()+12, (int)x, (int)bounds.getMinY()+19); } if(stepX/5 > 2) { for(double x2 = x+stepX/5; x2 < x+stepX; x2+=stepX/5) { if(x2 > 20) { g.drawLine((int)x2, (int)bounds.getMinY()+15, (int)x2, (int)bounds.getMinY()+19); } } for(double x2 = x+stepX/10; x2 < x+stepX; x2+=stepX/5) { if(x2 > 20) { g.drawLine((int)x2, (int)bounds.getMinY()+17, (int)x2, (int)bounds.getMinY()+19); } } } } // Horizontal ruler previousText = -100; for(double y = offsetY%stepY-stepY; y < bounds.getMaxY(); y+=stepY) { if(y > 20) { double val = (y-offsetY)/scaleY; if (MAP_Y_SCALING) val = Math.toDegrees(Math.atan(Math.sinh(Math.toRadians(val)))); String str = formatValue(val); FontMetrics fm = g.getFontMetrics(); Rectangle2D r = fm.getStringBounds(str, g); if(y-1+r.getHeight()/2 > previousText) { g.setColor(Color.BLACK); AffineTransform origTr = g.getTransform(); g.translate((int)(bounds.getMinX()), (int)(y+r.getWidth()/2)); g.rotate(-Math.PI / 2.0); g.drawString(str, 0, (int)r.getHeight()); g.setTransform(origTr); previousText = y-1+r.getHeight(); } g.setColor(GRAY); g.drawLine((int)bounds.getMinX()+12, (int)y, (int)bounds.getMinX()+19, (int)y); } if(stepY/5 > 2) { for(double y2 = y+stepY/5; y2 < y+stepY; y2+=stepY/5) { if(y2 > 20) { g.drawLine((int)bounds.getMinX()+15, (int)y2, (int)bounds.getMinX()+19, (int)y2); } } for(double y2 = y+stepY/10; y2 < y+stepY; y2+=stepY/5) { if(y2 > 20) { g.drawLine((int)bounds.getMinX()+17, (int)y2, (int)bounds.getMinX()+19, (int)y2); } } } } g.setTransform(tr); } private static final transient int MAX_DIGITS = 5; private static final transient double EPSILON = 0.01; private static final transient double TRIM_THRESHOLD_MAX_VALUE = Math.pow(10, 4); private static final transient String[] SI_UNIT_LARGE_PREFIXES = { "k", "M", "G", "T", "P", "E", "Z", "Y" }; 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); } } } double trimValue = value; if (Math.abs(value)+EPSILON >= TRIM_THRESHOLD_MAX_VALUE) { for (int i = 0; Math.abs(trimValue)+EPSILON >= TRIM_THRESHOLD_MAX_VALUE; ++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; } } if (valueStr.equals("-0")) valueStr = "0"; return valueStr; } @Override public Rectangle2D getBoundsInLocal() { return null; } }