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 = new Rectangle[] {
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}