001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.units; 006 007import java.lang.reflect.Constructor; 008import java.lang.reflect.InvocationTargetException; 009import java.util.Objects; 010 011/** 012 * Builder used for easily deriving new units from existing ones. When deriving a new unit, the base 013 * unit class <strong>must</strong> redeclare the constructor {@link Unit#Unit(Unit, UnaryFunction, 014 * UnaryFunction, String, String) (U, UnaryFunction, UnaryFunction, String, String)}. The unit 015 * builder class will invoke this constructor automatically and build the new unit. Alternatively, 016 * new units can be derived by passing an explicit constructor function to {@link 017 * #make(UnitConstructorFunction)}. 018 * 019 * @param <U> the type of the unit 020 */ 021public final class UnitBuilder<U extends Unit> { 022 private final U m_base; 023 private UnaryFunction m_fromBase = UnaryFunction.IDENTITY; 024 private UnaryFunction m_toBase = UnaryFunction.IDENTITY; 025 private String m_name; 026 private String m_symbol; 027 028 /** 029 * Creates a new unit builder object, building off of a base unit. The base unit does not have to 030 * be <i>the</i> base unit of its unit system; furlongs work just as well here as meters. 031 * 032 * @param base the unit to base the new unit off of 033 */ 034 public UnitBuilder(U base) { 035 this.m_base = Objects.requireNonNull(base, "Base unit cannot be null"); 036 } 037 038 /** 039 * Sets the unit conversions based on a simple offset. The new unit will have its values equal to 040 * (base value - offset). 041 * 042 * @param offset the offset 043 * @return this builder 044 */ 045 public UnitBuilder<U> offset(double offset) { 046 m_toBase = derivedValue -> derivedValue + offset; 047 m_fromBase = baseValue -> baseValue - offset; 048 return this; 049 } 050 051 /** 052 * Maps a value {@code value} in the range {@code [inMin..inMax]} to an output in the range {@code 053 * [outMin..outMax]}. Inputs outside the bounds will be mapped correspondingly to outputs outside 054 * the output bounds. Inputs equal to {@code inMin} will be mapped to {@code outMin}, and inputs 055 * equal to {@code inMax} will similarly be mapped to {@code outMax}. 056 * 057 * @param value the value to map 058 * @param inMin the minimum input value (does not have to be absolute) 059 * @param inMax the maximum input value (does not have to be absolute) 060 * @param outMin the minimum output value (does not have to be absolute) 061 * @param outMax the maximum output value (does not have to be absolute) 062 * @return the mapped output 063 */ 064 // NOTE: This method lives here instead of in MappingBuilder because inner classes can't 065 // define static methods prior to Java 16. 066 private static double mapValue( 067 double value, double inMin, double inMax, double outMin, double outMax) { 068 return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; 069 } 070 071 /** Helper class used for safely chaining mapping builder calls. */ 072 public final class MappingBuilder { 073 private final double m_minInput; 074 private final double m_maxInput; 075 076 private MappingBuilder(double minInput, double maxInput) { 077 this.m_minInput = minInput; 078 this.m_maxInput = maxInput; 079 } 080 081 /** 082 * Finalizes the mapping by defining the output range. 083 * 084 * @param minOutput the minimum output value (does not have to be absolute) 085 * @param maxOutput the maximum output value (does not have to be absolute) 086 * @return the unit builder, for continued chaining 087 */ 088 public UnitBuilder<U> toOutputRange(double minOutput, double maxOutput) { 089 UnitBuilder.this.m_fromBase = x -> mapValue(x, m_minInput, m_maxInput, minOutput, maxOutput); 090 UnitBuilder.this.m_toBase = y -> mapValue(y, minOutput, maxOutput, m_minInput, m_maxInput); 091 return UnitBuilder.this; 092 } 093 } 094 095 /** 096 * Defines a mapping for values within the given input range. This method call should be 097 * immediately followed by {@code .toOutputRange}, eg {@code mappingInputRange(1, 098 * 2).toOutputRange(3, 4)}, which will return the unit builder for continued chaining. 099 * 100 * @param minBase the minimum input value (does not have to be absolute) 101 * @param maxBase the maximum output value (does not have to be absolute) 102 * @return a builder object used to define the output range 103 */ 104 public MappingBuilder mappingInputRange(double minBase, double maxBase) { 105 return new MappingBuilder(minBase, maxBase); 106 } 107 108 /** 109 * Sets the conversion function to transform values in the base unit to values in the derived 110 * unit. 111 * 112 * @param fromBase the conversion function 113 * @return the unit builder, for continued chaining 114 */ 115 public UnitBuilder<U> fromBase(UnaryFunction fromBase) { 116 this.m_fromBase = Objects.requireNonNull(fromBase, "fromBase function cannot be null"); 117 return this; 118 } 119 120 /** 121 * Sets the conversion function to transform values in the derived unit to values in the base 122 * unit. 123 * 124 * @param toBase the conversion function 125 * @return the unit builder, for continued chaining 126 */ 127 public UnitBuilder<U> toBase(UnaryFunction toBase) { 128 this.m_toBase = Objects.requireNonNull(toBase, "toBase function cannot be null"); 129 return this; 130 } 131 132 /** 133 * Sets the name of the new unit. 134 * 135 * @param name the new name 136 * @return the unit builder, for continued chaining 137 */ 138 public UnitBuilder<U> named(String name) { 139 this.m_name = name; 140 return this; 141 } 142 143 /** 144 * Sets the symbol of the new unit. 145 * 146 * @param symbol the new symbol 147 * @return the unit builder, for continued chaining 148 */ 149 public UnitBuilder<U> symbol(String symbol) { 150 this.m_symbol = symbol; 151 return this; 152 } 153 154 /** 155 * Helper for defining units that are a scalar fraction of the base unit, such as centimeters 156 * being 1/100th of the base unit (meters). The fraction value is specified as the denominator of 157 * the fraction, so a centimeter definition would use {@code splitInto(100)} instead of {@code 158 * splitInto(1/100.0)}. 159 * 160 * @param fraction the denominator portion of the fraction of the base unit that a value of 1 in 161 * the derived unit corresponds to 162 * @return the unit builder, for continued chaining 163 */ 164 public UnitBuilder<U> splitInto(double fraction) { 165 if (fraction == 0) { 166 throw new IllegalArgumentException("Fraction must be nonzero"); 167 } 168 169 return toBase(x -> x / fraction).fromBase(b -> b * fraction); 170 } 171 172 /** 173 * Helper for defining units that are a scalar multiple of the base unit, such as kilometers being 174 * 1000x of the base unit (meters). 175 * 176 * @param aggregation the magnitude required for a measure in the base unit to equal a magnitude 177 * of 1 in the derived unit 178 * @return the unit builder, for continued chaining 179 */ 180 public UnitBuilder<U> aggregate(double aggregation) { 181 if (aggregation == 0) { 182 throw new IllegalArgumentException("Aggregation amount must be nonzero"); 183 } 184 185 return toBase(x -> x * aggregation).fromBase(b -> b / aggregation); 186 } 187 188 /** 189 * A functional interface for constructing new units without relying on reflection. 190 * 191 * @param <U> the type of the unit 192 */ 193 @FunctionalInterface 194 public interface UnitConstructorFunction<U extends Unit> { 195 /** 196 * Creates a new unit instance based on its relation to the base unit of measure. 197 * 198 * @param baseUnit the base unit of the unit system 199 * @param toBaseUnits a function that converts values of the new unit to equivalent values in 200 * terms of the base unit 201 * @param fromBaseUnits a function that converts values in the base unit to equivalent values in 202 * terms of the new unit 203 * @param name the name of the new unit 204 * @param symbol the shorthand symbol of the new unit 205 * @return a new unit 206 */ 207 U create( 208 U baseUnit, 209 UnaryFunction toBaseUnits, 210 UnaryFunction fromBaseUnits, 211 String name, 212 String symbol); 213 } 214 215 /** 216 * Creates the new unit based off of the builder methods called prior, passing them to a provided 217 * constructor function. 218 * 219 * @param constructor the function to use to create the new derived unit 220 * @return the new derived unit 221 * @throws NullPointerException if the unit conversions, unit name, or unit symbol were not set 222 */ 223 @SuppressWarnings("unchecked") 224 public U make(UnitConstructorFunction<U> constructor) { 225 Objects.requireNonNull(m_fromBase, "fromBase function was not set"); 226 Objects.requireNonNull(m_toBase, "toBase function was not set"); 227 Objects.requireNonNull(m_name, "new unit name was not set"); 228 Objects.requireNonNull(m_symbol, "new unit symbol was not set"); 229 230 return constructor.create( 231 (U) m_base.getBaseUnit(), 232 m_toBase.pipeTo(m_base.getConverterToBase()), 233 m_base.getConverterFromBase().pipeTo(m_fromBase), 234 m_name, 235 m_symbol); 236 } 237 238 /** 239 * Creates the new unit based off of the builder methods called prior. 240 * 241 * @return the new derived unit 242 * @throws NullPointerException if the unit conversions, unit name, or unit symbol were not set 243 * @throws RuntimeException if the base unit does not define a constructor accepting the 244 * conversion functions, unit name, and unit symbol - in that order 245 */ 246 public U make() { 247 return make( 248 (baseUnit, toBaseUnits, fromBaseUnits, name, symbol) -> { 249 var baseClass = baseUnit.getClass(); 250 251 try { 252 var ctor = getConstructor(baseUnit); 253 254 return ctor.newInstance(baseUnit, toBaseUnits, fromBaseUnits, name, symbol); 255 } catch (InstantiationException e) { 256 throw new RuntimeException("Could not instantiate class " + baseClass.getName(), e); 257 } catch (IllegalAccessException e) { 258 throw new RuntimeException("Could not access constructor", e); 259 } catch (InvocationTargetException e) { 260 throw new RuntimeException( 261 "Constructing " + baseClass.getName() + " raised an exception", e); 262 } catch (NoSuchMethodException e) { 263 throw new RuntimeException( 264 "No compatible constructor " 265 + baseClass.getSimpleName() 266 + "(" 267 + baseClass.getSimpleName() 268 + ", UnaryFunction, UnaryFunction, String, String)", 269 e); 270 } 271 }); 272 } 273 274 @SuppressWarnings({"unchecked", "PMD.AvoidAccessibilityAlteration"}) 275 private static <U extends Unit> Constructor<? extends U> getConstructor(U baseUnit) 276 throws NoSuchMethodException { 277 var baseClass = baseUnit.getClass(); 278 279 var ctor = 280 baseClass.getDeclaredConstructor( 281 baseClass, // baseUnit 282 UnaryFunction.class, // toBaseUnits 283 UnaryFunction.class, // fromBaseUnits 284 String.class, // name 285 String.class); // symbol 286 287 // need to flag the constructor as accessible so we can use private, package-private, 288 // and protected constructors 289 ctor.setAccessible(true); 290 291 return (Constructor<? extends U>) ctor; 292 } 293}