001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.util.List;
005import java.util.Locale;
006
007import org.openstreetmap.josm.data.osm.IPrimitive;
008import org.openstreetmap.josm.data.osm.Node;
009import org.openstreetmap.josm.data.osm.Way;
010
011/**
012 * Determines how an icon is to be rotated depending on the primitive to be displayed.
013 * @since  8199 (creation)
014 * @since 10599 (functional interface)
015 * @since 12756 (moved from {@code gui.util} package)
016 */
017@FunctionalInterface
018public interface RotationAngle {
019
020    /**
021     * The rotation along a way.
022     */
023    final class WayDirectionRotationAngle implements RotationAngle {
024        @Override
025        public double getRotationAngle(IPrimitive p) {
026            if (!(p instanceof Node)) {
027                return 0;
028            }
029            final Node n = (Node) p;
030            final List<Way> ways = n.getParentWays();
031            if (ways.isEmpty()) {
032                return 0;
033            }
034            final Way w = ways.iterator().next();
035            final int idx = w.getNodes().indexOf(n);
036            if (idx == 0) {
037                return -Geometry.getSegmentAngle(n.getEastNorth(), w.getNode(idx + 1).getEastNorth());
038            } else {
039                return -Geometry.getSegmentAngle(w.getNode(idx - 1).getEastNorth(), n.getEastNorth());
040            }
041        }
042
043        @Override
044        public String toString() {
045            return "way-direction";
046        }
047
048        @Override
049        public int hashCode() {
050            return 1;
051        }
052
053        @Override
054        public boolean equals(Object obj) {
055            if (this == obj) {
056                return true;
057            }
058            return obj != null && getClass() == obj.getClass();
059        }
060    }
061
062    /**
063     * A static rotation
064     */
065    final class StaticRotationAngle implements RotationAngle {
066        private final double angle;
067
068        private StaticRotationAngle(double angle) {
069            this.angle = angle;
070        }
071
072        @Override
073        public double getRotationAngle(IPrimitive p) {
074            return angle;
075        }
076
077        @Override
078        public String toString() {
079            return angle + "rad";
080        }
081
082        @Override
083        public int hashCode() {
084            return Double.hashCode(angle);
085        }
086
087        @Override
088        public boolean equals(Object obj) {
089            if (this == obj) {
090                return true;
091            }
092            if (obj == null || getClass() != obj.getClass()) {
093                return false;
094            }
095            StaticRotationAngle other = (StaticRotationAngle) obj;
096            return Double.doubleToLongBits(angle) == Double.doubleToLongBits(other.angle);
097        }
098    }
099
100    /**
101     * A no-rotation angle that always returns 0.
102     * @since 11726
103     */
104    RotationAngle NO_ROTATION = new StaticRotationAngle(0);
105
106    /**
107     * Calculates the rotation angle depending on the primitive to be displayed.
108     * @param p primitive
109     * @return rotation angle
110     * @since 13623 (signature)
111     */
112    double getRotationAngle(IPrimitive p);
113
114    /**
115     * Always returns the fixed {@code angle}.
116     * @param angle angle
117     * @return rotation angle
118     */
119    static RotationAngle buildStaticRotation(final double angle) {
120        return new StaticRotationAngle(angle);
121    }
122
123    /**
124     * Parses the rotation angle from the specified {@code string}.
125     * @param string angle as string
126     * @return rotation angle
127     */
128    static RotationAngle buildStaticRotation(final String string) {
129        try {
130            return buildStaticRotation(parseCardinalRotation(string));
131        } catch (IllegalArgumentException e) {
132            throw new IllegalArgumentException("Invalid string: " + string, e);
133        }
134    }
135
136    /**
137     * Converts an angle given in cardinal directions to radians.
138     * The following values are supported: {@code n}, {@code north}, {@code ne}, {@code northeast},
139     * {@code e}, {@code east}, {@code se}, {@code southeast}, {@code s}, {@code south},
140     * {@code sw}, {@code southwest}, {@code w}, {@code west}, {@code nw}, {@code northwest}.
141     * @param cardinal the angle in cardinal directions
142     * @return the angle in radians
143     */
144    static double parseCardinalRotation(final String cardinal) {
145        switch (cardinal.toLowerCase(Locale.ENGLISH)) {
146            case "n":
147            case "north":
148                return 0; // 0 degree => 0 radian
149            case "ne":
150            case "northeast":
151                return Utils.toRadians(45);
152            case "e":
153            case "east":
154                return Utils.toRadians(90);
155            case "se":
156            case "southeast":
157                return Utils.toRadians(135);
158            case "s":
159            case "south":
160                return Math.PI; // 180 degree
161            case "sw":
162            case "southwest":
163                return Utils.toRadians(225);
164            case "w":
165            case "west":
166                return Utils.toRadians(270);
167            case "nw":
168            case "northwest":
169                return Utils.toRadians(315);
170            default:
171                throw new IllegalArgumentException("Unexpected cardinal direction " + cardinal);
172        }
173    }
174
175    /**
176     * Computes the angle depending on the referencing way segment, or {@code 0} if none exists.
177     * @return rotation angle
178     */
179    static RotationAngle buildWayDirectionRotation() {
180        return new WayDirectionRotationAngle();
181    }
182}