package com.revolsys.swing.map; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.geom.AffineTransform; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.measure.Measurable; import javax.measure.quantity.Length; import javax.measure.quantity.Quantity; import javax.measure.unit.Unit; import javax.swing.JComponent; import com.revolsys.awt.CloseableAffineTransform; import com.revolsys.geometry.cs.CoordinateSystem; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.io.BaseCloseable; import com.revolsys.swing.map.layer.LayerGroup; import com.revolsys.swing.map.layer.Project; import com.revolsys.swing.parallel.Invoke; import com.revolsys.util.Property; import com.revolsys.value.GlobalBooleanValue; public class ComponentViewport2D extends Viewport2D implements PropertyChangeListener { private final JComponent component; private int maxDecimalDigits; private int maxIntegerDigits; private final ThreadLocal<Graphics2D> graphics = new ThreadLocal<>(); private final ThreadLocal<AffineTransform> graphicsTransform = new ThreadLocal<>(); private final ThreadLocal<AffineTransform> graphicsModelTransform = new ThreadLocal<>(); private final GlobalBooleanValue componentResizing = new GlobalBooleanValue(false); public ComponentViewport2D(final Project project, final JComponent component) { super(project); this.component = component; Property.addListener(project, "geometryFactory", this); Property.addListener(project, "viewBoundingBox", this); component.addComponentListener(new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { if (isInitialized()) { try ( BaseCloseable componentResizing = ComponentViewport2D.this.componentResizing .closeable(true)) { updateCachedFields(); } } } }); } /** * Get the bounding box for the dimensions of the viewport at the specified * scale, centred at the x, y model coordinates. * * @param x The model x coordinate. * @param y The model y coordinate. * @param scale The scale. * @return The bounding box. */ public BoundingBox getBoundingBox(final double x, final double y, final double scale) { final double width = getModelWidth(scale); final double height = getModelHeight(scale); final double x1 = x - width / 2; final double y1 = y - height / 2; final double x2 = x1 + width; final double y2 = y1 + height; final BoundingBox boundingBox = getGeometryFactory().newBoundingBox(x1, y1, x2, y2); return boundingBox; } /** * Get the bounding box in model units for the pair of coordinates in view * units. The bounding box will be clipped to the model's bounding box. * * @param x1 The first x value. * @param y1 The first y value. * @param x2 The second x value. * @param y2 The second y value. * @return The bounding box. */ public BoundingBox getBoundingBox(final double x1, final double y1, final double x2, final double y2) { final double[] c1 = toModelCoordinates(x1, y1); final double[] c2 = toModelCoordinates(x2, y2); final BoundingBox boundingBox = getGeometryFactory().newBoundingBox(c1[0], c1[1], c2[0], c2[1]); // Clip the bounding box with the map's visible area BoundingBox intersection = boundingBox.intersection(boundingBox); // If the clipped bounding box is null then move the map to the new BBOX if (intersection.isEmpty()) { intersection = boundingBox; } return intersection; } /** * Get the bounding box for the dimensions of the viewport at the specified * scale, centred at the x, y view coordinates. * * @param x The view x coordinate. * @param y The view y coordinate. * @param scale The scale. * @return The bounding box. */ public BoundingBox getBoundingBox(final int x, final int y, final double scale) { final double[] ordinates = toModelCoordinates(x, y); final double mapX = ordinates[0]; final double mapY = ordinates[1]; return getBoundingBox(mapX, mapY, scale); } public Cursor getCursor() { if (this.component == null) { return null; } else { return this.component.getCursor(); } } @Override public Graphics2D getGraphics() { return this.graphics.get(); } public double getMaxScale() { final BoundingBox areaBoundingBox = getGeometryFactory().getCoordinateSystem() .getAreaBoundingBox(); final Measurable<Length> areaWidth = areaBoundingBox.getWidthLength(); final Measurable<Length> areaHeight = areaBoundingBox.getHeightLength(); final Measurable<Length> viewWidth = getViewWidthLength(); final Measurable<Length> viewHeight = getViewHeightLength(); final double maxHorizontalScale = getScale(viewWidth, areaWidth); final double maxVerticalScale = getScale(viewHeight, areaHeight); final double maxScale = Math.max(maxHorizontalScale, maxVerticalScale); return maxScale; } public double getModelHeight(final double scale) { final Unit<Length> scaleUnit = getScaleUnit(scale); final Measurable<Length> viewHeight = getViewHeightLength(); final double height = viewHeight.doubleValue(scaleUnit); return height; } public <Q extends Quantity> Unit<Q> getModelToScreenUnit(final Unit<Q> modelUnit) { final double viewWidth = getViewWidthPixels(); final double modelWidth = getModelWidth(); return modelUnit.times(viewWidth).divide(modelWidth); } public double getModelWidth(final double scale) { final Unit<Length> scaleUnit = getScaleUnit(scale); final Measurable<Length> viewWidth = getViewWidthLength(); final double width = viewWidth.doubleValue(scaleUnit); return width; } /** * Get the rectangle in view units for the pair of coordinates in view units. * The bounding box will be clipped to the view's dimensions. * * @param x1 The first x value. * @param y1 The first y value. * @param x2 The second x value. * @param y2 The second y value. * @return The rectangle. */ public Rectangle getRectangle(final int x1, final int y1, final int x2, final int y2) { final int x3 = Math.min(getViewWidthPixels() - 1, Math.max(0, x2)); final int y3 = Math.min(getViewHeightPixels() - 1, Math.max(0, y2)); final int x = Math.min(x1, x3); final int y = Math.min(y1, y3); final int width = Math.abs(x1 - x3); final int height = Math.abs(y1 - y3); return new Rectangle(x, y, width, height); } public Unit<Length> getScaleUnit(final double scale) { final Unit<Length> lengthUnit = getGeometryFactory().getCoordinateSystem().getLengthUnit(); final Unit<Length> scaleUnit = lengthUnit.divide(scale); return scaleUnit; } public <Q extends Quantity> Unit<Q> getScreenToModelUnit(final Unit<Q> modelUnit) { final double viewWidth = getViewWidthPixels(); final double modelWidth = getModelWidth(); return modelUnit.times(modelWidth).divide(viewWidth); } public BoundingBox getValidBoundingBox(final BoundingBox boundingBox) { BoundingBox validBoundingBox = boundingBox; final double viewAspectRatio = getViewAspectRatio(); double modelWidth = validBoundingBox.getWidth(); double modelHeight = validBoundingBox.getHeight(); /* * If the new bounding box has a zero width and height, expand it by 50 view units. */ if (modelWidth == 0 && modelHeight == 0) { validBoundingBox = validBoundingBox.expand(getModelUnitsPerViewUnit() * 50, getModelUnitsPerViewUnit() * 50); modelWidth = validBoundingBox.getWidth(); modelHeight = validBoundingBox.getHeight(); } final double newModelAspectRatio = modelWidth / modelHeight; double modelUnitsPerViewUnit; if (viewAspectRatio <= newModelAspectRatio) { modelUnitsPerViewUnit = modelWidth / getViewWidthPixels(); } else { modelUnitsPerViewUnit = modelHeight / getViewHeightPixels(); } final double logUnits = Math.log10(Math.abs(modelUnitsPerViewUnit)); if (logUnits < 0 && Math.abs(Math.floor(logUnits)) > this.maxDecimalDigits) { modelUnitsPerViewUnit = 2 * Math.pow(10, -this.maxDecimalDigits); final double minModelWidth = getViewWidthPixels() * modelUnitsPerViewUnit; final double minModelHeight = getViewHeightPixels() * modelUnitsPerViewUnit; validBoundingBox = validBoundingBox.expand((minModelWidth - modelWidth) / 2, (minModelHeight - modelHeight) / 2); } return validBoundingBox; } public boolean isComponentResizing() { return this.componentResizing.isTrue(); } @Override public void propertyChange(final PropertyChangeEvent event) { if (isInitialized() && event.getSource() == getProject()) { if (event.getPropertyName().equals("viewBoundingBox")) { // final BoundingBox boundingBox = // (BoundingBox)event.getNewValue(); // if (isInitialized()) { // updateCachedFields(); // } else { // setBoundingBoxInternal(boundingBox); // } } else { Invoke.later(this::updateCachedFields); } } } public void repaint() { this.component.repaint(); } /** * Set the coordinate system the map is displayed in. * * @param coordinateSystem The coordinate system the map is displayed in. */ @Override public void setGeometryFactory(final GeometryFactory geometryFactory) { final GeometryFactory oldGeometryFactory = getGeometryFactory(); if (geometryFactory != oldGeometryFactory) { super.setGeometryFactory(geometryFactory); final CoordinateSystem coordinateSystem = geometryFactory.getCoordinateSystem(); if (coordinateSystem != null) { final BoundingBox areaBoundingBox = coordinateSystem.getAreaBoundingBox(); final double minX = areaBoundingBox.getMinX(); final double maxX = areaBoundingBox.getMaxX(); final double minY = areaBoundingBox.getMinY(); final double maxY = areaBoundingBox.getMaxY(); final double logMinX = Math.log10(Math.abs(minX)); final double logMinY = Math.log10(Math.abs(minY)); final double logMaxX = Math.log10(Math.abs(maxX)); final double logMaxY = Math.log10(Math.abs(maxY)); final double maxLog = Math .abs(Math.max(Math.max(logMinX, logMinY), Math.max(logMaxX, logMaxY))); this.maxIntegerDigits = (int)Math.floor(maxLog + 1); this.maxDecimalDigits = 15 - this.maxIntegerDigits; getPropertyChangeSupport().firePropertyChange("geometryFactory", oldGeometryFactory, geometryFactory); final BoundingBox boundingBox = getBoundingBox(); if (boundingBox != null) { final BoundingBox newBoundingBox = boundingBox.convert(geometryFactory); final BoundingBox intersection = newBoundingBox.intersection(areaBoundingBox); if (intersection.isEmpty()) { setBoundingBox(areaBoundingBox); } else { setBoundingBox(intersection); } } } } } public void setGraphics(final Graphics2D graphics) { if (graphics == null) { this.graphics.remove(); } else { this.graphics.set(graphics); this.graphicsTransform.set(graphics.getTransform()); } updateGraphicsTransform(); } @Override public void setInitialized(final boolean initialized) { if (initialized && !isInitialized()) { updateCachedFields(); } super.setInitialized(initialized); } @Override protected void setModelToScreenTransform(final AffineTransform modelToScreenTransform) { super.setModelToScreenTransform(modelToScreenTransform); updateGraphicsTransform(); } @Override public BaseCloseable setUseModelCoordinates(final boolean useModelCoordinates) { final Graphics2D graphics = getGraphics(); return setUseModelCoordinates(graphics, useModelCoordinates); } @Override public BaseCloseable setUseModelCoordinates(final Graphics2D graphics, final boolean useModelCoordinates) { if (graphics == null) { return null; } else { AffineTransform newTransform; if (useModelCoordinates) { newTransform = this.graphicsModelTransform.get(); } else { newTransform = this.graphicsTransform.get(); } if (newTransform == null) { return new CloseableAffineTransform(graphics); } else { return new CloseableAffineTransform(graphics, newTransform); } } } public void translate(final double dx, final double dy) { final BoundingBox boundingBox = getBoundingBox(); final BoundingBox newBoundingBox = boundingBox.getGeometryFactory().newBoundingBox( boundingBox.getMinX() + dx, boundingBox.getMinY() + dy, boundingBox.getMaxX() + dx, boundingBox.getMaxY() + dy); setBoundingBox(newBoundingBox); } @Override public void update() { repaint(); } private void updateCachedFields() { final LayerGroup project = getProject(); final GeometryFactory geometryFactory = project.getGeometryFactory(); if (geometryFactory != null) { if (geometryFactory != getGeometryFactory()) { setGeometryFactory(geometryFactory); } final Insets insets = this.component.getInsets(); final int viewWidth = this.component.getWidth() - insets.left - insets.right; final int viewHeight = this.component.getHeight() - insets.top - insets.bottom; setViewWidth(viewWidth); setViewHeight(viewHeight); setBoundingBox(getBoundingBox()); this.component.repaint(); } } private void updateGraphicsTransform() { if (this.graphicsTransform != null) { final AffineTransform modelToScreenTransform = getModelToScreenTransform(); final AffineTransform graphicsTransform = this.graphicsTransform.get(); if (modelToScreenTransform == null || graphicsTransform == null) { this.graphicsModelTransform.remove(); } else { final AffineTransform transform = (AffineTransform)graphicsTransform.clone(); transform.concatenate(modelToScreenTransform); this.graphicsModelTransform.set(transform); } } } }