diff --git a/build.gradle b/build.gradle index f39525c50d..f27e395866 100644 --- a/build.gradle +++ b/build.gradle @@ -232,6 +232,7 @@ project(":core") { compile group: 'org.bytedeco.javacpp-presets', name: 'videoinput', version: '0.200-1.1', classifier: os compile group: 'org.python', name: 'jython', version: '2.7.0' compile group: 'com.thoughtworks.xstream', name: 'xstream', version: '1.4.9' + compile group: 'com.github.zafarkhaja', name: 'java-semver', version: '0.9.0' compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.5' compile group: 'com.google.guava', name: 'guava', version: '20.0' compile group: 'com.google.auto.value', name: 'auto-value', version: '1.3' diff --git a/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java b/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java new file mode 100644 index 0000000000..0d7018db43 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/AbstractPipelineEntry.java @@ -0,0 +1,151 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.Socket; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.CaseFormat; + +import org.apache.commons.lang.RandomStringUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.NoSuchElementException; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A partial implementation of {@code PipelineEntry} that implements the removal and ID methods. + */ +public abstract class AbstractPipelineEntry implements PipelineEntry { + + @VisibleForTesting + static final IdPool idPool = IdPool.INSTANCE; + + protected final Object removedLock = new Object(); + private boolean removed = false; + private String id; + + /** + * Creates a unique ID string for the given subclass. + * + * @param entryClass the subclass + * + * @return an ID string for the subclass. + */ + protected static String makeId(Class entryClass) { + String id; + do { + id = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, entryClass.getSimpleName()) + + "." + RandomStringUtils.randomAlphanumeric(8); + } while (idPool.checkId(entryClass, id)); + return id; + } + + /** + * Creates a new pipeline entry. + * + * @param id the ID of the new entry. This must be unique for all instances of the concrete class. + * + * @throws NullPointerException if the ID is null + * @throws IllegalArgumentException if the ID is already taken + */ + protected AbstractPipelineEntry(String id) { + idPool.get(getClass()).add(id); + this.id = id; + } + + @Override + public final Socket getSocketByUid(String uid) throws NoSuchElementException { + checkNotNull(uid, "UID"); + for (InputSocket in : getInputSockets()) { + if (in.getUid().equals(uid)) { + return in; + } + } + for (OutputSocket out : getOutputSockets()) { + if (out.getUid().equals(uid)) { + return out; + } + } + throw new NoSuchElementException(uid); + } + + /** + * Cleans up this entry, such as by freeing resources or disabling callbacks. + */ + protected abstract void cleanUp(); + + @Override + public final void setRemoved() { + synchronized (removedLock) { + cleanUp(); + idPool.removeId(this); + removed = true; + } + } + + @Override + public final boolean removed() { + synchronized (removedLock) { + return removed; + } + } + + @Override + public final void setId(String id) throws NullPointerException, IllegalArgumentException { + checkNotNull(id, "The ID cannot be null"); + boolean inDeserialization = Arrays.stream(new Exception().getStackTrace()) + .map(e -> e.getClassName()) + .anyMatch(n -> n.matches(".*(Step|Source)Converter")); + if (!inDeserialization) { + throw new IllegalStateException( + "This method may only be called during project deserialization"); + } + idPool.get(getClass()).add(id); + this.id = id; + } + + @Override + public final String getId() { + return id; + } + + @Override + public String toString() { + return getId(); + } + + /** + * Pool of used IDs. + */ + @VisibleForTesting + static class IdPool extends HashMap, Set> { + private static final IdPool INSTANCE = new IdPool(); + + /** + * Checks if an ID is already used by an instance of a pipeline entry class. + */ + public boolean checkId(Class clazz, String id) { + return get(clazz).contains(id); + } + + /** + * Removes the ID of the given entry. + */ + public void removeId(PipelineEntry e) { + get(e.getClass()).remove(e.getId()); + } + + @Override + @SuppressWarnings("unchecked") + public Set get(Object key) { + return computeIfAbsent((Class) key, k -> new HashSet<>()); + } + + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java b/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java index 3fe4e49b4f..9a8ac845cc 100644 --- a/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java +++ b/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java @@ -19,6 +19,7 @@ import edu.wpi.grip.core.util.ExceptionWitness; import edu.wpi.grip.core.util.GripMode; +import com.github.zafarkhaja.semver.Version; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.SubscriberExceptionContext; import com.google.inject.AbstractModule; @@ -130,6 +131,9 @@ public void hear(TypeLiteral type, TypeEncounter encounter) { // Allow for just injecting the settings provider, instead of the whole pipeline bind(SettingsProvider.class).to(Pipeline.class); + // Bind the current GRIP version. + bind(Version.class).toInstance(VersionManager.CURRENT_VERSION); + install(new FactoryModuleBuilder().build(new TypeLiteral>() { })); diff --git a/core/src/main/java/edu/wpi/grip/core/Palette.java b/core/src/main/java/edu/wpi/grip/core/Palette.java index 1768fc5e6a..3288d45a1c 100644 --- a/core/src/main/java/edu/wpi/grip/core/Palette.java +++ b/core/src/main/java/edu/wpi/grip/core/Palette.java @@ -1,13 +1,18 @@ package edu.wpi.grip.core; import edu.wpi.grip.core.events.OperationAddedEvent; +import edu.wpi.grip.core.sockets.Socket; +import com.google.common.collect.ImmutableList; import com.google.common.eventbus.Subscribe; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.inject.Singleton; @@ -24,10 +29,54 @@ public class Palette { @Subscribe public void onOperationAdded(OperationAddedEvent event) { - final OperationMetaData operation = event.getOperation(); - map(operation.getDescription().name(), operation); - for (String alias : operation.getDescription().aliases()) { - map(alias, operation); + final OperationMetaData operationData = event.getOperation(); + map(operationData.getDescription().name(), operationData); + for (String alias : operationData.getDescription().aliases()) { + map(alias, operationData); + } + // Validate that every input and output socket has a unique name and UID + Operation operation = operationData.getOperationSupplier().get(); + try { + final List sockets = new ImmutableList.Builder() + .addAll(operation.getInputSockets()) + .addAll(operation.getOutputSockets()) + .build(); + checkDuplicates( + operationData, + "input socket names", + operation.getInputSockets(), s -> s.getSocketHint().getIdentifier() + ); + checkDuplicates( + operationData, + "output socket names", + operation.getOutputSockets(), s -> s.getSocketHint().getIdentifier() + ); + checkDuplicates(operationData, "socket IDs", sockets, Socket::getUid); + } finally { + operation.cleanUp(); + } + } + + private static void checkDuplicates(OperationMetaData operationMetaData, + String type, + List list, + Function extractionFunction) { + List duplicates = list.stream() + .map(extractionFunction) + .collect(Collectors.toList()); + list.stream() + .map(extractionFunction) + .distinct() + .forEach(duplicates::remove); + if (!duplicates.isEmpty()) { + throw new IllegalArgumentException( + String.format( + "Duplicate %s found in operation %s: %s", + type, + operationMetaData.getDescription().name(), + duplicates + ) + ); } } @@ -36,6 +85,7 @@ public void onOperationAdded(OperationAddedEvent event) { * * @param key The key the operation should be mapped to * @param operation The operation to map the key to + * * @throws IllegalArgumentException if the key is already in the {@link #operations} map. */ private void map(String key, OperationMetaData operation) { diff --git a/core/src/main/java/edu/wpi/grip/core/PipelineEntry.java b/core/src/main/java/edu/wpi/grip/core/PipelineEntry.java new file mode 100644 index 0000000000..89091a9e2b --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/PipelineEntry.java @@ -0,0 +1,61 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.Socket; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * An entry in the pipeline. + */ +public interface PipelineEntry { + + /** + * Gets a read-only list of the input sockets to this entry. + * This may be empty, but may never be null. + */ + List getInputSockets(); + + /** + * Gets a read-only list of the output sockets from this entry. + * This may be empty, but may never be null. + */ + List getOutputSockets(); + + /** + * Gets the socket with the given ID in this entry. + * + * @param uid the UID of the socket to get + * + * @throws NoSuchElementException if there is no socket with the given UID, + */ + Socket getSocketByUid(String uid) throws NoSuchElementException; + + /** + * Sets this entry as removed from the pipeline. + */ + void setRemoved(); + + /** + * Checks if this entry has been removed from the pipeline. + */ + boolean removed(); + + /** + * Sets the ID of this entry. This should only be used by deserialization. + * + * @param newId the new ID + * + * @throws NullPointerException if the ID is null + * @throws IllegalArgumentException if the ID is already taken + */ + void setId(String newId) throws NullPointerException, IllegalArgumentException; + + /** + * Gets the ID of this entry. This ID should be unique for all instances of the concrete class. + */ + String getId(); + +} diff --git a/core/src/main/java/edu/wpi/grip/core/Source.java b/core/src/main/java/edu/wpi/grip/core/Source.java index f3478645bc..969b0778b9 100644 --- a/core/src/main/java/edu/wpi/grip/core/Source.java +++ b/core/src/main/java/edu/wpi/grip/core/Source.java @@ -1,5 +1,7 @@ package edu.wpi.grip.core; +import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.ClassifierSource; @@ -11,6 +13,7 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; +import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; import java.io.IOException; @@ -23,17 +26,23 @@ /** * Base class for an input into the pipeline. */ -public abstract class Source { +public abstract class Source extends AbstractPipelineEntry { private static final Logger logger = Logger.getLogger(Source.class.getName()); private final ExceptionWitness exceptionWitness; /** * @param exceptionWitnessFactory Factory to create the exceptionWitness. */ - protected Source(ExceptionWitness.Factory exceptionWitnessFactory) { + protected Source(String id, ExceptionWitness.Factory exceptionWitnessFactory) { + super(id); this.exceptionWitness = exceptionWitnessFactory.create(this); } + @Override + public final List getInputSockets() { + return ImmutableList.of(); + } + /** * This is used by the GUI to distinguish different sources. For example, {@link * edu.wpi.grip.core.sources.ImageFileSource} returns the filename of the image. @@ -47,6 +56,7 @@ protected Source(ExceptionWitness.Factory exceptionWitnessFactory) { * * @return @return An array of {@link OutputSocket}s for the outputs that the source produces. */ + @Override public final ImmutableList getOutputSockets() { final List outputSockets = createOutputSockets(); for (OutputSocket socket : outputSockets) { @@ -101,6 +111,28 @@ public final void initializeSafely() { } } + /** + * Sets this source as removed if an event is posted to clean it up. + * Subclasses that need additional functionality should subscribe to the event bus. + */ + @Subscribe + public void onSourceRemoved(SourceRemovedEvent event) { + if (event.getSource() == this) { + setRemoved(); + } + } + + /** + * Cleans up this source when it gets removed from the pipeline. The default implementation is a + * NOP, but subclasses should override this method if they need to free any resources or + * de-register callbacks. + */ + @Override + @SuppressWarnings("PMD") + protected void cleanUp() { + // Default to NOP + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/core/src/main/java/edu/wpi/grip/core/Step.java b/core/src/main/java/edu/wpi/grip/core/Step.java index 1820dff556..6d0ea03f8c 100644 --- a/core/src/main/java/edu/wpi/grip/core/Step.java +++ b/core/src/main/java/edu/wpi/grip/core/Step.java @@ -24,7 +24,7 @@ * sockets, and it runs the operation whenever one of the input sockets changes. */ @XStreamAlias(value = "grip:Step") -public class Step { +public class Step extends AbstractPipelineEntry { private static final Logger logger = Logger.getLogger(Step.class.getName()); private static final String MISSING_SOCKET_MESSAGE_END = " must have a value to run this step."; @@ -35,8 +35,6 @@ public class Step { private final OperationDescription description; private final List inputSockets; private final List outputSockets; - private final Object removedLock = new Object(); - private boolean removed = false; /** * @param operation The operation that is performed at this step. @@ -52,6 +50,7 @@ public class Step { List outputSockets, ExceptionWitness.Factory exceptionWitnessFactory, Timer.Factory timerFactory) { + super(makeId(Step.class)); this.operation = operation; this.description = description; this.inputSockets = inputSockets; @@ -70,6 +69,7 @@ public OperationDescription getOperationDescription() { /** * @return An array of {@link InputSocket InputSockets} that hold the inputs to this step. */ + @Override public ImmutableList getInputSockets() { return ImmutableList.copyOf(inputSockets); } @@ -77,6 +77,7 @@ public ImmutableList getInputSockets() { /** * @return A list of {@link OutputSocket OutputSockets} that hold the outputs of this step. */ + @Override public ImmutableList getOutputSockets() { return ImmutableList.copyOf(outputSockets); } @@ -134,7 +135,7 @@ protected final void runPerform(boolean force) { // We need to ensure that if perform disabled is switching states that we don't run the // perform method while that is happening. synchronized (removedLock) { - if (!removed) { + if (!removed()) { timer.time(this.operation::perform); } } @@ -152,27 +153,9 @@ protected final void runPerform(boolean force) { witness.clearException(); } - /** - * Sets this step as having been removed. - */ - public final void setRemoved() { - // We need to wait for the perform method to complete before returning. - // if we don't wait then the perform method could end up being run concurrently with the - // perform methods execution - synchronized (removedLock) { - removed = true; - operation.cleanUp(); - } - } - - /** - * Allows checks to see if this step has had its perform method disabled. If this value ever - * returns false it will never return true again. - * - * @return true if runPerformIfPossible can run successfully - */ - protected boolean removed() { - return removed; + @Override + protected void cleanUp() { + operation.cleanUp(); } @Override @@ -196,6 +179,7 @@ public Factory(ExceptionWitness.Factory exceptionWitnessFactory, /** * @param operationData The operation data to use to construct the step. + * * @return The constructed Step. */ public Step create(OperationMetaData operationData) { diff --git a/core/src/main/java/edu/wpi/grip/core/VersionManager.java b/core/src/main/java/edu/wpi/grip/core/VersionManager.java new file mode 100644 index 0000000000..92053267bc --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/VersionManager.java @@ -0,0 +1,38 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.exception.IncompatibleVersionException; + +import com.github.zafarkhaja.semver.Version; + +/** + * Manager for GRIP versions. + */ +public final class VersionManager { + + /** + * The final release of GRIP without versioned saves. + */ + public static final Version LAST_UNVERSIONED_RELEASE = Version.valueOf("1.5.0"); + + /** + * The current version of GRIP. + */ + public static final Version CURRENT_VERSION = Version.valueOf("2.0.0-beta"); + + /** + * Checks compatibility between two versions of GRIP. + * + * @param current the current version of GRIP + * @param check the version to check + * + * @throws IncompatibleVersionException if the versions are incompatible + */ + public static void checkVersionCompatibility(Version current, Version check) + throws IncompatibleVersionException { + if (check.getMajorVersion() > current.getMajorVersion() + || check.getMinorVersion() > current.getMinorVersion()) { + throw new IncompatibleVersionException(check); + } + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java new file mode 100644 index 0000000000..49d6d2af75 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java @@ -0,0 +1,22 @@ +package edu.wpi.grip.core.exception; + +import com.github.zafarkhaja.semver.Version; + +/** + * An exception thrown when trying to load a saved project created in an incompatible version + * of GRIP. + */ +public class IncompatibleVersionException extends InvalidSaveException { + + private final Version loaded; + + public IncompatibleVersionException(Version loaded) { + super("Incompatible future version: " + loaded); + this.loaded = loaded; + } + + public Version getLoaded() { + return loaded; + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java b/core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java new file mode 100644 index 0000000000..3e33f3abbb --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/InvalidSaveException.java @@ -0,0 +1,16 @@ +package edu.wpi.grip.core.exception; + +/** + * An exception thrown when trying to load an invalid saved project. + */ +public class InvalidSaveException extends GripException { + + public InvalidSaveException(String message) { + super(message); + } + + public InvalidSaveException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java b/core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java new file mode 100644 index 0000000000..de20b6f32d --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/UnknownSaveFormatException.java @@ -0,0 +1,12 @@ +package edu.wpi.grip.core.exception; + +/** + * An exception thrown when loading a save file that can't be deserialized to a known type. + */ +public class UnknownSaveFormatException extends InvalidSaveException { + + public UnknownSaveFormatException(String message) { + super(message); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java b/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java index 37d9641175..2474fbf4b0 100644 --- a/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java +++ b/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java @@ -1,5 +1,6 @@ package edu.wpi.grip.core.http; +import edu.wpi.grip.core.exception.InvalidSaveException; import edu.wpi.grip.core.serialization.Project; import com.google.inject.Inject; @@ -38,8 +39,14 @@ protected void handleIfPassed(String target, baseRequest.setHandled(true); return; } - project.open(new String(IOUtils.toByteArray(request.getInputStream()), "UTF-8")); - response.setStatus(HttpServletResponse.SC_CREATED); - baseRequest.setHandled(true); + try { + project.open(new String(IOUtils.toByteArray(request.getInputStream()), "UTF-8")); + response.setStatus(HttpServletResponse.SC_CREATED); + baseRequest.setHandled(true); + } catch (InvalidSaveException e) { + // 406 - Not Acceptable if given an invalid save + response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); + baseRequest.setHandled(true); + } } } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java index 0baefa312d..da1f49379e 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/BlurOperation.java @@ -47,11 +47,11 @@ public class BlurOperation implements Operation { @SuppressWarnings("JavadocMethod") public BlurOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.typeSocket = inputSocketFactory.create(typeHint); - this.radiusSocket = inputSocketFactory.create(radiusHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.typeSocket = inputSocketFactory.create(typeHint, "blur-type"); + this.radiusSocket = inputSocketFactory.create(radiusHint, "blur-radius"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java index 583430fa50..331facbdf5 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ConvexHullsOperation.java @@ -26,9 +26,11 @@ public class ConvexHullsOperation implements Operation { .category(OperationDescription.Category.FEATURE_DETECTION) .build(); - private final SocketHint contoursHint = new SocketHint.Builder<>(ContoursReport - .class) - .identifier("Contours").initialValueSupplier(ContoursReport::new).build(); + private final SocketHint contoursHint = + new SocketHint.Builder<>(ContoursReport.class) + .identifier("Contours") + .initialValueSupplier(ContoursReport::new) + .build(); private final InputSocket inputSocket; private final OutputSocket outputSocket; @@ -36,9 +38,9 @@ public class ConvexHullsOperation implements Operation { @SuppressWarnings("JavadocMethod") public ConvexHullsOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(contoursHint); + this.inputSocket = inputSocketFactory.create(contoursHint, "contours"); - this.outputSocket = outputSocketFactory.create(contoursHint); + this.outputSocket = outputSocketFactory.create(contoursHint, "hulls"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java index 49284ed13e..9023de3b17 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/DesaturateOperation.java @@ -39,8 +39,8 @@ public class DesaturateOperation implements Operation { @SuppressWarnings("JavadocMethod") public DesaturateOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.outputSocket = outputSocketFactory.create(outputHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java index f720571c9c..f01238cbd9 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/DistanceTransformOperation.java @@ -46,11 +46,11 @@ public class DistanceTransformOperation implements Operation { @SuppressWarnings("JavadocMethod") public DistanceTransformOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.srcSocket = inputSocketFactory.create(srcHint); - this.typeSocket = inputSocketFactory.create(typeHint); - this.maskSizeSocket = inputSocketFactory.create(maskSizeHint); + this.srcSocket = inputSocketFactory.create(srcHint, "source-image"); + this.typeSocket = inputSocketFactory.create(typeHint, "transform-type"); + this.maskSizeSocket = inputSocketFactory.create(maskSizeHint, "mask-size"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java index 748f1f9fef..3ce1c96776 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java @@ -95,20 +95,20 @@ public class FilterContoursOperation implements Operation { @SuppressWarnings("JavadocMethod") public FilterContoursOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.contoursSocket = inputSocketFactory.create(contoursHint); - this.minAreaSocket = inputSocketFactory.create(minAreaHint); - this.minPerimeterSocket = inputSocketFactory.create(minPerimeterHint); - this.minWidthSocket = inputSocketFactory.create(minWidthHint); - this.maxWidthSocket = inputSocketFactory.create(maxWidthHint); - this.minHeightSocket = inputSocketFactory.create(minHeightHint); - this.maxHeightSocket = inputSocketFactory.create(maxHeightHint); - this.soliditySocket = inputSocketFactory.create(solidityHint); - this.minVertexSocket = inputSocketFactory.create(minVertexHint); - this.maxVertexSocket = inputSocketFactory.create(maxVertexHint); - this.minRatioSocket = inputSocketFactory.create(minRatioHint); - this.maxRatioSocket = inputSocketFactory.create(maxRatioHint); - - this.outputSocket = outputSocketFactory.create(contoursHint); + this.contoursSocket = inputSocketFactory.create(contoursHint, "contours"); + this.minAreaSocket = inputSocketFactory.create(minAreaHint, "min-area"); + this.minPerimeterSocket = inputSocketFactory.create(minPerimeterHint, "min-perimeter"); + this.minWidthSocket = inputSocketFactory.create(minWidthHint, "min-width"); + this.maxWidthSocket = inputSocketFactory.create(maxWidthHint, "max-width"); + this.minHeightSocket = inputSocketFactory.create(minHeightHint, "min-height"); + this.maxHeightSocket = inputSocketFactory.create(maxHeightHint, "max-height"); + this.soliditySocket = inputSocketFactory.create(solidityHint, "solidity"); + this.minVertexSocket = inputSocketFactory.create(minVertexHint, "min-vertices"); + this.maxVertexSocket = inputSocketFactory.create(maxVertexHint, "max-vertices"); + this.minRatioSocket = inputSocketFactory.create(minRatioHint, "min-ratio"); + this.maxRatioSocket = inputSocketFactory.create(maxRatioHint, "max-ratio"); + + this.outputSocket = outputSocketFactory.create(contoursHint, "filtered-contours"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java index 1f4e9f46f8..5067e65b6c 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterLinesOperation.java @@ -50,11 +50,11 @@ public class FilterLinesOperation implements Operation { @SuppressWarnings("JavadocMethod") public FilterLinesOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.minLengthSocket = inputSocketFactory.create(minLengthHint); - this.angleSocket = inputSocketFactory.create(angleHint); + this.inputSocket = inputSocketFactory.create(inputHint, "lines"); + this.minLengthSocket = inputSocketFactory.create(minLengthHint, "min-length"); + this.angleSocket = inputSocketFactory.create(angleHint, "angle"); - this.linesOutputSocket = outputSocketFactory.create(outputHint); + this.linesOutputSocket = outputSocketFactory.create(outputHint, "filtered-lines"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java index 24dc7116ed..d17e4f245b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindBlobsOperation.java @@ -54,12 +54,12 @@ public class FindBlobsOperation implements Operation { @SuppressWarnings("JavadocMethod") public FindBlobsOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.minAreaSocket = inputSocketFactory.create(minAreaHint); - this.circularitySocket = inputSocketFactory.create(circularityHint); - this.colorSocket = inputSocketFactory.create(colorHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.minAreaSocket = inputSocketFactory.create(minAreaHint, "min-area"); + this.circularitySocket = inputSocketFactory.create(circularityHint, "circularity"); + this.colorSocket = inputSocketFactory.create(colorHint, "find-dark-blobs"); - this.outputSocket = outputSocketFactory.create(blobsHint); + this.outputSocket = outputSocketFactory.create(blobsHint, "found-blobs"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java index 06d03f93be..3884488e63 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindContoursOperation.java @@ -52,10 +52,10 @@ public class FindContoursOperation implements Operation { @SuppressWarnings("JavadocMethod") public FindContoursOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.externalSocket = inputSocketFactory.create(externalHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.externalSocket = inputSocketFactory.create(externalHint, "find-external-only"); - this.contoursSocket = outputSocketFactory.create(contoursHint); + this.contoursSocket = outputSocketFactory.create(contoursHint, "found-contours"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java index 323adb1112..32adcd9b80 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/FindLinesOperation.java @@ -44,8 +44,8 @@ public class FindLinesOperation implements Operation { public FindLinesOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.linesReportSocket = outputSocketFactory.create(linesHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.linesReportSocket = outputSocketFactory.create(linesHint, "found-lines"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java index d8de90c6ed..5b7aff737b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSLThresholdOperation.java @@ -56,12 +56,12 @@ public class HSLThresholdOperation extends ThresholdOperation { @SuppressWarnings("JavadocMethod") public HSLThresholdOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.hueSocket = inputSocketFactory.create(hueHint); - this.saturationSocket = inputSocketFactory.create(saturationHint); - this.luminanceSocket = inputSocketFactory.create(luminanceHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.hueSocket = inputSocketFactory.create(hueHint, "hue"); + this.saturationSocket = inputSocketFactory.create(saturationHint, "saturation"); + this.luminanceSocket = inputSocketFactory.create(luminanceHint, "luminance"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java index c7e8700b95..9019a6deb5 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/HSVThresholdOperation.java @@ -56,12 +56,12 @@ public class HSVThresholdOperation extends ThresholdOperation { @SuppressWarnings("JavadocMethod") public HSVThresholdOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.hueSocket = inputSocketFactory.create(hueHint); - this.saturationSocket = inputSocketFactory.create(saturationHint); - this.valueSocket = inputSocketFactory.create(valueHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.hueSocket = inputSocketFactory.create(hueHint, "hue"); + this.saturationSocket = inputSocketFactory.create(saturationHint, "saturation"); + this.valueSocket = inputSocketFactory.create(valueHint, "value"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java index 6521f5947a..772089537b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/MaskOperation.java @@ -42,10 +42,10 @@ public class MaskOperation implements Operation { @SuppressWarnings("JavadocMethod") public MaskOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.maskSocket = inputSocketFactory.create(maskHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.maskSocket = inputSocketFactory.create(maskHint, "mask-image"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java index 6b98c5a979..44ae81ab96 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/NormalizeOperation.java @@ -47,12 +47,12 @@ public class NormalizeOperation implements Operation { @SuppressWarnings("JavadocMethod") public NormalizeOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.srcSocket = inputSocketFactory.create(srcHint); - this.typeSocket = inputSocketFactory.create(typeHint); - this.alphaSocket = inputSocketFactory.create(aHint); - this.betaSocket = inputSocketFactory.create(bHint); + this.srcSocket = inputSocketFactory.create(srcHint, "source-image"); + this.typeSocket = inputSocketFactory.create(typeHint, "normalize-type"); + this.alphaSocket = inputSocketFactory.create(aHint, "alpha"); + this.betaSocket = inputSocketFactory.create(bHint, "beta"); - this.outputSocket = outputSocketFactory.create(dstHint); + this.outputSocket = outputSocketFactory.create(dstHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java index a30153bb9b..612a4fd9df 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java @@ -142,9 +142,9 @@ public PublishVideoOperation(InputSocket.Factory inputSocketFactory) { throw new IllegalStateException("Only one instance of PublishVideoOperation may exist"); } this.inputSocket = inputSocketFactory.create(SocketHints.Inputs.createMatSocketHint("Image", - false)); + false), "source-image"); this.qualitySocket = inputSocketFactory.create(SocketHints.Inputs - .createNumberSliderSocketHint("Quality", 80, 0, 100)); + .createNumberSliderSocketHint("Quality", 80, 0, 100), "image-quality"); numSteps++; serverThread = new Thread(runServer, "Camera Server"); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java index 965d4fea68..755cee5898 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/RGBThresholdOperation.java @@ -54,12 +54,12 @@ public class RGBThresholdOperation extends ThresholdOperation { @SuppressWarnings("JavadocMethod") public RGBThresholdOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(inputHint); - this.redSocket = inputSocketFactory.create(redHint); - this.greenSocket = inputSocketFactory.create(greenHint); - this.blueSocket = inputSocketFactory.create(blueHint); + this.inputSocket = inputSocketFactory.create(inputHint, "source-image"); + this.redSocket = inputSocketFactory.create(redHint, "red"); + this.greenSocket = inputSocketFactory.create(greenHint, "green"); + this.blueSocket = inputSocketFactory.create(blueHint, "blue"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java index dcf66f3b6b..9594b330a1 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ResizeOperation.java @@ -46,16 +46,17 @@ public class ResizeOperation implements Operation { public ResizeOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { this.inputSocket = inputSocketFactory.create(SocketHints.Inputs - .createMatSocketHint("Input", false)); + .createMatSocketHint("Input", false), "source-image"); this.widthSocket = inputSocketFactory.create(SocketHints.Inputs - .createNumberSpinnerSocketHint("Width", 640)); + .createNumberSpinnerSocketHint("Width", 640), "new-width"); this.heightSocket = inputSocketFactory.create(SocketHints.Inputs - .createNumberSpinnerSocketHint("Height", 480)); - this.interpolationSocket = inputSocketFactory - .create(SocketHints.createEnumSocketHint("Interpolation", Interpolation.CUBIC)); + .createNumberSpinnerSocketHint("Height", 480), "new-height"); + this.interpolationSocket = inputSocketFactory.create( + SocketHints.createEnumSocketHint("Interpolation", Interpolation.CUBIC), "interp-type" + ); this.outputSocket = outputSocketFactory.create(SocketHints.Outputs - .createMatSocketHint("Output")); + .createMatSocketHint("Output"), "resized-image"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java index 37b179481a..72228bec03 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/SaveImageOperation.java @@ -79,13 +79,13 @@ public SaveImageOperation(InputSocket.Factory inputSocketFactory, FileManager fileManager) { this.fileManager = fileManager; - inputSocket = inputSocketFactory.create(inputHint); - fileTypesSocket = inputSocketFactory.create(fileTypeHint); - qualitySocket = inputSocketFactory.create(qualityHint); - periodSocket = inputSocketFactory.create(periodHint); - activeSocket = inputSocketFactory.create(activeHint); + inputSocket = inputSocketFactory.create(inputHint, "source-image"); + fileTypesSocket = inputSocketFactory.create(fileTypeHint, "file-type"); + qualitySocket = inputSocketFactory.create(qualityHint, "compression-quality"); + periodSocket = inputSocketFactory.create(periodHint, "period"); + activeSocket = inputSocketFactory.create(activeHint, "enable-saving"); - outputSocket = outputSocketFactory.create(outputHint); + outputSocket = outputSocketFactory.create(outputHint, "passthrough"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java index f3176ece20..8a9c42b01e 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/SwitchOperation.java @@ -40,7 +40,7 @@ public SwitchOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Fact final LinkedSocketHint linkedSocketHint = new LinkedSocketHint(inputSocketFactory, outputSocketFactory); - this.switcherSocket = inputSocketFactory.create(switcherHint); + this.switcherSocket = inputSocketFactory.create(switcherHint, "switch"); this.inputSocket1 = linkedSocketHint.linkedInputSocket("If True"); this.inputSocket2 = linkedSocketHint.linkedInputSocket("If False"); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java index 6f11218a7c..9a7a8a6df6 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ThresholdMoving.java @@ -30,8 +30,12 @@ public class ThresholdMoving implements Operation { @SuppressWarnings("JavadocMethod") public ThresholdMoving(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - imageSocket = inputSocketFactory.create(SocketHints.Inputs.createMatSocketHint("image", false)); - outputSocket = outputSocketFactory.create(SocketHints.Outputs.createMatSocketHint("moved")); + imageSocket = inputSocketFactory.create( + SocketHints.Inputs.createMatSocketHint("image", false), "source-image" + ); + outputSocket = outputSocketFactory.create( + SocketHints.Outputs.createMatSocketHint("moved"), "diff-image" + ); lastImage = new Mat(); } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java index aff111cc08..4f0e1a9b7d 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/ValveOperation.java @@ -35,7 +35,7 @@ public ValveOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Facto outputSocketFactory); final SocketHint switcherHint = SocketHints.createBooleanSocketHint("valve", true); - this.switcherSocket = inputSocketFactory.create(switcherHint); + this.switcherSocket = inputSocketFactory.create(switcherHint, "switcher"); this.inputSocket = linkedSocketHint.linkedInputSocket("Input"); this.outputSocket = linkedSocketHint.linkedOutputSocket("Output"); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java index bd388a4309..e655a736ef 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/WatershedOperation.java @@ -77,9 +77,9 @@ public class WatershedOperation implements Operation { @SuppressWarnings("JavadocMethod") public WatershedOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - srcSocket = inputSocketFactory.create(srcHint); - contoursSocket = inputSocketFactory.create(contoursHint); - outputSocket = outputSocketFactory.create(outputHint); + srcSocket = inputSocketFactory.create(srcHint, "source-image"); + contoursSocket = inputSocketFactory.create(contoursHint, "contours"); + outputSocket = outputSocketFactory.create(outputHint, "feature-contours"); markerPool = ImmutableList.copyOf( Stream.generate(Mat::new).limit(MAX_MARKERS).collect(Collectors.toList()) ); diff --git a/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java index 9ebf11534c..95cf184009 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkPublishOperation.java @@ -42,8 +42,8 @@ protected NetworkPublishOperation(InputSocket.Factory isf, Class dataType) { .identifier("Data") .build(); this.dataType = dataType; - this.dataSocket = isf.create(dataHint); - this.nameSocket = isf.create(nameHint); + this.dataSocket = isf.create(dataHint, "data-type"); + this.nameSocket = isf.create(nameHint, "published-name"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java index fb6dff4ac6..20c2d2cd89 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MatFieldAccessor.java @@ -48,14 +48,14 @@ public class MatFieldAccessor implements CVOperation { @SuppressWarnings("JavadocMethod") public MatFieldAccessor(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.inputSocket = inputSocketFactory.create(matHint); - - this.sizeSocket = outputSocketFactory.create(sizeHint); - this.emptySocket = outputSocketFactory.create(emptyHint); - this.channelsSocket = outputSocketFactory.create(channelsHint); - this.colsSocket = outputSocketFactory.create(colsHint); - this.rowsSocket = outputSocketFactory.create(rowsHint); - this.highValueSocket = outputSocketFactory.create(highValueHint); + this.inputSocket = inputSocketFactory.create(matHint, "mat"); + + this.sizeSocket = outputSocketFactory.create(sizeHint, "size"); + this.emptySocket = outputSocketFactory.create(emptyHint, "empty"); + this.channelsSocket = outputSocketFactory.create(channelsHint, "channels"); + this.colsSocket = outputSocketFactory.create(colsHint, "columns"); + this.rowsSocket = outputSocketFactory.create(rowsHint, "rows"); + this.highValueSocket = outputSocketFactory.create(highValueHint, "largest-value"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java index cc0771597c..cb8015ef4b 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/MinMaxLoc.java @@ -51,13 +51,13 @@ public class MinMaxLoc implements CVOperation { @SuppressWarnings("JavadocMethod") public MinMaxLoc(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.srcSocket = inputSocketFactory.create(srcInputHint); - this.maskSocket = inputSocketFactory.create(maskInputHint); + this.srcSocket = inputSocketFactory.create(srcInputHint, "source-image"); + this.maskSocket = inputSocketFactory.create(maskInputHint, "mask-image"); - this.minValSocket = outputSocketFactory.create(minValOutputHint); - this.maxValSocket = outputSocketFactory.create(maxValOutputHint); - this.minLocSocket = outputSocketFactory.create(minLocOutputHint); - this.maxLocSocket = outputSocketFactory.create(maxLocOutputHint); + this.minValSocket = outputSocketFactory.create(minValOutputHint, "min-value"); + this.maxValSocket = outputSocketFactory.create(maxValOutputHint, "max-value"); + this.minLocSocket = outputSocketFactory.create(minLocOutputHint, "min-value-location"); + this.maxLocSocket = outputSocketFactory.create(maxLocOutputHint, "max-value-location"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java index 5389ed710f..608c1a843d 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewPointOperation.java @@ -38,10 +38,10 @@ public class NewPointOperation implements CVOperation { @SuppressWarnings("JavadocMethod") public NewPointOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.xSocket = inputSocketFactory.create(xHint); - this.ySocket = inputSocketFactory.create(yHint); + this.xSocket = inputSocketFactory.create(xHint, "x"); + this.ySocket = inputSocketFactory.create(yHint, "y"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java index 8f2eaffb45..a7b3b920b4 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/opencv/NewSizeOperation.java @@ -37,10 +37,10 @@ public class NewSizeOperation implements CVOperation { @SuppressWarnings("JavadocMethod") public NewSizeOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { - this.widthSocket = inputSocketFactory.create(widthHint); - this.heightSocket = inputSocketFactory.create(heightHint); + this.widthSocket = inputSocketFactory.create(widthHint, "width"); + this.heightSocket = inputSocketFactory.create(heightHint, "height"); - this.outputSocket = outputSocketFactory.create(outputHint); + this.outputSocket = outputSocketFactory.create(outputHint, "result"); } @Override diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index 0324fcd77c..54bcebcfe7 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -2,8 +2,11 @@ import edu.wpi.grip.core.Pipeline; import edu.wpi.grip.core.PipelineRunner; +import edu.wpi.grip.core.VersionManager; import edu.wpi.grip.core.events.DirtiesSaveEvent; import edu.wpi.grip.core.events.WarningEvent; +import edu.wpi.grip.core.exception.InvalidSaveException; +import edu.wpi.grip.core.exception.UnknownSaveFormatException; import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.EventBus; @@ -11,7 +14,9 @@ import com.google.common.reflect.ClassPath; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.converters.ConversionException; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; +import com.thoughtworks.xstream.io.StreamException; import java.io.File; import java.io.FileInputStream; @@ -27,6 +32,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; + import javax.inject.Inject; import javax.inject.Singleton; @@ -41,25 +47,31 @@ public class Project { private EventBus eventBus; @Inject private Pipeline pipeline; + private ProjectModel model; @Inject private PipelineRunner pipelineRunner; private Optional file = Optional.empty(); private final ObservableBoolean saveIsDirty = new ObservableBoolean(); @Inject - public void initialize(StepConverter stepConverter, + public void initialize(ProjectConverter projectConverter, + StepConverter stepConverter, SourceConverter sourceConverter, SocketConverter socketConverter, ConnectionConverter connectionConverter, ProjectSettingsConverter projectSettingsConverter, - CodeGenerationSettingsConverter codeGenerationSettingsConverter) { + CodeGenerationSettingsConverter codeGenerationSettingsConverter, + VersionConverter versionConverter) { + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); xstream.setMode(XStream.NO_REFERENCES); + xstream.registerConverter(projectConverter); xstream.ignoreUnknownElements(); // ignores all unknown tags xstream.registerConverter(stepConverter); xstream.registerConverter(sourceConverter); xstream.registerConverter(socketConverter); xstream.registerConverter(connectionConverter); xstream.registerConverter(projectSettingsConverter); + xstream.registerConverter(versionConverter); xstream.registerConverter(codeGenerationSettingsConverter); try { ClassPath cp = ClassPath.from(getClass().getClassLoader()); @@ -116,10 +128,34 @@ public void open(String projectXml) { } @VisibleForTesting - void open(Reader reader) { + void open(Reader reader) throws InvalidSaveException { pipelineRunner.stopAndAwait(); this.pipeline.clear(); - this.xstream.fromXML(reader); + try { + Object loaded = xstream.fromXML(reader); + if (loaded instanceof Pipeline) { + // Unversioned pre-2.0.0 save. + // It's compatible with the current version because it loaded without exceptions. + // When saved, the old save file will be upgraded to the current version. + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); + } else if (loaded instanceof ProjectModel) { + // Version 2.0.0 or above. Since we got to this point, we know it's compatible + // (otherwise a ConversionException would have been thrown). + // Saves from different versions of GRIP will be upgraded/downgraded to the current version + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); + } else { + // Uhh... probably a future version + throw new UnknownSaveFormatException( + String.format("Unknown save format (loaded a %s)", loaded.getClass().getName()) + ); + } + } catch (ConversionException e) { + // Incompatible save, or a bug with de/serialization + throw new InvalidSaveException("There are incompatible sources or steps in the pipeline", e); + } catch (StreamException e) { + // Invalid XML + throw new InvalidSaveException("Invalid XML", e); + } pipelineRunner.startAsync(); saveIsDirty.set(false); } @@ -159,7 +195,7 @@ public void save(File file) throws IOException { } public void save(Writer writer) { - this.xstream.toXML(this.pipeline, writer); + this.xstream.toXML(model, writer); saveIsDirty.set(false); } diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java new file mode 100644 index 0000000000..0eeabc8993 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectConverter.java @@ -0,0 +1,40 @@ +package edu.wpi.grip.core.serialization; + +import edu.wpi.grip.core.Pipeline; +import edu.wpi.grip.core.VersionManager; + +import com.github.zafarkhaja.semver.Version; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +public class ProjectConverter implements Converter { + + private static final String VERSION_ATTRIBUTE = "version"; + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + ProjectModel model = (ProjectModel) source; + writer.addAttribute(VERSION_ATTRIBUTE, model.getVersion().toString()); + writer.startNode("grip:Pipeline"); + context.convertAnother(model.getPipeline()); + writer.endNode(); + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + String v = reader.getAttribute(VERSION_ATTRIBUTE); + Version version = v == null ? VersionManager.LAST_UNVERSIONED_RELEASE : Version.valueOf(v); + reader.moveDown(); + Pipeline pipeline = (Pipeline) context.convertAnother(null, Pipeline.class); + reader.moveUp(); + return new ProjectModel(pipeline, version); + } + + @Override + public boolean canConvert(Class type) { + return type.equals(ProjectModel.class); + } +} diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java new file mode 100644 index 0000000000..d4ee4f89dd --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/serialization/ProjectModel.java @@ -0,0 +1,37 @@ +package edu.wpi.grip.core.serialization; + +import edu.wpi.grip.core.Pipeline; + +import com.github.zafarkhaja.semver.Version; +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * Data model class for saving and loading projects from save files. + */ +@XStreamAlias("grip:Project") +public class ProjectModel { + + private Pipeline pipeline; + private Version version; + + /** + * Only used for XStream deserialization. + */ + public ProjectModel() { + this(null, null); + } + + public ProjectModel(Pipeline pipeline, Version version) { + this.pipeline = pipeline; + this.version = version; + } + + public Pipeline getPipeline() { + return pipeline; + } + + public Version getVersion() { + return version; + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java index 060f3a9012..fdad0ccdf3 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/SocketConverter.java @@ -18,6 +18,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; @@ -50,18 +51,13 @@ public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingCont // Save the location of the socket in the pipeline. socket.getStep().ifPresent(step -> { - final List sockets = (socket.getDirection() == Socket.Direction.INPUT) - ? step.getInputSockets() - : step.getOutputSockets(); - writer.addAttribute(STEP_ATTRIBUTE, String.valueOf(pipeline.getSteps().indexOf(step))); - writer.addAttribute(SOCKET_ATTRIBUTE, String.valueOf(sockets.indexOf(socket))); + writer.addAttribute(STEP_ATTRIBUTE, step.getId()); + writer.addAttribute(SOCKET_ATTRIBUTE, socket.getUid()); }); socket.getSource().ifPresent(source -> { - final List sockets = source.getOutputSockets(); - writer.addAttribute(SOURCE_ATTRIBUTE, String.valueOf(pipeline.getSources() - .indexOf(source))); - writer.addAttribute(SOCKET_ATTRIBUTE, String.valueOf(sockets.indexOf(socket))); + writer.addAttribute(SOURCE_ATTRIBUTE, source.getId()); + writer.addAttribute(SOCKET_ATTRIBUTE, socket.getUid()); }); // Save whether or not output sockets are previewed @@ -113,19 +109,13 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co // (such as connections) can reference sockets in the pipeline. Socket socket; if (reader.getAttribute(STEP_ATTRIBUTE) != null) { - final int stepIndex = Integer.parseInt(reader.getAttribute(STEP_ATTRIBUTE)); - final int socketIndex = Integer.parseInt(reader.getAttribute(SOCKET_ATTRIBUTE)); - - final Step step = pipeline.getSteps().get(stepIndex); + final Step step = stepFor(reader.getAttribute(STEP_ATTRIBUTE)); socket = (direction == Socket.Direction.INPUT) - ? step.getInputSockets().get(socketIndex) - : step.getOutputSockets().get(socketIndex); + ? socketFor(step.getInputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)) + : socketFor(step.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else if (reader.getAttribute(SOURCE_ATTRIBUTE) != null) { - final int sourceIndex = Integer.parseInt(reader.getAttribute(SOURCE_ATTRIBUTE)); - final int socketIndex = Integer.parseInt(reader.getAttribute(SOCKET_ATTRIBUTE)); - - final Source source = pipeline.getSources().get(sourceIndex); - socket = source.getOutputSockets().get(socketIndex); + final Source source = sourceFor(reader.getAttribute(SOURCE_ATTRIBUTE)); + socket = socketFor(source.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else { throw new ConversionException("Sockets must have either a step or source attribute"); } @@ -174,6 +164,51 @@ private Class getDeserializedType(Socket socket) { + socketType); } + private Source sourceFor(String attributeValue) { + if (attributeValue.matches("\\d+")) { + // All numbers, it's an index + return pipeline.getSources().get(Integer.parseInt(attributeValue)); + } else { + // Alphanumeric + return pipeline.getSources().stream() + .filter(s -> s.getId().equals(attributeValue)) + .findAny() + .get(); + } + } + + private Step stepFor(String attributeValue) { + if (attributeValue.matches("\\d+")) { + // All numbers, it's an index + return pipeline.getSteps().get(Integer.parseInt(attributeValue)); + } else { + // Alphanumeric + return pipeline.getSteps().stream() + .filter(s -> s.getId().equals(attributeValue)) + .findAny() + .get(); + } + } + + private S socketFor(List sockets, String attributeValue) { + if (attributeValue.matches("\\d+")) { + // All numbers, assume it's an index + return sockets.get(Integer.parseInt(attributeValue)); + } else { + // Letters -- assume it's a socket UID + List matching = sockets.stream() + .filter(s -> s.getUid().equals(attributeValue)) + .collect(Collectors.toList()); + if (matching.size() > 1) { + throw new ConversionException("Multiple sockets with UID '" + attributeValue + "'"); + } else if (matching.isEmpty()) { + throw new ConversionException("No sockets with UID '" + attributeValue + "'"); + } else { + return matching.get(0); + } + } + } + @Override public boolean canConvert(Class type) { return Socket.class.isAssignableFrom(type); diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java index 4db9491694..9837505e9e 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/SourceConverter.java @@ -27,6 +27,8 @@ */ public class SourceConverter implements Converter { + private static final String ID_ATTRIBUTE = "id"; + @Inject private EventBus eventBus; @Inject @@ -36,6 +38,7 @@ public class SourceConverter implements Converter { @Override public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) { + writer.addAttribute(ID_ATTRIBUTE, ((Source) obj).getId()); context.convertAnother(((Source) obj).getProperties()); } @@ -45,6 +48,7 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co try { final Class sourceClass = (Class) project.xstream.getMapper() .realClass(reader.getNodeName()); + final String id = reader.getAttribute(ID_ATTRIBUTE); final Properties properties = (Properties) context.convertAnother(null, Properties.class); // Although sources may block briefly upon creation, we intentionally do this in one thread @@ -53,6 +57,9 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co // sources that // are in the process of loading. final Source source = sourceFactory.create(sourceClass, properties); + if (id != null) { + source.setId(id); + } // Instead of returning the source, post it to the event bus so both the core and GUI // classes know it diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java index 885e034891..9c807c5ce3 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/StepConverter.java @@ -28,6 +28,7 @@ public class StepConverter implements Converter { private static final String NAME_ATTRIBUTE = "name"; + private static final String ID_ATTRIBUTE = "id"; @Inject private EventBus eventBus; @@ -43,6 +44,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC final Step step = ((Step) source); writer.addAttribute(NAME_ATTRIBUTE, step.getOperationDescription().name()); + writer.addAttribute(ID_ATTRIBUTE, step.getId()); // Also save any sockets in the step for (InputSocket socket : step.getInputSockets()) { @@ -64,11 +66,12 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co throw new ConversionException("Unknown operation: " + operationName); } - // Instead of simply returning the step and having XStream insert it into the pipeline using - // reflection, send a - // StepAddedEvent. This allows other interested classes (such as PipelineView) to also know - // when steps are added. - pipeline.addStep(stepFactory.create(operationMetaData.get())); + final String id = reader.getAttribute(ID_ATTRIBUTE); + Step step = stepFactory.create(operationMetaData.get()); + if (id != null) { + step.setId(id); + } + pipeline.addStep(step); while (reader.hasMoreChildren()) { context.convertAnother(this, Socket.class); diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java b/core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java new file mode 100644 index 0000000000..a735f13c8b --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/serialization/VersionConverter.java @@ -0,0 +1,31 @@ +package edu.wpi.grip.core.serialization; + +import com.github.zafarkhaja.semver.Version; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * Converter for {@link Version GRIP verisons}. + */ +public class VersionConverter implements Converter { + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + Version v = (Version) source; + writer.setValue(v.toString()); + } + + @Override + public Version unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + return Version.valueOf(reader.getValue()); + } + + @Override + public boolean canConvert(Class type) { + return Version.class.isAssignableFrom(type); + } + +} diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java index b582c84986..3952bbe97a 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocket.java @@ -29,7 +29,16 @@ public interface InputSocket extends Socket { void onValueChanged(); interface Factory { - InputSocket create(SocketHint hint); + /** + * Creates a new input socket from a socket hint. This should only be used for + * generated sockets (like for Python operations) or for templated operations. For + * everything else, use {@link #create(SocketHint, String)}. + */ + default InputSocket create(SocketHint hint) { + return create(hint, hint.getIdentifier().toLowerCase().replaceAll("\\s+", "-")); + } + + InputSocket create(SocketHint hint, String uid); } /** @@ -48,6 +57,11 @@ public Decorator(InputSocket socket) { this.decorated = socket; } + @Override + public String getUid() { + return decorated.getUid(); + } + @Override public Direction getDirection() { return decorated.getDirection(); diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java index 40456510e5..83537b11ec 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/InputSocketImpl.java @@ -3,6 +3,7 @@ import edu.wpi.grip.core.Connection; +import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -19,12 +20,18 @@ public class InputSocketImpl extends SocketImpl implements InputSocket { private final AtomicBoolean dirty = new AtomicBoolean(false); + @VisibleForTesting + InputSocketImpl(EventBus eventBus, SocketHint socketHint) { + this(eventBus, socketHint, socketHint.getIdentifier()); + } + /** * @param eventBus The Guava {@link EventBus} used by the application. * @param socketHint {@link #getSocketHint} + * @param uid a unique string for identifying this socket */ - InputSocketImpl(EventBus eventBus, SocketHint socketHint) { - super(eventBus, socketHint, Socket.Direction.INPUT); + InputSocketImpl(EventBus eventBus, SocketHint socketHint, String uid) { + super(eventBus, socketHint, Socket.Direction.INPUT, uid); } /** @@ -63,8 +70,8 @@ public static class FactoryImpl implements Factory { } @Override - public InputSocket create(SocketHint hint) { - return new InputSocketImpl<>(eventBus, hint); + public InputSocket create(SocketHint hint, String uid) { + return new InputSocketImpl<>(eventBus, hint, uid); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java index 3bb44e484f..27c9eaa8ad 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocket.java @@ -12,6 +12,7 @@ public interface OutputSocket extends Socket { /** * @return Whether or not this socket is shown in a preview in the GUI. + * * @see #setPreviewed(boolean) d(boolean) */ boolean isPreviewed(); @@ -27,6 +28,15 @@ public interface OutputSocket extends Socket { void resetValueToInitial(); interface Factory { - OutputSocket create(SocketHint hint); + /** + * Creates a new output socket from a socket hint. This should only be used for + * generated sockets (like for Python operations) or for templated operations. For + * everything else, use {@link #create(SocketHint, String)}. + */ + default OutputSocket create(SocketHint hint) { + return create(hint, hint.getIdentifier().toLowerCase().replaceAll("\\s+", "-")); + } + + OutputSocket create(SocketHint hint, String uid); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java index 7fed378fec..03e615c141 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/OutputSocketImpl.java @@ -3,6 +3,7 @@ import edu.wpi.grip.core.events.SocketPreviewChangedEvent; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; @@ -21,12 +22,18 @@ public class OutputSocketImpl extends SocketImpl implements OutputSocket socketHint) { + this(eventBus, socketHint, socketHint.getIdentifier()); + } + /** * @param eventBus The Guava {@link EventBus} used by the application. * @param socketHint {@link #getSocketHint} + * @param uid a unique string for identifying this socket */ - OutputSocketImpl(EventBus eventBus, SocketHint socketHint) { - super(eventBus, socketHint, Direction.OUTPUT); + OutputSocketImpl(EventBus eventBus, SocketHint socketHint, String uid) { + super(eventBus, socketHint, Direction.OUTPUT, uid); this.eventBus = eventBus; } @@ -72,8 +79,8 @@ public static class FactoryImpl implements OutputSocket.Factory { } @Override - public OutputSocket create(SocketHint hint) { - return new OutputSocketImpl<>(eventBus, hint); + public OutputSocket create(SocketHint hint, String uid) { + return new OutputSocketImpl<>(eventBus, hint, uid); } } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java b/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java index c8cdcfd038..1e207bb9dc 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/Socket.java @@ -25,6 +25,17 @@ public interface Socket { */ SocketHint getSocketHint(); + /** + * Gets a String that uniquely identifies this socket. This only needs to be unique for the set + * of sockets containing this one (e.g. only per-operation or per-source); it does not need to be + * universally unique. However, this is not allowed to change; even if + * the name, view, or even the type changes, the UID has to be constant in order for projects + * saved from different versions of GRIP to be compatible. + * + * @implSpec This value MAY NOT change + */ + String getUid(); + /** * Set the value of the socket using an {@link Optional}, and fire off a {@link * edu.wpi.grip.core.events.SocketChangedEvent}. @@ -60,6 +71,7 @@ default void setValue(@Nullable T value) { * If this socket is in a step return it. * * @return The step that this socket is part of + * * @see #getSource() */ Optional getStep(); @@ -73,6 +85,7 @@ default void setValue(@Nullable T value) { * If this socket is in a source return it. * * @return The source that this socket is part of. + * * @see #getStep() */ Optional getSource(); diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java b/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java index 84b902b727..80df912acb 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/SocketImpl.java @@ -28,6 +28,7 @@ public class SocketImpl implements Socket { private final Direction direction; private final Set connections = new HashSet<>(); private final SocketHint socketHint; + private final String uid; private Optional step = Optional.empty(); private Optional source = Optional.empty(); private Optional value = Optional.empty(); @@ -38,10 +39,11 @@ public class SocketImpl implements Socket { * @param socketHint {@link #getSocketHint} * @param direction The direction that this socket represents */ - SocketImpl(EventBus eventBus, SocketHint socketHint, Direction direction) { + SocketImpl(EventBus eventBus, SocketHint socketHint, Direction direction, String uid) { this.eventBus = checkNotNull(eventBus, "EventBus can not be null"); this.socketHint = checkNotNull(socketHint, "Socket Hint can not be null"); this.direction = checkNotNull(direction, "Direction can not be null"); + this.uid = checkNotNull(uid, "UID cannot be null"); } @Override @@ -49,6 +51,11 @@ public SocketHint getSocketHint() { return socketHint; } + @Override + public String getUid() { + return uid; + } + @Override public void setValueOptional(Optional optionalValue) { checkNotNull(optionalValue, "The optional value can not be null"); @@ -127,6 +134,7 @@ public void removeConnection(Connection connection) { @Override public String toString() { return MoreObjects.toStringHelper(this) + .add("UID", getUid()) .add("socketHint", getSocketHint()) .add("value", getValue()) .add("direction", getDirection()) diff --git a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java index b17f96e5b5..960a06f5d4 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java @@ -3,7 +3,6 @@ import edu.wpi.grip.core.Source; import edu.wpi.grip.core.events.SourceHasPendingUpdateEvent; -import edu.wpi.grip.core.events.SourceRemovedEvent; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.SocketHint; import edu.wpi.grip.core.sockets.SocketHints; @@ -16,7 +15,6 @@ import com.google.common.base.StandardSystemProperty; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; import com.google.common.util.concurrent.MoreExecutors; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -134,10 +132,10 @@ public class CameraSource extends Source implements RestartableService { final FrameGrabberFactory grabberFactory, final ExceptionWitness.Factory exceptionWitnessFactory, @Assisted final Properties properties) throws MalformedURLException { - super(exceptionWitnessFactory); + super(makeId(CameraSource.class), exceptionWitnessFactory); this.eventBus = eventBus; - this.frameOutputSocket = outputSocketFactory.create(imageOutputHint); - this.frameRateOutputSocket = outputSocketFactory.create(frameRateOutputHint); + this.frameOutputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); + this.frameRateOutputSocket = outputSocketFactory.create(frameRateOutputHint, "fps-output"); this.properties = properties; final String deviceNumberProperty = properties.getProperty(DEVICE_NUMBER_PROPERTY); @@ -342,36 +340,33 @@ public State state() { return cameraService.state(); } - @Subscribe - public void onSourceRemovedEvent(SourceRemovedEvent event) throws InterruptedException, - TimeoutException, IOException { - if (event.getSource() == this) { - try { - // Stop the camera service and wait for it to terminate. - // If we just use stopAsync(), the camera service won't always have terminated by the time - // a new camera source is added. For webcam sources, this means that the video stream - // won't be freed and new sources won't be able to connect to the webcam until the - // application is closed. - if (StandardSystemProperty.OS_NAME.value().toLowerCase().contains("mac")) { - // Workaround for #716. This affects webcams as well as IP camera sources. - // Use only stopAsync() to avoid blocking. Since we have no way of knowing when - // the capture has actually been freed, we use a dumb delay to try to make sure it's - // freed before returning. THIS IS NOT A GOOD SOLUTION. But it's the best one we have - // until the bug is fixed. - stopAsync(); - try { - // Wait a bit to try to make sure the capture is actually freed before returning - Thread.sleep(100); - } catch (InterruptedException ignore) { - // We did our best. Hopefully, the webcam has been freed at this point. - Thread.currentThread().interrupt(); - } - } else { - this.stopAndAwait(); + @Override + protected void cleanUp() { + try { + // Stop the camera service and wait for it to terminate. + // If we just use stopAsync(), the camera service won't always have terminated by the time + // a new camera source is added. For webcam sources, this means that the video stream + // won't be freed and new sources won't be able to connect to the webcam until the + // application is closed. + if (StandardSystemProperty.OS_NAME.value().toLowerCase().contains("mac")) { + // Workaround for #716. This affects webcams as well as IP camera sources. + // Use only stopAsync() to avoid blocking. Since we have no way of knowing when + // the capture has actually been freed, we use a dumb delay to try to make sure it's + // freed before returning. THIS IS NOT A GOOD SOLUTION. But it's the best one we have + // until the bug is fixed. + stopAsync(); + try { + // Wait a bit to try to make sure the capture is actually freed before returning + Thread.sleep(100); + } catch (InterruptedException ignore) { + // We did our best. Hopefully, the webcam has been freed at this point. + Thread.currentThread().interrupt(); } - } finally { - this.eventBus.unregister(this); + } else { + this.stopAndAwait(); } + } finally { + this.eventBus.unregister(this); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java index 78d8c1805a..7d6488c402 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ClassifierSource.java @@ -39,7 +39,7 @@ protected ClassifierSource(EventBus eventBus, OutputSocket.Factory osf, ExceptionWitness.Factory exceptionWitnessFactory, @Assisted String filePath) { - super(exceptionWitnessFactory); + super(makeId(ClassifierSource.class), exceptionWitnessFactory); this.classifierSocket = osf.create(classifierHint); this.filePath = filePath; } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java b/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java index 3c36260cd5..3ecfab72f2 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/HttpSource.java @@ -3,7 +3,6 @@ import edu.wpi.grip.core.Source; import edu.wpi.grip.core.events.SourceHasPendingUpdateEvent; -import edu.wpi.grip.core.events.SourceRemovedEvent; import edu.wpi.grip.core.http.ContextStore; import edu.wpi.grip.core.http.GripServer; import edu.wpi.grip.core.sockets.OutputSocket; @@ -13,7 +12,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import com.thoughtworks.xstream.annotations.XStreamAlias; @@ -87,10 +85,10 @@ public interface Factory { GripServer server, ContextStore store, @Assisted String path) { - super(exceptionWitnessFactory); + super(makeId(HttpSource.class), exceptionWitnessFactory); this.path = path; this.imageHandler = handlers.computeIfAbsent(path, p -> new HttpImageHandler(store, p)); - this.imageOutput = osf.create(outputHint); + this.imageOutput = osf.create(outputHint, "image-output"); this.eventBus = eventBus; // Will add the handler only when the first HttpSource is created -- no-op every subsequent time // (Otherwise, multiple handlers would be getting called and it'd be a mess) @@ -138,11 +136,9 @@ public void initialize() { imageHandler.getImage().ifPresent(this::setImage); } - @Subscribe - public void onSourceRemovedEvent(SourceRemovedEvent event) { - if (event.getSource() == this) { - imageHandler.removeCallback(callback); - } + @Override + protected void cleanUp() { + imageHandler.removeCallback(callback); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java index 94fd3989ec..2b2ade2416 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/ImageFileSource.java @@ -65,10 +65,10 @@ private ImageFileSource( final OutputSocket.Factory outputSocketFactory, final ExceptionWitness.Factory exceptionWitnessFactory, final String path) { - super(exceptionWitnessFactory); + super(makeId(ImageFileSource.class), exceptionWitnessFactory); this.path = checkNotNull(path, "Path can not be null"); this.name = Files.getNameWithoutExtension(this.path); - this.outputSocket = outputSocketFactory.create(imageOutputHint); + this.outputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); } /** diff --git a/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java b/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java index 0c51b9cc2e..b802492018 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/MultiImageFileSource.java @@ -99,9 +99,9 @@ private MultiImageFileSource( final ExceptionWitness.Factory exceptionWitnessFactory, final String[] paths, final int index) { - super(exceptionWitnessFactory); + super(makeId(MultiImageFileSource.class), exceptionWitnessFactory); this.eventBus = eventBus; - this.outputSocket = outputSocketFactory.create(imageOutputHint); + this.outputSocket = outputSocketFactory.create(imageOutputHint, "image-output"); this.index = new AtomicInteger(checkElementIndex(index, paths.length, "File List Index")); this.paths = Arrays.asList(paths); } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java index 9742a4392c..da0c868d09 100644 --- a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java +++ b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java @@ -102,12 +102,12 @@ public String toProperty() { @Named("ntManager") MapNetworkReceiverFactory networkReceiverFactory, @Assisted String path, @Assisted Types type) { - super(exceptionWitnessFactory); + super(makeId(NetworkTableEntrySource.class), exceptionWitnessFactory); this.eventBus = eventBus; this.path = path; this.type = type; networkReceiver = networkReceiverFactory.create(path); - output = osf.create(type.createSocketHint()); + output = osf.create(type.createSocketHint(), "data-output"); } @Override @@ -150,7 +150,13 @@ public void initialize() { @Subscribe public void onSourceRemovedEvent(SourceRemovedEvent event) { if (event.getSource() == this) { - networkReceiver.close(); + setRemoved(); } } + + @Override + protected void cleanUp() { + networkReceiver.close(); + } + } diff --git a/core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java b/core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java new file mode 100644 index 0000000000..a563eb1bfa --- /dev/null +++ b/core/src/test/java/edu/wpi/grip/core/AbstractPipelineEntryTest.java @@ -0,0 +1,95 @@ +package edu.wpi.grip.core; + +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.MockInputSocket; +import edu.wpi.grip.core.sockets.MockOutputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static edu.wpi.grip.core.AbstractPipelineEntry.idPool; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractPipelineEntryTest { + + private static class TestEntryImpl extends AbstractPipelineEntry { + + TestEntryImpl() { + super(makeId(TestEntryImpl.class)); + } + + @Override + public List getInputSockets() { + return ImmutableList.of(); + } + + @Override + public List getOutputSockets() { + return ImmutableList.of(); + } + + @Override + protected void cleanUp() { + // NOP + } + + } + + @Test + public void testMakeId() { + TestEntryImpl t = new TestEntryImpl(); + String id = t.getId(); + assertTrue("ID was not added to the pool", idPool.checkId(TestEntryImpl.class, id)); + t.setRemoved(); + assertFalse("ID was not removed from the pool", idPool.checkId(TestEntryImpl.class, id)); + } + + @Test(expected = IllegalStateException.class) + public void testSetId() { + TestEntryImpl t = new TestEntryImpl(); + t.setId("foo"); + fail("setId() should fail if called outside deserialization"); + } + + @Test + public void testSetRemoved() { + AtomicBoolean didCleanUp = new AtomicBoolean(false); + TestEntryImpl t = new TestEntryImpl() { + @Override + protected void cleanUp() { + didCleanUp.set(true); + } + }; + t.setRemoved(); + assertTrue("cleanUp was not called", didCleanUp.get()); + assertTrue("Entry was not removed", t.removed()); + } + + @Test + public void testGetSocketById() { + final InputSocket in = new MockInputSocket("foo"); + final OutputSocket out = new MockOutputSocket("bar"); + TestEntryImpl t = new TestEntryImpl() { + @Override + public List getInputSockets() { + return ImmutableList.of(in); + } + + @Override + public List getOutputSockets() { + return ImmutableList.of(out); + } + }; + assertEquals("Did not get correct socket", in, t.getSocketByUid(in.getUid())); + assertEquals("Did not get correct socket", out, t.getSocketByUid(out.getUid())); + } + +} diff --git a/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java b/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java index 9041b2c2df..40ef4ed0f0 100644 --- a/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java +++ b/core/src/test/java/edu/wpi/grip/core/CoreSanityTest.java @@ -9,6 +9,7 @@ import edu.wpi.grip.core.sockets.SocketHints; import edu.wpi.grip.core.util.service.SingleActionListener; +import com.github.zafarkhaja.semver.Version; import com.google.common.testing.AbstractPackageSanityTests; import com.google.common.util.concurrent.Service; @@ -42,5 +43,6 @@ public CoreSanityTest() { () -> null)); setDefault(OperationDescription.class, OperationDescription.builder().name("").summary("") .build()); + setDefault(Version.class, VersionManager.CURRENT_VERSION); } } diff --git a/core/src/test/java/edu/wpi/grip/core/MockSource.java b/core/src/test/java/edu/wpi/grip/core/MockSource.java index cdd48b77ec..fddbb09a16 100644 --- a/core/src/test/java/edu/wpi/grip/core/MockSource.java +++ b/core/src/test/java/edu/wpi/grip/core/MockSource.java @@ -12,7 +12,7 @@ public class MockSource extends Source { protected MockSource() { - super(origin -> null); + super(makeId(MockSource.class), origin -> null); } @Override diff --git a/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java b/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java index b119685574..55cf0bdbcf 100644 --- a/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java +++ b/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java @@ -12,6 +12,7 @@ import edu.wpi.grip.core.events.OperationAddedEvent; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.SourceAddedEvent; +import edu.wpi.grip.core.exception.InvalidSaveException; import edu.wpi.grip.core.operations.PythonScriptFile; import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.settings.ProjectSettings; @@ -130,6 +131,39 @@ public void testSerializeEmptyPipeline() throws Exception { 0, pipeline.getConnections().size()); } + @Test + public void testLoadInvalidXML() { + try { + project.open(""); + } catch (InvalidSaveException e) { + if (!e.getMessage().equals("Invalid XML")) { + // Not the exception we were expecting, throw it + throw e; + } + } + } + + @Test + public void testLoadIncompatibleEntries() { + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " " + + ""; + try { + project.open(xml); + } catch (InvalidSaveException e) { + if (!e.getMessage().equals("There are incompatible sources or steps in the pipeline")) { + throw e; + } + } + } + @Test public void testSerializePipelineWithSteps() throws Exception { pipeline.addStep(stepFactory.create(additionOperation)); diff --git a/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java index 4848729bbe..f69c9abfc1 100644 --- a/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java +++ b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java @@ -5,6 +5,8 @@ public class MockInputSocket extends InputSocketImpl { public MockInputSocket(String name) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(name, false)); + super(new EventBus(), + SocketHints.Outputs.createBooleanSocketHint(name, false), + "mock-input-" + name); } } diff --git a/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java index 8fa07a641d..103063b9df 100644 --- a/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java +++ b/core/src/test/java/edu/wpi/grip/core/sockets/MockOutputSocket.java @@ -3,7 +3,9 @@ import com.google.common.eventbus.EventBus; public class MockOutputSocket extends OutputSocketImpl { - public MockOutputSocket(String socketName) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(socketName, false)); + public MockOutputSocket(String name) { + super(new EventBus(), + SocketHints.Outputs.createBooleanSocketHint(name, false), + "mock-output-" + name); } } diff --git a/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java b/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java index 98f2790a09..daa2cf5d61 100644 --- a/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java +++ b/core/src/test/java/edu/wpi/grip/core/sources/MockNumberSource.java @@ -15,17 +15,17 @@ import java.util.Properties; public class MockNumberSource extends Source { - + private static int numberOf = 0; private final int id; private final OutputSocket outputSocket; - private final SocketHint outputSocketHint = + private final SocketHint outputSocketHint = SocketHints.Outputs.createNumberSocketHint("Num", Math.PI); @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "Do not need to synchronize inside of a constructor") public MockNumberSource(Factory exceptionWitnessFactory, double value, OutputSocket.Factory osf) { - super(exceptionWitnessFactory); + super(makeId(MockNumberSource.class), exceptionWitnessFactory); id = numberOf++; outputSocket = osf.create(outputSocketHint); outputSocket.setValue(new Double(value)); diff --git a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java index ed5a1bedd7..dd4dcbaa4a 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -7,10 +7,13 @@ import edu.wpi.grip.core.events.AppSettingsChangedEvent; import edu.wpi.grip.core.events.BenchmarkEvent; import edu.wpi.grip.core.events.CodeGenerationSettingsChangedEvent; +import edu.wpi.grip.core.events.ExceptionClearedEvent; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.TimerEvent; -import edu.wpi.grip.core.events.UnexpectedThrowableEvent; import edu.wpi.grip.core.events.WarningEvent; +import edu.wpi.grip.core.exception.IncompatibleVersionException; +import edu.wpi.grip.core.exception.InvalidSaveException; +import edu.wpi.grip.core.exception.UnknownSaveFormatException; import edu.wpi.grip.core.serialization.Project; import edu.wpi.grip.core.settings.AppSettings; import edu.wpi.grip.core.settings.CodeGenerationSettings; @@ -31,6 +34,7 @@ import org.apache.commons.lang3.tuple.Pair; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.Optional; import java.util.Set; @@ -60,6 +64,7 @@ /** * The Controller for the application window. */ +@SuppressWarnings("PMD.GodClass") // temporary public class MainWindowController { @FXML @@ -195,8 +200,28 @@ public void openProject() { Thread fileOpenThread = new Thread(() -> { try { project.open(file); + eventBus.post(new ExceptionClearedEvent(project)); + } catch (FileNotFoundException e) { + eventBus.post(new WarningEvent( + "File does not exist", + "The file at " + file.getAbsolutePath() + " does not exist.\n\n" + + "It may have been deleted by another program." + )); } catch (IOException e) { - eventBus.post(new UnexpectedThrowableEvent(e, "Failed to load save file")); + eventBus.post(new WarningEvent( + "Could not read file", + "The file at " + file.getAbsolutePath() + " could not be read.\n\n" + + "Caused by: " + e.getMessage() + )); + } catch (UnknownSaveFormatException e) { + pipeline.clear(); + eventBus.post(new WarningEvent("Unknown save format", e.getMessage())); + } catch (IncompatibleVersionException e) { + pipeline.clear(); + eventBus.post(new WarningEvent("Incompatible saved version", e.getLoaded().toString())); + } catch (InvalidSaveException e) { + pipeline.clear(); + eventBus.post(new WarningEvent("Invalid save file", e.getMessage())); } }, "Project Open Thread"); fileOpenThread.setDaemon(true); @@ -414,4 +439,5 @@ private void showAnalysis() { } analysisStage.showAndWait(); } + } diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java index 094b7dc5b7..5e82665b05 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/input/RangeInputSocketController.java @@ -107,6 +107,7 @@ private String getLowHighLabelText() { @Subscribe public void updateSliderValue(SocketChangedEvent event) { if (event.isRegarding(this.getSocket())) { + // There's no guarantee that the event was fired from the FX thread platform.runAsSoonAsPossible(() -> { this.slider.setLowValue(this.getSocket().getValue().get().get(0).doubleValue()); this.slider.setHighValue(this.getSocket().getValue().get().get(1).doubleValue()); diff --git a/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java b/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java index 632227c5d3..db17d25f83 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java @@ -111,6 +111,7 @@ public void testCreateAllKnownInputSocketControllers() throws Exception { verifyThat(controller.getHandle(), NodeMatchers.isEnabled()); verifyThat(controller.getHandle(), NodeMatchers.isVisible()); } + step.setRemoved(); }); } }