Rename to PhotonVision

This commit is contained in:
Matt
2020-06-27 14:58:03 -07:00
parent b28d0e046e
commit bdbd6b9d18
394 changed files with 1656 additions and 979 deletions

View File

@@ -0,0 +1,10 @@
package org.photonvision.common.util;
import java.awt.*;
import org.opencv.core.Scalar;
public class ColorHelper {
public static Scalar colorToScalar(Color color) {
return new Scalar(color.getBlue(), color.getGreen(), color.getRed());
}
}

View File

@@ -0,0 +1,36 @@
package org.photonvision.common.util;
/** A thread that tries to run at a specified loop time */
public abstract class LoopingRunnable implements Runnable {
protected volatile Long loopTimeMs;
protected abstract void process();
public LoopingRunnable(Long loopTimeMs) {
this.loopTimeMs = loopTimeMs;
}
@Override
public void run() {
while (!Thread.interrupted()) {
var now = System.currentTimeMillis();
// Do the thing
process();
// sleep for the remaining time
var timeElapsed = System.currentTimeMillis() - now;
var delta = loopTimeMs - timeElapsed;
try {
if (delta > 0.0) {
Thread.sleep(delta, 0);
} else {
Thread.sleep(1);
}
} catch (Exception ignored) {
}
}
}
}

View File

@@ -0,0 +1,68 @@
package org.photonvision.common.util;
public class MemoryManager {
private static final long MEGABYTE_FACTOR = 1024L * 1024L;
private int collectionThreshold;
private long collectionPeriodMillis = -1;
private double lastUsedMb = 0;
private long lastCollectionMillis = 0;
public MemoryManager(int collectionThreshold) {
this.collectionThreshold = collectionThreshold;
}
public MemoryManager(int collectionThreshold, long collectionPeriodMillis) {
this.collectionThreshold = collectionThreshold;
this.collectionPeriodMillis = collectionPeriodMillis;
}
public void setCollectionThreshold(int collectionThreshold) {
this.collectionThreshold = collectionThreshold;
}
public void setCollectionPeriodMillis(long collectionPeriodMillis) {
this.collectionPeriodMillis = collectionPeriodMillis;
}
private static long getUsedMemory() {
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
}
private static double getUsedMemoryMB() {
return ((double) getUsedMemory() / MEGABYTE_FACTOR);
}
private void collect() {
System.gc();
System.runFinalization();
}
public void run() {
run(false);
}
public void run(boolean print) {
var usedMem = getUsedMemoryMB();
if (usedMem != lastUsedMb) {
lastUsedMb = usedMem;
if (print) System.out.printf("Memory usage: %.2fMB\n", usedMem);
}
boolean collectionThresholdPassed = usedMem >= collectionThreshold;
boolean collectionPeriodPassed =
collectionPeriodMillis != -1
&& (System.currentTimeMillis() - lastCollectionMillis >= collectionPeriodMillis);
if (collectionThresholdPassed || collectionPeriodPassed) {
collect();
lastCollectionMillis = System.currentTimeMillis();
if (print) {
System.out.printf("Garbage collected at %.2fMB\n", usedMem);
}
}
}
}

View File

@@ -0,0 +1,106 @@
package org.photonvision.common.util;
import edu.wpi.first.wpiutil.RuntimeDetector;
import java.io.IOException;
@SuppressWarnings("unused")
public enum Platform {
// WPILib Supported (JNI)
WINDOWS_32("Windows x32"),
WINDOWS_64("Windows x64"),
LINUX_64("Linux x64"),
LINUX_RASPBIAN("Linux Raspbian"), // Raspberry Pi 3/4
LINUX_AARCH64BIONIC("Linux AARCH64 Bionic"), // Jetson Nano, Jetson TX2
MACOS_64("Mac OS x64"),
// ChameleonVision Supported (Manual install)
LINUX_ARM32("Linux ARM32"), // ODROID XU4, C1+
LINUX_ARM64("Linux ARM64"), // ODROID C2, N2
// Completely unsupported
UNSUPPORTED("Unsupported Platform");
public final String value;
public final boolean isRoot = checkForRoot();
Platform(String value) {
this.value = value;
}
private static final String OS_NAME = System.getProperty("os.name");
private static final String OS_ARCH = System.getProperty("os.arch");
public static final Platform CurrentPlatform = getCurrentPlatform();
private static String UnknownPlatformString =
String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
public boolean isWindows() {
return this == WINDOWS_64 || this == WINDOWS_32;
}
public boolean isLinux() {
return this == LINUX_64 || this == LINUX_RASPBIAN || this == LINUX_ARM64;
}
public boolean isMac() {
return this == MACOS_64;
}
public static boolean isRaspberryPi() {
return CurrentPlatform.equals(LINUX_RASPBIAN);
}
private static ShellExec shell = new ShellExec(true, false);
@SuppressWarnings("StatementWithEmptyBody")
private boolean checkForRoot() {
if (isLinux() || isMac()) {
try {
shell.execute("id", null, true, "-u");
} catch (IOException e) {
e.printStackTrace();
}
while (!shell.isOutputCompleted()) {
// TODO: add timeout
}
if (shell.getExitCode() == 0) {
return shell.getOutput().split("\n")[0].equals("0");
}
} else {
return true;
}
return false;
}
private static Platform getCurrentPlatform() {
if (RuntimeDetector.isWindows()) {
if (RuntimeDetector.is32BitIntel()) return WINDOWS_32;
if (RuntimeDetector.is64BitIntel()) return WINDOWS_64;
}
if (RuntimeDetector.isMac()) {
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
if (RuntimeDetector.is64BitIntel()) return MACOS_64;
}
if (RuntimeDetector.isLinux()) {
if (RuntimeDetector.is32BitIntel()) return UNSUPPORTED;
if (RuntimeDetector.is64BitIntel()) return LINUX_64;
if (RuntimeDetector.isRaspbian()) return LINUX_RASPBIAN;
}
System.out.println(UnknownPlatformString);
return Platform.UNSUPPORTED;
}
public String toString() {
if (this.equals(UNSUPPORTED)) {
return UnknownPlatformString;
} else {
return this.value;
}
}
}

View File

@@ -0,0 +1,42 @@
package org.photonvision.common.util;
public class ReflectionUtils {
public static StackTraceElement[] getFullStackTrace() {
return Thread.currentThread().getStackTrace();
}
public static StackTraceElement getNthCaller(int n) {
if (n < 0) n = 0;
return Thread.currentThread().getStackTrace()[n];
}
public static String getCallerClassName() {
StackTraceElement[] stElements = Thread.currentThread().getStackTrace();
for (int i = 1; i < stElements.length; i++) {
StackTraceElement ste = stElements[i];
if (!ste.getClassName().equals(ReflectionUtils.class.getName())
&& ste.getClassName().indexOf("java.lang.Thread") != 0) {
return ste.getClassName();
}
}
return null;
}
public static String getCallerCallerClassName() {
StackTraceElement[] stElements = Thread.currentThread().getStackTrace();
String callerClassName = null;
for (int i = 1; i < stElements.length; i++) {
StackTraceElement ste = stElements[i];
if (!ste.getClassName().equals(ReflectionUtils.class.getName())
&& ste.getClassName().indexOf("java.lang.Thread") != 0) {
if (callerClassName == null) {
callerClassName = ste.getClassName();
} else if (!callerClassName.equals(ste.getClassName())) {
return ste.getClassName();
}
}
}
return null;
}
}

View File

@@ -0,0 +1,179 @@
package org.photonvision.common.util;
import java.io.*;
/** Execute external process and optionally read output buffer. */
@SuppressWarnings({"unused", "ConstantConditions"})
public class ShellExec {
private int exitCode;
private boolean readOutput, readError;
private StreamGobbler errorGobbler, outputGobbler;
public ShellExec() {
this(false, false);
}
public ShellExec(boolean readOutput, boolean readError) {
this.readOutput = readOutput;
this.readError = readError;
}
/**
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
* && ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
*
* @param command Bash command to execute
* @return true if bash got started, but your command may have failed.
*/
public int executeBashCommand(String command) throws IOException {
boolean wait = true;
boolean success = false;
Runtime r = Runtime.getRuntime();
// Use bash -c so we can handle things like multi commands separated by ; and
// things like quotes, $, |, and \. My tests show that command comes as
// one argument to bash, so we do not need to quote it to make it one thing.
// Also, exec may object if it does not have an executable file as the first thing,
// so having bash here makes it happy provided bash is installed and in path.
String[] commands = {"bash", "-c", command};
Process process = r.exec(commands);
// Consume streams, older jvm's had a memory leak if streams were not read,
// some other jvm+OS combinations may block unless streams are consumed.
return doProcess(wait, process);
}
/**
* Execute a command in current folder, and wait for process to end
*
* @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
* @param args 0..n command line arguments
* @return process exit code
*/
public int execute(String command, String... args) throws IOException {
return execute(command, null, true, args);
}
/**
* Execute a command.
*
* @param command command ("c:/some/folder/script.bat" or "some/folder/script.sh")
* @param workdir working directory or NULL to use command folder
* @param wait wait for process to end
* @param args 0..n command line arguments
* @return process exit code
*/
public int execute(String command, String workdir, boolean wait, String... args)
throws IOException {
String[] cmdArr;
if (args != null && args.length > 0) {
cmdArr = new String[1 + args.length];
cmdArr[0] = command;
System.arraycopy(args, 0, cmdArr, 1, args.length);
} else {
cmdArr = new String[] {command};
}
ProcessBuilder pb = new ProcessBuilder(cmdArr);
File workingDir = (workdir == null ? new File(command).getParentFile() : new File(workdir));
pb.directory(workingDir);
Process process = pb.start();
// Consume streams, older jvm's had a memory leak if streams were not read,
// some other jvm+OS combinations may block unless streams are consumed.
return doProcess(wait, process);
}
private int doProcess(boolean wait, Process process) {
errorGobbler = new StreamGobbler(process.getErrorStream(), readError);
outputGobbler = new StreamGobbler(process.getInputStream(), readOutput);
errorGobbler.start();
outputGobbler.start();
exitCode = 0;
if (wait) {
try {
process.waitFor();
exitCode = process.exitValue();
} catch (InterruptedException ignored) {
}
}
return exitCode;
}
public int getExitCode() {
return exitCode;
}
public boolean isOutputCompleted() {
return (outputGobbler != null && outputGobbler.isCompleted());
}
public boolean isErrorCompleted() {
return (errorGobbler != null && errorGobbler.isCompleted());
}
public String getOutput() {
return (outputGobbler != null ? outputGobbler.getOutput() : null);
}
public String getError() {
return (errorGobbler != null ? errorGobbler.getOutput() : null);
}
// ********************************************
// ********************************************
/**
* StreamGobbler reads inputstream to "gobble" it. This is used by Executor class when running a
* commandline applications. Gobblers must read/purge INSTR and ERRSTR process streams.
* http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html?page=4
*/
@SuppressWarnings("WeakerAccess")
private static class StreamGobbler extends Thread {
private InputStream is;
private StringBuilder output;
private volatile boolean completed; // mark volatile to guarantee a thread safety
public StreamGobbler(InputStream is, boolean readStream) {
this.is = is;
this.output = (readStream ? new StringBuilder(256) : null);
}
public void run() {
completed = false;
try {
String NL = System.getProperty("line.separator", "\r\n");
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (output != null) output.append(line).append(NL);
}
} catch (IOException ex) {
// ex.printStackTrace();
}
completed = true;
}
/**
* Get inputstream buffer or null if stream was not consumed.
*
* @return Output stream
*/
public String getOutput() {
return (output != null ? output.toString() : null);
}
/**
* Is input stream completed.
*
* @return if input stream is completed
*/
public boolean isCompleted() {
return completed;
}
}
}

View File

@@ -0,0 +1,178 @@
package org.photonvision.common.util;
import edu.wpi.cscore.CameraServerCvJNI;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Path;
import org.opencv.core.Mat;
import org.opencv.highgui.HighGui;
public class TestUtils {
@SuppressWarnings("unused")
public enum WPI2019Image {
kCargoAngledDark48in(1.2192),
kCargoSideStraightDark36in(0.9144),
kCargoSideStraightDark60in(1.524),
kCargoSideStraightDark72in(1.8288),
kCargoSideStraightPanelDark36in(0.9144),
kCargoStraightDark19in(0.4826),
kCargoStraightDark24in(0.6096),
kCargoStraightDark48in(1.2192),
kCargoStraightDark72in(1.8288),
kCargoStraightDark72in_HighRes(1.8288),
kCargoStraightDark90in(2.286);
public static double FOV = 68.5;
public final double distanceMeters;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1);
return Path.of("2019", "WPI", filename + ".jpg");
}
WPI2019Image(double distanceMeters) {
this.distanceMeters = distanceMeters;
this.path = getPath();
}
}
@SuppressWarnings("unused")
public enum WPI2020Image {
kBlueGoal_060in_Center(1.524),
kBlueGoal_084in_Center(2.1336),
kBlueGoal_084in_Center_720p(2.1336),
kBlueGoal_108in_Center(2.7432),
kBlueGoal_132in_Center(3.3528),
kBlueGoal_156in_Center(3.9624),
kBlueGoal_180in_Center(4.572),
kBlueGoal_156in_Left(3.9624),
kBlueGoal_224in_Left(5.6896),
kBlueGoal_228in_ProtectedZone(5.7912),
kBlueGoal_330in_ProtectedZone(8.382),
kBlueGoal_Far_ProtectedZone(10.668), // TODO: find a more accurate distance
kRedLoading_016in_Down(0.4064),
kRedLoading_030in_Down(0.762),
kRedLoading_048in_Down(1.2192),
kRedLoading_048in(1.2192),
kRedLoading_060in(1.524),
kRedLoading_084in(2.1336),
kRedLoading_108in(2.7432);
public static double FOV = 68.5;
public final double distanceMeters;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1).replace('_', '-');
return Path.of("2020", "WPI", filename + ".jpg");
}
WPI2020Image(double distanceMeters) {
this.distanceMeters = distanceMeters;
this.path = getPath();
}
}
public enum PolygonTestImages {
kPolygons;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1).toLowerCase();
return Path.of("polygons", filename + ".png");
}
PolygonTestImages() {
this.path = getPath();
}
}
public enum PowercellTestImages {
kPowercell_test_1,
kPowercell_test_2,
kPowercell_test_3,
kPowercell_test_4,
kPowercell_test_5,
kPowercell_test_6;
public final Path path;
Path getPath() {
var filename = this.toString().substring(1).toLowerCase();
return Path.of(filename + ".png");
}
PowercellTestImages() {
this.path = getPath();
}
}
private static Path getResourcesFolderPath() {
return Path.of("src", "test", "resources").toAbsolutePath();
}
public static Path getTestImagesPath() {
return getResourcesFolderPath().resolve("testimages");
}
public static Path getCalibrationPath() {
return getResourcesFolderPath().resolve("calibration");
}
public static Path getPowercellPath() {
return getTestImagesPath().resolve("polygons").resolve("powercells");
}
public static Path getWPIImagePath(WPI2020Image image) {
return getTestImagesPath().resolve(image.path);
}
public static Path getWPIImagePath(WPI2019Image image) {
return getTestImagesPath().resolve(image.path);
}
public static Path getPolygonImagePath(PolygonTestImages image) {
return getTestImagesPath().resolve(image.path);
}
public static Path getPowercellImagePath(PowercellTestImages image) {
return getPowercellPath().resolve(image.path);
}
public static void loadLibraries() {
try {
CameraServerCvJNI.forceLoad();
} catch (IOException e) {
// ignored
}
}
private static int DefaultTimeoutMillis = 5000;
public static void showImage(Mat frame, String title, int timeoutMs) {
try {
HighGui.imshow(title, frame);
HighGui.waitKey(timeoutMs);
HighGui.destroyAllWindows();
} catch (HeadlessException ignored) {
}
}
public static void showImage(Mat frame, int timeoutMs) {
showImage(frame, "", timeoutMs);
}
public static void showImage(Mat frame, String title) {
showImage(frame, title, DefaultTimeoutMillis);
}
public static void showImage(Mat frame) {
showImage(frame, DefaultTimeoutMillis);
}
}

View File

@@ -0,0 +1,56 @@
package org.photonvision.common.util.file;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.util.Platform;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class FileUtils {
private FileUtils() {}
private static Logger logger = new Logger(FileUtils.class, LogGroup.General);
private static final Set<PosixFilePermission> allReadWriteExecutePerms =
new HashSet<>(Arrays.asList(PosixFilePermission.values()));
public static void setFilePerms(Path path) throws IOException {
if (!Platform.CurrentPlatform.isWindows()) {
File thisFile = path.toFile();
Set<PosixFilePermission> perms =
Files.readAttributes(path, PosixFileAttributes.class).permissions();
if (!perms.equals(allReadWriteExecutePerms)) {
logger.info("Setting perms on" + path.toString());
Files.setPosixFilePermissions(path, perms);
if (thisFile.isDirectory()) {
for (File subfile : thisFile.listFiles()) {
setFilePerms(subfile.toPath());
}
}
}
}
}
public static void setAllPerms(Path path) {
if (!Platform.CurrentPlatform.isWindows()) {
String command = String.format("chmod 777 -R %s", path.toString());
try {
Process p = Runtime.getRuntime().exec(command);
p.waitFor();
} catch (Exception e) {
e.printStackTrace();
}
} else {
logger.info("Cannot set directory permissions on Windows!");
}
}
}

View File

@@ -0,0 +1,86 @@
package org.photonvision.common.util.file;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
public class JacksonUtils {
public static <T> void serializer(Path path, T object) throws IOException {
serializer(path, object, false);
}
public static <T> void serializer(Path path, T object, boolean forceSync) throws IOException {
PolymorphicTypeValidator ptv =
BasicPolymorphicTypeValidator.builder().allowIfBaseType(object.getClass()).build();
ObjectMapper objectMapper =
JsonMapper.builder()
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
.build();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
saveJsonString(json, path, forceSync);
}
public static <T> T deserialize(Path path, Class<T> ref) throws IOException {
PolymorphicTypeValidator ptv =
BasicPolymorphicTypeValidator.builder().allowIfBaseType(ref).build();
ObjectMapper objectMapper =
JsonMapper.builder()
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)
.build();
File jsonFile = new File(path.toString());
if (jsonFile.exists() && jsonFile.length() > 0) {
return objectMapper.readValue(jsonFile, ref);
}
return null;
}
public static <T> T deserialize(Path path, Class<T> ref, StdDeserializer<T> deserializer)
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(ref, deserializer);
objectMapper.registerModule(module);
File jsonFile = new File(path.toString());
if (jsonFile.exists() && jsonFile.length() > 0) {
return objectMapper.readValue(jsonFile, ref);
}
return null;
}
public static <T> void serialize(Path path, T object, Class<T> ref, StdSerializer<T> serializer)
throws IOException {
serialize(path, object, ref, serializer, false);
}
public static <T> void serialize(
Path path, T object, Class<T> ref, StdSerializer<T> serializer, boolean forceSync)
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ref, serializer);
objectMapper.registerModule(module);
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
saveJsonString(json, path, forceSync);
}
private static void saveJsonString(String json, Path path, boolean forceSync) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(path.toFile());
fileOutputStream.write(json.getBytes());
fileOutputStream.flush();
if (forceSync) {
FileDescriptor fileDescriptor = fileOutputStream.getFD();
fileDescriptor.sync();
}
fileOutputStream.close();
}
}

View File

@@ -0,0 +1,39 @@
package org.photonvision.common.util.math;
import java.util.ArrayList;
import java.util.List;
public class IPUtils {
public static boolean isValidIPV4(final String ip) {
String PATTERN =
"^((0|1\\d?\\d?|2[0-4]?\\d?|25[0-5]?|[3-9]\\d?)\\.){3}(0|1\\d?\\d?|2[0-4]?\\d?|25[0-5]?|[3-9]\\d?)$";
return ip.matches(PATTERN);
}
public static List<Byte> getDigitBytes(int num) {
List<Byte> digits = new ArrayList<>();
collectDigitBytes(num, digits);
return digits;
}
private static void collectDigitBytes(int num, List<Byte> digits) {
if (num / 10 > 0) {
collectDigitBytes(num / 10, digits);
}
digits.add((byte) (num % 10));
}
public static List<Integer> getDigits(int num) {
List<Integer> digits = new ArrayList<>();
collectDigits(num, digits);
return digits;
}
private static void collectDigits(int num, List<Integer> digits) {
if (num / 10 > 0) {
collectDigits(num / 10, digits);
}
digits.add(num % 10);
}
}

View File

@@ -0,0 +1,33 @@
package org.photonvision.common.util.math;
import org.apache.commons.math3.util.FastMath;
public class MathUtils {
MathUtils() {}
public static double sigmoid(Number x) {
double bias = 0;
double a = 5;
double b = -0.05;
double k = 200;
if (x.doubleValue() < 50) {
bias = -1.338;
}
return ((k / (1 + Math.pow(Math.E, (a + (b * x.doubleValue()))))) + bias);
}
public static double toSlope(Number angle) {
return FastMath.atan(FastMath.toRadians(angle.doubleValue() - 90));
}
public static double roundTo(double value, int to) {
double toMult = Math.pow(10, to);
return (double) Math.round(value * toMult) / toMult;
}
public static double nanosToMillis(long nanos) {
return nanos / 1000000.0;
}
}

View File

@@ -0,0 +1,27 @@
package org.photonvision.common.util.numbers;
import org.opencv.core.Point;
public class DoubleCouple extends NumberCouple<Double> {
public DoubleCouple() {
super(0.0, 0.0);
}
public DoubleCouple(Double first, Double second) {
super(first, second);
}
public DoubleCouple(Point point) {
super(point.x, point.y);
}
public Point toPoint() {
return new Point(first, second);
}
public void fromPoint(Point point) {
first = point.x;
second = point.y;
}
}

View File

@@ -0,0 +1,12 @@
package org.photonvision.common.util.numbers;
public class IntegerCouple extends NumberCouple<Integer> {
public IntegerCouple() {
super(0, 0);
}
public IntegerCouple(Integer first, Integer second) {
super(first, second);
}
}

View File

@@ -0,0 +1,58 @@
package org.photonvision.common.util.numbers;
import com.fasterxml.jackson.annotation.JsonIgnore;
public abstract class NumberCouple<T extends Number> {
protected T first;
protected T second;
public NumberCouple(T first, T second) {
this.first = first;
this.second = second;
}
public void setFirst(T first) {
this.first = first;
}
public T getFirst() {
return first;
}
public void setSecond(T second) {
this.second = second;
}
public T getSecond() {
return second;
}
public void set(T first, T second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof NumberCouple)) {
return false;
}
var couple = (NumberCouple) obj;
if (!couple.first.equals(first)) {
return false;
}
if (!couple.second.equals(second)) {
return false;
}
return true;
}
@JsonIgnore
public boolean isEmpty() {
return first.intValue() == 0 && second.intValue() == 0;
}
}

View File

@@ -0,0 +1,98 @@
package org.photonvision.common.util.numbers;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.StringJoiner;
@SuppressWarnings("unused")
public class NumberListUtils {
/**
* @param collection an ArrayList of Comparable objects
* @return the median of collection
*/
public static <T extends Number> double median(List<T> collection, Comparator<T> comp) {
double result;
int n = collection.size() / 2;
if (collection.size() % 2 == 0) // even number of items; find the middle two and average them
result =
(nthSmallest(collection, n - 1, comp).doubleValue()
+ nthSmallest(collection, n, comp).doubleValue())
/ 2.0;
else // odd number of items; return the one in the middle
result = nthSmallest(collection, n, comp).doubleValue();
return result;
}
public static <T extends Number> String toString(List<T> collection) {
return toString(collection, "");
}
public static <T extends Number> String toString(List<T> collection, String suffix) {
StringJoiner joiner = new StringJoiner(", ");
for (T x : collection) {
String s = x.doubleValue() + suffix;
joiner.add(s);
}
return joiner.toString();
}
/**
* @param collection an ArrayList of Numbers
* @return the mean of collection
*/
public static double mean(final List<? extends Number> collection) {
BigDecimal sum = BigDecimal.ZERO;
for (final Number number : collection) {
sum = sum.add(BigDecimal.valueOf(number.doubleValue()));
}
return (sum.doubleValue() / collection.size());
}
/**
* @param collection a collection of Comparable objects
* @param n the position of the desired object, using the ordering defined on the collection
* elements
* @return the nth smallest object
*/
public static <T> T nthSmallest(List<T> collection, int n, Comparator<T> comp) {
T result, pivot;
ArrayList<T> underPivot = new ArrayList<>(),
overPivot = new ArrayList<>(),
equalPivot = new ArrayList<>();
// choosing a pivot is a whole topic in itself.
// this implementation uses the simple strategy of grabbing something from the middle of the
// ArrayList.
pivot = collection.get(n / 2);
// split collection into 3 lists based on comparison with the pivot
for (T obj : collection) {
int order = comp.compare(obj, pivot);
if (order < 0) // obj < pivot
underPivot.add(obj);
else if (order > 0) // obj > pivot
overPivot.add(obj);
else // obj = pivot
equalPivot.add(obj);
} // for each obj in collection
// recurse on the appropriate collection
if (n < underPivot.size()) result = nthSmallest(underPivot, n, comp);
else if (n < underPivot.size() + equalPivot.size()) // equal to pivot; just return it
result = pivot;
else // everything in underPivot and equalPivot is too small. Adjust n accordingly in the
// recursion.
result = nthSmallest(overPivot, n - underPivot.size() - equalPivot.size(), comp);
return result;
}
}