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; 007 008import org.openstreetmap.josm.gui.MapViewState; 009import org.openstreetmap.josm.gui.draw.MapViewPath; 010import org.openstreetmap.josm.gui.draw.MapViewPositionAndRotation; 011 012/** 013 * Places the label / icon so that it is completely inside the area. 014 * 015 * @author Michael Zangl 016 * @since 11722 017 * @since 11748 moved to own file 018 */ 019public class CompletelyInsideAreaStrategy implements PositionForAreaStrategy { 020 /** 021 * An instance of this class. 022 */ 023 public static final CompletelyInsideAreaStrategy INSTANCE = new CompletelyInsideAreaStrategy(0, 0); 024 025 protected final double offsetX; 026 protected final double offsetY; 027 028 protected CompletelyInsideAreaStrategy(double offsetX, double offsetY) { 029 this.offsetX = offsetX; 030 this.offsetY = offsetY; 031 } 032 033 @Override 034 public MapViewPositionAndRotation findLabelPlacement(MapViewPath path, Rectangle2D nb) { 035 // Using the Centroid is Nicer for buildings like: +--------+ 036 // but this needs to be fast. As most houses are | 42 | 037 // boxes anyway, the center of the bounding box +---++---+ 038 // will have to do. ++ 039 // Centroids are not optimal either, just imagine a U-shaped house. 040 041 Rectangle pb = path.getBounds(); 042 043 // quick check to see if label box is smaller than primitive box 044 if (pb.width < nb.getWidth() || pb.height < nb.getHeight()) { 045 return null; 046 } 047 048 final double w = pb.width - nb.getWidth(); 049 final double h = pb.height - nb.getHeight(); 050 051 final int x2 = pb.x + (int) (w / 2.0); 052 final int y2 = pb.y + (int) (h / 2.0); 053 054 final int nbw = (int) nb.getWidth(); 055 final int nbh = (int) nb.getHeight(); 056 057 Rectangle centeredNBounds = new Rectangle(x2, y2, nbw, nbh); 058 059 // slower check to see if label is displayed inside primitive shape 060 if (path.contains(centeredNBounds)) { 061 return centerOf(path.getMapViewState(), centeredNBounds); 062 } 063 064 // if center position (C) is not inside osm shape, try naively some other positions as follows: 065 final int x1 = pb.x + (int) (.25 * w); 066 final int x3 = pb.x + (int) (.75 * w); 067 final int y1 = pb.y + (int) (.25 * h); 068 final int y3 = pb.y + (int) (.75 * h); 069 // +-----------+ 070 // | 5 1 6 | 071 // | 4 C 2 | 072 // | 8 3 7 | 073 // +-----------+ 074 Rectangle[] candidates = { 075 new Rectangle(x2, y1, nbw, nbh), 076 new Rectangle(x3, y2, nbw, nbh), 077 new Rectangle(x2, y3, nbw, nbh), 078 new Rectangle(x1, y2, nbw, nbh), 079 new Rectangle(x1, y1, nbw, nbh), 080 new Rectangle(x3, y1, nbw, nbh), 081 new Rectangle(x3, y3, nbw, nbh), 082 new Rectangle(x1, y3, nbw, nbh) 083 }; 084 // Dumb algorithm to find a better placement. We could surely find a smarter one but it should 085 // solve most of building issues with only few calculations (8 at most) 086 for (int i = 0; i < candidates.length; i++) { 087 centeredNBounds = candidates[i]; 088 if (path.contains(centeredNBounds)) { 089 return centerOf(path.getMapViewState(), centeredNBounds); 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 final int prime = 31; 125 int result = 1; 126 long temp; 127 temp = Double.doubleToLongBits(offsetX); 128 result = prime * result + (int) (temp ^ (temp >>> 32)); 129 temp = Double.doubleToLongBits(offsetY); 130 result = prime * result + (int) (temp ^ (temp >>> 32)); 131 return result; 132 } 133 134 @Override 135 public boolean equals(Object obj) { 136 if (this == obj) { 137 return true; 138 } 139 if (obj == null || getClass() != obj.getClass()) { 140 return false; 141 } 142 CompletelyInsideAreaStrategy other = (CompletelyInsideAreaStrategy) obj; 143 return Double.doubleToLongBits(offsetX) == Double.doubleToLongBits(other.offsetX) 144 && Double.doubleToLongBits(offsetY) == Double.doubleToLongBits(other.offsetY); 145 } 146}