diff --git a/build.gradle b/build.gradle index 081223afa5..bef96e15eb 100644 --- a/build.gradle +++ b/build.gradle @@ -252,6 +252,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.4' compile group: 'com.google.guava', name: 'guava', version: '19.0' compile group: 'com.google.auto.value', name: 'auto-value', version: '1.2' 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 314e77170a..d77b65bb90 100644 --- a/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java +++ b/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java @@ -15,6 +15,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; @@ -125,6 +126,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/VersionManager.java b/core/src/main/java/edu/wpi/grip/core/VersionManager.java new file mode 100644 index 0000000000..a512ec70d9 --- /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("Incompatible future version: " + 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..0a95532cc2 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/exception/IncompatibleVersionException.java @@ -0,0 +1,17 @@ +package edu.wpi.grip.core.exception; + +/** + * An exception thrown when trying to load a saved project created in an incompatible version + * of GRIP. + */ +public class IncompatibleVersionException extends InvalidSaveException { + + public IncompatibleVersionException(String message) { + super(message); + } + + public IncompatibleVersionException(String message, Throwable cause) { + super(message, cause); + } + +} 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/http/HttpPipelineSwitcher.java b/core/src/main/java/edu/wpi/grip/core/http/HttpPipelineSwitcher.java index d349427193..004c6c39e0 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 edu.wpi.grip.core.util.GripMode; @@ -43,9 +44,15 @@ protected void handleIfPassed(String target, } switch (mode) { case HEADLESS: - 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) { + // 403 - Forbidden if given an invalid save + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + baseRequest.setHandled(true); + } break; case GUI: // Don't run in GUI mode, it doesn't make much sense and can easily deadlock if pipelines 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 433ccd06cb..07a40d777a 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 7abab98167..ba5bcb399a 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 b05ea36034..e58dbca94e 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 13181916e2..11ff0e9e84 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 @@ -67,9 +67,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"); } @Override 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 64812c491e..ca8d2fdaae 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,13 +2,17 @@ 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.exception.IncompatibleVersionException; +import edu.wpi.grip.core.exception.InvalidSaveException; import com.google.common.annotations.VisibleForTesting; import com.google.common.eventbus.Subscribe; 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 java.io.File; @@ -25,6 +29,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; + import javax.inject.Inject; import javax.inject.Singleton; @@ -37,6 +42,7 @@ public class Project { protected final XStream xstream = new XStream(new PureJavaReflectionProvider()); @Inject private Pipeline pipeline; + private ProjectModel model; @Inject private PipelineRunner pipelineRunner; private Optional file = Optional.empty(); @@ -47,13 +53,16 @@ public void initialize(StepConverter stepConverter, SourceConverter sourceConverter, SocketConverter socketConverter, ConnectionConverter connectionConverter, - ProjectSettingsConverter projectSettingsConverter) { + ProjectSettingsConverter projectSettingsConverter, + VersionConverter versionConverter) { + model = new ProjectModel(pipeline, VersionManager.CURRENT_VERSION); xstream.setMode(XStream.NO_REFERENCES); xstream.registerConverter(stepConverter); xstream.registerConverter(sourceConverter); xstream.registerConverter(socketConverter); xstream.registerConverter(connectionConverter); xstream.registerConverter(projectSettingsConverter); + xstream.registerConverter(versionConverter); try { ClassPath cp = ClassPath.from(getClass().getClassLoader()); cp.getAllClasses() @@ -109,10 +118,31 @@ public void open(String projectXml) { } @VisibleForTesting - void open(Reader reader) { + void open(Reader reader) throws IncompatibleVersionException { 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 InvalidSaveException( + 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("Incompatible operations in save file", e); + } pipelineRunner.startAsync(); saveIsDirty.set(false); } @@ -129,7 +159,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/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..2c6b4d6226 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,14 @@ 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(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(SOCKET_ATTRIBUTE, socket.getUid()); }); // Save whether or not output sockets are previewed @@ -114,18 +111,16 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co 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); 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); + socket = socketFor(source.getOutputSockets(), reader.getAttribute(SOCKET_ATTRIBUTE)); } else { throw new ConversionException("Sockets must have either a step or source attribute"); } @@ -174,6 +169,25 @@ private Class getDeserializedType(Socket socket) { + socketType); } + 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/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 84b5450998..6bc8e68684 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 @@ -28,7 +28,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); } /** @@ -47,6 +56,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 35a46ab2e5..e2e2b4d71d 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 0fbb07e5d5..9936ec1ecb 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 @@ -135,8 +135,8 @@ public class CameraSource extends Source implements RestartableService { @Assisted final Properties properties) throws MalformedURLException { super(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); 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..a78f65cfc6 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 @@ -90,7 +90,7 @@ public interface Factory { super(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) 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 d7bded3c70..397f84fa35 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 @@ -72,7 +72,7 @@ private ImageFileSource( super(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 bd3fbdbbc7..d643dcb1f9 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 @@ -106,7 +106,7 @@ private MultiImageFileSource( final int index) { super(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..f7b4863599 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 @@ -107,7 +107,7 @@ public String toProperty() { this.path = path; this.type = type; networkReceiver = networkReceiverFactory.create(path); - output = osf.create(type.createSocketHint()); + output = osf.create(type.createSocketHint(), "data-output"); } @Override 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 5dcc0ce5b5..a7f5d5c29a 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; @@ -41,5 +42,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/sockets/MockInputSocket.java b/core/src/test/java/edu/wpi/grip/core/sockets/MockInputSocket.java index 4848729bbe..3855983097 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,6 @@ 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"); } } 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..72a59780d7 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 @@ -4,6 +4,6 @@ public class MockOutputSocket extends OutputSocketImpl { public MockOutputSocket(String socketName) { - super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(socketName, false)); + super(new EventBus(), SocketHints.Outputs.createBooleanSocketHint(socketName, false), "mock"); } } 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 e6d67b1905..3da431ab8f 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -3,8 +3,11 @@ import edu.wpi.grip.core.Palette; import edu.wpi.grip.core.Pipeline; import edu.wpi.grip.core.PipelineRunner; +import edu.wpi.grip.core.events.ExceptionClearedEvent; +import edu.wpi.grip.core.events.ExceptionEvent; import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.UnexpectedThrowableEvent; +import edu.wpi.grip.core.exception.InvalidSaveException; import edu.wpi.grip.core.serialization.Project; import edu.wpi.grip.core.settings.ProjectSettings; import edu.wpi.grip.core.settings.SettingsProvider; @@ -12,6 +15,7 @@ import edu.wpi.grip.core.util.service.SingleActionListener; import edu.wpi.grip.ui.codegeneration.Exporter; import edu.wpi.grip.ui.codegeneration.Language; +import edu.wpi.grip.ui.components.ExceptionWitnessResponderButton; import edu.wpi.grip.ui.components.StartStoppableButton; import edu.wpi.grip.ui.util.DPIUtility; @@ -83,6 +87,8 @@ public class MainWindowController { private Palette palette; @Inject private Project project; + @Inject + private ExceptionWitnessResponderButton.Factory ewrbFactory; private Stage aboutDialogStage; @@ -90,6 +96,7 @@ public class MainWindowController { protected void initialize() { pipelineView.prefHeightProperty().bind(bottomPane.heightProperty()); statusBar.getLeftItems().add(startStoppableButtonFactory.create(pipelineRunner)); + statusBar.getRightItems().add(ewrbFactory.create(project, "Incompatible save file")); pipelineRunner.addListener(new SingleActionListener(() -> { final Service.State state = pipelineRunner.state(); final String stateMessage = @@ -169,8 +176,12 @@ public void openProject() { Thread fileOpenThread = new Thread(() -> { try { project.open(file); + eventBus.post(new ExceptionClearedEvent(project)); } catch (IOException e) { eventBus.post(new UnexpectedThrowableEvent(e, "Failed to load save file")); + } catch (InvalidSaveException e) { + pipeline.clear(); + eventBus.post(new ExceptionEvent(project, e.getMessage())); } }, "Project Open Thread"); fileOpenThread.setDaemon(true); @@ -254,10 +265,11 @@ protected void quit() { SafeShutdown.exit(0); } } - + /** * Controls the export button in the main menu. Opens a filechooser with language selection. * The user can select the language to export to, save location and file name. + * * @param actionEvent Unused event passed by the controller. */ public void generate(ActionEvent actionEvent) { 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(); }); } }