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}