package org.simantics.maps.sg; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import org.geotools.referencing.CRS; import org.geotools.referencing.GeodeticCalculator; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.simantics.maps.sg.Formatting.FormatMode; import org.simantics.scenegraph.utils.DPIUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MapScaleNode extends MapInfoNode { private static final Logger LOGGER = LoggerFactory.getLogger(MapScaleNode.class); private static final long serialVersionUID = -2738682328944298290L; private static final Color GRAY = new Color(50, 50, 50); private static final transient int SCALE_VERTICAL_MARGIN = 4; private static final transient int SCALE_MAJOR_TICK_HEIGHT = 8; private static final transient int SCALE_MINOR_TICK_HEIGHT = 5; private static final transient int SCALE_MICRO_TICK_HEIGHT = 3; private static final transient double SCALE_RIGHT_MARGIN = 20; private static final transient int MAX_DIGITS = 0; private static final transient double TRIM_THRESHOLD_MAX_VALUE = Math.pow(10, 2); private static final transient String[] SI_LENGTH_UNIT = { "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, 5000000, 10000000, 20000000, 50000000 }; private GeodeticCalculator calculator; @Override public void render(Graphics2D g) { if (!enabled || calculator == null) 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()); Rectangle2D controlBounds = g.getClipBounds(); if (controlBounds == null) return; // FIXME Font font = MapInfoConstants.getInfoFont(); g.setFont(font); FontMetrics fm = g.getFontMetrics(); int majorTickHeight = DPIUtil.upscale(SCALE_MAJOR_TICK_HEIGHT); int minorTickHeight = DPIUtil.upscale(SCALE_MINOR_TICK_HEIGHT); int microTickHeight = DPIUtil.upscale(SCALE_MICRO_TICK_HEIGHT); double yOffsetFromBottom = getMapInfoNextY(g); double scaleTotalHeight = Math.max(majorTickHeight + SCALE_VERTICAL_MARGIN * 2 , font.getSize() + MapInfoConstants.TEXT_VERTICAL_MARGIN * 2); double scaleMaxX = controlBounds.getMaxX() - SCALE_RIGHT_MARGIN; double scaleMaxY = controlBounds.getMaxY() - yOffsetFromBottom; double scaleMinY = scaleMaxY - scaleTotalHeight; int textY = (int) scaleMinY + MapInfoConstants.TEXT_VERTICAL_MARGIN + fm.getMaxAscent(); double meterPerPixel = getMeterPerPixel(scaleMaxX - offsetX, scaleMinY - offsetY, scaleX, scaleY); // Prevent the scale from getting too long in the X-direction if (meterPerPixel < 0.005) return; double scaleWidth = findScaleWidth(meterPerPixel); double scaleMinX = scaleMaxX - scaleWidth; double value = scaleWidth * meterPerPixel; String formattedValue = formatValue(value); Rectangle2D textBounds = fm.getStringBounds(formattedValue, g); double addedTextWidth = textBounds.getWidth() + MapInfoConstants.TEXT_HORIZONTAL_MARGIN * 2; scaleMinX -= addedTextWidth; scaleMaxX -= addedTextWidth; rect.setFrame(scaleMinX, scaleMinY, scaleWidth + addedTextWidth, scaleTotalHeight); // System.out.println("----"); // System.out.println("scale: " + scaleX + ", " + scaleY); // System.out.println("scaleMaxX: " + scaleMaxX + ", offsetX: " + offsetX); // System.out.println("scaleMinY: " + scaleMinY + ", offsetY: " + offsetY); // System.out.println("meterPerPixel: " + meterPerPixel); Composite oc = g.getComposite(); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); g.setStroke(MapInfoConstants.INFO_STROKE); g.setColor(MapInfoConstants.TEXT_BG_COLOR); g.fill(rect); g.setColor(Color.BLACK); g.drawString(formattedValue, (int) (scaleMinX + scaleWidth) + MapInfoConstants.TEXT_HORIZONTAL_MARGIN, textY); double stepX = scaleWidth; g.setColor(GRAY); int yOffset = -SCALE_VERTICAL_MARGIN; g.drawLine((int) scaleMinX, (int) scaleMaxY + yOffset + 1, (int) scaleMaxX, (int) scaleMaxY + yOffset + 1); g.drawLine((int) scaleMinX, (int) scaleMaxY - majorTickHeight + yOffset, (int) scaleMinX, (int) scaleMaxY + yOffset); g.drawLine((int) scaleMaxX, (int) scaleMaxY - majorTickHeight + yOffset, (int) scaleMaxX, (int) scaleMaxY + yOffset); double x = scaleMinX; if (stepX / 5 > 2) { for (double x2 = x + stepX / 5; x2 < x + stepX; x2 += stepX / 5) { if (x2 > 20) { g.drawLine((int) x2, (int) scaleMaxY - minorTickHeight + yOffset, (int) x2, (int) scaleMaxY + yOffset); } } for (double x2 = x + stepX / 10; x2 < x + stepX; x2 += stepX / 5) { if (x2 > 20) { g.drawLine((int) x2, (int) scaleMaxY - microTickHeight + yOffset, (int) x2, (int) scaleMaxY + yOffset); } } } g.setComposite(oc); g.setTransform(ot); setMapInfoNextY(g, yOffsetFromBottom + scaleTotalHeight + MapInfoConstants.INFO_ROW_SPACING); } private String formatValue(double value) { int lengthUnit = 0; if (value > 1000.0) { value /= 1000.0; lengthUnit = 1; } return Formatting.formatValue(value, MAX_DIGITS, true, FormatMode.LIMIT_DECIMALS, TRIM_THRESHOLD_MAX_VALUE, false) + " " + SI_LENGTH_UNIT[lengthUnit]; } @Override public void init() { try { CoordinateReferenceSystem EPSG4326 = CRS.decode("EPSG:4326"); calculator = new GeodeticCalculator(EPSG4326); } catch (FactoryException e) { LOGGER.error("Failed to initialize GeodeticCalculator with coordinate reference system EPSG:4326", e); } super.init(); } private 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; } private double findScaleWidth(double meterPerPixel) { double scaleWidth = 0; for (int i = 0; i < SCALE_VALUES.length; i++) { scaleWidth = SCALE_VALUES[i] / meterPerPixel; if (scaleWidth > 100) break; } return scaleWidth; } }