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 final int prime = 31; 085 int result = 1; 086 long temp = Double.doubleToLongBits(angle); 087 result = prime * result + (int) (temp ^ (temp >>> 32)); 088 return result; 089 } 090 091 @Override 092 public boolean equals(Object obj) { 093 if (this == obj) { 094 return true; 095 } 096 if (obj == null || getClass() != obj.getClass()) { 097 return false; 098 } 099 StaticRotationAngle other = (StaticRotationAngle) obj; 100 return Double.doubleToLongBits(angle) == Double.doubleToLongBits(other.angle); 101 } 102 } 103 104 /** 105 * A no-rotation angle that always returns 0. 106 * @since 11726 107 */ 108 RotationAngle NO_ROTATION = new StaticRotationAngle(0); 109 110 /** 111 * Calculates the rotation angle depending on the primitive to be displayed. 112 * @param p primitive 113 * @return rotation angle 114 * @since 13623 (signature) 115 */ 116 double getRotationAngle(IPrimitive p); 117 118 /** 119 * Always returns the fixed {@code angle}. 120 * @param angle angle 121 * @return rotation angle 122 */ 123 static RotationAngle buildStaticRotation(final double angle) { 124 return new StaticRotationAngle(angle); 125 } 126 127 /** 128 * Parses the rotation angle from the specified {@code string}. 129 * @param string angle as string 130 * @return rotation angle 131 */ 132 static RotationAngle buildStaticRotation(final String string) { 133 try { 134 return buildStaticRotation(parseCardinalRotation(string)); 135 } catch (IllegalArgumentException e) { 136 throw new IllegalArgumentException("Invalid string: " + string, e); 137 } 138 } 139 140 /** 141 * Converts an angle diven in cardinal directions to radians. 142 * The following values are supported: {@code n}, {@code north}, {@code ne}, {@code northeast}, 143 * {@code e}, {@code east}, {@code se}, {@code southeast}, {@code s}, {@code south}, 144 * {@code sw}, {@code southwest}, {@code w}, {@code west}, {@code nw}, {@code northwest}. 145 * @param cardinal the angle in cardinal directions 146 * @return the angle in radians 147 */ 148 static double parseCardinalRotation(final String cardinal) { 149 switch (cardinal.toLowerCase(Locale.ENGLISH)) { 150 case "n": 151 case "north": 152 return 0; // 0 degree => 0 radian 153 case "ne": 154 case "northeast": 155 return Utils.toRadians(45); 156 case "e": 157 case "east": 158 return Utils.toRadians(90); 159 case "se": 160 case "southeast": 161 return Utils.toRadians(135); 162 case "s": 163 case "south": 164 return Math.PI; // 180 degree 165 case "sw": 166 case "southwest": 167 return Utils.toRadians(225); 168 case "w": 169 case "west": 170 return Utils.toRadians(270); 171 case "nw": 172 case "northwest": 173 return Utils.toRadians(315); 174 default: 175 throw new IllegalArgumentException("Unexpected cardinal direction " + cardinal); 176 } 177 } 178 179 /** 180 * Computes the angle depending on the referencing way segment, or {@code 0} if none exists. 181 * @return rotation angle 182 */ 183 static RotationAngle buildWayDirectionRotation() { 184 return new WayDirectionRotationAngle(); 185 } 186}