001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint.styleelement.placement; 003 004import java.awt.Rectangle; 005import java.awt.geom.Point2D; 006import java.awt.geom.Rectangle2D; 007import java.util.Objects; 008 009import org.openstreetmap.josm.gui.MapViewState; 010import org.openstreetmap.josm.gui.draw.MapViewPath; 011import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation; 012 013/** 014 * Places the label / icon so that it is completely inside the area. 015 * 016 * @author Michael Zangl 017 * @since 11722 018 * @since 11748 moved to own file 019 */ 020public class CompletelyInsideAreaStrategy implements PositionForAreaStrategy { 021 /** 022 * An instance of this class. 023 */ 024 public static final CompletelyInsideAreaStrategy INSTANCE = new CompletelyInsideAreaStrategy(0, 0); 025 026 protected final double offsetX; 027 protected final double offsetY; 028 029 protected CompletelyInsideAreaStrategy(double offsetX, double offsetY) { 030 this.offsetX = offsetX; 031 this.offsetY = offsetY; 032 } 033 034 @Override 035 public MapViewPositionAndRotation findLabelPlacement(MapViewPath path, Rectangle2D nb) { 036 // Using the Centroid is Nicer for buildings like: +--------+ 037 // but this needs to be fast. As most houses are | 42 | 038 // boxes anyway, the center of the bounding box +---++---+ 039 // will have to do. ++ 040 // Centroids are not optimal either, just imagine a U-shaped house. 041 042 final Rectangle pb = path.getBounds(); 043 044 // quick check to see if label box is smaller than primitive box 045 if (pb.width < nb.getWidth() || pb.height < nb.getHeight()) { 046 return null; 047 } 048 049 final double w = pb.width - nb.getWidth(); 050 final double h = pb.height - nb.getHeight(); 051 052 final int x2 = pb.x + (int) (w / 2.0); 053 final int y2 = pb.y + (int) (h / 2.0); 054 055 final int nbw = (int) nb.getWidth(); 056 final int nbh = (int) nb.getHeight(); 057 058 final Rectangle centeredNBounds = new Rectangle(x2, y2, nbw, nbh); 059 060 // slower check to see if label is displayed inside primitive shape 061 if (path.contains(centeredNBounds)) { 062 return centerOf(path.getMapViewState(), centeredNBounds); 063 } 064 065 // if center position (C) is not inside osm shape, try naively some other positions as follows: 066 final int x1 = pb.x + (int) (.25 * w); 067 final int x3 = pb.x + (int) (.75 * w); 068 final int y1 = pb.y + (int) (.25 * h); 069 final int y3 = pb.y + (int) (.75 * h); 070 // +-----------+ 071 // | 5 1 6 | 072 // | 4 C 2 | 073 // | 8 3 7 | 074 // +-----------+ 075 Rectangle[] candidates = { 076 new Rectangle(x2, y1, nbw, nbh), 077 new Rectangle(x3, y2, nbw, nbh), 078 new Rectangle(x2, y3, nbw, nbh), 079 new Rectangle(x1, y2, nbw, nbh), 080 new Rectangle(x1, y1, nbw, nbh), 081 new Rectangle(x3, y1, nbw, nbh), 082 new Rectangle(x3, y3, nbw, nbh), 083 new Rectangle(x1, y3, nbw, nbh) 084 }; 085 // Dumb algorithm to find a better placement. We could surely find a smarter one but it should 086 // solve most of building issues with only few calculations (8 at most) 087 for (Rectangle candidate : candidates) { 088 if (path.contains(candidate)) { 089 return centerOf(path.getMapViewState(), candidate); 090 } 091 } 092 093 // none found 094 return null; 095 } 096 097 private MapViewPositionAndRotation centerOf(MapViewState mapViewState, Rectangle centeredNBounds) { 098 double x = centeredNBounds.getCenterX() + offsetX; 099 double y = centeredNBounds.getCenterY() + offsetY; 100 return new MapViewPositionAndRotation(mapViewState.getForView(x, y), 0); 101 } 102 103 @Override 104 public boolean supportsGlyphVector() { 105 return false; 106 } 107 108 @Override 109 public PositionForAreaStrategy withAddedOffset(Point2D addToOffset) { 110 if (Math.abs(addToOffset.getX()) < 1e-5 && Math.abs(addToOffset.getY()) < 1e-5) { 111 return this; 112 } else { 113 return new CompletelyInsideAreaStrategy(offsetX + addToOffset.getX(), offsetY - addToOffset.getY()); 114 } 115 } 116 117 @Override 118 public String toString() { 119 return "CompletelyInsideAreaStrategy [offsetX=" + offsetX + ", offsetY=" + offsetY + "]"; 120 } 121 122 @Override 123 public int hashCode() { 124 return Objects.hash(offsetX, offsetY); 125 } 126 127 @Override 128 public boolean equals(Object obj) { 129 if (this == obj) { 130 return true; 131 } 132 if (obj == null || getClass() != obj.getClass()) { 133 return false; 134 } 135 CompletelyInsideAreaStrategy other = (CompletelyInsideAreaStrategy) obj; 136 return Double.doubleToLongBits(offsetX) == Double.doubleToLongBits(other.offsetX) 137 && Double.doubleToLongBits(offsetY) == Double.doubleToLongBits(other.offsetY); 138 } 139}