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. 013 * 014 * @param <U> the type of the unit 015 */ 016public final class UnitBuilder<U extends Unit<U>> { 017 private final U m_base; 018 private UnaryFunction m_fromBase; 019 private UnaryFunction m_toBase; 020 private String m_name; 021 private String m_symbol; 022 023 /** 024 * Creates a new unit builder object, building off of a base unit. The base unit does not have to 025 * be <i>the</i> base unit of its unit system; furlongs work just as well here as meters. 026 * 027 * @param base the unit to base the new unit off of 028 */ 029 public UnitBuilder(U base) { 030 this.m_base = Objects.requireNonNull(base, "Base unit cannot be null"); 031 } 032 033 /** 034 * Sets the unit conversions based on a simple offset. The new unit will have its values equal to 035 * (base value - offset). 036 * 037 * @param offset the offset 038 * @return this builder 039 */ 040 public UnitBuilder<U> offset(double offset) { 041 m_toBase = derivedValue -> derivedValue + offset; 042 m_fromBase = baseValue -> baseValue - offset; 043 return this; 044 } 045 046 /** 047 * Maps a value {@code value} in the range {@code [inMin..inMax]} to an output in the range {@code 048 * [outMin..outMax]}. Inputs outside the bounds will be mapped correspondingly to outputs outside 049 * the output bounds. Inputs equal to {@code inMin} will be mapped to {@code outMin}, and inputs 050 * equal to {@code inMax} will similarly be mapped to {@code outMax}. 051 * 052 * @param value the value to map 053 * @param inMin the minimum input value (does not have to be absolute) 054 * @param inMax the maximum input value (does not have to be absolute) 055 * @param outMin the minimum output value (does not have to be absolute) 056 * @param outMax the maximum output value (does not have to be absolute) 057 * @return the mapped output 058 */ 059 // NOTE: This method lives here instead of in MappingBuilder because inner classes can't 060 // define static methods prior to Java 16. 061 private static double mapValue( 062 double value, double inMin, double inMax, double outMin, double outMax) { 063 return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; 064 } 065 066 /** Helper class used for safely chaining mapping builder calls. */ 067 public class MappingBuilder { 068 private final double m_minInput; 069 private final double m_maxInput; 070 071 private MappingBuilder(double minInput, double maxInput) { 072 this.m_minInput = minInput; 073 this.m_maxInput = maxInput; 074 } 075 076 /** 077 * Finalizes the mapping by defining the output range. 078 * 079 * @param minOutput the minimum output value (does not have to be absolute) 080 * @param maxOutput the maximum output value (does not have to be absolute) 081 * @return the unit builder, for continued chaining 082 */ 083 public UnitBuilder<U> toOutputRange(double minOutput, double maxOutput) { 084 UnitBuilder.this.m_fromBase = x -> mapValue(x, m_minInput, m_maxInput, minOutput, maxOutput); 085 UnitBuilder.this.m_toBase = y -> mapValue(y, minOutput, maxOutput, m_minInput, m_maxInput); 086 return UnitBuilder.this; 087 } 088 } 089 090 /** 091 * Defines a mapping for values within the given input range. This method call should be 092 * immediately followed by {@code .toOutputRange}, eg {@code mappingInputRange(1, 093 * 2).toOutputRange(3, 4)}, which will return the unit builder for continued chaining. 094 * 095 * @param minBase the minimum input value (does not have to be absolute) 096 * @param maxBase the maximum output value (does not have to be absolute) 097 * @return a builder object used to define the output range 098 */ 099 public MappingBuilder mappingInputRange(double minBase, double maxBase) { 100 return new MappingBuilder(minBase, maxBase); 101 } 102 103 /** 104 * Sets the conversion function to transform values in the base unit to values in the derived 105 * unit. 106 * 107 * @param fromBase the conversion function 108 * @return the unit builder, for continued chaining 109 */ 110 public UnitBuilder<U> fromBase(UnaryFunction fromBase) { 111 this.m_fromBase = Objects.requireNonNull(fromBase, "fromBase function cannot be null"); 112 return this; 113 } 114 115 /** 116 * Sets the conversion function to transform values in the derived unit to values in the base 117 * unit. 118 * 119 * @param toBase the conversion function 120 * @return the unit builder, for continued chaining 121 */ 122 public UnitBuilder<U> toBase(UnaryFunction toBase) { 123 this.m_toBase = Objects.requireNonNull(toBase, "toBase function cannot be null"); 124 return this; 125 } 126 127 /** 128 * Sets the name of the new unit. 129 * 130 * @param name the new name 131 * @return the unit builder, for continued chaining 132 */ 133 public UnitBuilder<U> named(String name) { 134 this.m_name = name; 135 return this; 136 } 137 138 /** 139 * Sets the symbol of the new unit. 140 * 141 * @param symbol the new symbol 142 * @return the unit builder, for continued chaining 143 */ 144 public UnitBuilder<U> symbol(String symbol) { 145 this.m_symbol = symbol; 146 return this; 147 } 148 149 /** 150 * Helper for defining units that are a scalar fraction of the base unit, such as centimeters 151 * being 1/100th of the base unit (meters). The fraction value is specified as the denominator of 152 * the fraction, so a centimeter definition would use {@code splitInto(100)} instead of {@code 153 * splitInto(1/100.0)}. 154 * 155 * @param fraction the denominator portion of the fraction of the base unit that a value of 1 in 156 * the derived unit corresponds to 157 * @return the unit builder, for continued chaining 158 */ 159 public UnitBuilder<U> splitInto(double fraction) { 160 if (fraction == 0) { 161 throw new IllegalArgumentException("Fraction must be nonzero"); 162 } 163 164 return toBase(x -> x / fraction).fromBase(b -> b * fraction); 165 } 166 167 /** 168 * Helper for defining units that are a scalar multiple of the base unit, such as kilometers being 169 * 1000x of the base unit (meters). 170 * 171 * @param aggregation the magnitude required for a measure in the base unit to equal a magnitude 172 * of 1 in the derived unit 173 * @return the unit builder, for continued chaining 174 */ 175 public UnitBuilder<U> aggregate(double aggregation) { 176 if (aggregation == 0) { 177 throw new IllegalArgumentException("Aggregation amount must be nonzero"); 178 } 179 180 return toBase(x -> x * aggregation).fromBase(b -> b / aggregation); 181 } 182 183 /** 184 * Creates the new unit based off of the builder methods called prior. 185 * 186 * @return the new derived unit 187 * @throws NullPointerException if the unit conversions, unit name, or unit symbol were not set 188 * @throws RuntimeException if the base unit does not define a constructor accepting the 189 * conversion functions, unit name, and unit symbol - in that order 190 */ 191 @SuppressWarnings("PMD.AvoidAccessibilityAlteration") 192 public U make() { 193 Objects.requireNonNull(m_fromBase, "fromBase function was not set"); 194 Objects.requireNonNull(m_toBase, "toBase function was not set"); 195 Objects.requireNonNull(m_name, "new unit name was not set"); 196 Objects.requireNonNull(m_symbol, "new unit symbol was not set"); 197 Class<? extends U> baseType = m_base.m_baseType; 198 try { 199 Constructor<? extends U> ctor = 200 baseType.getDeclaredConstructor( 201 UnaryFunction.class, // toBaseUnits 202 UnaryFunction.class, // fromBaseUnits 203 String.class, // name 204 String.class); // symbol 205 // need to flag the constructor as accessible so we can use private, package-private, and 206 // protected constructors 207 ctor.setAccessible(true); 208 return ctor.newInstance( 209 m_toBase.pipeTo(m_base.getConverterToBase()), 210 m_base.getConverterFromBase().pipeTo(m_fromBase), 211 m_name, 212 m_symbol); 213 } catch (InstantiationException e) { 214 throw new RuntimeException("Could not instantiate class " + baseType.getName(), e); 215 } catch (IllegalAccessException e) { 216 throw new RuntimeException("Could not access constructor", e); 217 } catch (InvocationTargetException e) { 218 throw new RuntimeException("Constructing " + baseType.getName() + " raised an exception", e); 219 } catch (NoSuchMethodException e) { 220 throw new RuntimeException("No compatible constructor", e); 221 } 222 } 223}